[Frontend] Make the wiki proposal/draft review pages robust: busy states, inline errors, and a poll timeout #395

Closed
opened 2026-08-25 20:42:31 +00:00 by claude-bot · 3 comments
Contributor

Impact: MEDIUM

Found in the August 2026 session lifecycle review (#319).

What the user experiences

Several wiki pages fail in ways that destroy more than they should. A failed draft-generation poll on WikiDraftReview only handles the "not found" case — a 500 or a dropped connection is swallowed entirely, leaving the GM staring at "Generating draft…" forever with no error and no timeout. On WikiArticle, a failed version-history fetch or a failed version restore both call the page-level error setter, which replaces the entire rendered article with a bare red string — so a failed restore destroys the GM's view of the article they were trying to restore. CampaignStoryline's "Full regenerate" is destructive and has no confirmation dialog, unlike comparable destructive actions elsewhere in the same file (WikiArticle.jsx:1570, WikiProposals.jsx:234), and its "Append chapter"/"Full regenerate" actions tell the user to "refresh in a moment" with no polling, no progress, and no completion signal on one of the longest LLM jobs in the product.

Evidence

  • webapp/frontend/src/pages/WikiDraftReview.jsx:244-249 — the poll's catch only navigates on a "not found" error string; any other error (500, network drop) is swallowed and the page stays on "Generating draft…" indefinitely.
  • webapp/frontend/src/pages/WikiArticle.jsx:1512,1537fetchLoreVersions and handleRestoreVersion failures both call the page-level setError, replacing the whole article with a bare red string.
  • webapp/frontend/src/pages/CampaignStoryline.jsx:354,363 — "Append chapter"/"Full regenerate" show "Working…" then "Chapter append queued — refresh in a moment.", with no polling, progress, or completion signal.
  • webapp/frontend/src/pages/CampaignStoryline.jsx — "Full regenerate" is destructive with no confirm(), unlike WikiArticle.jsx:1570 and WikiProposals.jsx:234 in the same feature area.
  • webapp/frontend/src/pages/CampaignStoryline.jsx:179loadMoreEntries failure also calls page-level setError, replacing the entire rendered storyline.

Why it matters for a hosted product

A page-level error that nukes the whole article on a failed restore is the worst possible moment for that pattern — it takes away the one thing (the current article content) the GM needed to see to decide what to do next.

Proposed fix

Add a timeout and a generic-error branch to WikiDraftReview's poll catch, alongside its existing "not found" handling. Change fetchLoreVersions/handleRestoreVersion/loadMoreEntries failures to inline errors next to the failing control, not page-level setError. Add a confirm() to "Full regenerate", matching the pattern already used correctly elsewhere in the same file family. This is the audit's P25 (partial), P26, and P31.

Acceptance criteria

  • WikiDraftReview's generation poll times out and shows an error state for non-"not found" failures, instead of polling forever.
  • fetchLoreVersions and handleRestoreVersion failures show an inline error without replacing the rendered article.
  • loadMoreEntries failure shows an inline error without replacing the rendered storyline.
  • "Full regenerate" on CampaignStoryline requires confirmation before running, matching the confirm pattern used elsewhere in the wiki.
**Impact: MEDIUM** Found in the August 2026 session lifecycle review (#319). ## What the user experiences Several wiki pages fail in ways that destroy more than they should. A failed draft-generation poll on `WikiDraftReview` only handles the "not found" case — a 500 or a dropped connection is swallowed entirely, leaving the GM staring at "Generating draft…" forever with no error and no timeout. On `WikiArticle`, a failed version-history fetch or a failed version restore both call the page-level error setter, which replaces the *entire rendered article* with a bare red string — so a failed restore destroys the GM's view of the article they were trying to restore. `CampaignStoryline`'s "Full regenerate" is destructive and has no confirmation dialog, unlike comparable destructive actions elsewhere in the same file (`WikiArticle.jsx:1570`, `WikiProposals.jsx:234`), and its "Append chapter"/"Full regenerate" actions tell the user to "refresh in a moment" with no polling, no progress, and no completion signal on one of the longest LLM jobs in the product. ## Evidence - `webapp/frontend/src/pages/WikiDraftReview.jsx:244-249` — the poll's catch only navigates on a "not found" error string; any other error (500, network drop) is swallowed and the page stays on "Generating draft…" indefinitely. - `webapp/frontend/src/pages/WikiArticle.jsx:1512,1537` — `fetchLoreVersions` and `handleRestoreVersion` failures both call the page-level `setError`, replacing the whole article with a bare red string. - `webapp/frontend/src/pages/CampaignStoryline.jsx:354,363` — "Append chapter"/"Full regenerate" show "Working…" then "Chapter append queued — refresh in a moment.", with no polling, progress, or completion signal. - `webapp/frontend/src/pages/CampaignStoryline.jsx` — "Full regenerate" is destructive with no `confirm()`, unlike `WikiArticle.jsx:1570` and `WikiProposals.jsx:234` in the same feature area. - `webapp/frontend/src/pages/CampaignStoryline.jsx:179` — `loadMoreEntries` failure also calls page-level `setError`, replacing the entire rendered storyline. ## Why it matters for a hosted product A page-level error that nukes the whole article on a *failed restore* is the worst possible moment for that pattern — it takes away the one thing (the current article content) the GM needed to see to decide what to do next. ## Proposed fix Add a timeout and a generic-error branch to `WikiDraftReview`'s poll catch, alongside its existing "not found" handling. Change `fetchLoreVersions`/`handleRestoreVersion`/`loadMoreEntries` failures to inline errors next to the failing control, not page-level `setError`. Add a `confirm()` to "Full regenerate", matching the pattern already used correctly elsewhere in the same file family. This is the audit's P25 (partial), P26, and P31. ## Acceptance criteria - [ ] `WikiDraftReview`'s generation poll times out and shows an error state for non-"not found" failures, instead of polling forever. - [ ] `fetchLoreVersions` and `handleRestoreVersion` failures show an inline error without replacing the rendered article. - [ ] `loadMoreEntries` failure shows an inline error without replacing the rendered storyline. - [ ] "Full regenerate" on `CampaignStoryline` requires confirmation before running, matching the confirm pattern used elsewhere in the wiki.
Author
Contributor

Picking this up as v4.3.0 phase 6 (#514). The draft-generation poll gets a timeout and a generic-error branch; version-history, restore and load-more failures become inline errors beside the failing control instead of replacing the whole article or storyline; "Full regenerate" gets the confirm its siblings already have.

Picking this up as v4.3.0 phase 6 (#514). The draft-generation poll gets a timeout and a generic-error branch; version-history, restore and load-more failures become inline errors beside the failing control instead of replacing the whole article or storyline; "Full regenerate" gets the confirm its siblings already have.
Author
Contributor

Done in PR #527 (auto-merging on green); ships with v4.3.0.

The draft poll's only error branch was "not found", so a 500 or a dropped connection left "Generating draft…" up for as long as the tab stayed open. It now reports a failed check inline and keeps trying, gives up after 3 minutes, and offers Check again beside #416's reset. Three minutes sits between the banner's own "usually under 30 seconds" and the backend's 15-minute DRAFT_STALE_AFTER, so a slow LLM call is never declared stuck.

fetchLoreVersions, handleRestoreVersion and loadMoreEntries all reported through the page-level setError, which unmounts the article (or the whole campaign story) and replaces it with one red string. Each is inline now, beside the control that failed; the history panel also stops asserting "No edit history yet" when what happened is that it could not read it. "Full regenerate" asks first, matching the three other destructive actions in this file family; "Append chapter" still does not, because it only adds.

One gap left open deliberately: the generating banner (and hence Check again and reset) is inside the non-convert_stats layout branch, so a stuck stat-conversion draft still cannot reach either control; the top status banner does now report the stall in every mode. That is #416's surface.

9 new tests (the timeout test fakes only Date, so the interval and the test library's waiting stay real). No backend change.

Done in PR #527 (auto-merging on green); ships with v4.3.0. The draft poll's only error branch was "not found", so a 500 or a dropped connection left "Generating draft…" up for as long as the tab stayed open. It now reports a failed check inline and keeps trying, gives up after 3 minutes, and offers **Check again** beside #416's reset. Three minutes sits between the banner's own "usually under 30 seconds" and the backend's 15-minute `DRAFT_STALE_AFTER`, so a slow LLM call is never declared stuck. `fetchLoreVersions`, `handleRestoreVersion` and `loadMoreEntries` all reported through the page-level `setError`, which unmounts the article (or the whole campaign story) and replaces it with one red string. Each is inline now, beside the control that failed; the history panel also stops asserting "No edit history yet" when what happened is that it could not read it. "Full regenerate" asks first, matching the three other destructive actions in this file family; "Append chapter" still does not, because it only adds. One gap left open deliberately: the generating banner (and hence Check again and reset) is inside the non-`convert_stats` layout branch, so a stuck stat-conversion draft still cannot reach either control; the top status banner does now report the stall in every mode. That is #416's surface. 9 new tests (the timeout test fakes only `Date`, so the interval and the test library's waiting stay real). No backend change.
Author
Contributor

For the record: the "poll gives up" test flaked again on the merge run for #538 (2026-09-06), despite PR #529's 20 s wait. The cause was an ordering race, not a timing margin: the test jumped the clock as soon as the page had rendered, but the poll's deadline is captured in a passive effect that can flush after that on a loaded runner, so the deadline itself moved and the banner could never appear. PR #541 waits for the first real poll tick before moving the clock. Test-only change; the behaviour this issue shipped is unchanged.

For the record: the "poll gives up" test flaked again on the merge run for #538 (2026-09-06), despite PR #529's 20 s wait. The cause was an ordering race, not a timing margin: the test jumped the clock as soon as the page had rendered, but the poll's deadline is captured in a passive effect that can flush after that on a loaded runner, so the deadline itself moved and the banner could never appear. PR #541 waits for the first real poll tick before moving the clock. Test-only change; the behaviour this issue shipped is unchanged.
Sign in to join this conversation.
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
rbrooks/Quest-Board#395
No description provided.