feat(webapp): the wiki lane: findable suggestions, one review flow, robust pages, search (phase 6 of v4.3.0) #527

Merged
claude-bot merged 4 commits from feat/phase6-wiki into main 2026-09-06 05:12:19 +00:00
Contributor

Closes #375, closes #385, closes #395, closes #396. Phase 6 of the v4.3.0 build order (#514). Frontend only, four commits, no backend change, no migration.

#375 suggestion discovery. The SessionDetail link named in the issue had already been fixed by #417 (shipped as "#375, partial" in v4.2.3); it now has the regression test it never got. The wiki home's only route to the review queue lived in its phone-hidden rail; it now also sits beside "New article" in the main column, with the pending count. CampaignDetail's 105-line inline copy of the queue is gone (it omitted relationship suggestions, clamped every body, and vanished when the count was zero); in its place one always-visible row, "N suggested wiki updates" / "No pending suggestions" / "Couldn't load suggested wiki updates.", with Review going to WikiProposals as the single page. Both fetch sites stopped swallowing errors, and both counts now include relationship proposals.

#385 one review flow. The copy was rewritten from what the backend actually does: approve_draft writes current_body; a generated backstory reproduces the body plus a new section (an append in effect); stat blocks never touch the body; merge is an LLM merge, never a verbatim append. So "Approve" and "Review merge…" are separate buttons naming the destination before the click; draft kinds come from one map keyed on mode with the pre-#130 fallback, so a generated backstory stops calling itself an LLM rephrase and each kind's approve button names its own effect; the start-draft modal promises what the review page performs. All four queue actions disable while one is in flight, report failure on their row, and the product's last four alert() calls are gone; a direct approve leaves a receipt with a View article link.

#395 robust pages. The draft poll reports non-404 failures inline, keeps trying, gives up after 3 minutes (between the banner's "usually under 30 seconds" and the backend's 15-minute stale threshold) and offers Check again. Version-history, restore and load-more failures are inline beside the failing control instead of replacing the article or storyline. "Full regenerate" confirms, matching its siblings. One gap left deliberately: the generating banner is inside the non-convert_stats layout branch, so a stuck stat-conversion draft cannot reach Check again; that is #416's surface.

#396 search and pagination. The backend already has ?search= on the list endpoint (title, body, aliases, visible fields, tested), so the search is a pure frontend addition: a debounced WikiSearch box, one per page, in the wiki home's main column and the article page's rail. WikiArticle's rail moves onto the paginated fetch the wiki home uses, with "Load more (X of Y)", which also widens the entry set behind wiki links and the relationship picker past the old 200 cap.

vitest 616 passed (53 files, +45 on this lane), eslint clean apart from the pre-existing CampaignDetail warning, vite build green. Rebased over phases 4 and 5 with one test-file conflict resolved by keeping both sets.

🤖 Generated with Claude Code

Closes #375, closes #385, closes #395, closes #396. Phase 6 of the v4.3.0 build order (#514). Frontend only, four commits, no backend change, no migration. **#375 suggestion discovery.** The `SessionDetail` link named in the issue had already been fixed by #417 (shipped as "#375, partial" in v4.2.3); it now has the regression test it never got. The wiki home's only route to the review queue lived in its phone-hidden rail; it now also sits beside "New article" in the main column, with the pending count. `CampaignDetail`'s 105-line inline copy of the queue is gone (it omitted relationship suggestions, clamped every body, and vanished when the count was zero); in its place one always-visible row, "N suggested wiki updates" / "No pending suggestions" / "Couldn't load suggested wiki updates.", with Review going to `WikiProposals` as the single page. Both fetch sites stopped swallowing errors, and both counts now include relationship proposals. **#385 one review flow.** The copy was rewritten from what the backend actually does: `approve_draft` writes `current_body`; a generated backstory reproduces the body plus a new section (an append in effect); stat blocks never touch the body; merge is an LLM merge, never a verbatim append. So "Approve" and "Review merge…" are separate buttons naming the destination before the click; draft kinds come from one map keyed on `mode` with the pre-#130 fallback, so a generated backstory stops calling itself an LLM rephrase and each kind's approve button names its own effect; the start-draft modal promises what the review page performs. All four queue actions disable while one is in flight, report failure on their row, and the product's last four `alert()` calls are gone; a direct approve leaves a receipt with a View article link. **#395 robust pages.** The draft poll reports non-404 failures inline, keeps trying, gives up after 3 minutes (between the banner's "usually under 30 seconds" and the backend's 15-minute stale threshold) and offers Check again. Version-history, restore and load-more failures are inline beside the failing control instead of replacing the article or storyline. "Full regenerate" confirms, matching its siblings. One gap left deliberately: the generating banner is inside the non-`convert_stats` layout branch, so a stuck stat-conversion draft cannot reach Check again; that is #416's surface. **#396 search and pagination.** The backend already has `?search=` on the list endpoint (title, body, aliases, visible fields, tested), so the search is a pure frontend addition: a debounced `WikiSearch` box, one per page, in the wiki home's main column and the article page's rail. `WikiArticle`'s rail moves onto the paginated fetch the wiki home uses, with "Load more (X of Y)", which also widens the entry set behind wiki links and the relationship picker past the old 200 cap. vitest 616 passed (53 files, +45 on this lane), eslint clean apart from the pre-existing `CampaignDetail` warning, `vite build` green. Rebased over phases 4 and 5 with one test-file conflict resolved by keeping both sets. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Three routes lead to the suggestions the backend drafts from a session
summary, and each one was broken in a different way.

On the wiki's front page the only entry point lived inside the category
rail, which is `hidden md:flex` — there was no route to the queue at all
from a phone. It now sits beside "New article" in the main column, badge
and all, which every breakpoint renders.

