• v4.3.0 9dd77986b5

    v4.3.0
    All checks were successful
    CI / Bot/backend version sync (push) Successful in 42s
    Release / Create Forgejo release (push) Successful in 12s
    CI / Backend lint (ruff) (push) Successful in 55s
    CI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m29s
    CI / Frontend tests, audit, and build (push) Successful in 2m16s
    CI / Bot tests and audit (push) Successful in 2m36s
    CI / Docker image build (push) Successful in 4m13s
    Release / Build and push versioned images (push) Successful in 3m53s
    CI / Backend migration, tests, and audit (push) Successful in 10m13s
    CI / Synthetic session harness (no GPU, no LLM) (push) Successful in 14m47s
    Stable

    claude-bot released this 2026-09-06 09:51:10 +00:00 | 113 commits to main since this release

    Product Surface: Navigation, Attention & Feedback. The features Quest Board already had are now findable, acknowledged and self-diagnosing: a navigation bar on every page, one name for each thing, a "Needs you" list on the dashboard, RSVP on the web, a session page that shows every post-session task, a Prep workspace with a front door and a sheet that looks forward, a wiki you can search and review from one place, a Discord bot that answers back and explains itself, and an invite link you can paste. Structural fixes, not a redesign; that is v4.4.0.

    Three migrations, applied in this order by the migrate container: f0ab1c2d3e4f (a delivery-status column on campaigns, #369), fa0b1c2d3e4f (an RSVP status on session attendance, #370), 0b1c2d3e4f5a (post-session task status on sessions, #380). All are additive; nothing is backfilled. Head after upgrade: 0b1c2d3e4f5a.

    Bot API contract v1, unchanged. Everything new on /api/bot/* is additive: POST /bot/campaigns/{id}/delivery-status, GET /bot/guilds/{id}/campaigns, GET /bot/guilds/{id}/sessions/recordable, a state field on link-status, a reason field on /ask, and a link field in every notification payload. An older bot works against this backend; an older backend leaves a newer bot's new features quietly off. Deploy both images together as usual. BOT_EXPECTED_APP_VERSION is bumped so the bot's startup log matches.

    One new public route on the backend: GET /api/invites/{code} resolves an invite code to a campaign preview so /join/<code> links work before sign-in. The code is the secret; the route is rate-limited and a wrong code is an ordinary 404. Nothing else on /api is unauthenticated.

    Two HTML pages served by the backend (the account-linking outcomes) carry a slightly relaxed Content-Security-Policy that allows inline styles for text/html responses only; JSON responses keep the strict policy.

    No new settings. Nothing to add to .env. Operator notes carried from the patch releases: the Message Content toggle in the Discord Developer Portal can be turned off; if a campaign's notification channel is misconfigured the campaign page now says so, and the per-field "Test" buttons on the Edit form prove a fix. The old /sessions/<id>/recording address redirects to the session page, which is now the one live-session screen.

    Added

    • [webapp] You can now RSVP to a session in Quest Board (#370). Saying whether
      you were coming used to mean reacting or to a message in Discord — so if
      your group does not live in Discord, or you never linked your account, you
      could not answer at all, and nobody could see who had. A confirmed or
      tentative session now has an "Are you in?" control at the top of its page with
      Yes, Maybe and No, and the whole party can see the roster of who has answered
      and who is still to. It is the same answer either way: a reaction in Discord
      shows up in the roster, and an answer given here shows up for everyone. Your
      GM's after-the-fact attendance list is untouched — saying you are coming is not
      the same as having been there.

    • [webapp] A "Needs you" list on the dashboard (#368). The dashboard showed
      your campaigns and nothing else, so the only way to find out that a vote was
      waiting on you, or that a session next week still needed your RSVP, or that a
      transcript had been sitting ready for you to approve, was to open each
      campaign and look. Anything actually waiting on you now appears at the top of
      the dashboard — open votes, missing RSVPs, sessions in progress, and for GMs
      transcripts to review and suggested wiki updates — and every line takes you
      straight there. When nothing is pending the section is simply not there. The
      "Next session in 3d 4h" line on each campaign is now a link to that session.

    • [webapp] A "Post-session processing" checklist on every session (#380).
      Finishing a recording sets six things going: the transcript and summary, the
      suggested wiki updates, the campaign storyline, the session's journal entry,
      the next session's title ideas, and the recap email to the party. Only the
      first two ever said anything, so if the storyline never rebuilt or the recap
      email was refused by the mail server, nothing anywhere told you — it looked
      exactly like a job that had not got round to it yet. All six now appear as a
      checklist on the session page, and "failed" reads differently from "hasn't
      run" and from "deliberately skipped" (journal entries turned off, campaign
      archived, storyline written by hand). The recap email says whether it is
      queued, sent, or failed. Includes a migration (0b1c2d3e4f5a), which
      adds one column and backfills nothing — sessions processed before this
      release read as "no record", because that is the truth about them.

    • [webapp] A navigation bar that is the same on every page (#378). Until now
      the only thing every screen shared was a corner strip with your name, Profile
      and Sign out; getting from a session to the wiki meant walking back up the
      breadcrumbs one page at a time. There is now a single bar across the top of
      the whole app: the Quest Board wordmark always goes home, a switcher jumps
      straight to any of your other campaigns, and while you are inside a campaign —
      including on a session, its recording page or its shelf — the bar carries that
      campaign's Sessions, Prep, Wiki and Highlights. The two wiki links on the
      campaign page also stop opening a second browser tab, so the back button
      works through the wiki again.

    • [bot] /help, a welcome when the bot joins a server, and a presence that
      points at both
      (#373). The bot arrived in a server and said nothing, ever:
      there was no command list, no message on join, and nothing under its name in
      the member list, so the only way to find out that /next existed was to be
      told by somebody who already knew. /help now lists every command with one
      line each; joining a server posts the three setup steps — link a campaign to
      this server (quoting the server's own ID, ready to paste), everyone runs
      /link, then try /help — and the member list reads "Watching Quest Board ·
      /help". The welcome also warns that Discord takes up to an hour to roll
      out a new bot's slash commands, because the first impression of a working
      install was otherwise a server where /help did not exist.

    • [webapp] The wiki has a search box (#396). Until now the only way to
      reach an article was to know which of thirteen categories it had been filed
      under and scroll the list — fine for a campaign with twenty articles, useless
      for one that has been recorded for a year. There is now a search box on the
      wiki's front page and in the article page's category rail. It searches every
      article in the campaign rather than just the ones already on screen, and it
      looks at each article's text and its alternative names as well as its title,
      so "the innkeeper with the limp" finds the innkeeper even when nobody at the
      table remembers his name.

    • [webapp] An invite link you can actually send someone (#387). Inviting a
      player used to mean finding a 12-pixel "▼ Show invite code" disclosure under
      the Archive and Delete buttons, copying eleven characters out of it, and
      hoping the recipient worked out that they had to create an account, find the
      Join button on their own dashboard, and paste it in. There is now an Invite
      players
      control in the member list, which gives you a link — paste it into
      your group chat and whoever opens it sees which campaign they have been
      invited to and who invited them, signs in if they need to, comes straight back
      to the invite, and joins. It works for somebody who has never heard of Quest
      Board. The code is still there underneath for when a link will not survive the
      trip, and the "+ Add" member search now says so too, instead of ending at "No
      users found." — which it always did for the one person you were most likely to
      be looking for.

    • [webapp] A first-run checklist on a new campaign (#387). A campaign you
      had just created looked exactly like one you had finished setting up: an Edit
      form, a member list of one, and an empty session list. Four things actually
      need doing — set the timezone, invite the players, connect Discord, schedule
      the first session — and each was discoverable on its own but not as a set.
      They now appear as a short card at the top of the campaign page, each with a
      button that takes you to the control that does it rather than near it. Items
      tick themselves off from the campaign's own state, so a GM who did all four
      last year never sees it, and Dismiss makes it go away for good on that
      campaign.

    • [webapp] Guides for GMs and players, one click from where you are (#390).
      Everything written down about Quest Board was for whoever installs it. A GM
      running their first campaign, or a player handed an invite link, had the
      interface and nothing else. There are now two guides — "Running your first
      campaign" and "Joining a campaign" — that live in the app, linked from the
      bottom of your dashboard, from the invite page and from the sign-in page. They
      are readable without an account, on purpose: the reader who needs "Joining a
      campaign" most has not signed up yet. The sign-in page also stops describing
      the product as scheduling, which it has not only been for a long time, and its
      button says "Sign in" rather than "Sign in with SSO".

    • [webapp] Players get the "Previously on" recap too (#388). A short recap of
      last session, and the quests still open, has been waiting at the top of the
      GM's shelf since v4.1 — where nobody but the GM could ever see it. The same
      strip now sits at the top of a session's page and of "At the table", for
      everyone in the campaign, while the session is upcoming or in progress. It is
      the same recap from the same place, narrowed rather than rewritten: players
      see the open quests your GM has marked visible to the party, and not the ones
      they haven't. You can fold it away, and it stays folded for that session on
      that device, but it comes back with one click — it is not a dismissal you can
      make by accident and never undo.

    • [webapp] Three things you could do, but had no way to undo or reach
      (#389). Showing a card to the table was one-way: once revealed it stayed on
      the players' table view for the rest of the session, however plainly you had
      meant to reveal a different card. The shelf now lists what is currently up,
      with an "Un-reveal" on each — and the players' view drops it on its next
      refresh, exactly as it picks up a reveal. XP was the same shape of problem
      from the other side: the ledger showed totals only, so a mistyped award could
      be made and never unmade. Every award is now listed, party and per-member,
      each with a delete that names the award it is about to remove. And "Add to
      Calendar" offered Google and nothing else, which is a dead end if you keep
      your calendar in Apple Calendar, Outlook or Thunderbird; there is now a
      "Download .ics" beside it, offered wherever the Google link is.

    Changed

    • [webapp] One screen for a live session (#386). Running a session meant
      keeping two pages open that described the same recording and disagreed about
      it. The session page had a "Recording & Transcript" section; a separate
      Recording page had the live picture — the clock, who the bot could hear, who
      was in the voice channel and silent, and Start and Stop — plus a second copy
      of the summary you could not edit and a second attendance list whose toggle
      said "✓ Here" where the session page said "Present". Worse, the link to that
      page only appeared once a session had a confirmed time, so the one moment you
      most want to check the bot can hear the table — before committing to a date —
      was the one moment the controls were out of reach. All of it is now on the
      session page: a Recording panel with the live clock, per-speaker capture,
      who is present but not captured yet, Start and Stop, and the "connection
      lost, showing the last reading from … ago" honesty added in #377. It is there
      from the moment a session is proposed, and the shelf — the screen a GM
      actually has open during play — now has a Recording link straight to it. The
      summary is editable in one place and read-only in none, attendance says
      "Present" or "Absent" wherever you meet it, and the roster is available from
      the moment a session is under way rather than only after it is over. The old
      /sessions/…/recording address still works: it takes you to the panel, so
      bookmarks and older Discord links are unaffected.

    • [webapp] One name per thing, across the whole app (#379). The same concept
      answered to a different name on almost every screen, which made two screens
      about the same thing look like two different features. The GM prep workspace
      was "GM Workbench" in its own heading, "Session Prep" in the palette and
      "Session Prep Sheet" in the panel; it is Prep everywhere now, and its
      output is "prep notes". The wiki landing page said "Wiki" in the breadcrumb
      and "Campaign Story" in its heading; it says Wiki. Plot threads were
      "Thread Tracker", "loose ends" and "open loops"; they are Quests, and the
      shelf's cold open lists "Open quests". The Quote Board is Highlights. A
      suggested wiki entry was "AI Proposals", "Entry Proposals", "Bot Proposals" or
      "lore proposals" depending where you stood — and "Bot" was wrong besides,
      since the backend worker writes them, not the Discord bot; they are
      suggested wiki updates. The generation history rail and its filter now
      name each tool ("Session Prep", "NPC Voice Cues") instead of printing the
      backend's session_prep and npc_voice at you. The link from a session to
      its recording page says "Recording", not "Dashboard". Nothing moved: every URL
      is unchanged, so bookmarks and Discord links still work. A character's
      timeline also stops rendering twice on a wiki article — the full section
      stays, the cramped duplicate in the infobox is gone.

    • [webapp] "Approve" on a suggested wiki update stops meaning two different
      things
      (#385). One button either wrote the article straight into the wiki
      or carried you off to a merge-review page you had not asked for, depending on
      whether the suggestion changed an article you already had — a difference
      signalled only by a small "Update" chip. A suggestion that creates a new
      article now says Approve; one that changes an existing article says
      Review merge…, so the destination is on the button. The words either side
      of that click have been made true as well: the queue promised the suggestion
      would be "appended" and the review page then offered to "replace the
      article"; every draft that was not a merge or a stat conversion called itself
      an "LLM rephrase", generated backstories included; and the approve button
      read "Approve and replace article" even for the two kinds of draft that
      replace nothing at all. Each kind is now named for what it is and its button
      for what it does — "Generated backstory for…" approves as Add to article,
      a stat block approves as Approve stat block and says the article's text is
      left alone. Alongside that: all four actions in the queue now show they are
      working, so a second click cannot approve or discard the same thing twice;
      when one fails it says so on the suggestion that failed, instead of in a
      browser alert box — the last one in the product; and approving an article
      confirms it and offers a link to the article you just made, rather than
      making the row vanish and leaving you to guess.

    Fixed

    • [webapp] Pages that are not for you now say so, instead of vanishing or
      bouncing you
      (#382). A player who followed a Prep link a GM had shared was
      redirected to the campaign page with no message, which reads as a broken link
      rather than as "Prep is the GM's workspace" — it now says which it is, and
      offers the way back. Four wiki pages ended a player's navigation with the
      words "GM access only." on an otherwise empty screen, with nowhere to go but
      the back button; each now explains the restriction and links back to the
      article, the wiki, the campaign and the dashboard. Opening an article's
      /edit URL without permission silently showed the ordinary read view, as
      though the link were broken; it says you can read but not edit this one. The
      "Add to shelf" control disappeared entirely when a campaign had no upcoming
      session, so the answer to "why can't I put this on a shelf?" was a blank
      space; it now says there is no upcoming session and points at where to
      schedule one. A highlight's "Draft" badge said only that it was not visible
      to players, never what would make it visible; it now names the thing that
      publishes it — approving the session summary, which is on the same page. And
      the archived-campaign banner named no way back: archiving is something a GM
      can do, un-archiving is not, so the banner now says an instance administrator
      restores it, and that scheduled sessions stay on the calendar but stop
      sending reminders.

    • [webapp] Prep's session pickers open on your next session, not the one
      furthest away
      (#302). Every session dropdown in Prep — Session Prep, Beat
      Planner, the Name Generator's session context, Series Titles, "Add to shelf"
      — was seeded from whichever session the API happened to list first, which is
      the most recently created one. For a weekly game that is the last occurrence
      on the calendar, so a GM who opened Prep to plan Tuesday was silently pointed
      at a session three months out, and notes saved there landed on the wrong
      night. Those lists are now ordered by when the session actually is, soonest
      first (undated ones last), and default to the session in progress if there is
      one, otherwise the next one up.

    • [webapp] Prep notes can no longer be wiped by a button you did not mean to
      press
      (#303). "Save Notes" in the Beat Planner is now inert until you have
      actually changed something, so an editor you opened and did not type in
      cannot write an empty note over what was stored — and if the notes fail to
      load at all, the box and the Save button are both disabled and say why,
      rather than presenting a blank page you could save. "Append to prep notes" on
      a generated prep sheet re-reads the stored notes at the moment you click it,
      so notes you edited in another tab since the sheet appeared are kept, and the
      button is not offered at all when those notes cannot be read. Notes written
      by the prep sheet also show up in the Beat Planner without a reload, and if
      you had unsaved typing when that happened, your typing wins and you are told.

    • [webapp] Prep no longer throws away what it just generated (#304). Switching
      tools in Prep wiped the sheet you were reading, and "Re-open" in Generation
      History only moved you to the right tool and then showed you an empty panel —
      even though every run had been saved on the server the whole time. Re-open now
      brings the run back: its output, and the inputs it was made with. Opening a
      tool also brings back its last run when that run is still about what you are
      looking at — the same session, say — and deliberately shows nothing when it is
      not, rather than handing you a prep sheet for a session that already happened.

    • [webapp] The generated prep sheet stops handing you back last session
      (#305). The only forward-looking thing the prep prompt had to work with was
      your notes for the upcoming session — which were almost always empty, because
      nothing in Prep ever loaded them (#302, #303). Left with a pile of recent
      summaries and asked for a prep sheet, the model wrote a recap. Three changes:
      the notes box for the selected session is now in the Session Prep panel
      itself, so writing down where you want the evening to go is part of asking
      for a sheet; a "What I want from this session" box steers one sheet without
      being saved anywhere ("they finally meet the Duchess", "keep it in the
      sewers", "no combat"); and the prompt now says what the recent summaries are
      for — build on them, do not repeat them — with every scene and secret
      required to move past where they left off, and only "loose ends" allowed to
      look backwards.

    • [webapp] Prep has a front door, opens on the right tool, and its output
      goes somewhere you can find
      (#374). You could only reach Prep from the
      campaign page or the nav bar, and it opened on the Beat Planner with a
      campaign-wide picker that had already chosen a session — usually not yours.
      Every upcoming session row on the campaign page, and every session page you
      run, now has a Prep this session button that takes you straight to the
      prep sheet for that session. Prep opens on Session Prep. Untitled sessions
      read as their date in every picker instead of four identical rows saying
      "Untitled session". The three unlabelled buttons under a generated sheet
      ("Append to prep notes", "Replace prep notes", and a second shelf picker) are
      now one Save to this session and one Add to shelf, each of which names
      the session it wrote to and links there. Generation History refreshes when a
      run finishes, instead of insisting there were no generations while one sat on
      the screen beside it. And the tool palette is six entries — Session Prep,
      Story Arcs, Quests, NPCs, Names, At the Table — instead of thirteen siblings:
      nothing was removed, the smaller tools now sit inside the job they serve.

    • [webapp] "Processing…" now tells you how long it has been, and how long it
      usually takes
      (#381). A spinner with no clock is indistinguishable from a
      hang after about ninety seconds — which is roughly when a GM decides the thing
      is broken and hits Retry, adding a second job to the queue that was the delay.
      The session page, the recording page and every Prep tool now count the time
      elapsed and say what normal looks like beside it ("usually 2–5 minutes for a
      3-hour session, longer when other recordings are queued"). A Prep run that
      overruns also points at "Generation History" by its actual name, instead of
      telling you to check "the history rail", which is not a thing on the page.

    • [bot/webapp] Discord account linking no longer dead-ends (#392). Two of
      the ways linking can fail answered the browser with raw JSON — literally
      {"detail":"Link token is invalid or expired"} on a blank page — and one of
      them was a trap. If the Discord account was already connected to a different
      Quest Board account, the bot's poller could not tell that from "hasn't
      clicked the link yet", so ten minutes later it DMed the player that their
      link had expired; they ran /link again, got the same silence, and went
      round a loop that /link could never resolve, because the fix was
      /unlink on the other account and nothing had ever said so. Both failures
      now render a real page that names the next step, the bot reports the actual
      outcome (and reports it as soon as it happens, rather than at the end of the
      window), and the conflict message names /unlink. Two smaller silences went
      with it: a "you need to link" DM to somebody with DMs closed used to vanish
      entirely — it now falls back to a short self-deleting note in the channel —
      and the vote reminder that lists players who are not linked now tells them
      to run /link. The profile page names the command too, instead of alluding
      to "the bot's account-linking flow".

    • [bot/webapp] /record start knows which session you mean, and a restart no
      longer swallows a recording in silence
      (#393). The command asked for a
      session ID that nothing in Discord could give you — /next shows one session
      and omits its id, /history lists only finished ones — so the answer was to
      go and copy a UUID out of a browser. It now offers this server's upcoming and
      in-progress sessions as you type, and a session that does not exist is
      refused when you start rather than discovered at the end of the evening, which
      is when the mistake used to surface. Every recording started from Discord also
      announced itself as "Recording started — Unknown Session", and said it again
      at upload, because the slash command fetched the session's details and then
      dropped the title; it now uses the real one. /record stop no longer leaves
      "Quest Board is thinking…" on screen for ever — it answers the person who ran
      it, as well as posting to the channel. And a bot restart mid-session used to
      leave the channel showing "🔴 Recording started" with no ending, /record stop insisting no recording was active, and a GM with no way to know the
      audio had in fact been kept: the active recording is now remembered outside
      the process, so when the bot comes back it says what happened to the audio —
      recovered and being transcribed, kept and awaiting a retry, or never captured
      — and /record stop answers honestly instead of denying the session
      happened. Failure messages have been rewritten for the people reading them:
      no raw exception text in a public channel, and "a GM can retry from the
      session page" with a link, rather than advice to use a console that a hosted
      customer does not have.

    • [bot/webapp] Discord now tells you what actually went wrong (#373). Every
      Quest Board API call the bot made ended the same way when it failed — "Could
      not reach Quest Board right now. Please try again shortly." — including when
      the backend had answered with something you could act on. "LLM endpoint not
      configured. Set it in Admin → Bot Settings." was being thrown away and
      replaced with a message that said the service was down, which it was not.
      The bot now shows the backend's own sentence when the backend is answering
      rather than failing (a 4xx, or a 503 naming a dependency that is not set up),
      and keeps the generic line for a genuine outage or an unhandled error, where
      it is the only true thing to say. Related: /next, /quests and /history
      used to answer "nothing scheduled" for a server nobody had finished setting
      up — they now say when no campaign is linked to this Discord server, name
      the field to fill in, and quote the server's ID; /record start's "you are
      not the GM" refusal now mentions that Quest Board only recognises a GM whose
      account is linked, and points at /link; and /ask on a campaign with
      nothing recorded finally gives the "record a session first" advice it always
      had written down, which a placeholder answer from the backend had made
      unreachable.

    • [bot/webapp] A wrong Discord channel ID no longer swallows every
      notification in silence
      (#369). The bot notification channel is a snowflake
      a GM types into a plain text box. Get it wrong — a typo, a channel that was
      later deleted, one the bot cannot see or cannot post in — and every
      notification that campaign would ever send vanished, permanently, with no
      error anywhere: the bot logged a warning nobody reads, and the durable event
      queue acked the event exactly as if it had been delivered. The campaign simply
      went quiet, and the only visible clue was the absence of messages nobody was
      looking for. The bot now records why it could not deliver, against the
      campaign, and the campaign page carries a warning at the top of the page —
      outside the collapsed Edit form, where the broken field lives — naming the
      actual problem ("Discord has no channel with that ID", "the bot isn't allowed
      to see that channel", "give it Send Messages permission") and offering a
      button that opens the field. The warning clears itself the moment a
      notification lands. The event is still acked, because a message the bot cannot
      post must not wedge the queue for every other campaign — but if the failure
      cannot even be recorded, that now goes to the log as an error rather than
      disappearing too. The "Send test message" button is fixed in the same breath:
      it was admin-only on a page any GM can open (so most GMs got a permission
      error), it reported success on having queued the attempt rather than on
      anything arriving, and it tested the bot channel and the webhook as one thing.
      There are now two buttons, one per field, each waiting for the real answer,
      and the channel ID field itself rejects anything that is not 17–20 digits
      before you can save it. Adds a migration (f0ab1c2d3e4f).

    • [bot/webapp] Voting in Discord now answers back, and a GM can change one
      time without cancelling the vote
      (#371). Reacting to a poll or an RSVP
      message did nothing visible: no confirmation, no error, and a vote that failed
      to reach Quest Board looked exactly like one that landed. Every reaction now
      gets a short reply in the channel that mentions you and deletes itself — "your
      yes for Option A is recorded", or the reason it was not — including,
      at last, the rule nobody could have guessed: removing a reaction records a
      no for that time, not a blank. The poll message says so itself now, and
      says to use Quest Board for "maybe", which reactions cannot express. Four
      further asymmetries go with it. A vote cast in Discord now fires the same
      "🗳️ Vote Update" post that a vote cast in the web app always did, so the
      channel's own tally stops going stale. A reaction on a poll for an
      already-confirmed session is refused instead of quietly recorded, the reaction
      is taken back off, and the player is told when the session was confirmed for —
      and the poll message is relabelled "Voting closed" the moment a time is
      confirmed, so it stops inviting reactions in the first place. A GM can add or
      remove a single time option on an open vote from the session page, keeping
      every vote already cast; the only previous route was "Reopen for voting",
      which replaced the slate and discarded them. And when the bot has to fall back
      to keeping its poll bookkeeping in memory — no Redis, or Redis unreachable —
      it now says so loudly at the moment it starts, and logs every reaction it
      drops, because the symptom otherwise is a channel full of polls that look
      perfectly alive and record nothing.

    • [webapp] The wiki updates the app suggests are findable again (#375). On
      the wiki's own front page the only way into the suggestions queue was a link
      in the left-hand rail, and that rail is hidden on anything narrower than a
      tablet — so from a phone there was no route to it at all. It now sits beside
      New article in the main column, with the number of pending suggestions on
      it, at every screen size. The campaign page carried a second, cut-down copy
      of the queue: it left out suggested relationships entirely, truncated every
      suggested article to three lines, and disappeared when the queue was empty —
      which is exactly the moment a GM hunting for it needed to be told there was
      nothing waiting. That copy is gone, replaced by one row that is always there
      and always says which of "3 suggested wiki updates", "no pending suggestions"
      or "couldn't load them" is true, with Review going to the one page that
      shows the whole queue. Both pages now tell a failed read apart from an empty
      one, instead of showing you an empty queue when the request simply failed.

    • [webapp] A wiki draft can no longer claim to be generating for ever, and a
      failure in one panel stops taking the whole page with it
      (#395). While the
      LLM writes a draft, the review page asks the server for it every couple of
      seconds — and that check only understood one kind of failure. A server error,
      a dropped connection or a worker that died mid-run left "Generating draft…"
      on screen indefinitely, with nothing to suggest anything had gone wrong. The
      page now says when a check fails, stops pretending it is still watching after
      three minutes, and offers Check again beside the existing reset. Three
      other failures used to replace an entire page with a single red line: a wiki
      article's edit history failing to load, a Restore of an old version
      failing, and the wiki's Load more failing each wiped out the article — or
      the whole campaign story — you were reading. Each now reports itself beside
      the control that failed and leaves the page alone. And Full regenerate,
      which throws the campaign story away and rewrites it from every session, now
      asks first, like every other destructive action in the wiki.

    • [webapp] The wiki's category list stops running out after 200 articles
      (#396). The article page's category rail asked for the entire wiki in one
      request, and the server sends at most 200 entries — so on a campaign with
      more than that the rail simply ended, with nothing to say anything was
      missing, and every article past the cut-off was unreachable by browsing. It
      now pages the way the wiki's front page already did, with the same Load
      more (200 of 250)
      button, so both lists reach the end of the wiki.

    • [webapp] Things that failed now say they failed (#377). A dozen places in
      the web app treated a failed request as if nothing had happened, so there was
      no way to tell a working screen from a broken one. The worst of them was the
      recording page: if the connection dropped mid-session, the page redrew itself
      as "No active recording", idle, with a Start Recording button — while the
      bot was still recording in Discord — because "the server says nothing is
      recording" and "we could not reach the server" were the same value in the
      code. It now keeps the last thing the server actually said, says "Connection
      lost, retrying…"
      over it with how long ago that was true, and clears the
      warning the moment it hears back; where it has never heard anything, it says
      Unknown rather than guessing at the answer that invites a second
      recording of the same evening. The players' table view says the same thing
      when its own polling stops working, instead of quietly showing a frozen list.
      Elsewhere: a vote the server refused, and a session-shelf card whose new
      position never saved, both now say so and offer to try again — the shelf was
      the sneakiest, because the board looked right until the next reload put every
      card back. A session's attendance and its loot & XP stop reporting a failed
      read as an evening where nobody came and nothing was won. The last session's
      recap, the title suggestions on a new session, and Prep's name pre-generation
      each say when they could not load rather than simply not appearing. And in the
      wiki, "Check for link suggestions" answers "No link suggestions found"
      instead of doing nothing visible, and a stat block whose game-system schema
      could not be read says why its labels have turned into raw field names.

    Downloads