[Frontend] Stop swallowing errors: ~18 places where a failed request looks identical to success or nothing happened #377

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

Impact: HIGH

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

What the user experiences

Across the app, a long list of user actions and background fetches fail silently: nothing changes, no toast appears, no inline error shows, and the UI simply looks the same as it would if the action had never been attempted or had succeeded. A vote that didn't stick looks like a vote. A shelf card position that didn't save looks saved until the next reload. A live recording that lost its network connection is displayed as "not recording" — actively misleading mid-session, not just uninformative.

Evidence

Each of these swallows an error with no user-visible signal:

  • webapp/frontend/src/components/VotingGrid.jsx:159-161 — a failed vote console.errors only; the cell silently reverts.
  • webapp/frontend/src/pages/SessionShelf.jsx:969updateShelfCard(..., { position }) is .catch(() => {}); a drag/resize appears to work and silently doesn't persist.
  • webapp/frontend/src/pages/CampaignDetail.jsx:467-469 and webapp/frontend/src/pages/CampaignStoryline.jsx:160-161fetchLoreProposals/fetchRelationshipProposals failures are swallowed; the proposals block and its badge simply don't render, indistinguishable from "no proposals" (see also the lore-proposal discovery issue in this milestone, which owns the destination/duplication fixes for this same surface).
  • webapp/frontend/src/pages/CampaignPlanning.jsx:848setPrefetchStatus("error") is stored in state and never rendered anywhere.
  • webapp/frontend/src/components/TitleSuggestions.jsx:49 — a suggestion-fetch failure is swallowed; the helper just doesn't appear.
  • webapp/frontend/src/components/ColdOpenPanel.jsx:59 — a cold-open fetch failure is swallowed by design; the "Previously on" panel silently doesn't appear.
  • webapp/frontend/src/pages/SessionDetail.jsx:518-532 — attendance-proposal fetch failure swallowed as "best-effort".
  • webapp/frontend/src/pages/SessionDetail.jsx:535-553 — loot/XP fetch failure swallowed.
  • webapp/frontend/src/pages/RecordingDashboard.jsx:100-102 — a failed fetchRecordingLive sets {active:false}, i.e. a network failure is displayed as "not recording" during a live session.
  • webapp/frontend/src/pages/SessionTable.jsx:40-44 — poll failures after the initial load are silent by design; a player's table view can freeze on stale reveals with no indication.
  • webapp/frontend/src/pages/WikiArticle.jsx:2007-2016 and webapp/frontend/src/pages/WikiNewEntry.jsx:336-340 — "Check for link suggestions" is synchronous; when it returns [] there is literally zero visible change on click, so a user cannot tell whether it ran.
  • webapp/frontend/src/pages/WikiArticle.jsx:1376-1378fetchGameSystemSchema failure silently returns null, degrading stat labels to raw keys with no explanation of why.

Why it matters for a hosted product

Each of these is individually small — a one-to-five-line fix — but collectively they mean a user genuinely cannot trust that the app told them the truth about whether their action worked. RecordingDashboard.jsx:100-102 is the most severe: it turns a transient network blip into an actively false "not recording" status in the middle of a live session, which is exactly when a GM needs the status to be trustworthy.

Proposed fix

