Backend shell: route repair lives in TreePane, so bad URLs strand the editor #35

Closed
opened 2026-07-14 20:54:38 +02:00 by dominik.polakovics · 0 comments

This was generated by AI while landing a PR.

Follow-up to #33 / PR #34, which shipped the TYPO3-style backend shell. Three defects found while landing that PR were accepted as known and deferred to here. They share one root cause: route repair is implemented as an effect inside TreePane — a conditionally-mounted component — instead of in the shell, and it navigates with a push instead of a replace.

1. Nonexistent-page repair only runs while the tree column is mounted

web/src/components/TreePane.tsx:184 (prune effect), web/src/App.tsx:352 (<Show when={showTree()}>).

The only code that repairs a URL naming a page the tree does not have is TreePane's prune effect, and TreePane renders only when treeAvailable() && !treeCollapsed(). Collapse is persisted in deckle:tree-collapsed.

Repro: collapse the page tree (or set sessionStorage["deckle:tree-collapsed"]="1"), then load /page/en/ghost.md. selectedPage() is non-null, PagePane fetches, the server 404s, and the page-error alert (PagePane.tsx:954) renders permanently — the URL never repairs and the empty state never appears.

#33 requires the opposite: "A URL naming a nonexistent page falls back to the empty state (no error page)." This is the everyday case of a stale bookmark or a link to a page a colleague deleted.

The same hole opens for any future module declaring tree: false, which the registry is explicitly designed to allow. Even with the tree visible, the error pane flashes for one frame before the tree arrives and prunes.

2. The repair pushes instead of replacing — the back button is trapped

web/src/selection.ts:111selectPage calls deps.navigate(...) with no {replace: true}, and the prune goes through it.

Repro: load /page/en/ghost.md (tree visible). It prunes to /page/en and history.length grows by one. Press Back: you return to /page/en/ghost.md, the prune fires again and pushes /page/en again. The editor can never navigate back past that point.

Second manifestation: handleRenamed / handleMoved (TreePane.tsx:296) await props.refetch() first, which makes the prune fire synchronously (the old path is gone) and push /page/en, then push the new path. So every rename, move and bundle conversion leaves a spurious empty-state entry in history.

A route repair must replace, never push.

3. An unknown language is never repaired

web/src/App.tsx:106 — the route-repair effect fires only when activeModule() === undefined, i.e. for an unknown module id. An unknown lang gets nothing.

Repro: open /page/de/about.md on a single-language (en) site. The module resolves, so nothing redirects. GET /api/page?lang=de returns 400 unknown lang (internal/server/server.go:413) and the error pane shows; the tree then prunes and rebuilds the route with route.lang, leaving the editor parked on /page/de with a permanently empty page module while every tree click silently rewrites the URL back to /page/en/.... With the tree collapsed (defect 1) it is a permanent error page.

A lang the tree does not report should be repaired exactly as an unknown module id is.

Suggested fix

Lift route repair out of TreePane and into the shell, where it belongs as a route-level rule: it should run regardless of whether any tree is mounted, cover module / lang / page alike, and navigate with replace. That addresses all three defects together. Rename/move handlers should navigate to the new path without the intermediate prune.

Not blocking, found in the same review

  • web/src/guard.ts:133saveAndContinue is re-entrant: the dialog's "Save & continue" button has no busy state and the guard keeps no in-flight flag, so a double-click issues two concurrent POST /api/page/save calls for the same file — precisely 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 first leaves it stashed, and it can hijack the element selection when that page is next opened.
> *This was generated by AI while landing a PR.* Follow-up to #33 / PR #34, which shipped the TYPO3-style backend shell. Three defects found while landing that PR were accepted as known and deferred to here. They share **one root cause**: route repair is implemented as an effect inside `TreePane` — a conditionally-mounted component — instead of in the shell, and it navigates with a push instead of a replace. ## 1. Nonexistent-page repair only runs while the tree column is mounted `web/src/components/TreePane.tsx:184` (prune effect), `web/src/App.tsx:352` (`<Show when={showTree()}>`). The only code that repairs a URL naming a page the tree does not have is TreePane's prune effect, and TreePane renders only when `treeAvailable() && !treeCollapsed()`. Collapse is persisted in `deckle:tree-collapsed`. **Repro:** collapse the page tree (or set `sessionStorage["deckle:tree-collapsed"]="1"`), then load `/page/en/ghost.md`. `selectedPage()` is non-null, PagePane fetches, the server 404s, and the `page-error` alert (`PagePane.tsx:954`) renders **permanently** — the URL never repairs and the empty state never appears. #33 requires the opposite: *"A URL naming a nonexistent page falls back to the empty state (no error page)."* This is the everyday case of a stale bookmark or a link to a page a colleague deleted. The same hole opens for any future module declaring `tree: false`, which the registry is explicitly designed to allow. Even with the tree visible, the error pane flashes for one frame before the tree arrives and prunes. ## 2. The repair pushes instead of replacing — the back button is trapped `web/src/selection.ts:111` — `selectPage` calls `deps.navigate(...)` with no `{replace: true}`, and the prune goes through it. **Repro:** load `/page/en/ghost.md` (tree visible). It prunes to `/page/en` and `history.length` grows by one. Press Back: you return to `/page/en/ghost.md`, the prune fires again and pushes `/page/en` again. The editor can never navigate back past that point. Second manifestation: `handleRenamed` / `handleMoved` (`TreePane.tsx:296`) `await props.refetch()` first, which makes the prune fire synchronously (the old path is gone) and push `/page/en`, *then* push the new path. So every rename, move and bundle conversion leaves a spurious empty-state entry in history. A route repair must `replace`, never `push`. ## 3. An unknown language is never repaired `web/src/App.tsx:106` — the route-repair effect fires only when `activeModule() === undefined`, i.e. for an unknown module id. An unknown `lang` gets nothing. **Repro:** open `/page/de/about.md` on a single-language (`en`) site. The module resolves, so nothing redirects. `GET /api/page?lang=de` returns 400 `unknown lang` (`internal/server/server.go:413`) and the error pane shows; the tree then prunes and rebuilds the route with `route.lang`, leaving the editor parked on `/page/de` with a permanently empty page module while every tree click silently rewrites the URL back to `/page/en/...`. With the tree collapsed (defect 1) it is a permanent error page. A lang the tree does not report should be repaired exactly as an unknown module id is. ## Suggested fix Lift route repair out of `TreePane` and into the shell, where it belongs as a route-level rule: it should run regardless of whether any tree is mounted, cover module / lang / page alike, and navigate with `replace`. That addresses all three defects together. Rename/move handlers should navigate to the new path without the intermediate prune. ## Not blocking, found in the same review - `web/src/guard.ts:133` — `saveAndContinue` is re-entrant: the dialog's "Save & continue" button has no busy state and the guard keeps no in-flight flag, so a double-click issues two concurrent `POST /api/page/save` calls for the same file — precisely 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 first leaves it stashed, and it can hijack the element selection when that page is next opened.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#35
No description provided.