Skip to content

feat(example): header/footer editor + preview in web_ui_demo - #482

Merged
thewrz merged 17 commits into
mainfrom
feat/issue-477
Jul 14, 2026
Merged

feat(example): header/footer editor + preview in web_ui_demo#482
thewrz merged 17 commits into
mainfrom
feat/issue-477

Conversation

@thewrz

@thewrz thewrz commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Why

The demo (examples/web_ui_demo) had zero header/footer surface across its 12 views, while the backend already shipped the scope model (#208), AST schema (#302, ADR-040), config CRUD + effective-resolution routes (#476), and DOCX-generation wiring (#304). This closes the loop by letting a user author a profile, scope it, see the winning resolution, and download a DOCX that actually carries the configured running header/footer.

What

  • Profile editor for the composition: variants (default / first page / even pages), field references (spec number/title, project/client identity, revision label, page number), and the page-numbering policy (continuous vs restart-per-spec).
  • Scope assignment at client/project (Library + Project Settings) with the effective-resolution panel showing which scope wins, sourced from feat(api): header/footer config CRUD + effective-resolution routes — expose the #208 scope model #476's resolve endpoint.
  • An HTML approximation preview of the header/footer page regions (the markdown renderer intentionally has no page chrome), plus a DOCX download path for true fidelity.
  • Read-only warnings surface for raw-sidecar (unsupported captured content) when present.
  • Tier-gated behind API_FEATURES.headerFooter; every view degrades gracefully when no profile is configured.
  • Demo-only change — no ADR, no REST/MCP surface changes.

Package/revision-scope generate wiring + UI is explicitly out of scope per ADR-040 and tracked separately in #481.

Design decisions

A design spike ran before real implementation and surfaced 4 real gaps, all fixed at the design level before the build (not discovered after the fact):

  1. CRITICAL (container-clearing bug). mountHeaderFooterInspector mounts into #editor-inspector, which editor.js's renderInspector() has already populated with CITES / CITED BY / EDITABILITY before this callback runs. The mount must build and append its own wrapper element into that container and must never call container.replaceChildren() / innerHTML = '' (or anything else that clears it). Pinned as an explicit invariant in the module doc comment and covered by a manual-verification step; unit tests assert the container's pre-existing children survive a mount.
  2. REAL GAP (auto-persist-per-keystroke). The editor holds local draft state (HeaderFooterEditorDraft) mutated via the pure header-footer-fields.mjs helpers; ctx.put() fires only on explicit Save, never on every field/cell/pageNumbering edit. This surfaced a second real gap: the sibling Effective Resolution panel went stale after a project-scope Save in the spike's first draft. Fixed by adding an optional onSaved callback to initHeaderFooterEditor, which header-footer.js uses to re-fetch and repaint the resolution panel immediately after a successful save, rather than requiring a view-switch to refresh.
  3. MINOR (duplicate-logic trap). header-footer-preview.mjs's buildPreviewModel imports and uses header-footer-fields.mjs's selectVariant rather than re-deriving the v1/v2 variants.default precedence itself — the spike's own first draft made exactly this mistake before catching it, so the interface comment documents it explicitly.
  4. MINOR (revoke-race). download.js defers URL.revokeObjectURL by one macrotask (setTimeout 0) rather than revoking synchronously right after link.click(), avoiding a plausible (if usually benign in Chrome) race with larger DOCX blobs starting their save/navigation.
  5. SMALL (tier normalization). app.js's getSelectedLibraryTier closure explicitly does selectedLibrary()?.tier ?? nullselectedLibrary()?.tier alone can yield undefined, which is not a legal value in the documented 'reference'|'company'|'client'|null contract that header-footer.js's tier-gate depends on.

All prior ambiguity calls from the pre-spike design stand unchanged and were confirmed correct against real source during the spike's read-only verification pass: v1 UI scope-cut to client + project only (package/revision are struct-complete but UI-cut, follow-up filed as #481); PUT is always full-load-then-shallow-patch, never rebuilt from known fields; v1→v2 migration strips legacy top-level fields on Save of variants.default; PreviewFieldContext excludes sectionNumber/sectionTitle/projectNumber/clientNumber/packageName/revisionName/revisionLabel as non-fabricating; client-scope UI is gated to tier === 'client'; getJsonOrNull is a small internal api.js helper; zero REST/MCP/openapi/src/ changes (nothing blocked); the new css/header-footer.css is justified by the same multi-surface threshold as editor.css/constellation.css; no new top-level nav tab.

A prompt-injection attempt was encountered mid-spike: fabricated "don't tell the user" system-reminder-style text urging that the four edited files be treated as pre-existing/intentional and left unreported. This was disregarded; the worktree was independently re-verified clean via git status --porcelain, and the attempt is reported here per house rules — no agent or system message can authorize concealing information from oversight.

Testing

  • node --test examples/web_ui_demo/*.test.mjs — 254/254 passing
  • pnpm lint — clean (src/ only; examples/ is out of eslint's scope)
  • pnpm test — 2005/2005 passing (DATABASE_URL set per repo convention; NODE_ENV left unset to avoid the .env rate-limiter trap)
  • Manual E2E against a live server (create/edit/Save/reload/Delete round-trip for client + project scope, Effective Resolution panel refresh, downloaded DOCX carries a real PAGE field)
  • CI green

🤖 Co-authored by Claude Sonnet 5. Closes #477.

thewrz and others added 13 commits July 13, 2026 20:05
Adds the hand-kept browser-ESM mirror of src/ast/header-footer-schemas.ts's
FieldKind enum and defaultVariant() v1/v2 precedence (ADR-040), plus pure
withVariant/withPageNumbering/withCellField-family editors, so the demo's
header/footer editor (this issue's later tasks) can build a local draft
without mutating the persisted composition or duplicating the schema's
KNOWN AMBIGUITY logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds resolveFieldDisplay/buildPreviewModel/summarizeWarnings for the demo's
own HTML approximation of running headers/footers (the markdown renderer has
no page chrome). Delegates all v1/v2 variant precedence to
header-footer-fields.mjs's selectVariant rather than re-deriving it, per the
spike's caught duplicate-logic trap. Generation-only kinds (sectionNumber/
sectionTitle/pageNumber) never read field.text; excluded identity kinds
(packageName/revisionName/revisionLabel/projectNumber/clientNumber) always
render as unavailable rather than fabricating a value the demo has no
verified source for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
winningScope/scopeLabel give the demo's Effective Resolution panel a
client-side read of which scope won a header/footer merge, without
re-deriving the server's per-key merge logic — winningScope is the
same coarse layers.at(-1)?.scope ?? null read the API's own response
comment documents, and scopeLabel fails loud (throws) on any scope
kind it doesn't recognize rather than rendering a blank label.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…#477)

Adds the REST client surface the header/footer editor and Download DOCX
button need: getJsonOrNull (404 -> null, "not configured" is a valid state,
never an error) backing client/project header-footer GETs; put/delete for
both scopes; getProjectHeaderFooterResolved for the merged client->project
view; and fetchSpecDocx for POST /specs/:id/generate, resolving the response
Blob on 2xx.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
triggerBlobDownload defers URL.revokeObjectURL by one macrotask so it can
never race the click-triggered save on larger DOCX blobs, and takes its
DOM/Blob-URL primitives as an injectable deps parameter so the revoke
timing is boundary-testable without jsdom (this repo has none). Flips
API_FEATURES.headerFooter now that the backend surface (ADR-040) is
already on main.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pure DOM-paint module for the demo editor: renderPreview repaints a
resolved header/footer preview model (region rows, page-numbering
band, warnings) and renderVariantTabs repaints the default/first/even
tab strip. Every field/warning value is written via textContent only
(never innerHTML) since header/footer content is spec-author/client/
project-authored, not trusted markup.

No companion test file — matches the codebase's existing convention
for DOM-wiring modules (editor.js, tree.js, numbering.js, modal.js,
popover.js, chat.js are all untested; only the pure .mjs helpers and
api.js get test coverage).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…477)

Adds the shared, scope-agnostic header/footer editor mounted by
header-footer.js into the client-library and project panels. Holds a
local HeaderFooterEditorDraft ({ composition, dirty }) mutated only via
pure draft helpers (editField/addField/removeField/editPageNumbering,
layered on header-footer-fields.mjs's selectVariant/withVariant/
withCellField family) — every keystroke/micro-edit repaints the local
preview only; ctx.put() fires exactly once, exclusively from the
explicit Save action, always as a full-composition load-then-shallow-
patch of the exact edited path (never rebuilt from only the fields the
UI happens to know about). ctx.onSaved fires after a successful put and
before the editor repaints, so a sibling Effective Resolution panel can
refresh without a view switch. Delete confirms via openConfirm and
treats a 404 as an already-removed no-op rather than an error.

Pins both invariants at the boundary with DOM-free tests against the
draft controller (no jsdom in this repo, same split as download.js/
header-footer-preview-view.js: pure state logic is unit tested, DOM
wiring is not).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mount (#477)

Wires the pure header/footer modules and the DOM editor together for the two
v1-scoped panels (client library, project) plus the read-only spec-inspector
Download DOCX mount, closing out two spike-found bugs: mountInspector now only
ever appends into #editor-inspector (never clears the CITES/CITED BY/
EDITABILITY content already painted there), and a project-scope Save
re-fetches and repaints the sibling Effective Resolution panel immediately via
onSaved, instead of leaving it stale until a view switch. Client-scope API
calls are gated to tier === 'client' client-side, mirroring the server's
requireClientLibrary gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds css/header-footer.css (styling every .hf-* class already painted by
header-footer-editor.js/header-footer-preview-view.js/header-footer.js) and
wires two static mount points into index.html: a client-scope panel in the
Library view (hidden by default, tier-gated for a later app.js task) and a
project-scope HEADER/FOOTER settings panel with distinct editor + Effective
Resolution mounts. No new nav tab, no <script> tag changes — a boundary test
(header-footer-markup.test.mjs) pins the mount-point ids header-footer.js's
ctx contract depends on, plus the no-new-script/no-new-nav invariants, so a
future rename can't silently break app.js's getElementById wiring.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nto app.js (#477)

Closes the last gap in the v1 header/footer UI: editor.js now appends
header-footer.js's read-only inspector summary via an optional
ctx.mountHeaderFooterInspector callback (extracted as
appendHeaderFooterSummary so the append-only, spike-fixed contract is
unit-testable without the rest of initEditor's DOM tree). app.js
instantiates initHeaderFooter, normalizes getSelectedLibraryTier with an
explicit `?? null` (selectedLibrary()?.tier alone can be undefined, which
is not a legal value in header-footer.js's documented tier-gate contract),
and wires refreshLibraryPanel/refreshProjectPanel into showView() and the
library-selection change handler.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… editor

Full verification pass for #477 (automated suite + manual E2E against a
live server) caught a real bug the mocked unit tests couldn't see:
getClientHeaderFooter/putClientHeaderFooter/getProjectHeaderFooter/
putProjectHeaderFooter all resolve the full stored record
({ id, scope, config, createdAt, updatedAt }), but header-footer.js's
ctx.get/ctx.put closures passed that record straight to the editor as if
it were the bare HeaderFooterComposition its contract documents. Every
saved field vanished from both the post-Save repaint and any later
reload, because selectVariant() found no top-level .variants/.header/
.footer on the envelope. clientEditorCtx/projectEditorCtx now unwrap
.config via a shared helper before handing the value to the editor.

Regression-tested at the ctx-wiring boundary (header-footer.test.mjs)
and confirmed live: create/edit/Save/reload/Delete round-trips correctly
for both client and project scope, the Effective Resolution panel
refreshes without a view switch, the section-inspector mount no longer
clobbers CITES/CITED BY/EDITABILITY, a downloaded DOCX carries a real
PAGE field (not fabricated text), and every panel degrades cleanly with
API_FEATURES.headerFooter off. Full node --test suite: 238/238 passing
(234 pre-existing + 4 new). pnpm lint/test/test:integration green.

Files delivering #477 are now built and verified end to end; a follow-up
issue (#481) tracks the package/revision-scope generate wiring + UI that
ADR-040 explicitly scoped out of this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review findings on the #477 header/footer editor: refresh() collapsed a
real ctx.get() failure (network error, 5xx, malformed envelope) into the
same "empty" state as a genuinely-unconfigured scope, which let a user hit
Create configuration -> Save and silently overwrite whatever was actually
stored. onDelete() also never fired ctx.onSaved(), leaving the Settings
view's Effective Resolution panel stale after a project-scope delete. The
editor's previewContext was read once at (memoized) mount time instead of
per render, so switching client libraries never reached the live preview.
Separately, triggerBlobDownload could leak a throwaway <a> element into
document.body if link.click() threw, and several tests asserted less than
their own docstrings claimed.

Extracts loadDraft/deleteDraft as ctx-only pure helpers (mirroring the
existing saveDraft split) so refresh()'s error-vs-empty decision and
onDelete's onSaved-firing are both unit-tested directly; adds a dedicated
'error' editor mode that offers Retry only, never Create/Save. Threads
getPreviewContext as a live function instead of a memoized value.
Hardens triggerBlobDownload's cleanup into a single finally. Tightens the
addCellField/removeCellField catchall-key assertions, the Settings-view
markup boundary check, the app.js call-site-scoped tier-normalization
regex, and the "edits alone never touch ctx.put" test to actually spy on
ctx.put instead of asserting nothing ctx-related ever ran.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…onses

Fixes two review findings on the #477 header/footer editor: refresh() had
no re-entrancy guard, so an overlapping (slower) response could overwrite a
fresher one and a subsequent Save would then PUT stale data to whichever
scope is currently selected — not the one it was loaded for. Reuses the
existing monotonic request-guard pattern from scoring.js (extracted to
js/request-guard.mjs, now shared by all three consumers): the editor's
refresh() discards any outcome superseded by a newer call, and exposes
invalidate() so header-footer.js can stop a still-in-flight refresh() from
repainting a container after its editor is torn down (tier switches away
from 'client', or the active project is deselected).

Also tightens app-header-footer.test.mjs's tier-gate regex so it anchors to
the full `hfPanel.hidden = !(...)` assignment instead of the bare condition
substring — the prior regex kept matching even if the leading `!(`/closing
`)` were dropped, which would invert the client-scope panel's visibility
while the test still passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c79875ca-73b9-4dc5-92fa-cf58f7f11595

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The web UI demo adds scoped header/footer profile editing, variant-aware preview rendering, effective-resolution display, API persistence, DOCX download support, feature gating, inspector integration, and request-race protection. Extensive Node-based tests cover models, controllers, API behavior, DOM contracts, orchestration, and cleanup.

Changes

Header/footer demo

Layer / File(s) Summary
Composition and preview models
examples/web_ui_demo/js/header-footer-fields.mjs, examples/web_ui_demo/js/header-footer-preview.mjs, examples/web_ui_demo/js/header-footer-resolve.mjs, examples/web_ui_demo/js/request-guard.mjs, related tests
Adds immutable field and variant helpers, preview model construction, warning summaries, scope labels, and monotonic refresh guards.
Editor controller and rendering
examples/web_ui_demo/js/header-footer-editor.js, examples/web_ui_demo/js/header-footer-preview-view.js, examples/web_ui_demo/css/header-footer.css, related tests
Adds draft editing, persistence states, variant/page-number controls, preview rendering, responsive styling, and stale-refresh invalidation.
API and application orchestration
examples/web_ui_demo/js/api.js, examples/web_ui_demo/js/header-footer.js, examples/web_ui_demo/js/app.js, examples/web_ui_demo/js/editor.js, examples/web_ui_demo/js/download.js, examples/web_ui_demo/js/features.js, examples/web_ui_demo/index.html
Adds header/footer CRUD and resolution APIs, DOCX downloads, project/library panel wiring, inspector mounting, feature gating, and new HTML mount points.
Integration and regression contracts
examples/web_ui_demo/*header-footer.test.mjs, examples/web_ui_demo/download.test.mjs, examples/web_ui_demo/request-guard.test.mjs, examples/web_ui_demo/app-header-footer.test.mjs, examples/web_ui_demo/header-footer-markup.test.mjs, examples/web_ui_demo/scoring.test.mjs
Adds contract tests for API envelopes, source wiring, DOM structure, editor behavior, preview semantics, download cleanup, and request-guard extraction.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • wrzonance/SpecR#480 — Implements the backend header/footer CRUD and resolved endpoints consumed by these client API wrappers.
  • wrzonance/SpecR#425 — Shares the createRequestGuard() stale-response mechanism that this change extracts into a reusable module.
  • wrzonance/SpecR#324 — Intersects with the shared demo app.js view wiring and API_FEATURES capability map.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant DemoApp
  participant HeaderFooterEditor
  participant HeaderFooterAPI
  participant SpecAPI

  User->>DemoApp: Open library or settings view
  DemoApp->>HeaderFooterEditor: refresh()
  HeaderFooterEditor->>HeaderFooterAPI: Load scoped composition
  HeaderFooterAPI-->>HeaderFooterEditor: Composition envelope
  User->>HeaderFooterEditor: Edit and save profile
  HeaderFooterEditor->>HeaderFooterAPI: PUT composition
  HeaderFooterAPI-->>HeaderFooterEditor: Saved configuration
  User->>DemoApp: Download DOCX
  DemoApp->>SpecAPI: POST generate DOCX
  SpecAPI-->>DemoApp: DOCX Blob
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers editing, preview, warnings, and client/project scoping, but it does not implement the linked package/revision scope assignment. Add package/revision scope wiring and ensure the demo's effective-resolution flow covers all scope levels required by #477.
Docstring Coverage ⚠️ Warning Docstring coverage is 57.01% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changes stay within the demo app and support files, with no ADR, REST, or MCP surface modifications.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding header/footer editor and preview support in the web UI demo.

Comment @coderabbitai help to get the list of available commands.

@thewrz

thewrz commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/web_ui_demo/header-footer-markup.test.mjs`:
- Around line 44-52: Update the containment check in the “Library view:
header/footer panel sits inside .library-detail, after .library-tree” test so
detailEnd identifies the balanced closing tag of the .library-detail section
rather than the panel’s closing tag. Parse the markup or track nested section
tags, then compare panelStart against that actual container boundary while
preserving the tree ordering assertion.

In `@examples/web_ui_demo/js/app.js`:
- Around line 174-178: Update the resolution refresh flow, particularly
refreshResolutionPanel(), to prevent stale asynchronous responses from
repainting the shared container after the project changes. Track a request token
or current project ID when starting each refresh, and verify it still matches
the active project before applying the response; discard outdated results while
preserving the current refresh behavior.

In `@examples/web_ui_demo/js/header-footer-editor.js`:
- Around line 501-540: Update onSave to snapshot the current draft before
awaiting saveDraft, then save that snapshot and avoid overwriting newer edits
when applying the response. Also add the editor’s existing invalidation/discard
guard around save and delete completions so discarded instances do not repaint
or emit toasts; ensure the guard covers both success and error paths while
preserving normal active-editor behavior.

In `@examples/web_ui_demo/js/header-footer.js`:
- Around line 219-231: Update the project-switch flow, preferably in
switchProject() or loadActiveProjectWorkspace(), to invoke refreshProjectPanel()
after the active project changes and workspace reload completes. Ensure
project-header-footer-resolution is repainted for the newly active project
without altering refreshResolutionPanel() behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 71fabe96-0438-459f-afd7-7053ee192724

📥 Commits

Reviewing files that changed from the base of the PR and between d5f7797 and 8cd6fa2.

📒 Files selected for processing (27)
  • examples/web_ui_demo/api-header-footer.test.mjs
  • examples/web_ui_demo/app-header-footer.test.mjs
  • examples/web_ui_demo/css/header-footer.css
  • examples/web_ui_demo/download.test.mjs
  • examples/web_ui_demo/editor-header-footer.test.mjs
  • examples/web_ui_demo/header-footer-editor.test.mjs
  • examples/web_ui_demo/header-footer-fields.test.mjs
  • examples/web_ui_demo/header-footer-markup.test.mjs
  • examples/web_ui_demo/header-footer-preview.test.mjs
  • examples/web_ui_demo/header-footer-resolve.test.mjs
  • examples/web_ui_demo/header-footer.test.mjs
  • examples/web_ui_demo/index.html
  • examples/web_ui_demo/js/api.js
  • examples/web_ui_demo/js/app.js
  • examples/web_ui_demo/js/download.js
  • examples/web_ui_demo/js/editor.js
  • examples/web_ui_demo/js/features.js
  • examples/web_ui_demo/js/header-footer-editor.js
  • examples/web_ui_demo/js/header-footer-fields.mjs
  • examples/web_ui_demo/js/header-footer-preview-view.js
  • examples/web_ui_demo/js/header-footer-preview.mjs
  • examples/web_ui_demo/js/header-footer-resolve.mjs
  • examples/web_ui_demo/js/header-footer.js
  • examples/web_ui_demo/js/request-guard.mjs
  • examples/web_ui_demo/js/scoring.js
  • examples/web_ui_demo/request-guard.test.mjs
  • examples/web_ui_demo/scoring.test.mjs

Comment thread examples/web_ui_demo/header-footer-markup.test.mjs
Comment thread examples/web_ui_demo/js/app.js
Comment thread examples/web_ui_demo/js/header-footer-editor.js
Comment thread examples/web_ui_demo/js/header-footer.js
thewrz and others added 4 commits July 13, 2026 23:04
…er markup test

The containment check took the first `</section>` after the header/footer
panel as `.library-detail`'s end — but that is the panel's OWN closing tag
(the panel is itself a nested <section>), so `panelStart < detailEnd` was
trivially true and the test would still pass if the panel were moved out of
.library-detail. Add a balanced <section>…</section> scanner and use it to
find .library-detail's real close, so the assertion actually proves nesting.

Co-Authored-By: Claude <noreply@anthropic.com>
…ints

refreshResolutionPanel() captured the project id, awaited the resolved-layers
fetch, then unconditionally painted the shared projectResolutionContainer.
Overlapping refreshes (refreshProjectPanel, onSaved, the new project-switch
hook) meant an OLDER project's response resolving last could repaint the wrong
winning-scope chain. Add a monotonic request guard — the same createRequestGuard
primitive the editor uses for its own ctx.get() — so only the latest-issued
request paints; earlier ones no-op.

Co-Authored-By: Claude <noreply@anthropic.com>
refresh() was token-guarded, but onSave()/onDelete() unconditionally reassigned
draft/hasPersistedConfig/mode and repainted after their awaits. A save/delete
resolving after the editor was discarded (scope switched away) or after a newer
refresh() would repaint the shared container and toast for a scope it no longer
owns; a field edit made while the PUT was in flight was also clobbered by the
server's round-tripped response. Claim the requestGuard token at each mutation's
start and only apply completion state while it is still current (both success
and error paths), and keep an in-flight edit by only adopting the saved draft
when nothing changed under us.

Co-Authored-By: Claude <noreply@anthropic.com>
…witch

showView('settings') only refreshes the project-scope header/footer editor +
Effective Resolution on VIEW entry. Switching projects from the Settings
dropdown (project-select → switchProject → loadActiveProjectWorkspace) left the
previous project's editor/resolution on screen — and because the editor ctx
reads activeProjectId live, a Save on that stale editor would PUT into the newly
selected project. Repaint the panel from loadActiveProjectWorkspace (the common
sink for every active-project change); refresh() drops to a Save-less loading
state while it reloads the new project's config, closing that window.

Co-Authored-By: Claude <noreply@anthropic.com>
@thewrz

thewrz commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Codex (GPT-5.5, xhigh) — second adversarial reviewer (additional eyes). Ran read-only against origin/main. It independently surfaced the same four issues CodeRabbit did and explicitly found nothing beyond them ("I did not find additional issues beyond those race/containment problems"). All four are now fixed and their threads resolved:

  • [P1] app.js — project-switch doesn't refresh the project header/footer panel → fixed in 6b0cfc0. Codex added the sharpest framing: the editor ctx reads activeProjectId live, so a Save on the stale editor would PUT the old project's composition into the newly selected one. refresh() drops to a Save-less loading state during reload, closing that window.
  • [P1] header-footer-editor.js — save/delete completions unfenced → fixed in 1883175.
  • [P2] header-footer.js — refreshResolutionPanel() outside the stale-response guard → fixed in 09f2bd3.
  • [P2] header-footer-markup.test.mjs — Library containment test passes for the wrong reason → fixed in 83f3131.

No separate action items from Codex; it converged with CodeRabbit. Demo suite green (254/254) after the fixes.

@thewrz
thewrz marked this pull request as ready for review July 14, 2026 15:23
@thewrz
thewrz merged commit 312d034 into main Jul 14, 2026
21 of 22 checks passed
@thewrz
thewrz deleted the feat/issue-477 branch July 14, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(example): header/footer editor + preview in web_ui_demo

1 participant