Apply the inline-error pattern already used correctly elsewhere in the codebase (e.g. Dashboard.jsx's import success/error box at :358-368) to each site above: a toast, an inline error message, or a visibly distinct state, rather than a silent catch. RecordingDashboard.jsx:100-102 specifically must not collapse a network failure into {active:false} — show "connection lost, retrying" instead. This is the audit's P5 plus the additional sites the audit's wiki appendix (§9.4) identified.

Acceptance criteria

  • Each of the twelve sites listed above shows a visible signal (inline error, toast, or distinct UI state) on failure, rather than looking identical to "nothing happened" or "succeeded".
  • RecordingDashboard.jsx distinguishes a network failure from a genuine "not recording" state during a live session.
  • VotingGrid and SessionShelf position-save both surface a retry affordance on failure, not just an error message.
  • CampaignPlanning's prefetch error (:848) is rendered somewhere in the UI.
  • The "Check for link suggestions" action gives visible feedback (even "No suggestions found") when it returns no results.
**Impact: HIGH** Found in the August 2026 session lifecycle review (#319). ## What the user experiences Across the app, a long list of user actions and background fetches fail silently: nothing changes, no toast appears, no inline error shows, and the UI simply looks the same as it would if the action had never been attempted or had succeeded. A vote that didn't stick looks like a vote. A shelf card position that didn't save looks saved until the next reload. A live recording that lost its network connection is displayed as "not recording" — actively misleading mid-session, not just uninformative. ## Evidence Each of these swallows an error with no user-visible signal: - `webapp/frontend/src/components/VotingGrid.jsx:159-161` — a failed vote `console.error`s only; the cell silently reverts. - `webapp/frontend/src/pages/SessionShelf.jsx:969` — `updateShelfCard(..., { position })` is `.catch(() => {})`; a drag/resize appears to work and silently doesn't persist. - `webapp/frontend/src/pages/CampaignDetail.jsx:467-469` and `webapp/frontend/src/pages/CampaignStoryline.jsx:160-161` — `fetchLoreProposals`/`fetchRelationshipProposals` failures are swallowed; the proposals block and its badge simply don't render, indistinguishable from "no proposals" (see also the lore-proposal discovery issue in this milestone, which owns the destination/duplication fixes for this same surface). - `webapp/frontend/src/pages/CampaignPlanning.jsx:848` — `setPrefetchStatus("error")` is stored in state and never rendered anywhere. - `webapp/frontend/src/components/TitleSuggestions.jsx:49` — a suggestion-fetch failure is swallowed; the helper just doesn't appear. - `webapp/frontend/src/components/ColdOpenPanel.jsx:59` — a cold-open fetch failure is swallowed by design; the "Previously on" panel silently doesn't appear. - `webapp/frontend/src/pages/SessionDetail.jsx:518-532` — attendance-proposal fetch failure swallowed as "best-effort". - `webapp/frontend/src/pages/SessionDetail.jsx:535-553` — loot/XP fetch failure swallowed. - `webapp/frontend/src/pages/RecordingDashboard.jsx:100-102` — a failed `fetchRecordingLive` sets `{active:false}`, i.e. a network failure is displayed as "not recording" **during a live session**. - `webapp/frontend/src/pages/SessionTable.jsx:40-44` — poll failures after the initial load are silent by design; a player's table view can freeze on stale reveals with no indication. - `webapp/frontend/src/pages/WikiArticle.jsx:2007-2016` and `webapp/frontend/src/pages/WikiNewEntry.jsx:336-340` — "Check for link suggestions" is synchronous; when it returns `[]` there is literally zero visible change on click, so a user cannot tell whether it ran. - `webapp/frontend/src/pages/WikiArticle.jsx:1376-1378` — `fetchGameSystemSchema` failure silently returns `null`, degrading stat labels to raw keys with no explanation of why. ## Why it matters for a hosted product Each of these is individually small — a one-to-five-line fix — but collectively they mean a user genuinely cannot trust that the app told them the truth about whether their action worked. `RecordingDashboard.jsx:100-102` is the most severe: it turns a transient network blip into an actively false "not recording" status in the middle of a live session, which is exactly when a GM needs the status to be trustworthy. ## Proposed fix Apply the inline-error pattern already used correctly elsewhere in the codebase (e.g. `Dashboard.jsx`'s import success/error box at `:358-368`) to each site above: a toast, an inline error message, or a visibly distinct state, rather than a silent catch. `RecordingDashboard.jsx:100-102` specifically must not collapse a network failure into `{active:false}` — show "connection lost, retrying" instead. This is the audit's P5 plus the additional sites the audit's wiki appendix (§9.4) identified. ## Acceptance criteria - [ ] Each of the twelve sites listed above shows a visible signal (inline error, toast, or distinct UI state) on failure, rather than looking identical to "nothing happened" or "succeeded". - [ ] `RecordingDashboard.jsx` distinguishes a network failure from a genuine "not recording" state during a live session. - [ ] `VotingGrid` and `SessionShelf` position-save both surface a retry affordance on failure, not just an error message. - [ ] `CampaignPlanning`'s prefetch error (`:848`) is rendered somewhere in the UI. - [ ] The "Check for link suggestions" action gives visible feedback (even "No suggestions found") when it returns no results.
Author
Contributor

Picking this up now that phases 1–6 of v4.3.0 (#514) are on main, since it touches twelve files across every lane and is cheapest once they have settled. One small reusable inline-error component used at every site; the recording page gets a real state (connection lost, retrying, last known state kept) rather than a message, because a network blip reading as "not recording" mid-session is the worst of the twelve; the two proposal-fetch sites were already fixed by #375 and will be verified rather than redone.

Picking this up now that phases 1–6 of v4.3.0 (#514) are on main, since it touches twelve files across every lane and is cheapest once they have settled. One small reusable inline-error component used at every site; the recording page gets a real state (connection lost, retrying, last known state kept) rather than a message, because a network blip reading as "not recording" mid-session is the worst of the twelve; the two proposal-fetch sites were already fixed by #375 and will be verified rather than redone.
Author
Contributor

Done in the #377 PR (auto-merging on green); ships with v4.3.0. Frontend only, ten commits.

Every site in the audit now shows a visible signal on failure. Site 3 (fetchLoreProposals / fetchRelationshipProposals on CampaignDetail and CampaignStoryline) needed nothing: #375 had already fixed both with the shared "Couldn't load suggested wiki updates." state; verified on main, nothing added.

One small reusable InlineError (components/InlineError.jsx: message, optional retry, optional boxed look) carries eleven of them, and CampaignPlanning's private copy, which was the prototype, was folded into it rather than left as a second implementation.

The one that got a state machine is the recording page. fetchRecordingLive rejecting used to set {active: false}, which is the same value the server sends when nothing is recording, so a dropped connection mid-session redrew the page as "No active recording", idle, with a Start button, while the bot was still capturing in Discord. live now only ever holds something the server actually said, a separate connection state says whether we are still hearing from it, and a failed read raises "Connection lost, retrying… Showing the last reading, from Xs ago." over the last known state instead of replacing it. Any successful read or SSE event clears it; where nothing has ever been read the page says "Unknown" rather than picking the answer that invites a second recording of the same evening. The players' table view got the same indicator for its own polling.

Retry affordances: the vote grid re-sends the exact vote that failed; the shelf's position save re-reads the card's current position, because by the time anyone clicks Try again the card has usually been nudged again. CampaignPlanning's prefetch error now says what it means (name suggestions were not pre-generated; the tools still work, the first model call may be slower). "Check for link suggestions" is computed locally, so its fix is empty-result feedback: "No link suggestions found." A genuine failure to load the game-system schema is reported in the stat block and distinguished from a 404 that legitimately means "this system has no schema".

Two notes where the audit and the code differed. The vote grid never "silently reverted" a cell: it is not optimistic, so a rejected vote produced no DOM change at all, which is why it looked like a misclick. And ColdOpenPanel is still mounted only from the GM's shelf, not for players (#388 changes that); the copy is quiet and does not leak the backend's message either way.

Tests: 696 passing across 59 files (was 667/57). Every site has a test that the failure renders its signal, every retry has a test that it re-calls the API, and the recording page has tests that a failed poll keeps the last known active state, says how stale the reading is, clears on the next success, and says "Unknown" when never answered.

Done in the #377 PR (auto-merging on green); ships with v4.3.0. Frontend only, ten commits. Every site in the audit now shows a visible signal on failure. Site 3 (`fetchLoreProposals` / `fetchRelationshipProposals` on CampaignDetail and CampaignStoryline) needed nothing: #375 had already fixed both with the shared "Couldn't load suggested wiki updates." state; verified on main, nothing added. One small reusable `InlineError` (`components/InlineError.jsx`: message, optional retry, optional boxed look) carries eleven of them, and `CampaignPlanning`'s private copy, which was the prototype, was folded into it rather than left as a second implementation. The one that got a state machine is the recording page. `fetchRecordingLive` rejecting used to set `{active: false}`, which is *the same value* the server sends when nothing is recording, so a dropped connection mid-session redrew the page as "No active recording", idle, with a Start button, while the bot was still capturing in Discord. `live` now only ever holds something the server actually said, a separate `connection` state says whether we are still hearing from it, and a failed read raises "Connection lost, retrying… Showing the last reading, from Xs ago." over the last known state instead of replacing it. Any successful read or SSE event clears it; where nothing has ever been read the page says "Unknown" rather than picking the answer that invites a second recording of the same evening. The players' table view got the same indicator for its own polling. Retry affordances: the vote grid re-sends the exact vote that failed; the shelf's position save re-reads the card's *current* position, because by the time anyone clicks Try again the card has usually been nudged again. `CampaignPlanning`'s prefetch error now says what it means (name suggestions were not pre-generated; the tools still work, the first model call may be slower). "Check for link suggestions" is computed locally, so its fix is empty-result feedback: "No link suggestions found." A genuine failure to load the game-system schema is reported in the stat block and distinguished from a 404 that legitimately means "this system has no schema". Two notes where the audit and the code differed. The vote grid never "silently reverted" a cell: it is not optimistic, so a rejected vote produced no DOM change at all, which is why it looked like a misclick. And `ColdOpenPanel` is still mounted only from the GM's shelf, not for players (#388 changes that); the copy is quiet and does not leak the backend's message either way. Tests: 696 passing across 59 files (was 667/57). Every site has a test that the failure renders its signal, every retry has a test that it re-calls the API, and the recording page has tests that a failed poll keeps the last known active state, says how stale the reading is, clears on the next success, and says "Unknown" when never answered.
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#377
No description provided.