ADR-0011: Snapshot/Version model consolidation¶
Status: Proposed Date: 2026-05-30 Deciders: Ramón Kamibayashi + Claude Opus 4.7 Supersedes parts of: ADR-0010 WP-Pulse (F4 visual snapshots table, F-versions naming) Technical story: WP-Pulse v1.0 shipped with three distinct database tables and conceptual surfaces around the word "snapshot". The current tab labelled "Snapshots" in the web UI points at the wrong one. Operators are confused. Backend has dead code carrying weight.
Context¶
Across the v1.0 build cycle, the word "snapshot" got attached to three independent things that grew at different times:
| Table | Born | Original intent | Reality today |
|---|---|---|---|
snapshots |
Alembic 001 (F0 boot) | Placeholder for Playwright visual snapshots ("screenshot the prod page, compare pixel-by-pixel against local") | Empty. Never populated. The F4 visual diff that actually shipped writes to pulls.visual_diff JSONB instead. The table is dead weight. |
snapshot_ingests |
Alembic 002 (F1 pull pipeline) | Receipt + chunk tracker for the agent's DB dump + wp-content tar upload. Verifies SHA256 before assembly. | Working. Pure plumbing for the per-pull upload protocol. Operators never see it. |
site_versions |
Alembic 006 (F-versions, 2026-05-30) | Operator-facing immutable backups: load locally, diff, push to prod, delete. Auto-created per pull, plus manual wpdev version save. |
Working. This is what the operator means by "backup". |
The UI's "Snapshots" tab is wired to snapshots (empty forever) instead of site_versions (where the actual backups live). The operator clicks → expects backups → sees empty state → confused.
Root cause: the codebase grew by phase without ever returning to consolidate the conceptual vocabulary. F0 modelled "snapshots" as a Playwright artefact; F1 needed an upload-receipt table and reused the prefix; F4 quietly stored its results elsewhere (JSONB on pulls); F-versions invented yet another word for what is, operationally, the same primitive.
What the operator actually means¶
Step back to a one-operator-many-WordPress-sites mental model. The vocabulary that maps cleanly to that mental model:
| Operator concept | What it is | Frequency |
|---|---|---|
| Site | One real WordPress install enrolled in WP-Pulse | static |
| Pull | "Fetch prod state to local now" — an event with a timestamp + status | manual or scheduled |
| Push | "Ship local file edits to prod" — an event with file diff + rollback link | rare, high-stakes |
| Backup / "version" / "snapshot" | An immutable, point-in-time, restorable copy of the site (DB + wp-content) | auto on each pull, plus manual saves |
| Visual diff | A 3-up image (prod | diff-mask | local) attached to a specific pull. Used to verify "did anything visually drift?" | per-pull, optional |
Notice: the operator's vocabulary has ONE concept of "backup", not three. The other two backend rows are implementation details that should not surface in the UI vocabulary.
Proposed model¶
Single operator-facing primitive: Version¶
(We pick "Version" because "Snapshot" is overloaded with "screenshot" in tech vernacular. "Backup" is fine in Spanish; the noun in the UI can be "Snapshot" if that's what reads better — what matters is internal consistency.)
Every Version is:
- Immutable. Once created, never modified. Delete is the only mutation.
- Restorable. Has a DB dump + a wp-content tarball + a manifest (PHP/WP/plugin versions, table prefix, origin host).
- Sourceable. Either source=pull (auto-tagged from a successful Pull) or source=save (manual operator trigger).
- Lineageable. Optional parent_version_id link — for "I saved a version before this edit, then made changes, then saved again".
Operations on a Version:
- Load → tear down current local Docker stack, restore DB + files, bring up. Updates sites.current_version_id.
- Diff → file-level git diff --no-index between two versions' wp-content. Truncated for UI.
- Push to prod → load locally + then run normal F3 push flow against this version's state.
- Delete → physical rm + DB row removal. Rejected if version is currently loaded.
Backend plumbing renamed for clarity¶
| Old (current) | New (proposed) | Why |
|---|---|---|
site_versions |
unchanged | already the right name |
snapshot_ingests |
pull_uploads |
it's per-pull chunk receipts, not "snapshots" |
snapshots table |
DROPPED | dead since v1.0; visual diff already lives in pulls.visual_diff |
pulls.visual_diff JSONB |
unchanged | it's a property of the pull event, not a separate row |
Web tab /snapshots |
renamed to /versions (route alias) |
matches the primitive name |
CLI wpdev version * |
unchanged | already correct |
Web VersionsPanel.tsx |
unchanged | already correct |
Visual diff stays on Pull¶
Playwright visual diffs are tied to a specific pull event ("when this pull landed, here's how prod vs local rendered"). They are NOT a separate kind of snapshot. The pulls.visual_diff JSONB is the right home; the dead snapshots table was never used for this.
Future v2 idea: add a "compare two versions visually" feature that re-runs Playwright across two loaded versions. That would still write to pulls.visual_diff (or a new version_diffs table if it grows), not revive the dead snapshots table.
Why this consolidation matters¶
- Less cognitive load. The operator opens the UI and sees ONE list of backups. They don't have to translate between "Snapshots", "Versions", and "the snapshot_ingests on disk".
- No more empty-state confusion. The "Snapshots" tab pointing at an empty table that nobody populates is a UX foot-gun. Removing it (or aliasing to versions) closes the loop.
- Backend simpler. One dead table (
snapshots) dropped. One renamed for clarity (snapshot_ingests→pull_uploads). No data lost, no behavior changed. - Documentation honest. The ADR-0010 phase list still mentions "F4 visual snapshots" as if they live in
snapshotstable — they don't. This ADR corrects the record. - Names match implementation. "Version" is what the CLI calls it, what the model file calls it, what the UI panel calls it. Renaming the URL tab from
/snapshotsto/versions(or aliasing) closes the last inconsistency.
Migration plan¶
Phased to minimise blast radius. Each phase ships independently and is reversible.
Phase 1 — UI alias (zero risk)¶
- ✅ ALREADY DONE today (2026-05-30, commit
cbf0b25):/sites/[slug]/snapshots/page.tsxnow mountsVersionsPanel. So clicking "Snapshots" tab shows the real backups. - Add a
/sites/[slug]/versionsalternate route mounting the same panel for URL clarity. Both URLs work; canonical is/versions.
Phase 2 — Drop dead table (low risk)¶
- Alembic 011:
DROP TABLE snapshots. No code reads or writes it. - Update ADR-0010 phase list to note F4 visual diff actually lives in
pulls.visual_diff. - One-line change in
api/models/__init__.pyremoving the export.
Phase 3 — Rename for clarity (medium risk)¶
- Alembic 012:
ALTER TABLE snapshot_ingests RENAME TO pull_uploads. - Rename file + class:
api/models/snapshot_ingests.py→api/models/pull_uploads.py,SnapshotIngest→PullUpload. - Rename router:
api/routers/snapshots_ingest.py→api/routers/pull_uploads.py(or fold intopulls.pysince they only matter during a pull). - Endpoint path stays the same OR becomes
/api/v1/sites/{slug}/pulls/{pull_id}/uploads/{kind}/chunkfor clarity. Decide based on agent compat — if we change endpoints, agent v0.4.0 needs to know both paths during transition.
Phase 4 — Tab label sweep (cosmetic)¶
- Rename
web/app/sites/[slug]/snapshots/page.tsx→web/app/sites/[slug]/versions/page.tsx. - Add server-side 301 from
/snapshots→/versions. - Update
SiteTabNav.tsxlabel "Snapshots" → "Versions" (or whatever reads best in Spanish, e.g. "Copias").
Trade-offs¶
| Decision | Pros | Cons |
|---|---|---|
| Keep "site_versions" name | Already shipped, no migration pain | "Version" connotes git-like linear history; backups are more like Git tags |
Drop snapshots table |
Removes dead code | Closes the door on revival; if future visual diff wants its own table it has to invent a new name |
Rename snapshot_ingests → pull_uploads |
Clarity in code review + new operator onboarding | Agent compatibility: existing plugins talk to /snapshots/<id>/chunk URLs. Either keep the URL path with the old name and only rename the DB table, or bump plugin to v0.4 with new URL support. |
| Keep visual_diff on pulls.visual_diff JSONB | Simplest, already works, matches "diff is a property of a pull event" | If we ever want version-vs-version visual compare, this column won't carry it; need a new table |
Open questions¶
- Spanish UI wording. "Snapshots" vs "Versiones" vs "Copias de seguridad". Operator is Spanish-native; pick one and stick to it. Recommendation: "Versiones" because it matches the model + CLI commands (
wpdev version *) + is what the operator already calls them in conversation. - Phase 3 endpoint compatibility. Renaming the upload routes affects agent v0.3.5 which is live on hotelaldamagolf-com. Either:
- (a) Keep both URL paths during transition (legacy
/snapshots/...+ new/pull-uploads/...), drop legacy in agent v0.5. - (b) Don't rename the URL paths — only rename the database table + model class internally. Operator-facing tools don't notice. (Recommended for v1.x.)
- Visual diff future. If we add "compare any two versions visually", do we (a) expand
pulls.visual_diffto also live onsite_versions.visual_diff, or (b) create a newversion_diffstable? Probably (a) — JSONB columns are cheap and the entity that owns a visual diff is the entity being captured. - Auto-version retention. Currently
wpdev version savekeeps forever. Pull-auto versions also keep forever. The scheduler (F6) has retention_days + max_versions for auto-pulls only. Should manual saves also age out? Recommendation: no — manual saves are operator intent, never expire without explicit operator delete.
Decision¶
Proceed with all 4 phases, starting with Phase 1 (already shipped) + Phase 2 (drop dead table) in the next maintenance window.
Phase 3 (rename) is non-urgent — defer to v1.1 to avoid agent compat headaches.
Phase 4 (tab label sweep) blocks on phase 3 naming finalisation.
Implementation status¶
- Phase 1: Snapshots tab shows real versions (commit
cbf0b25, 2026-05-30). - Phase 2: Drop
snapshotstable via Alembic 011. Update ADR-0010. - Phase 3: Rename
snapshot_ingests→pull_uploads(deferred to v1.1). - Phase 4: Rename URL + tab label (depends on phase 3).