feat(web): TYPO3-style backend shell — module rail | page tree | module content #34
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle!34
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/33"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Replaces the always-visible
tree | edit | previewthree-column layout with a TYPO3-style module shell: module rail | page tree | module content.Closes #33
What changed
Module registry. Page and View are the first two entries in a real registry (
web/src/modules.tsx: id, label, icon, whether it wants the page tree, component). The shell never names a module — it looks one up by the id in the URL, asks whether it wants a tree, and renders it. A third module costs one entry.Routing (
@solidjs/router)./:module/:lang/*path, e.g./page/en/blog/post.md. Module, language and selected page are deep-linkable and reload-safe; the URL replaces thedeckle:selectionsessionStorage mirror as the source of truth (two stores for one fact can only drift). Element selection stays ephemeral./and unknown modules redirect to/page/<defaultLang>once the tree reports its language; a URL naming a page the tree does not have falls back to the empty state, never an error page. Switching modules preserves the page and switching pages preserves the module — both fall out of the URL scheme.One catch-all route, parsed in a pure
routing.ts, rather than route patterns: a page path carries slashes and dots, and whether a module id is a module is the registry's call, not the route table's.Module rail. Icon-only, 48px, tooltips, icons from
lucide-solid. The page-tree toggle lives in the rail — a control that hides the column it sits in cannot bring it back.Page tree column. Drag- and keyboard-resizable (
role="separator"), width clamped 180–560px and persisted (deckle:tree-width), collapsible (deckle:tree-collapsed). Architecturally per-module optional; both current modules use it.Unsaved-changes guard (new feature). Module switch, tree page switch, and
beforeunloadprompt discard / save & continue / stay. Because module and page switches are both router navigations now, a singleuseBeforeLeavecovers both.FormPaneandPageSettingsregister as dirty sources (they now track field dirtiness, not just the rich-text body, and expose a programmatic save). Navigations that follow a confirmed server mutation — rename, move, delete, tree prune, bundle conversion — run unguarded: offering to save a page that no longer exists would be offering nonsense.Preview bridge → cross-module navigation. Clicking an element in the View preview jumps to the Page module with that element selected. In-iframe link browsing still does not sync tree selection or the URL.
Notes for the reviewer
deckle:highlightprotocol — the SPA's contract with the theme'sce-bridge.js— is kept and covered at thePreviewPanecomponent level rather than pretending it works end-to-end. A future split-view module can revive it.selectedElementis therefore derived (an element selection is valid iff it names the selected page) rather than cleared from an effect — an effect would race the transition and throw away the very selection the conversion was preserving.Verification
npm test— 389 passing across 23 suites (new:routing,guardunit,Guardend-to-end through the shell,ShellRouting,PreviewPanebridge protocol).npx tsc --noEmitclean;npm run buildgreen;go test ./...green.GET /page/en/posts/first.mdreturns the SPA (200,text/html) via the existingserveStaticindex.html fallback, with real assets still resolving.🤖 Generated with Claude Code
Landing audit — FAIL (do not merge yet)
Verification signal
CI run 23 is red on this head (
b079daa). I could not read the job log (needs a forge session), so I reproduced the CI recipe locally on the PR head instead:npm ci,npm run build,npm test— greengo vet ./...,go test ./...,go build ./cmd/harness— green, with the pinned Hugo 0.163.3+extended on PATH, so the four Hugo integration tests that skip without it actually ranThe base is red too.
mainhas been failing CI since the v1 rebuild landed: run 20 (PR #32), runs 21 and 22 (pushes tomain), now run 23 (this PR). Failure durations are scattered (4m / 5m / 8m / 14m), which does not look like one deterministic broken test. This is a repo-level CI breakage that deserves its own issue — it is not evidence against this PR, but it does mean nothing currently vouches for this branch.Conventions
feat(web): …) — OKCloses #33— OKafk/33merges cleanly intomain(base is an ancestor of the head; no conflict) — OKFindings
Blocking — spec divergences from #33:
web/src/components/TreePane.tsx:184— the "URL naming a nonexistent page falls back to the empty state, never an error page" rule is implemented as an effect insideTreePane, which is mounted only when the tree column is visible. With the tree collapsed (deckle:tree-collapsed), loading/page/en/ghost.mdrenders thepage-errorpane permanently. The same hole opens for any future module declaringtree: false— which the registry is designed to allow. Route repair belongs in the shell, not in a conditionally-mounted column.web/src/selection.ts:111— the prune navigates with a push, not a replace. Loading a bad page URL prunes to/page/enand grows history; Back re-enters the bad URL, which prunes and pushes again — the back button is trapped. Every rename/move/bundle conversion also leaves a spurious empty-state entry in history (TreePane.tsx:296, which refetches before navigating).web/src/App.tsx:106— the route-repair effect fires only when the module is unknown; an unknown language is never repaired./page/de/about.mdon anen-only site leaves the editor parked on/page/dewith a permanently empty page module.Non-blocking:
web/src/guard.ts:133—saveAndContinueis re-entrant. The dialog button has no busy state and the guard keeps no in-flight flag, so double-clicking "Save & continue" issues two concurrentPOST /api/page/savecalls for the same file — the read-modify-write race the sequential-save loop is documented to prevent.web/src/App.tsx:146— a pending preview-bridge selection is cleared only when its exact page loads. Navigating elsewhere before it resolves leaves it stashed, and it can hijack the element selection when that page is next opened.Cleared
Listener hygiene (
beforeunload,message,mousedown, pointer) all have matching cleanups;parseRoute/buildRouteround-trip paths with slashes, dots, spaces,%,#and reject malformed input without throwing; same-URL navigation does not false-positive the guard;deckle:selectionis genuinely gone with the URL as sole source of truth; the Go SPA fallback serves the deep links.The restructure otherwise delivers #33 as specified: module registry, rail, resizable/collapsible tree column, URL-as-source-of-truth routing, unsaved-changes guard, cross-module preview bridge.
CI is green — root cause was a root-hostile test, not this PR
TestEnsureBundleRollbackOnWriteFailure(internal/pageops/ensurebundle_test.go) forced theindex.mdwrite to fail by creating the bundle directory read-only (umask 0222→ mode 0555). Root ignores permission bits, so under CI's root runner the write succeeded, no error came back, and the test failed at its first assertion. Reproduced locally by re-running the test as root inside a user namespace (unshare -r): same failure, same line. That is whymainand every branch off it has been red since the v1 rebuild (runs 20–23), while everything passed on developer machines.Fixed in
6a689d9on this branch: the write goes through awriteBundlePageseam that the test substitutes with a failing writer, so the rollback is exercised as root and as an ordinary user alike. The process-globalsyscall.Umask— which could leak into any concurrently running test — is gone. Verified the test still fails when the rollback is mutated away, so it retains its teeth.CI run 24: success (5m53s).
The three blocking findings from the audit above still stand
Green CI does not clear them — they are frontend routing defects no Go test covers:
TreePane.tsx:184— nonexistent-page repair only runs while the tree column is mounted (collapsed tree ⇒ permanent error pane, which #33 forbids)selection.ts:111— the repair pushes instead of replacing, trapping the back buttonApp.tsx:106— an unknown language gets none of the repair an unknown module getsThey share one root cause: route repair lives in a conditionally-mounted component instead of the shell.