Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
7ddd68e
feat(branches): site branches with preview links, three-way merge, an…
DavidBabinec Sep 3, 2026
9a0bca6
feat(branches): merge review page with page renders, file diffs, thre…
DavidBabinec Sep 3, 2026
5851562
feat(branches): review polish, update-only dialog, docs
DavidBabinec Sep 3, 2026
3b7baaf
fix(branches): harden the merge review after its first review pass
DavidBabinec Sep 3, 2026
414f4c9
refactor(branches): review page in the house tile style
DavidBabinec Sep 4, 2026
bf927b1
fix(spotlight): stop the branch publish gate from outranking recency
DavidBabinec Sep 5, 2026
36f825e
fix(branches): renumber the migrations after main took 026
DavidBabinec Sep 5, 2026
7d8a80b
fix(branches): stop the merge request reading "requested now ago"
DavidBabinec Sep 5, 2026
8437c6f
feat(branches): split forking from managing with site.branches.create
DavidBabinec Sep 5, 2026
2605d66
feat(branches): merge review polish, undo, and confirmations
DavidBabinec Sep 5, 2026
7d41e78
fix(admin): mount the confirm provider once at the root so the merge …
DavidBabinec Sep 5, 2026
7691f0a
fix(branches): ISO timestamps on touch, rename, and revoke; review co…
DavidBabinec Sep 5, 2026
a698a07
fix(branches): let a stale branch header through the account routes
DavidBabinec Sep 5, 2026
150ee61
feat(branches): drop changes from the review, and leave it when the b…
DavidBabinec Sep 5, 2026
45b92c4
fix(branches): resolve viewport units in the review frames against a …
DavidBabinec Sep 5, 2026
dffe67b
fix(branches): show the whole page in the review frames
DavidBabinec Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Three categories, three voices:
| [features/data-workspace.md](features/data-workspace.md) | Data workspace UI: DataInspector, field management, DataGrid |
| [features/auth-and-access.md](features/auth-and-access.md) | Sessions, MFA, step-up, lockout, CSRF, capabilities |
| [features/site-shell.md](features/site-shell.md) | The persisted site config (breakpoints, classes, files, deps) |
| [features/branches.md](features/branches.md) | Site branches: fork, edit in isolation, preview links, three-way merge, version restore |
| [features/modules.md](features/modules.md) | Module engine, defining first-party blocks |
| [features/dashboard.md](features/dashboard.md) | Dashboard workspace, widgets, grid, customize mode |
| [features/spotlight.md](features/spotlight.md) | Cmd+K command palette |
Expand Down
3 changes: 2 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ server/router.ts ← match path
→ 301 redirect / 200 HTML / 404
```

Handlers validate request bodies with TypeBox before doing work, talk to repositories for persistence, and return `{ error: string }` envelopes on failure. Validation helpers live in `server/http.ts`. Per-handler logging uses the prefix `console.error('[<module>]', err)`.
Handlers validate request bodies with TypeBox before doing work, talk to repositories for persistence, and return `{ error: string }` envelopes on failure. CMS requests carry the site branch they address in the `X-Instatic-Branch` header; the dispatcher resolves it once into a `BranchScope` and public routes always read main (or a branch draft behind a preview cookie). Validation helpers live in `server/http.ts`. Per-handler logging uses the prefix `console.error('[<module>]', err)`.

---

Expand Down Expand Up @@ -183,6 +183,7 @@ The shape and cell types are defined by the `data_tables` schema. There is no se
### Storage conventions

- JSON columns end in `_json`. The SQLite adapter auto-parses any `*_json` string on read and auto-stringifies any plain object on write. Gated by `db-json-column-naming.test.ts`.
- `site`, `data_tables`, and `data_rows` carry `branch_id` (default `main`) and a generated `logical_id`. Every row keeps its logical id on every site branch; the physical primary key is `physicalId(branchId, logicalId)` from `src/core/branches/ids.ts` — the logical id itself on main. Repositories on these tables take an explicit `BranchScope`. See [features/branches.md](features/branches.md).
- Migrations are split per dialect with identical IDs. PG uses `jsonb`, `timestamptz`, `bigint`, `distinct on`; SQLite uses `text`, `text`, `integer`, window-function rewrites. Parity gated by `migration-parity.test.ts`.
- Repositories use only ANSI-standard SQL. The five Postgres-isms — `now()` in DML, `::int`, `::jsonb`, `any($N::...)`, `distinct on` — are banned in any `DbClient`-importing file. Gated by `db-postgres-isms.test.ts`.

Expand Down
16 changes: 16 additions & 0 deletions docs/e2e/feature-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,22 @@ CAP-005 note: plugin read/install/configure/lifecycle affordance splits, install

Page management note: `page-management.e2e.ts` creates disposable pages from the Site Explorer, verifies new pages appear and open in the canvas, renames a page through the context menu, deletes a page through the confirmation dialog, and switches away from and back to an unsaved edited page before saving/reloading to prove draft state is retained.

## Site Branches And Versions

| ID | Priority | Auto | Area | User Goal | Setup | Path | Expected Outcome | Watch For |
|---|---:|:---:|---|---|---|---|---|---|
| BRANCH-001 | P1 | ✅ | Branches | Create a branch from the toolbar, edit on it, and return to main | Owner logged in | Toolbar branch chip → Create branch… → strip | The chip shows the branch, the strip appears above the toolbar, Publish is disabled with the reason inline, the branch survives a reload, and "Switch to main" clears the strip | branch lost on reload, publish enabled on a branch, strip missing |
| BRANCH-002 | P1 | ✅ | Branches | Switch branches by search and from the command palette | Owner logged in, a branch exists | Chip search + Enter; ⌘K "Switch to main" | Enter switches to the first match; the palette command returns to main | stale palette results, switch without remount |
| BRANCH-003 | P1 | ✅ | Branches | Rename and delete a branch from the manage dialog | Fresh login (step-up) | Chip → Manage branches… | Search narrows the list and clearing it restores every branch; inline rename updates the row; delete confirms, steps up, and removes the branch | delete without step-up, rename lost |
| BRANCH-004 | P1 | ✅ | Branches | Share a preview link and open it as a visitor | Fresh login (step-up for cleanup) | Strip → Share preview; visitor context opens the URL | The visitor sees the branch draft with the "Previewing branch" banner and can exit; revoking kills the link | banner missing, link still works after revoke, main content shown |
| BRANCH-005 | P1 | ✅ | Branches | Merge a branch into main from the review page | Fresh login (step-up) | Strip → Merge into main… | The review page lists the branch-only page as new, merging steps up, the branch is deleted, and the page exists on main | empty plan while the relay still holds the edit, merge without step-up |
| REVIEW-001 | P1 | ✅ | Branches | Merge review across two accounts | Owner + a site editor without `site.branches.manage` | Editor edits on the branch, opens the review, comments, requests; owner sees the conflict, declines with a note; editor re-requests; owner resolves and merges with step-up | Before/after page renders with the changed and added nodes outlined, threads per change, decline note, merge lands the branch title on main, audit shows every step | request without merge rights merging, conflict merged without a decision, comments lost across a decline |
| VERSION-001 | P1 | ✅ | Versions | Restore a published version of the active page | Fresh login (step-up for publish) | Publish menu → Version history… | The page's first version is listed as Latest; Restore asks for confirmation, then the draft is replaced and a toast confirms | empty list after publish, restore publishing instead of drafting |

BRANCH-001 … BRANCH-005 note: `tests/e2e/branches.e2e.ts` drives the real toolbar chip, palette, in-place creator, context strip, manage dialog, preview-link visitor flow (a second browser context without an admin session), and the merge review dialog; every step captures evidence under `.tmp/evidence/branches-*.png`. Tests that step up run on a fresh login because step-up rotates the shared owner session.

VERSION-001 note: `tests/e2e/version-history.e2e.ts` creates and opens a page of its own (so the history it asserts on is independent of earlier specs), publishes, opens the version list from the publish split menu, and restores version 1 through the inline confirmation.

## Visual Builder

| ID | Priority | Auto | Area | User Goal | Setup | Path | Expected Outcome | Watch For |
Expand Down
7 changes: 7 additions & 0 deletions docs/e2e/feature-validation.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,10 @@ CONTENT-010 Entry SEO title and description reach the published <head> As a cont
ADMIN-009 Settings Escape dismissal after focus loss As an admin user, I want Escape to close the Settings modal even after I click non-focusable chrome so the dialog never traps me. Escape closes the Settings modal after a click on a heading or dead space moves focus to body. Focus on body; focus inside nested controls; backdrop click still works. Escape handling is document-level, not a React onKeyDown scoped to the dialog subtree. src/admin/modals/Settings/SettingsModal.tsx; tests/e2e/settings-escape.e2e.ts Regression spec for the fix shipped in PR #272; unit coverage drives fireEvent at chosen elements, only a browser reproduces real focus state. Happy: open Settings from the Site editor toolbar, click the first heading, press Escape, dialog hides. Passing in the repaired suite run 2026-08-30 0 None Rescued spec recovered from a prior verification session and added as tests/e2e/settings-escape.e2e.ts. Repair details in PR #461. 2026-08-30
ADMIN-010 AI workspace section navigation entry As an owner, I want the AI workspace reachable from the admin section navigation so I do not have to type the URL. A capability-gated AI link renders in the section navigation with href /admin/ai and routes on click. Users without AI capabilities see no entry; landing routes still include /admin/ai. Navigation uses the in-house admin router; the entry is gated on ai.providers.manage or ai.audit.read. src/admin/shared/AdminSectionNavigation/AdminSectionNavigation.tsx; tests/e2e/ai-nav.e2e.ts Regression spec for the fix shipped in PR #346. Happy: from Dashboard, the AI link is visible with the right href and clicking it lands on /admin/ai. Passing in the repaired suite run 2026-08-30 0 None Rescued spec recovered from a prior verification session and added as tests/e2e/ai-nav.e2e.ts. Repair details in PR #461. 2026-08-30
AI-010 Workspace MCP bridge stream stays readable As a connector user, I want the editor bridge stream to survive reverse proxies so connected agents keep working. The bridge responds 200 with content-type text/event-stream, cache-control no-cache, no-transform, and x-accel-buffering no, and the browser client holds the stream open without parse or stream errors. Buffering proxies reframing the body; client tearing the stream down as unreadable. The body stays newline-delimited JSON while the media type advertises an event stream so intermediaries flush incrementally. server/ai/mcp/editorBridge.ts; src/admin/ai/useMcpWorkspaceBridge.ts; tests/e2e/mcp-editor-bridge.e2e.ts Regression spec for the fix shipped in PR #282; unit coverage stubs fetch, only a browser exercises the real stream reader. Happy: open the Site editor, capture the bridge response, assert the streaming headers, and verify no mcp-workspace-bridge console errors for three seconds. Passing in the repaired suite run 2026-08-30 0 None Rescued spec recovered from a prior verification session and added as tests/e2e/mcp-editor-bridge.e2e.ts. Repair details in PR #461. 2026-08-30
BRANCH-001 Create and switch site branches from the toolbar As an editor, I want to fork the site into a branch and edit it in isolation so live content stays untouched until I merge. The toolbar chip opens a palette with an in-place creator; creating switches the tab, shows the tinted context strip above the toolbar, disables Publish with an inline reason, and the branch persists per tab across reloads. Duplicate slug is refused inline; a deleted branch drops the tab back to main; Publish/Schedule stay disabled everywhere on a branch. Branch ids match /^[a-z0-9][a-z0-9.-]{0,63}$/; creation requires site.branches.manage; the X-Instatic-Branch header scopes every CMS request. src/admin/shared/BranchSwitcher/; src/admin/state/branchStore.ts; server/handlers/cms/branches.ts; tests/e2e/branches.e2e.ts Branch content is a full copy of main; media, plugins, and users are shared. Happy: create Spring Redesign, see the strip and disabled Publish, reload, switch back to main. Passing 2026-09-02 0 None Evidence: .tmp/evidence/branches-1..4-*.png 2026-09-02
BRANCH-002 Search-to-switch and palette branch commands As an editor, I want to reach any branch by typing its name so switching is instant. Typing in the chip palette filters branches and Enter switches to the first match; the ⌘K palette offers Switch to main and Switch to <branch>. No match offers Create <slug>… to managers only. Palette rows come from the branch registry; commands are gated on site.read. src/admin/spotlight/commands/branches.ts; src/admin/spotlight/providers/branchesProvider.ts The chip refreshes the registry when opened. Happy: type spring, Enter, then ⌘K Switch to main. Passing 2026-09-02 0 None 2026-09-02
BRANCH-003 Manage branches dialog As a manager, I want to rename or delete branches in one place. The dialog lists branches with inline rename; delete confirms, steps up, and removes the branch and its content. Main cannot be renamed or deleted; deleting the active branch returns the tab to main. Requires site.branches.manage; delete requires step-up. src/admin/shared/BranchSwitcher/ManageBranchesDialog.tsx; server/branches/deleteBranch.ts Runs on a fresh login because step-up rotates the session. Happy: rename to Spring 2027, delete with step-up. Passing 2026-09-02 0 None Evidence: .tmp/evidence/branches-5-manage.png 2026-09-02
BRANCH-004 Branch preview links As an editor, I want to share a link that shows a branch draft to someone without an admin account. Share preview issues a tokenised link; opening it sets a cookie and renders the branch draft with a banner and an exit link; revoking kills the link. Sharing again rotates the token; a dead token clears the cookie; a route missing on the branch falls through to the 404 page. Tokens are stored hashed; the cookie is HttpOnly SameSite=Lax; responses are no-store and noindex. server/branches/previewLinks.ts; server/publish/branchPreview.ts; server/publish/publicRoutes.ts A second browser context plays the visitor. Happy: share, open as visitor, exit, revoke, open again. Passing 2026-09-02 0 None Evidence: .tmp/evidence/branches-6-preview-shared.png, branches-7-visitor-preview.png 2026-09-02
BRANCH-005 Merge a branch into main As a manager, I want to review what a branch changes and land it on main. The review page plans a three-way merge (one timeline node per change, conflicts with a two-way choice); merging steps up, writes main's draft, and optionally deletes the branch. Unresolved conflicts keep the button disabled and answer 409 server-side; a change that landed after the plan is re-planned on apply. Row status never merges; bases move to the merged result; requires site.branches.manage + step-up. server/branches/merge.ts; src/core/branches/threeWayMerge.ts; src/admin/pages/branches/BranchReviewPage.tsx The relay is flushed before planning so live edits count. Happy: create Branch Page on the branch, merge with delete, find the page on main. Passing 2026-09-02 0 None Evidence: .tmp/evidence/branches-8-merge-review.png 2026-09-02
VERSION-001 Page version history and restore As an editor, I want to see the published versions of a page and bring one back into the draft. The publish menu opens Version history; versions list newest first with the live one marked; Restore confirms inline and replaces the draft on the active branch. Unpublished pages show an empty state; restoring never publishes. Requires row edit access; restore is audited as version.restore. src/admin/shared/VersionHistoryDialog/; server/handlers/cms/data/rows.ts; tests/e2e/version-history.e2e.ts Publish steps up, so the spec runs on a fresh login. Happy: publish, open history, restore version 1. Passing 2026-09-02 0 None 2026-09-02
REVIEW-001 Merge review across accounts As a site editor, I want to ask for a merge and discuss each change, and as a branch manager I want to read before/after renders, resolve conflicts, decline with a note, or merge. /admin/branches/:id/review shows one timeline node per change: pages as before/after frames with the changed nodes outlined from the tree diff, entries as field tables, files as line diffs, each with a comment thread; requests open/close with the branch; merging steps up. A declined request can be re-requested; a stale request is flagged; unresolved conflicts keep merge disabled and answer 409; rows of content tables the reader cannot open are withheld. Request and comment need site.read; decline and merge need site.branches.manage; one open request per branch (unique index); audit events for every step. server/branches/review.ts; server/branches/changeDetail.ts; server/publish/branchReviewRender.ts; src/admin/pages/branches/ The relay is flushed before the plan and the request hash so live edits count. Happy: editor edits, requests; owner conflicts, comments, declines; editor re-requests; owner resolves, merges with delete. Passing 2026-09-03 0 None Evidence: .tmp/evidence/branch-review-*.png 2026-09-03
Loading
Loading