The campaign page carried a second, ~110-line inline copy of the queue.
It rendered only entry proposals, so suggested relationships were
invisible from here; it truncated bodies to three lines, so approving
from this page meant approving text you had not read; and it was gated
on `count > 0`, so it vanished at precisely the moment a GM looking for
"where did my suggestions go" needed an answer. Deleted in favour of one
always-visible summary row that links to WikiProposals — the single page
that shows the whole queue, relationships included.

Both `fetchLoreProposals` call sites were `.catch(() => {})`, which
renders a failed read and an empty queue identically. They now say
"Couldn't load suggested wiki updates." The counts on both pages include
relationship proposals, since that is what "suggested wiki updates"
means in vocabulary.js.

`fetchRelationshipProposals` gains an `options` argument so the campaign
page can abort it with the rest of its loads.

The SessionDetail link this issue also named was already fixed in #417
(it shipped as "#375, partial"); a regression test pins the destination.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The suggestions queue had one "Approve" button doing two unrelated
things. With no `proposed_for_entry_id` it wrote the article into the
wiki; with one it started a merge draft and navigated away. The only
signal for which was about to happen was a small "Update" chip. Split
into "Approve" and "Review merge…", so the destination is named before
the click rather than discovered after it.

The copy on both sides of that click disagreed with itself and with the
backend:

- The queue said a suggestion would be "appended to" an article; the
  draft page then offered "Approve and replace article". Neither is what
  `approve_draft` does for a merge — the LLM merges the suggestion into
  the whole body and the GM approves the result. Both now say merge.
- Every draft that was not a merge or a convert_stats read "LLM rephrase
  of", so a generated backstory announced itself as a rephrasing. Draft
  kinds now come from a single map keyed on `mode` (with the
  `source_proposal_id` fallback the backend's generator also makes for
  pre-#130 merge drafts).
- "Approve and replace article" was offered for generate_statblock,
  which replaces nothing, and for expand_backstory, whose body comes
  back whole with a section added — an append in effect. Each kind now
  names its own effect, and WikiArticle's start-draft modal promises the
  same thing the review page performs.

None of the four queue actions had a busy state, so a double-click fired
the request twice; all four reported failure through `alert()`, the only
one in the product; and a direct approve made the row disappear with no
other feedback. Actions now disable while one is in flight, report
failure on the row that failed, and confirm an approve with a link to
the article it created.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Four ways the wiki lied about, or over-reported, a failure.

The draft review page polls while the LLM works, and its only error
branch was "not found" — everything else was swallowed. A 500, a
dropped connection, or a worker that died mid-generation therefore left
"Generating draft…" on screen for as long as the tab stayed open. The
poll now reports a failed check inline and keeps trying, gives up after
three minutes rather than pretending it is still watching, and offers
"Check again" next to #416's reset. Three minutes sits between the
banner's own "usually under 30 seconds" and the backend's 15-minute
stale-draft threshold, so a slow LLM call is never declared stuck.

`fetchLoreVersions` and `handleRestoreVersion` on WikiArticle, and
`loadMoreEntries` on CampaignStoryline, all reported through the
page-level `setError` — which unmounts the article (or the entire
campaign story) and renders one bare red string in its place, for a
failure in a panel the reader may not even have opened. Each now
reports beside the control that failed. The history panel also stops
claiming "No edit history yet" when what actually happened is that we
could not read it.

"Full regenerate" discards the campaign story and rewrites it from every
session, and was the only destructive action in this file family with no
confirm — "Discard draft", "Discard this suggestion" and "Restore to
version" all ask. It asks now. "Append chapter" still does not, because
it only adds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat(frontend): a wiki search box, and a sidebar that reaches the last article (#396)
All checks were successful
CI / Docker image build (pull_request) Successful in 29s
CI / Bot/backend version sync (pull_request) Successful in 31s
CI / Backend lint (ruff) (pull_request) Successful in 36s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m1s
CI / Bot tests and audit (pull_request) Successful in 2m3s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m5s
CI / Backend migration, tests, and audit (pull_request) Successful in 7m14s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 17m9s
e591fb7eb4
The wiki had no search. The only way to reach an article was to know
which of thirteen categories it had been filed under and scroll the
rail, which stops scaling around the point a campaign becomes worth
having a wiki for.

`WikiSearch` queries the backend rather than filtering what is already
on screen — the sidebar holds one page, so a client-side filter could
only find what was already visible, and `list_lore_entries`' `search`
already spans title, body, aliases and the viewer-visible structured
fields. No backend change was needed: `GET /campaigns/:id/lore?search=`
has existed since the endpoint did, with tests in test_campaigns.py, so
the `q` parameter the issue offered as a fallback would have been a
second name for it.

One search box per page, placed where that page navigates from: the
main column on the wiki home (reachable at every width, unlike the
`hidden md:flex` rail) and the rail on the article page, which has no
other navigation surface.

The article page's rail also called the unpaginated `fetchLoreEntries`,
which the backend caps at 200 — so a campaign with more entries had a
rail that simply ended, with nothing to say so. It now uses the same
`fetchLoreEntriesPage` + "Load more (X of Y)" the wiki home already had,
with the inline load failure from #395. That also widens the entry set
behind `[[wiki links]]` and the relationship picker, which were capped
at the same 200.

`fetchLoreEntriesPage` gains an `options` argument so the article
page's initial load can still abort with the rest of its Promise.all.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-09-06 04:55:17 +00:00
claude-bot deleted branch feat/phase6-wiki 2026-09-06 05:12:20 +00:00
Sign in to join this conversation.
No description provided.