-
v4.5.0-rc2
Pre-releaseSome checks failedRelease / Create Forgejo release (push) Successful in 25sCI / Docker image build (push) Successful in 29sCI / Bot/backend version sync (push) Successful in 30sCI / Backend lint (ruff) (push) Successful in 32sCI / Does this change touch the recording path? (push) Successful in 36sCI / Feedback site tests (push) Successful in 48sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m1sCI / Bot tests and audit (push) Successful in 1m53sRelease / Build and push versioned images (push) Failing after 3m7sCI / Frontend (shard 2/2) (push) Successful in 8m59sCI / End-to-end journeys (phone) (push) Successful in 9m35sCI / Frontend (shard 1/2) (push) Successful in 10m49sCI / End-to-end journeys (desktop) (push) Successful in 12m48sCI / Synthetic session harness (no GPU, no LLM) (push) Successful in 15m10sCI / End-to-end journeys (keyboard) (push) Successful in 16m45sCI / Backend migration, tests, and audit (push) Successful in 23m54sreleased this
2026-10-03 18:17:01 +00:00 | 861 commits to main since this releasePrerelease of 4.5.0 for testing on the dev server; not for production.
Added
- [webapp] The bell reaches older notifications with "Show older". The
panel showed only the newest twenty; a button at the foot of the list now
adds the next twenty, moves focus to the first of them, and goes away when
nothing older is left (#1000). - [webapp] Setup's AI step asks for the model's context window. It is
the same field as Admin → AI models ("How much it can read at once"), saved
through the same setting, so a server set up through the wizard no longer
falls back to 32,768 tokens without anyone having been asked (#1001). - [webapp] In a campaign that levels by milestones, the GM can hand a level
to the player (#1007). Until now only the GM could level a character
there. Beside Level up in the sheet's header a GM now has "Let Priya level
up": it marks the character's next level as waiting, the player is told in
the bell, and their sheet says "Level 4 is waiting" with Level up and Not
yet, exactly as it does in an XP campaign when the XP is there. The GM's
header then reads "Level 4 is waiting for Priya", with Take it back.
Reaching the level clears the mark, whether the player does it or the GM
levels the character as before. It is one level at a time, a lock on Level
still stops the player, and the server refuses a player's level-up in a
milestone campaign unless the mark is set. Marking a level makes no version
of the sheet, is not part of a sheet's export, and nothing about it goes to
email or Discord. An XP campaign is unchanged: its levels wait on the XP.
The demo seed's Hollow & Ash now levels by milestones, with Tam Ridley's
level 4 waiting. Migrationc1007a1b2c3dadds three nullable columns to
character_sheetsand thelevel_waitingnotification kind. - [webapp] A result in the prep sheet's Rules & bestiary and Wiki drawers
can be read before it is inserted. Its title opens the entry in the
drawer, in the results' place — Table mode's own Find preview: a wiki page
as the GM reads it, a creature as its block, a rule as its text — with
Insert still there and "Back to results". Escape goes back too, and focus
returns to the result it came from. It works the same in the phone's Tools
sheet (IS-472). - [webapp] The campaign search has a page of results, and a rule you find opens
where you are (#1033). The results under the search box now show three of each
kind — sessions, people, wiki, rules — then "All 14 in Wiki" and "See all 23
results", which lead to a page listing every match, grouped the same way, with a
row of links to narrow it to one kind. Pressing Enter in the box goes there too.
Until now a kind stopped at ten with no way to the rest, and "Rules · 10" could be
hiding more. The page has no search box of its own: the one in the bar is still
the only one. A GM gets the same "Look inside transcripts" switch on the page, and
a player still never sees a GM's material there. Choosing a rule used to do
nothing; it now opens the rule in place — its text, or a creature's block — and
closing it puts you back on the result you chose. The wiki's own search box works
the same way. A kind with more than 200 matches lists the first 200 and says how
many there are. - [repo] The dev demo seed has lane 8's wiki and lane 10's bot tests. Both
parties and who has been in them, reputation, downtime, custody and an
unidentified ring; the 27 quests as pages with givers, rewards and
objectives; mid-text:::gm,:::forand:::partyblocks and pages for
every audience; six handouts with their files and reveals; private page
pictures; three atlases with maps drawn and tiled at seed time, markers of
every kind and the party's trail; players' suggestions (the Tuesday Tide is
now "Players suggest, I decide"); the link index and Recent changes; and six
finished bot tests with what they spent. Files go under
media/private/, only after the commit. The map tiling task's cutting is
now a purecut_tilesthe seed calls directly (#734, #736). - [webapp] An article's read view, History, More, Recent changes and Deleted
articles (lane 8 PR P2, #734; WIKI-3, WIKI-5, WIKI-6, WIKI-17, WIKI-18).- An article (
/wiki/:entryId) renders the body throughWikiText
(PR P1) with the server's own?as_user=for a GM's "See it as…"; a page
outside the reader's audience says only "This page isn't shared with you"
(W3). Relationships, appearances, mentioned-in, references and a timeline
follow, with a margin for the picture, facts, a stat block, and (an item
page) who has had it. - History (
/wiki/:entryId/history) restores through the server's own
POST …/versions/{id}/restore, replacing the old article page's plain
PATCH, which never called it. - The ⋯ More menu: adding to a session's shelf, the three LLM drafts,
Convert the stats, merging a page into this one, and deleting with an
undo — replacing the lastalert()-shaped delete in the product. - Recent changes (
/wiki/recent) and Deleted articles
(/wiki/deleted, GM only) round out the wiki's four remaining P1/P2
surfaces; the front page's "All recent changes" now leads to the former. - Lane 7's Read · Talk · History tab bar is kept; History is now a plain
link to its own page rather than an inline panel. - The read half of
WikiArticle.jsxis deleted; its edit half stays at
/wiki/:entryId/edituntil PR 3 rebuilds the editor. - Frontend only: no backend or bot change.
- An article (
- [webapp] A draft, Suggested wiki updates and Wiki health on the new wiki
(lane 8 page P4, #734; WIKI-9, WIKI-10, WIKI-16).- A draft shows the page beside the draft with each stage in plain
words, typed stats, Send it back and Throw it away. A player's
suggestion opens here too, compared line by line with the page as she
reads it, never the GM's text. - Suggested wiki updates shows where each session's suggestions stand,
then the pages and relationships waiting, one action at a time. - Wiki health lists wanted pages, unlinked mentions, orphans, possible
duplicates and stale pages, each with its fix. - The Contents gains Wiki health for a GM, and Suggested wiki updates shows
its count.
- A draft shows the page beside the draft with each stage in plain
- [webapp] The wiki's editor, a new page and Settings → Wiki, rebuilt on lane 8's
text (WIKI-7, WIKI-8, CAMP-14, D72, #734). Write on the left and the page on the
right (Write | Preview on a phone), with a toolbar,[[autocomplete, and ◐, which
opens the audience picker and writes a:::gm,:::partyor:::forblock for
whoever was chosen. One Save sends the text, the fields, the facts and the timeline
together; the text is kept on the device as it is typed, per person. A save that
started from an older version says who saved since and offers merge by hand, use
theirs or keep mine. One heading's section can be edited on its own. Pasting a
picture uploads it to the page's private files; pasting from a document brings its
headings, bold and lists across as Markdown. A GM sets who sees the whole page
(D166). Relationships, "Who plays them", the picture, the stat block and links you
could add moved in from the old edit page. A player's save is only ever her own
copy of the page, edited: the blocks she can't see are never in the editor, and
the server puts them back where they were. A new page starts from its kind's
template; where players suggest, it goes to the GM's queue. A name two pages
share opens a page listing both (D72), and "See it as…" can now pick a character.
Settings → Wiki sets who may edit, what each kind of page starts with, and the
reputation levels.WikiArticle.jsx,WikiNewEntry.jsxandWikiSearch.jsxare
gone. - [webapp] History per reader, Recent changes, Wiki health, merging two pages,
personal notes, and canon in a block (lane 8 wave 2 PR 2b, #734; IS-051,
IS-055 to IS-060, IS-105, IS-147, IS-148, D1, D69, D197, D219). Server only.- History (
GET /lore/{id}/versions) opens to anyone who may edit the page,
not only the GM and the primary owner, and drops a version that changed
nothing this reader could see. Restoring an earlier version widens the same
way: a character's own player may restore her own page, safely — her restore
is merged against the live body, never overwriting a block she can't see. - Recent changes (
GET /wiki/recent) is one feed — made, saved, deleted,
put back, renamed, merged — filtered so nobody learns that something they
cannot see has changed. Migration8c3d4e5f6a7baddswiki_events. - Wiki health (
GET /wiki/health, GM-only): wanted links, unlinked
mentions, possible duplicates, orphans and stale pages. - Merging two pages (IS-060): "Merge or Keep both" starts the existing
LLM-merge draft (POST /lore/{id}/merge); approving it now also moves the
other page's aliases, links, relationships, references, appearances, owners,
personal notes and Discussions threads onto the survivor, and trashes it. - Personal notes (IS-105,
lore_personal_notes): a player's own notes on a
page, read by the writer and every GM (D197), and never AI context. - Canon answers wrap in a block (IS-148, D219) instead of being refused: a
party or private answer is written as:::partyor:::for, naming a player
as well as a character, pre-filled from the thread's or post's own audience.
A new page made this way starts at that same audience, so its title doesn't
leak to someone who can't read the one block on it. Every canon write adds a
decided infootnote automatically, and a backfill adds one to every post
already made canon. - The NPC tracker folds into the wiki (D1): the migration makes a People
page for any tracked NPC with no page of that name, carrying its session
appearances. The old tracker stays working for one more release. - The 60 s wait before wiki proposals land is now usually instant: each
match pass fires consolidation itself once every category is in, rather than
waiting for the 60 s backstop — which still runs, and now waits longer for a
slow local model before giving up.
- History (
- [webapp] The wiki's front page and a group, rebuilt (lane 8 PR P1, #734;
WIKI-1, WIKI-2, D60).- The front page (
/wiki) leads with a search box scoped to the wiki and
the rules, then "Recently revealed or updated" (the top of Recent changes,
D60), "People you've met" (appearances the reader may see, most recent
session first), and Browse by group. A GM's copy also carries "Waiting for
you" (the top of Suggested wiki updates) and Wiki health's counts. - A group (
/wiki/g/:group) lists a group's pages A to Z, narrowed by
type with a chip when it holds more than one — People, Places, Groups and
Things are now Contents entries; Events and Lore stay reachable only from
Browse by group.WikiFront.jsxand its test are deleted. WikiText(src/wiki/): the parser and renderer for the wiki's block
syntax (:::gm,:::for,:::party, footnotes,[[links]]) that PR P2
wires into the article page — built here, tested, and not yet on any page.- Frontend only: no backend or bot change.
- The front page (
- [webapp] Handouts, private files, and rules' "Make it ours" (lane 8 wave 2
PR 5, #734, §3.8, §3.10; W7, W8). Server and the bot's own side; the pages
follow.- A handout is a
documentwiki page with a file (lore_attachments): PDFs
are validated by magic bytes with their page count read, images re-encoded
through the existing picture pipeline. Migration8a7b8c9d0e1f, on top of
lane 8's quest pages. - Private files stay private (W7). A file narrower than the table no
longer lives at a guessable, cached-forever address:GET /api/campaigns/{id}/files/{attachment_id}decides on every request whether
the caller may have it — the page's own audience, or a live reveal's — and
answers withX-Accel-Redirectto a newinternalnginx location that
refuses a direct request. - Reveals carry an audience, a page and an optional session (W8).
shelf_revealsgainslore_entry_id,piece_label(for a torn letter
shown in pieces), and lane 7's audience columns;session_idis now
nullable, so a handout can be shown between sessions. A handout's own
audience is the union of its live reveals. handout_shown, a bell kind ([wiki]category) and a new, additive bot
event: the bot fetches the file withGET /api/bot/files/{id}and posts it
to the campaign's Discord channel, or DMs it to each named person instead of
posting to the channel.BOT_CONTRACT_VERSIONstays 2.- "Make it ours" (
POST /api/reference/{id}/make-ours): a rules-pack
entry becomes a GM-only campaign page that keeps a link back
(source_reference_entry_id), so the rules chapter needs no endpoint of its
own.
- A handout is a
- [webapp] Handouts and Rules & bestiary, two new chapters of the wiki
(lane 8 page P6, #734; WIKI-14, WIKI-15).- Handouts (
/wiki/handouts): a GM's handouts by session with who each
went to, the ones not shown yet first as GM prep, and a reveal outside
play as "Between sessions". Show it picks who (the audience picker,
characters first), posts to Discord unless switched off (D61), says
when, and names the piece of a letter shown in parts; Take it back
uses the server's own revoke route. A player reads what was shown to
her, by session, "Just you" on hers alone, and may add one (just her and
the GMs) where the campaign lets her write pages. - A handout's own page shows its file: an image, a PDF previewed in the
browser's viewer with "Download the PDF" beside it (download only on a
phone), or plain text. Every file is read through the authorised files
route, never a/media/address. - Rules & bestiary (
/wiki/rules): search the campaign's loaded rules,
each hit tagged as rules; a rule opens with its source, a creature as a
stat block, and a GM can Make it ours. The licence line for each
loaded pack sits at the foot; with no pack loaded the page says where a
GM loads one. - Server (#916): a live reveal that covers a player opens that
handout's page to her to read (rendered for her, so GM blocks never
reach her), and nothing else — not the wiki's lists, search or any
write; taking it back closes it again.GET /api/campaigns/{cid}/handouts
lists the handouts shown to the reader, grouped by session. A player's
own handout page is hers and the GMs'. A page made with "Make it ours"
says which rules entry it came from. - Both chapters join the Contents' wiki group.
- Handouts (
- [webapp] The wiki's links, references and names, one Save, and who may edit
(lane 8 wave 1 PR 2a, #734, #779; IS-033, IS-036, IS-037, IS-042, IS-046, IS-048,
IS-049, IS-053, IS-054, IS-085, IS-088, IS-399; D72). Server only; the pages follow.- Links are records. Every
[[link]]and footnote in a page's text is written
tolore_links/lore_referenceson each save (and backfilled by migration
8b2c3d4e5f6a), each with the block it sits in, read per reader: "Mentioned in"
never names a page, or a link inside a block, the reader can't see. An article
now says what each name it links means to its reader (a page, the
disambiguation page, plain words for a page they can't see, or wanted), and
lists the footnotes they may open (https, a thread or post, a moment in a
session, a rule), numbered for them. The server never fetches a URL. - Names (D72). A name another page has is a 409 that says what to call the
two ("Kettlebridge (town)", "Kettlebridge (boat)"); the bare name then opens a
disambiguation page (GET /lore/by-name/{name}), unless a GM points it at one
page. A renamed page keeps its old name as another name, and can rewrite the
links that use it; a renamed character keeps their:::forblocks. - One Save. A save sends the
version_idit started from; if someone saved
since, it is a 409 carrying the page as it now is, and nothing is saved. A
phone can save one heading's section (GET /lore/{id}/sections/{n}). - Who may edit (Settings → Wiki): only the GMs, players their own pages (the
default, and today's behaviour — now including pages they made, which they may
start), players anything, or players suggest. A suggestion — a new page, an
edit, a relationship — waits in the GMs' queue and is merged as its writer, so
nothing they couldn't see is touched. - Relationships have an audience of their own and may start and end in a
session. Templates per kind and the reputation levels (Pathfinder's
seven to start) are the campaign's to change; exports carry all of it.
- Links are records. Every
- [webapp] The map, a new chapter of the wiki (lane 8 page P7, #734; WIKI-11).
- The map (
/wiki/map) draws each map over its private tiles, with the
atlas's levels as crumbs, filters by kind, the party's trail (a stop per
place, its sessions and days on the road) and "The party is here". A
marker opens its own map, travels a link, or opens its page; a GM reveals
a GM-only marker to the players, with an undo. - A player adds her own markers, for herself and the GMs, the party, or the
table (IS-076). A GM adds a map, as its own atlas or opened from a marker,
and where the party ended a session. - Every marker is also listed under the map, and the map pans with the arrow
keys and zooms with its buttons. - The wiki's front page says where the party is (IS-124), and an article
says where its page is on the map (IS-102, IS-087).
- The map (
- [webapp] The party, what it carries, where it stands and what it did between
sessions (lane 8 wave 2 PR 3, #734; IS-107 to IS-122, IS-088, IS-116, IS-206,
IS-218 to IS-221; D168, D198, D12). Server only; the party page follows.- Parties. Every campaign has a party, and may have more (
/parties), with
who has been in each over time: joined and left at a session, with why
(left, retired, died, captured, swapped), and companions, hirelings, mounts
and vehicles as pages. Swapping characters keeps the record in step. Migration
8d4e5f6a7b8cmakes each campaign's party and places every character at the
first session they were played in. - "The party" names one party (D168). Threads, posts, pages, relationships,
facts and blocks can be for a party by name (:::party The Tuesday Watch);
everything already for "the party" is the first party, so with one party
nothing changes, and a renamed party keeps its blocks. - Items belong to characters (D198): a line is held by a character, the
party's pool, an NPC or a place, can link its own page, and records who has
had it. An unidentified item is named as the party knows it everywhere a
player reads it, until a GM identifies it. Existing lines, XP and gold went
to the character each player was playing in that session. - Removing a line, an award or a movement is undone (D12), not confirmed.
- Where the party stands with each organisation, changed at a session, on
the campaign's levels; and downtime, logged by the player and approved by
the GM, with Pathfinder's activities to pick from.
- Parties. Every campaign has a party, and may have more (
- [webapp] The party and the quest log, as wiki pages (lane 8 PR P5, #734;
WIKI-12, WIKI-13).- The party (
/wiki/party): each party's home base, fund, XP pool,
members with pronouns and companions, and active quests; What we carry
(notable items, then the ledger, filtered by holder; add, give, change);
XP and gold side by side with each character's total; downtime with
Pathfinder's activities to pick from and a GM's Approve; Who has been in
it, current then former, with moving a character to another party and
why; and Where we stand in the margin. Removing a line is undone, never
confirmed (D12). A player adds to the ledger only where the campaign
allows it and changes only her own lines (D6); an unplaced row counts in
the main party (D236). - Quests (
/wiki/quests): Rumours waiting for the GM first, then each
status, with its colour, a failed quest struck through, when it started,
and "The quest log did not load" rather than an empty list. A new quest is
a new page, opened in its editor. - The Contents gains Quests and The party under The wiki; "Quests & loot"
goes./campaigns/:id/managenow redirects to the party page, and the
old campaign page is deleted withLootLedger,QuestLogand the
components only it used. An ownership offer is answered with Accept and
Decline on This week's banner itself. - Frontend only: no backend or bot change.
- The party (
- [webapp] Quests are wiki pages now (lane 8 wave 2 PR 4, #734, §3.7; D196).
Server only; the quest page follows.- Every existing
plot_threadsrow becomes a page of kindquest, keeping its
title, status, sessions and reward note: the GM's private notes move into a
:::gmblock at the top of the body, andplayer_visiblebecomes the page's
own audience —tablewhen it was visible,gmwhen it wasn't. Migrations
8e5f6a7b8c9dand8f6a7b8c9d0e. - A rumour may be about no quest yet (D196/W10). Confirming one offers
"Make it a quest," which creates the page and posts it under whoever started
the rumour. - The bot's
/quests, the pre-session reminder's open threads, Prep's context,
the shelf's open loops and the Quests drawer's quick add all read quest pages
now;GET/POST/PATCH/DELETE .../planning/threadsandGET .../questsare
removed in favour ofGET/POST .../lore/quests, filtered per reader like any
other page. The bot's response shape is unchanged — no contract bump.
- Every existing
- [webapp] "Test the bot", a GM's way to check the recording pipeline
before the evening without a real session (lane 10, PR 7a, #736, D124).
Settings → Recording, retention & AI gains a launch (30 seconds, stoppable
early, keep-or-delete after), the running stages, the result — per-stage
timing, the transcript, the summary, and a plain sentence naming who can
fix a failure — and the kept test runs, with a confirmed delete (D12). The
session page offers a link to it while Start is refused for being early
(D149, D222). - [webapp] Two public pages for an email's links, the notification grid's
Discord DM column, and the installed app's update and offline notices
(lane 10 PR 7b, #736)./email/stopand/email/answerwork signed out
(D154, D155): stopping a kind of email needs one click and never reveals
more than the token names, and answering "Are you in?" records the RSVP
through the same writer the session page uses. The grid's DM column shows
"Discord can't reach you" when the bot's last message failed, without
hiding the switch (HOME-6), and the star-and-wish ask is its own indented
line under Sessions (D232). The installed app now offers a quiet "Quest
Board has been updated" notice instead of reloading silently (D13), says
so while offline, and no longer locks to portrait orientation. - [webapp] Email grew a one-click stop, answering "Are you in?" without signing in,
and a proper recap timing (D154, D155, D162, O7, O8, O15, #736). Every outbound
email now goes through one template, withList-Unsubscribe/List-Unsubscribe-Post
headers (RFC 8058) and a footer link to/email/stop(POST /api/email/stop, with an
undo). Session-confirmed and reminder emails carry "I'm in / Maybe / I can't" buttons
that record an RSVP through/email/answer, good until the session starts. Calendar
invites carry the session number inSUMMARYand aURL:back to it, no longer ask
for a reply (RSVP=FALSE— nothing read the old one), and put a cancellation's reason
at the top ofDESCRIPTION. The post-session recap now sends when the GM approves the
summary, not the moment it is first written, and never twice. A developer-only
GET /api/dev/email-preview/{kind}(behindENVIRONMENT=development) renders every
template from invented fixture data, for reviewing them without real mail. - [webapp] The star-and-wish ask is its own row under Sessions in notification
settings (D232, O29, #736). It used to share the "Sessions" cell; existing
preferences carry over as the new row's starting value (migration10d4e5f6a7b8). - [repo] Journeys J2 and J4b cover lane 10 (#736). J2 asserts what reaches
the bot — a vote's number and closing time, the confirmation's number, a ❔ from
Discord showing on the session page, the cancel reason, and a Discord DM with the
bell's line — and what reaches Mailpit: the confirmation to the player's own
address, the time in her zone, the unsubscribe headers, answering from the email,
and Stop → Undo → Stop. The new J4b tests the bot from Settings → Recording and
checks nothing else happened, and that a recording whose announcement lands
nowhere still records and says so (D171). The fake bot hands a test's
generated near-silent WAVs over as the real bot does. J2's bell check no longer
depends on how many rows earlier runs added (#887). - [repo] The dev demo seed has character sheets and stat blocks (lane 11,
wave A). Every sheet is made through the sheet services the routes call,
from the mini character packs loaded as campaign packs. The Amberwake Accord
(and its GM copy): Ossian Vale exactly as drawn, Wren's Spells page and focus
pool, Bastian, Sister Quill, the owner's Hollis (Level & XP locked by the
GM, "just you and the GM" campaign notes, and a row from the disabled Salt
Crown pack that keeps its name and numbers), retired Aldous and Hesper's old
sheet; conditions and hurt HP on two; every old stat block converted, Odile
Marsh filled out to the drawing, one block partly shown to the party, one
wholly the GMs', one projected from Hesper's sheet; a rules creature on
Session 24's shelf with its own hit points; histories of 3 to 6 versions.
And a new thin 5e campaign, Hollow & Ash, where the owner plays Tam
Ridley (Fighter 3, HP 31 from rolls of 8 and 7) with four more characters
and two blocks. - [webapp] Sheets reach Admin, the rules, Prep and Members & characters (#737).
Admin → Content packs lists every sheet definition on the instance — built
in, from a pack, or made from a pack's older stat blocks — with its version,
whether it is active or deprecated, how many sheets are on it and how its
fixtures did; a pack's row names its own definitions and which sheets are
still on an older version. Loading a pack that carries definitions shows the
validator's report for each: activated or not, every error (with a "did you
mean" where there is one), every warning, and which fixtures failed and why.
One error stops that definition only; the pack's content still goes in, and
a campaign's own pack says its definitions are not used. An open rules
result has Add to a sheet ▾, listing the characters and blocks the reader
may edit that take it — a player's own character, a GM's every sheet — and
puts the entry on with where it came from; a reader with nothing to edit
gets no menu. Instant NPC can Save as a page, with a block: a new NPC
page with a block drafted for the campaign's game, waiting in Suggested wiki
updates to be checked (no button where the game has no block to draft).
Members & characters has Open the sheet on a character whose page has
one, and the members list names each character'ssheet_id. - [webapp] Level up on the sheet, with XP from the campaign's ledger (#737).
A character's XP is now every XP award to them, or to their party, since
their sheet was made. When it reaches the next level the sheet's header says
"Level 7 is waiting", with Level up and Not yet; a GM can level any character
at any time. Settings → General chooses whether the campaign levels by XP or
by milestone; a milestone campaign shows no XP and its GM says when. Level up
walks the level in the system's own order: hit points (Pathfinder's worked
out from the level; 5e's rolled on the class die or taken as the fixed value,
the die kept with the sheet and the roll standing), the class's own features,
each feat from the loaded content (one that can't be taken yet is shown,
disabled, with why), a skill increase as far as the level allows, attribute
boosts, and 5e's improvement (+2 to one score or +1 to two, none past 20) or a
feat. What changes lists every number that moves, before and after, and why.
Reach level n keeps the sheet as it was as a version first, so it can be put
back, and adds "Ossian Vale reached level 7" to the campaign's milestones, on
the last session played. - [repo] J7, the character journey, and the rest of the demo's sheets
(lane 11, wave B).make journeysnow walks a character from the first
step to level 2, on desktop, a phone and the keyboard alone, with an
accessibility scan at every page: Priya starts a Pathfinder character from
Members & characters (ancestry, background and class from the loaded
content, attributes by boosts, nothing rolled), leaves it and carries on,
and finishes it; Dana shows Odile Marsh's block to the table by field, and
Priya is handed the shown lines and no others at the table, on the page and
in the answer behind it; Dana awards XP, the sheet says "Level 2 is
waiting", and Priya levels up through What changes to Reach level 2, which
keeps a version first and adds the milestone. The journeys' seed gains a
campaign for it, with the sample character pack imported through the GM's
own import. The dev demo seed gains: "Level 7 is waiting" on Ossian Vale's
sheet, reached through the XP ledger; Tam Ridley's third level taken on the
sheet, with its hit-point die kept and "Tam Ridley reached level 3" in the
campaign's history; and two characters begun and left, waiting on Members as
"Carry on with …" — one with its 4d6 kept, the lowest die of each struck. - [webapp] D&D 5e character sheets and stat blocks, built in (SRD 5.2.1).
The character sheet works out ability modifiers, saving throws, the
proficiency bonus by level, all eighteen skills with proficiency or
expertise and Passive Perception, Armor Class from the armor and shield
worn (medium armor capping Dex at 2), hit points from each level's rolled
or fixed hit die plus Constitution, Hit Point Dice, death saves, weapon
attacks and damage, and spellcasting (save DC, spell attack, slots for full
and half casters, Pact Magic). Exhaustion and the conditions that stop a
creature moving change the numbers they affect. Creation (standard array,
27-point buy, 4d6 drop the lowest), levels 1–20 with the Ability Score
Improvement or a feat at 4, 8, 12, 16 and 19, short and long rests, and
the stat block in the SRD's order are all defined. Old 5e stat blocks map
onto the new block. Includes material from the SRD 5.2.1 under CC BY 4.0,
credited on the sheet. Nothing on screen yet (#737). - [webapp] The block on its page, Show the table by field, Make a block, and the block at the table (#737).
An NPC's or a creature's page with a sheet draws its block in the game's own
order under the page's lead. The GM sees every line and which the party can
read — a GM-only line has the GM's rule down its side, with a legend — and
has Show the table…, Edit the block, Put on the shelf and Export… beside it.
Show the table is a checklist by the game's groups (name, level and traits;
perception and senses; languages; skills; AC, saves and HP; strikes by name
only, or with their attack and damage; abilities…); GM notes are never
offered. What is ticked becomes the party's for good, on the page, in search
and on the table, and unticking a group or Take it back hides it again.
While a session is being played it is also shown at the table at once — the
party's lines on the players' screens and, if asked, in the campaign's
Discord channel — and Take it back takes that showing down, as a handout's
is, telling the other GMs. That showing opens nothing else: a GM-only NPC's
page stays the GMs'. A GM can make a block from a character's sheet (Make a
block…): a new NPC page, the GMs' alone, with a copy of the character as it
is now that later changes to the sheet do not touch. At the table, Find says
which pages have a block, and a wiki card or the preview draws the block with
its hit points (−5 / −1 / +) and conditions changed from the card with no
version made; a monster from the rules keeps its own hit points and
conditions on its card for the session. Showing a card with a block to the
table now asks what to show, by field. - [webapp] Rules content now carries the numbers a sheet reads. Every
reference entry has structuredfieldsper kind — an item's Bulk or weight,
a weapon's damage and traits, armour's AC and Dex cap, a class's hit die, a
condition's modifiers — filled when a Foundry PF2e or Open5e pack is
imported, or written directly in a Quest Board pack (checked per kind; ones
that don't fit are kept as data with a warning). PF2e ancestries and
heritages, and 5e species, import as their own kinds. The starter-pack
scripts now build the Player Core's classes, ancestries, heritages,
backgrounds, feats and equipment (ORC, setting prose stripped) and the
SRD 5.2.1 only for 5e; publishing them is a release step
(docs/OPERATIONS.md, "Building the starter packs"). A small mechanics-only
character pack per system backs the tests, the journeys and the demo
(#737). - [webapp] The engine that will work out character sheets and stat blocks.
A sheet's definition (its fields, the sums behind them, the bonuses and
penalties that apply) is now checked and evaluated on the server: every
number comes with the named terms it was added up from, bonuses of the same
kind stack the way the game says, conditions and equipped items apply
themselves, and a definition that refers to a field that does not exist is
refused with a "did you mean" suggestion. Ships with a generic sheet for any
game without its own, and a shared set of test cases the browser will be
held to as well. Nothing on screen yet (#737). - [webapp] The sheet at the table: hit points, conditions, spells, rests, and adding from the loaded content (#737).
A character's player and the GM change the hot half straight from the sheet,
on a phone too, with no version made: hit points with −5 / −1 / + and their
temporary points, hero points, dying and wounded as tracks to press, and
conditions as chips with − and + (Frightened 1), an "until" note (Off-guard
until the start of your turn) and Add a condition; every figure a condition
changes shows the penalty in its terms. A caster's spells have Cast, which
spends the rank's slot (a focus point for a focus spell) and marks a prepared
spell expended, and the expended mark takes it back; Refocus sits by the focus
points. Rest… in the header lists the game's own rests and says what each
gives back before it is taken: Pathfinder's daily preparations (hit points by
Constitution and level, every spell slot and focus point, fatigued, drained
and doomed eased) and Refocus, which is off for a character with no focus
points and says why; 5e's short rest spends the Hit Point Dice the table
rolled (the page rolls nothing; Constitution is added for each die), and the
long rest gives back every hit point and every Hit Point Die (SRD 5.2.1) and
takes off a level of Exhaustion. Hero points come back to 1 when a session
starts being played, once per session. While a sheet is open it picks up what
the other side changed within a few seconds. Editing a sheet now offers Add
from the loaded content — search, the entry's card (damage, traits, category,
bulk, price, and where it is from with its licence), then how many and where —
and each row's own quantity, where it is, invested or equipped, prepared, and
Remove; a row whose pack has gone keeps its name and says where it came from. - [webapp] The sheet engine runs in the browser too. The same sums the
server works out for a character sheet or stat block can now be worked out
in the page while someone types, so numbers will update as you edit; the
server's answer still replaces them on every save. The browser's copy is
held to the very same shared test cases as the server's, down to how it
rounds, orders text and prints large numbers. It can also show dice the
server rolled (which dice were kept and which dropped); the browser never
rolls. Nothing on screen yet (#737). - [webapp] Sheets can name their numbers the way the page shows them.
A line in the working-out under a number can now take its name from the
sheet ("Dex 4" rather than "Attribute", "Prof 8" with "trained" beside
it), a bonus or penalty can name itself ("Frightened 1"), and a sheet can
pick one name out of a list ("Chain mail 16", "d10 → 8, 7"). A sheet
definition that misspells a function now gets a "did you mean". Also, a
malformed item row or an impossible rank no longer stops the whole sheet
from being worked out (#737). - [webapp] A content pack can carry sheet definitions (pack format v2). A
.qbpackmay listsheets/*.json(andsheets/fixtures/*.json) undersheet_definitionsin its manifest. An administrator's pack has each definition checked (unknown names come back with a did-you-mean, fixtures are run) and activated only if it passes; a broken one is refused while the pack's content still imports, and a GM's campaign pack keeps its content but has its definitions ignored, with a note. The import response carries the per-definition report,GET /api/admin/sheet-definitionslists every definition, disabling or removing a pack deprecates its definitions (sheets on them stay readable), export writes them back, and a pack'ssystem.jsonmay now update a game system that same pack introduced. - [webapp] Pathfinder 2e character sheets and creature blocks, built in.
The Remaster character sheet is defined in full: attributes with boosts,
Armor Class with the armor's Dex cap, saves, Perception and class DC by
proficiency rank, Hit Points from ancestry, class and Constitution, dying,
wounded and Hero Points, all sixteen skills and Lore with the armor check
penalty, Strikes worked out from the weapons carried (attack, the second and
third attack, damage), feats by level, Bulk and encumbrance, invested items,
a spells page for casters with slots by rank and Focus Points, and each
condition's real effect (frightened, sickened, clumsy, enfeebled, drained,
stupefied, fatigued, off-guard, prone, unconscious, encumbered). The
creature block follows Monster Core's order, takes over today's stat blocks
field for field, and can be made from a character. Both carry the ORC
License notice and no setting names. Nothing on screen yet (#737). - [webapp] Character sheets and stat blocks are kept on the server. A
player character's page, an NPC's or a creature's can now carry a sheet,
made with the game's own definition (or a generic one for any game without,
and for a campaign with no system). Every value is checked against the
definition as it is saved; each field can be shown to the table, the party,
just its player and the GMs, or chosen people, and a character's player
always reads the whole of their own sheet. A GM can lock part of a
character's sheet. Autosaves that cross another window's edit are refused
with the sheet as it now stands, edits keep a history (one entry per person
per ten minutes of editing) that can be put back, and hit points and
conditions change at the table without filling that history. Rows added
from loaded rules keep their numbers if the pack is later taken down. The
page's stat block is written from the sheet, and every reader gets only the
lines they may see. A sheet exports as Quest Board JSON. Nothing on screen
yet (#737). - [webapp] Edit a character's or a creature's sheet, and it saves itself (#737).
Edit turns a sheet's source fields into inputs of the right kind (numbers,
text, choices that also take a name of your own, ranks as pips you press,
rows you add and remove on the generic sheet) while what is worked out stays
text and moves as you type. The header says where the sheet stands (Saving…,
Saved at, Not saved with Try again) and there is no Save button; what you
typed is kept on this device until it lands. If somebody else saved first, the
sheet says so and shows both versions of each field before you keep yours or
take theirs. The GM can lock a field or a group, and a locked one reads as
such to the player; each group that has its own audience can be set to who
sees it. Every save is a version, and any of them can be put back from
Versions. A person's or a creature's page with no sheet has Make a sheet, for
the GM and for a character's own player. Also: a page with no sheet no longer
ends its crumb on a Sheet that is not there, printing leaves the header
controls off the paper, a private group's pill names the player by their first
name, and a derivation whose constant is labelled with its own figure prints
it once. - [webapp] A character's or a creature's sheet, to read: Open the sheet from its page (#737).
The sheet is a document of its own with its pages (Statistics, Abilities &
inventory, Notes & actions, and Spells for a caster) in the Contents and, on a
phone, in the Contents sheet. Every figure is shown once with what it is made
of beneath it, on tap on a phone; ranks, hit points, dying, wounded and hero
points, strikes worked out from the inventory, feats by level, the inventory
with its bulk and where each thing is, spells by rank with how many slots are
left, and the character's own actions with their cost and full text are all
drawn from the system's definition, so Pathfinder 2e, D&D 5e and the generic
sheet are one page. An item whose pack has gone says so and keeps its last
numbers. What the page itself says (appearance, backstory) is shown with Edit
on the page; a group a player keeps for themselves and the GM wears the GM
colour. A reader who may not change the sheet is told so quietly. Export
gives Quest Board JSON, and Print the sheet prints every page. A page that is
not a person or a creature says it has no sheet. Read only: editing comes
next. - [webapp] The design system gains the character sheet's pieces: Derivation, RankPips, Track, Resource, Dice, StepStrip, WhatChanges and Picker, each with its states on
/design(#737, #364). - [webapp] Start a character, in the game system's own steps (#737).
Members & characters has a Start a character for you, and for the GM beside
each player. A Pathfinder 2e character walks Concept, Ancestry, Background,
Class, Attributes, Skills, Equipment and Details; a D&D 5e one walks Class,
Origin, Ability scores, Alignment and Details. Pathfinder attributes are
boosts by source, with the cap and how many free boosts are left. 5e ability
scores come from the standard array (drag a number to an ability, or tap both
to place or swap them), from point buy with its 27 points and the cost of each
score, or from rolling 4d6 and dropping the lowest, with every die shown, the
dropped one struck through, and no second roll where the method allows none.
The dice are rolled by the server and kept with the sheet. Next says why it
is held back, each step saves itself, and Finish makes the character's page at
once, with its prose sections named and empty, and the sheet is the player's.
A character left half-made is kept for 30 days as "Carry on with …" on Members
& characters (and on its page), and Open the sheet appears beside a character
that has one. - [docs] Lane 10's drawings, for review (#736, #824): the prototype's lane 8 page draws Test the
bot (D124, D149, D150), the two pages an email opens (D154, D155), Discord DMs on the notification
grid (D151, D161), the voice channel and "Where" (D159), and a recording announcement nobody could
post (D171), and brings its older stills into line with D96, D97, D123 and D151–D163. The owner's
answers to its questions are recorded as D221–D232, and Settings → Discord is redrawn with pickers
for the server, channel and voice channel (D226). Catalogue rows IS-443 to IS-458;
coverage/lane8.pyand the CAMP-12, CAMP-13, SESS-2, TABLE-4 and HOME-6 scripts classify them. - [bot] The bot says it can run Test the bot (D124, #824, #864).
GET /identity's
featureslistsrecord_testbesidedm, so Quest Board can tell a bot too old for
Test the bot before a GM presses the button (additive; bot contract unchanged). - [webapp] Admin's recordings table says when each recording goes. "Kept 90
days, as the campaign asks: until 23 November", or whose setting it is when
that is the instance's or its cap. The date is the one the nightly clean-up
acts on, taken from the same function;GET /api/admin/recordingsgains
audio_expires_at,audio_retention_mode,audio_retention_days,
audio_retention_sourceandcontent_approved(#872). - [webapp][bot] Taking a handout back deletes its Discord post or DMs (D157,
D238, #896). The handout path never recorded where it landed, so its own
Undo neither told anyone nor deleted anything — this closes both gaps.- A handout reveal now records where it went: the campaign's channel post
reuses lane 10'sshelf_reveals.discord_message_id/discord_channel_id
(POST /api/bot/reveals/{id}/message), and achosenaudience's DMs each
get a row in a newshelf_reveal_messagestable via a new, additive
POST /api/bot/reveals/{id}/dm-message. Migration8c9d0e1f2a3b, on top
of lane 8's map. The reveal is now committed before the bot is asked to
post it, so the bot's report of where it posted can never beat the row. - A handout's Undo and a shelf card's take-back are now one code path
(shelf_service.take_back): stamp it, tell the campaign's other GMs
(D238), commit, then publishreveal_taken_backcarrying every recorded
post or DM asextra.messages, so the bot deletes each — best-effort,
silent on a message already gone. Lane 10's single-post fields stay in the
event for an older bot.BOT_CONTRACT_VERSIONstays 2. - Handout embeds are ported to
faces.handout, the bot's one post shape
(EXT-2), with a golden row intest_faces.pyand a dev-preview specimen.
- A handout reveal now records where it went: the campaign's channel post
- [webapp] A quest can be changed (D242, #925). A quest's page shows its status, main or side, who gave it, the reward, the sessions it started and ended in, and its objectives; a GM edits all of these, with an objective kept for the GMs never shown to players. Changing the status puts a line on the quest's timeline. A GM confirms a rumour as a quest, or marks it not canon, from the quest log.
- [repo] A shared Claude Code setup for tiered delegation (#935). Specialist
agents in.claude/agents/:qb-implementerandqb-frontend(Sonnet,
medium effort),qb-preflightandqb-render-check(Sonnet, low), and
qb-test-runnerandqb-ci-triage(Haiku, low). Skills:/qb-test,
/qb-ci-log,/qb-render,/qb-preflightand/qb-deploy-dev. Workflows:
/qb-verify,/qb-build-sliceand/qb-review. The local CI recipes now
build test images from the Dockerfile'sbackend-devandbot-devtargets,
so they can no longer go stale. - [repo] The dev demo seed writes a GM copy of The Amberwake Accord. A fifth
campaign, "The Amberwake Accord (GM copy)", mirrors the original row for row
under its own ids, with the seats swapped: the owner's account owns and runs
it and is credited with everything Dana did as GM, and Dana plays the
owner's character. Every GM-only surface can now be reviewed on Amberwake's
data. Its GM's bell also has a session short of players and a recording
about to be deleted. The same builder writes both (engine.Variant), and
--resetrebuilds the copy with the rest (#951). - [webapp] A Test-the-bot run's AI usage now counts in the campaign's usage and credits like any other run, marked as a test (D227).
session_usage.session_idis nullable; a test row carriesrecording_test_idandpurpose="test"instead, and Admin → AI usage & speed shows it as "Test the bot". - [webapp] Settings → Discord chooses its server, channel and voice channel from
pickers, once Discord is linked (D226, D159). The bot answers two new internal
routes — the Discord servers it shares with a given user, and the text/voice
channels it can see in one of them — and the backend asks them only for the
signed-in GM's own verified Discord link, never a Discord id from the client. A
GM with no verified link, whose bot can't be reached, or who just prefers to, keeps
typing IDs, exactly as before. - [bot]
GET /discord/guildsandGET /discord/channels, bot-auth and additive
(BOT_CONTRACT_VERSIONstays 2): list a user's shared servers, and the channels the
bot can see in one of them. Both re-check that the given user is actually a member
of the guild in question before answering.
Changed
- [webapp] A wiki page in the bin now travels in a campaign's export, and
an import puts it straight into the bin (#1009). Since #990 a binned page
was left out of the file, so moving a campaign ended the days a GM has to
restore it (30 unless the instance says otherwise). It is now carried in
data.json(a newwiki_binsection; export schema 16) with the time it
was binned, and an import writes it already binned: for the GMs only until
one restores it, never live for a moment, and with the days it had left
rather than a fresh count. What stood
beside it comes back as it stood (its aliases, owners, relationships, name
targets, picture, shelf cards and prep items, the thread about it), so
Restore brings back what it would have in the old campaign. What was shown
at the table stays shown until a GM takes it back, as it does where it came
from: a card shown from the page, and the page's own reveal when it was
shown in a session, are still listed under that session for the people they
were shown to, by the title they were shown with; a player's handouts list
leaves the page out until it is restored. A binned page is in nothing a person reads:
no file underwiki/, no count inREADME.md, its picture under
media/bin/lore/. A page already past the bin's days when the file is
imported is deleted by the next daily purge, as it would have been where
it came from, so restore it as soon as you have imported. Every other bin still
stays behind, and an archive from before this release imports as it did. A
Quest Board older than this one refuses a schema 16 file by its version;
this one now says "made by a newer Quest Board … update, then import it
again" when it meets a file newer than it reads. A fact or a timeline row
that names a page further down the file now keeps its link on import, where
it used to lose it. Seedocs/API.md. - [webapp] Shortening how long a campaign keeps transcripts is an ordinary
confirmation now (#1010). It was the serious kind, with a red button, from
when the older transcripts were destroyed that night. Since #680 they go to a
bin and can be restored, so the question keeps its words and its offer to
export the campaign first, and loses the red. - [webapp] The footer is at the bottom of the page, not a strip pinned under it.
The version and the project, Help, Accessibility, Privacy and Licences
links used to sit under the page's scroll area at every width, taking two
lines of a phone's screen for good and cutting off the end of long lists.
They are now the last thing in the page itself, at the window's bottom on a
short page and reached by scrolling on a long one, beside the Contents
rather than under it (#1020). - [webapp] The party page shows XP and gold as their figures, with a small
History beside each. History opens every entry, newest first, in a dialog
("XP history" / "Gold history"), with the same Remove and Undo; the page no
longer lists them all (#1020). - [webapp] The "Quest Board" wordmark in the app bar is now a link to Home.
Inside a campaign it was only a picture of the name; it now takes you back
out to Home from every page of the signed-in app, and is named "Quest Board,
home" for a screen reader (#1020). - [repo] The Claude Code setup is now a thin layer over a shared, user-level set (#1037).
Quest Board's test recipes, preflight checks, release rules, deploy facts and UI
settings moved into a## Project profilesection ofCLAUDE.md./qb-test,
/qb-ci-log,/qb-preflight,/qb-deploy-devand/qb-renderkeep their names
but delegate to the global skills and agents, and/qb-verify,/qb-reviewand
/qb-build-slicedelegate to the global workflows. Theqb-test-runner,
qb-ci-triage,qb-preflightandqb-render-checkagents are gone, replaced by
the globaltest-runner,ci-triage,preflightandrender-check. The pull
request template now asks for achangelog.d/fragment instead of an edit to
CHANGELOG.md. - [repo] Prereleases are cut for the dev server, and the Quest Board hosts
run the published images. AvX.Y.Z-rcNtag now publishes release notes
rendered from the pending changelog fragments (changelog.py render, which
consumes nothing), and its images are:X.Y.Z-rcNonly. A committed
docker-compose.images.ymlpins the six app services to the release's images
and removes theirbuild:; its tags must equalAPP_VERSION, which
check_version_sync.pynow enforces. A host that adopts it sets
COMPOSE_FILE=docker-compose.yml:docker-compose.images.ymlin its.envand
updates by checking out a release tag. A self-hoster who builds from source
sets nothing and is unaffected.docs/OPERATIONS.mdcovers cutting a
prerelease, the image update, rollback and production updates (#1043). - [webapp] Cutting a recording into speech spans no longer starts ffmpeg
once per span (#684). With silence trimming on — and always on a managed
provider with a per-request size limit — every speaker's track is cut into
its speech spans before transcription, and each span was its own ffmpeg
process: around 1,300 of them for a three-hour, five-speaker session, most
of whose time was process start-up. The spans of a track are now cut
twenty-five to a process. The files are byte for byte what they were, so
transcripts, timestamps and the counts on a summarisation run are unchanged;
on the 30-minute test scene the cutting went from 208 processes and about 17
seconds to 11 and about 2. If a batch fails, its spans are cut one at a time
exactly as before and the worker log says so once
(… in one ffmpeg run failed … cutting them one at a time instead). ffmpeg
briefly uses a little more memory for a batch than for a single cut —
roughly 65 to 80 MB against 56 MB; the batch size was chosen to keep that
small in a worker limited to 1 GB. Nothing to configure. - [repo] The synthetic-session guard tests take about half as long (#684).
Besides the batched cuts above, the guards no longer re-runsilencedetect
over a track ffmpeg has already scanned unchanged (test-only; a rewritten or
copied track is always scanned again), and a new guard compares the batched
cuts of the scene's tracks with single cuts, byte for byte, in the CI job
that is not allowed to skip. - [webapp] Anyone who may edit a wiki page directly can upload or remove its
picture (D234, #734). It follows the campaign's Who may edit: a player's own
pages by default, any page they can see under "players anything", none under
"suggest" (D233). Setting a picture from a web address stays the GMs', since it
makes the server fetch something. - [webapp] Admin's Recording & retention, Backup, Import and Audit log are
rebuilt on their own pages (lane 9 page 4b, #735). Recording & retention
gains the default/cap split (D24) with IS-311's confirm before a tighter
cap catches a campaign's sessions, and says D179's retention promises
correctly. Backup queues without asking first (D12), polls while a run is
in progress, and no longer claims uploaded images are left out, since
D143 already put them in. Import a campaign now shares its form with
Home's New campaign → Import a file. The audit log filters by name
rather than id, groups "What happened" into categories, and reads its
detail as labelled lines with the record as stored one click further. - [webapp] Admin's AI models, AI usage & speed, Discord, Email and
Instance health are rebuilt on their own pages (lane 9 page 4c, #735).
The old 49-field Bot Settings tab splits into AI models (the capability
panel, the transcriber and summariser, the summary and wiki modes, and
who is using their own) and Discord (the bot's address and its key,
with IS-394's identity check replacing the token field per D139).
AI usage & speed gets an address of its own for the hardware preflight
and the usage report. Email's test now sends what is typed, saved or
not, and answers with the SMTP server's own reply instead of queuing.
Instance health has a screen for the first time: the AI queue's
free/busy/stuck verdict, polled every 10 seconds, and which secrets
this server can read. Also fixes a date column (Joined, Last here,
Started, When) wrapping mid-date on Users, Campaigns and Audit at 390px. - [webapp] Admin's Content packs, Content notices and Plans & entitlements
are rebuilt, and the legacy Admin page is gone (lane 9 page 4d, #735).
Content packs takes lane 3's kit pieces rather than the old Tailwind
component, and adds starter packs and an instance pack's own default
("Every member", AD-120). Content notices gets a screen for the first
time: where a rightsholder writes, filing and filtering notices, and
acting on one (turning a pack off, or removing it or just the records
named, all or nothing). Plans & entitlements shows the four prices
Plan & billing already reads, hosted servers only.pages/Admin.jsx
and its last two orphaned panels (AdminAiUsage,AdminHardwareProfile)
are deleted. Also fixes two dialogs (making an administrator, handing
the server or a campaign on) that drew no buttons at all, and a narrow
screen squeezing a table's columns instead of letting it scroll. - [bot] Commands, and a private way to raise a concern (lane 10, EXT-1, #736).
/recapnow also takes a session's number (/recap 13), resolved to the right
campaign in a server that binds more than one (O9);/next,/quests,/history
and/askresolve the same way, and every one of them offers acampaign:picker
when it needs to ask./historyand/asknow answer privately,/historylists
numbers instead of session IDs, and/asknames the sessions its answer was drawn
from — worked out by Quest Board, never the model (O16)./nextshows RSVP counts.
/noteand/help's words follow D97/D106. A new/concernopens a private
Discord form, in a server or a DM, that reaches only the campaign's GMs through the
same anonymous queue as the web's Table rules & safety flag (O13, D160) — nothing it
carries is ever echoed into a channel, logged, or stored with a sender. All
additive:BOT_CONTRACT_VERSIONstays at 2. - [webapp] The server side of the above (#736).
GET /api/bot/guilds/{id}/sessions/by-number/{n}resolves a session by number within a
guild's campaign;channel_id/campaign_idare new optional parameters on
/next-session,/quests,/sessions/listand/sessions/askfor the same
resolution, returning 409 with the campaign names when a server holds more than one
and neither narrows it./sessions/ask's response gainssources. A newPOST /api/bot/safety/submissionsis/concern's door into the existing, sender-less
safety queue, kept out of the access log the same way the web route is. No
migration. - [bot] Every Discord post has one shape (lane 10, EXT-2, #736). A coloured edge
for its kind (six tones that read on Discord's light and dark themes), a title in words
with the kind first and the session by number ("On: Session 14", "Cancelled: Session
14", "Recording saved · Session 14"), the facts as fields — When, Where (the
campaign's voice channel, D159) and Who's in — one link back to Quest Board and
none on a server with no public address, and Discord timestamps throughout. All built
in one place,questboard_bot/faces.py. A time added to an open vote is now also
posted on its own ("A time was added · Session 14"); a cancellation says who and why
(D101); the approved summary is the only summary post and says who approved it (D96);
no post promises a summary "when it is ready" any more, and asession_summarised
event still queued from before D96 is dropped rather than posted. The "Recording
stopped" line is gone: the table hears once, after the handoff. The bot's status reads
"Recording · /help" while any recording runs (D158).tests/test_faces.pypins every
face, andpython -m questboard_bot.preview --channel <id> --yesposts every one,
from invented examples, into a channel of the developer's own test server (D164, O17). - [webapp] Bot events carry what the posts need (lane 10, #736). Every session event
addsnumberandvoice_channel_id; the poll addscloses_at, the reminderrsvp,
the cancellationcancelled_by, the played postduration_s, the approval
approved_by, and a slate change says whether a time wasaddedand by whom. The
bot's/api/bot/sessions/{id}/timeslotsaddsnumber. All additive; the bot contract
stays at 2. - [bot] Reactions and taking things back (lane 10, EXT-3, #736). Reacting to a
confirmed session gains ❔ ("I might make it", D50) alongside ✅ and ❌, with mutual
exclusion between the three enforced on the message (Manage Messages permitting); the
confirmed post's footer says so. Voting on a proposed session now names the voter's
whole slate in the acknowledgement ("A and C work for you"), not only the option that
just changed. Taking a revealed card back down in Quest Board (O10) now deletes the
bot's own Discord post too, and tells the campaign's other GMs (reveal_taken_back, a
new bell kind). All additive:BOT_CONTRACT_VERSIONstays at 2. - [webapp] The reveal message, and its removal (#736).
POST /api/bot/reveals/{id}/messagerecords where ashelf_revealembed landed
(shelf_reveals.discord_message_id/discord_channel_id, migration
10b2c3d4e5f6);revoke_revealnow publishesreveal_taken_backfor the bot to
delete it, and notifies the campaign's GMs other than the one who took it back.
BotAttendanceRequest.attendedis now optional alongside a newrsvpfield
(yes/maybe/no); the vote endpoint's reply carries the voter'syour_answers. - [webapp] The D&D 5e sheet names what it adds up, as drawn. Each number's
working now reads "Chain mail 16 + Shield 2", "10 + 2 × (d10 → 8, 7) +
Con 2 × 3", "Str 3 + Prof 2" and "2 + ⌊(3 − 1) / 4⌋" instead of generic
labels, and penalties say their level ("Exhaustion 2") or their cause
("Chain mail, needs Str 13") (#737). - [webapp] Existing stat blocks become blocks on the new sheets, automatically.
On upgrade, every person's and creature's stat block turns into a creature
sheet for its game — Pathfinder 2e, D&D 5e, a game added by a content pack,
or the generic stat block when its game is gone — keeping who could see it:
a block everyone could read stays readable by everyone at the table, and a
GM-only or player-private one stays with the GMs. Nothing is lost: the old
block is kept with the sheet, a value that doesn't fit the new sheet is kept
aside rather than dropped, and the page's block is rebuilt the first time
it's opened. The stat block editor on a page, the importer, "Stat block" and
"Convert stats" all work on the sheet now; a drafted stat block fills the
new block's fields and, once approved, lands on the page's sheet instead of
in its text. Administrators can runpython -m app.sheets.rebuild_projections
(with--dry-runto see what it would do); the worker also does it once
when it starts (#737). - [docs] The lane 9 drawing gains Actions and Rest… for the sheet build (#737).
Notes & actions lists a character's actions with their cost and full text, and
the sheet header gains Rest… (D209); the drawing also drops its explanatory
lines, the recap promise and the Foundry export (D208, D210, D200). The
catalogue gains IS-459 to IS-462 and the survey is committed as
docs/design/lane11-build-contract.md. - [webapp] The old stat block editor is gone; stat blocks are edited on the sheet.
On an NPC's or a creature's page, the editor now shows "Open the sheet" when
the page has one and "Make a sheet" when it does not, in place of the form of
fields. Saving a page with astatsvalue is refused with "Stats are edited
on the sheet." Pages still show their stat block, and importing a campaign
still brings stat blocks in (#737). - [webapp] Pathfinder 2e sheets read the way the game prints them. The
sums under a number now name the attribute they use ("Dex 4", "Wis 4") for
class DC, spell DC and attack, and Strikes; Hit Points read "Ancestry 8 +
6 levels 60 (Class 8 + Con 2 each)"; and a condition that has a value says
it ("Frightened 1", "Stupefied 2", "Drained 1"). The numbers themselves are
unchanged (#737). - [webapp] Admin follow-ups and the demo seed's own surfaces (lane 9 PR 6, #879).
GET /admin/settings/bot/identitypasses through the bot's own reason for
a disabled Discord connection, so the Discord section says "Discord
refused the token" separately from "No Discord token set" instead of one
generic message. Instance health's "Campaigns' own keys (N)" line is real:
the secrets audit now counts every campaign's own transcriber/summariser
key and how many this host'sSECRET_KEYcan no longer read.AdminAi
andAdminRecordingregain the test coverage the legacy Admin page's
deletion (#735) didn't carry over — gap severity ordering, a provider
guessed from the address, the retention form's defaults and caps together,
and the wiki pipeline's mode change and error states. - [repo] The demo seed gains two thin campaigns purely for Admin's own
pages (#879, D145). The Salt Crown (archived) and Brinewatch (a
one-shot, binned), both owned and GM'd by an invented person who has not
signed in since — a stale owner, a failed recording, a transcript kept
"for good" for the retention cap-confirm to name, a second campaign pack
disabled after a content notice, and nine more audit-log events. Nobody
plays these two tables; seewebapp/backend/demo/README.mdfor why they
stay that thin. - [webapp] The email answer page now says when a session has started or been cancelled, and links to it; the Notifications page says which of Discord's two refusals stopped your DMs.
Answering a started or cancelled session from an email is refused (409) instead of reading as an expired link. - [webapp] Awarding XP is the GMs' alone (D241). The campaign's "players can add loot" setting still lets players add items and gold, but no longer XP; removing or restoring an award is the GMs' too.
- [repo] The backend test suite is much faster. The local-Whisper
/healthprobe is stubbed by default (tests that exercise it opt in with@pytest.mark.real_asr_probe), removing the ~8 s DNS-plus-timeout wait from about thirty recording-pipeline tests; a test network guard now fails any test that resolves an unmocked hostname (opt out with@pytest.mark.allow_network); and the synthetic-session harness (@pytest.mark.synthetic) is deselected from the backend CI job'spytest -m "not synthetic"since its own job already runs it. Full run: ~1,061 s before, ~536 s after (about 9 min). - [repo] CI's two slowest jobs now run in parallel pieces (#939).
- Vitest was running on one worker. CI jobs get 2 CPUs, and vitest's default is CPUs − 1, so the whole suite took 1,603 s. The frontend job is now two shards, each with
--maxWorkers=2. Lint, the build and the audit run once, in shard 1. - The journeys were running one project after another. They're now a matrix of three jobs (desktop, phone, keyboard), each with its own stack. The one-worker constraint (per-IP login rate limit, one shared seeded database) is per stack, so no spec changes.
- Three prep-sheet test waits no longer fail under load. Their explicit
timeout: 4000was shorter than the suite-wide 5 sasyncUtilTimeout. The two that wait on the 1 s autosave now wait 8 s.
- Vitest was running on one worker. CI jobs get 2 CPUs, and vitest's default is CPUs − 1, so the whole suite took 1,603 s. The frontend job is now two shards, each with
- [webapp] Session emails no longer repeat "Quest Board" as a heading under the brand line; they open with the news itself, as drawn (EX-040).
- [webapp] The design system reference page,
/design, now ships in production
builds, for server administrators only. Anyone else, signed in or not, gets
the same "not found" page as any unknown address. Development builds still
open it to everyone. - [webapp] Cards on the GM's table board are packed, and a new one lands beside
the last card on screen. A card nobody has moved used to take a slot by its
place in the whole shelf, so a shelf whose cards had mostly been placed by hand
drew a new one far down the board with empty slots above it. Unplaced cards now
fill the first free spots around the ones the GM placed (which never move), and
a note or a card added from Find goes next to the last card in view — to its
right, else under it — never on top of another card. - [repo] Changelog entries are now fragments in
changelog.d/, gathered
intoCHANGELOG.mdat release. Every PR used to add its bullet
directly at the top of[Unreleased]'s section lists, which clashed
between PRs merged close together. Now each PR adds a small file under
changelog.d/instead;make changelog(scripts/changelog.py assemble)
gathers them at release time, andmake changelog-check(also run by CI)
validates them. Seechangelog.d/README.md.
Fixed
- [webapp] Level up no longer offers another class's feat as if it could be taken. At a class feat step a Fighter was listed a rogue's feat, because feats carry their class as a trait rather than a prerequisite. It is shown disabled with the reason ("A rogue feat."), the way a feat with an unmet prerequisite is, and applying the level with it is refused. A feat can still be typed by name (D261, #1008).
- [webapp] A temporary hit point figure typed straight after a sheet opens is no longer put back to the old number. The box reset itself to the server's figure on mount, which could run after an early edit (#1017).
- [webapp] An article's text no longer sits against the rule under its
tabs. The Read tab now leaves the same gap below the rule as the other
pages, for a player's view and a GM's, at every width (#1020). - [webapp] On
/design, pressing anything no longer blanks the open panels.
The bell, menus, account menu and search drawn open closed on a press
anywhere else on the page, or on Escape, and nothing brought them back; the
ones drawn open now stay open (#1020). - [webapp] A page shown to you as a handout no longer logs a 404 for its map
markers. "On the map" asked about a page the reader may open through a
reveal, and the route only knew the page's own audience; it now answers with
the markers she can see, usually none (#1020). - [webapp] Opening a notification from the bell now marks it read. Following a
row's link (a click, Enter, or a new tab) did not clear it in a browser,
because the frame closed the panel before the row heard the click; the row
now marks itself first. The tick beside a row still marks it without opening
it (#1020). - [webapp] The page a player meets on a GM's prep address reads as
English. It said prep "is where each session is got ready"; it now says
"Prep is for the GMs of this campaign: it is where a GM gets each session
ready." Planning's own version of the line is reworded the same way
(#1020). - [webapp] A scene can be deleted from the prep sheet, and a card's "File it
in" menu there can be read and used. The sheet's Shelf section could make
and rename a scene but not delete one; each scene now has Delete beside
Rename, the same delete Table mode's Scenes panel has: at once, with an Undo,
the scene's cards staying on the shelf under "No scene yet" until the undo
files them back. The menu on a shelf card was drawn inside its list and cut
off at the list's edge, so only its heading and "No scene" could be seen; it
now hangs clear of the list, and it offers every scene with the card's own
marked, where it used to leave that one out (#1020). - [webapp] A search puts the thing you named first. Searching the wiki for
"Odile" listed Bastian Crowe, whose page mentions her, above Odile Marsh:
a wiki search had no ranking at all and answered in the wiki's browsing
order (kind, then title), where a character comes before an NPC. Every
search now follows one rule, written once on the server
(app/services/search_rank.py): the name that is the query, then a name
that starts with it, then a name with a word that starts with it, then the
same for its aliases, then everything that only mentions it — each of those
in the order the search had before. Case, accents and extra spaces don't
count. It applies to the app bar's search, the wiki's own, Table mode's
Find, the prep sheet's Rules & bestiary and Wiki drawers and its article
pickers, and the rules search, and it is applied before any limit, so the
exact match is never the row a cap drops. A player's results are ranked by
the names a player knows: an unidentified item by what the party calls it,
and a page they can't see is not there to be ranked. A rules record is also
now found by the start of any word in its name ("stalk" finds Reed Stalker). - [webapp] The wiki front page's results open under its search box. They
hung from the far right of the page's main column, away from what was
typed: the box is the app bar's search component, whose results hang from
the right edge of a box that, on this page, was as wide as the column. The
box is now up to 32rem wide and the results start at its left edge at the
same width, on a phone too. - [webapp] The last results in a prep-sheet drawer are no longer cut off by
the footer. The tool margin beside the sheet was sized from the window
(100dvh - 6rem), but it sticks inside the page's scroller, which is the
window less the app bar, any banner and the footer — so its last 36px or
more sat behind the footer, where neither its own scroll nor the page's
could reach. It is now sized from the scroller itself, measured, so it holds
whatever the bar and the footer do. - [webapp] A result found by its own name no longer says the name twice.
"Odile Marsh" sat over "npc · Odile Marsh". The line under it now says how
the page starts — in the text that reader may read — or, for a session, how
its summary starts; with nothing to say it is just the kind or the date. - [webapp] Table mode's Find no longer scrolls sideways. A row was as wide
as its whole snippet (778px in a 590px list), so the list scrolled across
and the snippet was cut at the list's edge. Rows now fit the list, the title
keeps its room and the snippet ends in an ellipsis. - [webapp] In the prep sheet's lookup drawers, "In the sheet" stays on one
line on a phone, and a preview that opens is brought into view so its
Insert is not below the fold of the Tools sheet. - [webapp] A GM can find the sweep, and the table, from a session that has
been played. "Sweep the shelf" was only at the foot of the Review tab's
checklist, and nothing on a played session's page led back to its table. A
GM now has both under the tabs on Read as well as on Review: Sweep the
shelf while any card from the session is still on its shelf, and Open
the table always — the evening's board, its scenes and what was shown are
still there. In Table mode the sweep's button is called Sweep the shelf…
too, in the bar and at the foot of the phone view, where it used to say
"End of session…". Closing a sweep opened from the session page takes
?sweep=1off the address, so reloading or going Back does not open it
again (#1020). - [webapp] Showing something to the table from a GM's phone no longer turns
large. A card put on stage on a phone was set in the desktop stage's
display type — everything a size up, its first line at heading size — when
only the title had been brought down for a phone; the phone's stage is now at
the phone's size, as drawn. "Show the table" opened a dialog whose field was
13px, which makes iOS Safari zoom the whole page in as the field takes focus;
on a phone the field is 16px. A long session or card name wraps instead of
pushing the page sideways, the bar's clock is the phone's size, and every
button on the phone view and in its dialogs is at least 44px, including
Leave, the stage's ×, ‹ and ›, and the sweep's rows (#1020). - [webapp] A thread marks the posts that were unread when you opened it.
Each carries a dot and the word "New" (not colour alone), with a "New since
you were last here" line above the first; they stay marked for the visit
and are read on the next. The thread's read answer gainsis_newon each
post andread_throughon the thread, both additive (#1020). - [webapp] The wiki's front page lists Quests under Browse by group.
Every entry type had a row there but a quest's, which was only reachable
from the Contents' quest log; the row opens it and counts the quest pages
the reader can see (#1020). - [webapp] The Contents' wiki chapters now include Events and Lore.
They were reachable only from the front page's Browse by group, so the
sidebar and the phone's Contents looked as if categories were missing; both
now list all six groups in Browse by group's order (#1020). - [webapp] Back and forward return to where you were on pages that load their content after the page opens. A session's Review tab and Admin → Audit log came back short, or at the top, because the position was set before the page was tall enough; it is now kept trying until the page can reach it, and stops the moment you scroll yourself (#1023).
- [webapp] The notifications panel no longer scrolls inside itself. Its hidden "unread" labels were not contained by the list, which made the panel taller than its box; the heading could end up half cut off at the top (#1023).
- [webapp] The wiki editor's Save row stays at the bottom of the window all the way down the page. It used to leave the screen once the reader scrolled past it to the stat block, picture and relationships; it is now the last thing in the editor and settles at the end of the page, above the footer (#1023).
- [webapp] The page no longer lets the whole window scroll under the app frame, and the bell and Contents buttons are 44 pixels wide on a phone. A visually hidden label far down a long page could make the window itself scrollable; the page's own scroller now contains it. The footer's links no longer leave a dangling "·" at the end of a wrapped line (#1023).
- [webapp] A menu no longer hangs off the side of a phone screen. A menu
hangs from one edge of the button that opens it, which is right only while
that button sits near that edge of the page. A wiki article's "See it as…"
wraps to the left on a phone, and its menu ran 30px off the screen with its
names cut short. Every menu now checks where it has opened and moves back
inside the page, and is never wider than the screen (#1029). - [webapp] Choosing a feat or a class from the loaded content no longer
loses the list while it searches. Level up's feat picker and Start a
character's "Choose from the loaded content" emptied their list for the
length of every search, said "Nothing matches" until the answer came, and
took a row out from under whoever had just reached it with the keyboard. The
results on screen now stay until the new ones arrive (#1030). - [webapp] Coming back to a discussion just as it finishes loading now
refreshes it. A thread, a list of threads and a Talk tab's count read
again when the window regains focus. A focus arriving in the instant the
page finished loading was dropped, because the page had not yet been told it
was loaded; the next one worked. It also made one test fail now and then on a
busy build machine (#1036). - [webapp] A flaky Preview test waits for the scroll it asserts (#1039).
The test for bringing the preview panel into view a second time checked the
call on the line after the entry's text appeared, which on a busy CI runner
is before the effect that makes the call has run. Test only. - [webapp] Accessibility gaps closed (#367). A transcript turning ready or failed is now announced, not only "working". Public pages move focus to their heading on a change of page, as the rest of the app does. The phone Contents sheet keeps Tab inside it and is announced as a dialog. A dialog whose opener has gone returns focus to the page's heading rather than the top of the document, and opens on its first enabled control. The account menu has an Accessibility entry, and the statement lists two more known gaps: small map markers, and status changes not always announced by every screen reader. The public pages Help, Privacy, Accessibility, Licences, auth-error and the not-found page are now scanned by axe in the journeys.
- [webapp] The admin tables for provider health, notices, backups and a person's campaigns can be scrolled from the keyboard (#367). Each scrolling table is a named region you can Tab to, as Admin → Usage, Users and Audit already were, so it can be read on a narrow screen without a mouse.
- [webapp] Links inside a sentence are underlined where axe found them told apart by colour alone, and Admin → Usage's tables scroll from the keyboard (#367).
The links are on About this server, Settings → Ownership and the wiki's Rules page (WCAG 1.4.1). The three tables on Admin → Usage can now be reached and scrolled from the keyboard on a narrow screen. The journeys' axe scan now also covers every campaign, wiki, session, profile and admin page no journey reached. - [webapp] A card's notes for tonight are no longer lost when the card
closes mid-sentence. The notes save a second and a half after you stop
typing. Closing the opened card, or the stage moving to another card, inside
that moment threw away what had just been typed. A save still waiting is now
sent as the field goes (#536). - [webapp] A character can no longer be given a name that another character
is "heard as". Listing "Wyatt" as a misspelling of Idani and then naming a
second character Wyatt was accepted, and from then on that misspelling
silently stopped being corrected in transcripts, with nothing to say why.
Creating or renaming a character, finishing a sheet, or assigning an imported
member to a name like that is now refused, saying whose misspelling it is. A
campaign already in that state is unaffected: no transcript was ever
rewritten wrongly, and its characters can still be edited (#634). - [webapp] Shortening how long transcripts are kept no longer destroys them
on the next nightly run (#680). A transcript past its campaign's window
now goes to a bin first, as a recording does, and is deleted for good only
once the bin's grace has passed — the same number of days the bin keeps
audio, never fewer than seven. While it is in the bin nobody can read or
search it, it is in no export and is sent to no model; the session's summary
and highlights are untouched. The campaign's GMs are told three days before
it is deleted (one notification when a session's recording and transcript
are due together; nothing new is posted to Discord), and a GM can restore it
from the session's Review tab. Lengthening the window again lets every
transcript it no longer reaches back out of the bin on the next run, so a
window shortened by mistake is undone by putting it back. Erasing a member
still reaches a transcript in the bin. Settings → Recording, retention & AI
now says where the transcripts go and for how long, instead of "There is no
bin behind it". Upgrading runs one migration
(sessions.transcript_trashed_at,sessions.transcript_deletion_warned_at);
a transcript the old sweep already deleted is not brought back by it. See
docs/OPERATIONS.md→ "How retention removes recordings and transcripts". - [webapp] A player's Handouts page no longer breaks when a handout was shown to her with no file of its own; it is listed as a page.
- [webapp] The party page's character links are underlined, not told apart from the text around them by colour alone (WCAG 1.4.1).
- [webapp] Maps are cut into tiles. The worker mounted the media volume
read-only, so every map's tiling failed on every install. It is writable
now; a customiseddocker-compose.override.ymlthat redefines the worker's
volumes must drop:rofrommedia:/app/media. A failed map now says why in
words rather than showing the server's error and file paths. - [webapp] A party's trail stays in its own campaign. Its routes did not
check the party's campaign, so a member of one campaign could read another
campaign's trail by its id, and a GM could add stops to it. A stop on a map a
player can't see no longer tells her where it is. - [webapp] Creating an atlas, a map or a marker no longer fails with a
server error. - [webapp] The first person to sign in to a new server is its owner as well as its administrator, so Admin's "Run by" line and hand-the-server-on work from the first day. The J1 journey now walks Admin's leftovers, Backup and the not-an-administrator stop in all three projects.
- [webapp] Fixed the Notifications grid's narrow-screen note always saying "always comes to the bell" even for the star-and-wish row, which shows on Home instead — it now names any such exception.
- [webapp] A mail app's own Unsubscribe button now stops the emails it is on.
TheList-Unsubscribeheader named the "Stop these emails" page, which answers
the one-click POST (RFC 8058) with a 405, so pressing Unsubscribe in Gmail or
Apple Mail stopped nothing. It now namesPOST /api/email/stop/one-click; opening
that address instead shows the page, which asks first. - [webapp] A sheet no longer says "Someone else changed this sheet" about
your own save (#737). Leaving a sheet, or a character being started, in
the moment its changes were being saved kept a copy of them on the device;
coming back, that copy was sent again and refused as a change made
elsewhere. What the server already holds is now let go of, and only what is
really still unsaved is put back. - [webapp] A character who has left the party stops collecting its XP
(#737). A retired character's sheet went on counting every award to the
party, and said a level was waiting. Their own awards are still theirs. - [webapp] Each "Choose from the loaded content" on Start a character says
what it chooses (#737). The Ancestry step had three buttons with one
name; a screen reader now hears which is the ancestry's, the heritage's
and the ancestry feat's. - [webapp] A failed recording's retention clock no longer restarts when its
session is written to (#798). A session whose processing only ever failed
counted its audio retention fromupdated_at, which moves on any write to
the row, so erasing a member from it (the erasure note) or editing its title
kept the audio for another full window. It now counts from the new
write-oncesessions.audio_received_at, stamped when the bot's audio is
first accepted. Migrationa798c0d1e2f3adds the column and backfills
existing failed runs fromupdated_at, so nothing is caught earlier or
later on upgrade day. The campaign export and import carry the new field. - [webapp] A campaign's export now carries its prep sheets, story arcs and
the shelf, and an import puts them back (#830). A GM who exported a
campaign to move it, or to keep a copy, lost their prep and their table
setup. The file (schema 15) now holds each session's sheet, beats, plan and
items (For later included), the arcs, and the shelf's scenes, pins, cards and
reveals, each section marked as the GMs'. A pin rides the later sheets and
shelves as it did, and a card taken off a shelf stays off it. A reveal comes
back no wider than it was shown (one for named people becomes the GMs'), and
one taken back comes back taken back. Discord message ids and Workbench runs
do not travel: a generated or a rules card reads from what it kept. Older
files import as before. Seedocs/API.md. - [webapp] A paused or unreachable Redis no longer hangs the request that
touches it, andnotify()stops trying recipients once the broker is down.
Every async and sync Redis client the app builds now has a 2 s connect timeout and a 10 s
reply timeout (the Celery side already had its own), built in one place,
app/redis_pool.py; the one client that waits on pub/sub (the live
recording-status stream) keeps no read timeout. A dead broker used to cost one
publish timeout per recipient; the first connection failure now skips the
rest of that batch (the in-app notifications are still written). (#841) - [webapp] Notification emails are sent again (#865, #736). The delivery task
read its SMTP settings on a second event loop inside its own, which cannot start,
so every per-person notification email since #655 was logged as "Failed to load
SMTP config" and dropped — including, once lane 10 moved them onto that path, a
group without Discord's time votes and confirmations (D153). The task now reads
the settings in its own session and sends off the loop. - [webapp] A phone's Contents no longer names Admin twice. With Home's
new "This instance" group on screen (lane 9 page 4a), the account rows
below it dropped their own Admin link rather than repeat the door under
the same name. - [repo] Two backend tests stop leaking rows into later test files (#869).
test_recap_emaildeletes the SMTP config row rather than nulling it, so it no
longer breakstest_admin's test-email tests when it runs first, and
test_backup_offsitecleans up the BackupLog rows its task tests commit. - [webapp] The Review checklist's "Recap email" row is truthful before approval (#871,
D162). Since the recap now fires on approval rather than at processing time, the row had
no record to read yet and showed a bare "No record". It now reads a server-computed
forecast — "To four people, once you approve" when the campaign has recaps on and someone
would get one, or "Skipped" with why (recaps off, or nobody on the attendance list has
opted in) — the same check the send itself makes, made read-only. - [webapp] Admin → Campaigns lists the campaigns in the bin, dimmed, with the day
each goes for good, as the drawing does. The page never asked the server for
them.GET /api/admin/campaignsgainspurge_at. Its Hand it on… dialog also
hands on to the member it shows selected, where it sent an empty choice unless
the list was touched first. A server with no campaigns says so (#872). - [webapp] Admin → Campaigns' "Keeps its own" column says what a campaign keeps
("Audio 90 days", "Transcripts for good") instead of always "Its own settings",
and a failed recording's error no longer ends in ".”." (#872). - [webapp] Admin's Audit, Users and Campaigns tables scroll sideways on a phone
instead of wrapping every cell over several lines. Each table has a width of
its own, dates and short cells stay on one line, and the scrolling box is
named and reachable by keyboard (#872). - The GM who confirms a session, books one directly or moves it is no longer sent a bell row and an email about it; the rest of the table still is (#887).
- [webapp] A page's picture is as private as the page (#894). Pictures were stored publicly, so a GM-only page's picture could be opened by anyone with its address. They are stored privately now and served only to people who can read the page. A picture uploaded before this moves to the private store the first time it is shown, and its old address no longer works. The backend's own media mount (used where nginx isn't in front of it) no longer serves private files either.
- [webapp] A player's wiki save can no longer rename an item that isn't
identified yet (#912, IS-122). A player only ever reads the party's name
for such an item, never its real title, soPATCH .../lore/{id}now keeps
the real title silently if she sends back what she was shown, and refuses
the save with a 403 if she sends anything else — a GM's rename still works. - [repo] Fixed every upload failing on a fresh install: the backend image now creates
/app/mediaowned by its own user, so a newmediavolume is writable (#914). - Choosing people to show something to, then clicking the next control in the dialog (such as "Send it to them on Discord"), now does both; the first click is no longer lost (#918).
- A session page no longer flips back to what it said before a GM's action (Withdraw, say) when a background refresh that left earlier lands late (#932).
- A slow answer can no longer overwrite a newer one anywhere the app re-reads while it waits: the Review tab, the vote results fold, and the Try again buttons (#941).
- [webapp] A raw
DELETE FROM campaignsno longer fails when a quest
objective was done in one of the campaign's sessions (#943). The objective
was reached by a cascade through its quest and a SET NULL through the
session in one statement; its quest foreign key is now deferred to commit.
The demo seed's delete-order workaround for it is gone. - [webapp] The demo seed's played sessions load again (#978). The seed wrote each played session's characters into
canonical_names, which holds the name generator's picks, so every one of them failed with "This session could not be loaded"; a test now builds every seeded session's page as its GM and as a player. - [webapp] One star, one wish asks about every campaign that played, at
once. A player in two campaigns that both played last night was asked
about one, and the other appeared only after answering and reloading,
which read as the same question twice. Home now shows one card per
campaign, each naming its campaign when there is more than one, and
GET /api/me/star-wishreturnsprompts(withpromptkept as the
first of them) (#980). - [webapp] Campaign Settings → Recording's "Test the bot" box has room inside
its border. Its text and buttons ran right up against the edge; the box now
has the same inner padding as the AI panel above it. - [webapp] The models that write about people are now told their pronouns, so a saved recap no longer has to guess. The session summary, its event log and key moments, the wiki suggestions,
/ask, the wiki's draft tools and one-line page summaries, and the GM's prep tools each get the pronouns of the people they can name, and only of those who have set some: players' (from their account), their characters', and those on wiki pages the table can read. A player's and their character's are sent as two separately labelled lists and never stand in for each other, and the model is told to use a name or singular they for anyone not listed rather than guess. Nothing is copied: they are read each time a prompt is built, so clearing them, leaving a campaign or deleting an account takes them out of every later prompt. Pronouns are personal data and now go to the configured model along with the names and transcript that already did;/privacyanddocs/OPERATIONS.md("What the LLM is sent") say so. (#999, finishes #653) - [webapp] A tap on a phone no longer goes missing while the page is still
scrolling. The page's scroller hadscroll-behavior: smooth, which
animated every scroll — the browser's own scroll to whatever took focus, a
restored position, the scroll to a button about to be pressed — and a tap's
click, which a phone sends a moment after the finger lifts, landed on
whatever had slid under it by then. It was J5's intermittent phone-only
failure (Strike, Reword and "Accurate" not taking). Scrolls are now
immediate; a jump you ask for (a Contents entry, an event's "at 1:09:12")
still glides, unless your system asks for reduced motion. - A scheduling test no longer fails on Tuesdays: it put a session past the 14-day RSVP horizon on that one weekday.
- [webapp] The web fonts are back. No production build had shipped a
font file since the design system moved to self-hosted fontsource faces, so
every page rendered in the browser's fallback sans-serif: Tailwind's PostCSS
pass lost the path Vite needed to find the font files, and Vite only warns
about that. The faces are now imported from JavaScript, and Vite emits them
with hashed names (none inlined, which the CSP would refuse).npm run build
now fails if anyurl()in the built CSS points at a file that is not there,
and nginx answers a missing/assets/file with a 404 instead of the app's
index.html(#891). - The wiki no longer names the demo campaign to everyone: its front page, a group and Recent changes said "the Amber Coast" and "Dana" in every campaign. They say "the GMs" now (D174).
- A wiki change reads "Dana started it", not "Dana new page it", on the front page and in Recent changes.
Security
- [webapp] The add-member and attach-to-an-account search needs two letters
before it finds anyone (#702). One letter, or an empty query, used to list a
slice of every account on the instance whichever scope was set; it now answers
nothing, and the page does not ask until two letters are typed.%and_
in a search are plain characters rather than wildcards. - [webapp] An unidentified item's true name no longer reaches players
outside the wiki (#877). A page with a name the party knows it by ("A
tarnished silver ring") showed that name on every wiki read, but not
everywhere a page is named. A player could read the real one in the
page's History, in the bell line, email and Discord post for a handout
shown to them, in a reveal's own title, in "another page is already
called that" when they guessed it, in a campaign timeline row with no
words of its own, in the quest log, the bot's/quests, the pre-session
reminder's open threads and the session page's open loops, in "Add to a
sheet", in the name a new character sheet starts with, in "… reached
level 7" and in a rest's "… has no hit dice". The wiki's list was also
sorted by the real name. Each now uses the name the reader knows; a GM
still reads the real one in their own views, and words written once for
everyone (a reveal, a Discord post, a milestone) carry the party's name.
A new test lists every read of a title in the server and fails on one
nobody has classified (tests/wiki/test_title_readers.py). - pypdf, which reads the page count of uploaded handout PDFs, is on 6.19.0 rather than 5.1.0, clearing the parser advisories pip-audit reports against it (#930).
- [bot] pyjwt 2.13.0 → 2.14.0, clearing ten advisories (CVE-2026-101917, CVE-2026-102265 to 102274) against the version the bot pulled in through redis.
- [frontend] Dev tooling: brace-expansion and fast-uri patch bumps, clearing four advisories (GHSA-q2hr-2g5m-vwhr, GHSA-qhr7-859c-m2p7, GHSA-6j4f-fj2g-mc7p, GHSA-hrr3-gc8f-f4qj) in build-time dependencies; nothing shipped to the browser changes.
- [webapp] A wiki page in the bin is no longer in a campaign's export, so
an import can no longer bring it back live (#990). The export wrote a
binned page like any other, and the import has no way to put a page in the
bin, so it came back as a live page with the audience it last had: a player
of the new campaign could read what the GM had taken away. A soft-deleted
row now stays behind, with everything in the file that hangs off it — the
page's Markdown file and picture, its aliases, owners, name targets and
relationships, any reveal of it, and the wiki cards, pins and prep items
that carry a copy of it. A row that pointed at it keeps its own words and
loses the link. The import also skips any row a file marks as deleted
rather than importing it live. Nothing that is only ended is left out: a
cancelled session, a hidden post, a reveal taken back and a card taken off
a shelf travel as before. An archive made before this release may still
contain binned pages, which import live — export the campaign again
instead of importing an old file. Seedocs/API.md. - [webapp] The draft tools (rephrase, expand backstory, promote, stat block, merge) and the relationship-proposal prompt now show the model an unidentified item by the name the table knows it by, so a model can no longer write its true name into text meant for the table. What a GM reviews and what an approval writes back are unchanged, and an approved proposal never renames the page to its player title. A proposal about an unidentified item now finds its page by the table's name instead of arriving as a new page (two items sharing a player title are not guessed between). (#993, follows #877)
- [bot] pyjwt 2.14.0 → 2.15.0, clearing CVE-2026-101918 against the version the bot pulls in through redis; and [webapp] serialize-javascript 7.1.1 → 7.1.2 in the frontend's lockfile (GHSA-gfhx-hw2g-v5hg), a build-time dependency that is not shipped (#995).
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- [webapp] The bell reaches older notifications with "Show older". The
-
v4.5.0-rc1
Pre-releaseSome checks failedCI / Docker image build (push) Successful in 22sRelease / Create Forgejo release (push) Successful in 32sCI / Does this change touch the recording path? (push) Successful in 39sCI / Bot/backend version sync (push) Successful in 1m8sCI / Backend lint (ruff) (push) Successful in 1m14sCI / Feedback site tests (push) Successful in 1m30sRelease / Build and push versioned images (push) Failing after 1m24sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m59sCI / Bot tests and audit (push) Successful in 3m4sCI / Frontend (shard 2/2) (push) Successful in 9m13sCI / End-to-end journeys (phone) (push) Successful in 9m39sCI / Synthetic session harness (no GPU, no LLM) (push) Successful in 9m37sCI / Frontend (shard 1/2) (push) Successful in 10m50sCI / End-to-end journeys (keyboard) (push) Successful in 10m19sCI / End-to-end journeys (desktop) (push) Successful in 10m45sCI / Backend migration, tests, and audit (push) Successful in 35m58sreleased this
2026-10-03 15:28:44 +00:00 | 876 commits to main since this releasePrerelease of 4.5.0 for testing on the dev server; not for production.
Added
- [webapp] The bell reaches older notifications with "Show older". The
panel showed only the newest twenty; a button at the foot of the list now
adds the next twenty, moves focus to the first of them, and goes away when
nothing older is left (#1000). - [webapp] Setup's AI step asks for the model's context window. It is
the same field as Admin → AI models ("How much it can read at once"), saved
through the same setting, so a server set up through the wizard no longer
falls back to 32,768 tokens without anyone having been asked (#1001). - [webapp] In a campaign that levels by milestones, the GM can hand a level
to the player (#1007). Until now only the GM could level a character
there. Beside Level up in the sheet's header a GM now has "Let Priya level
up": it marks the character's next level as waiting, the player is told in
the bell, and their sheet says "Level 4 is waiting" with Level up and Not
yet, exactly as it does in an XP campaign when the XP is there. The GM's
header then reads "Level 4 is waiting for Priya", with Take it back.
Reaching the level clears the mark, whether the player does it or the GM
levels the character as before. It is one level at a time, a lock on Level
still stops the player, and the server refuses a player's level-up in a
milestone campaign unless the mark is set. Marking a level makes no version
of the sheet, is not part of a sheet's export, and nothing about it goes to
email or Discord. An XP campaign is unchanged: its levels wait on the XP.
The demo seed's Hollow & Ash now levels by milestones, with Tam Ridley's
level 4 waiting. Migrationc1007a1b2c3dadds three nullable columns to
character_sheetsand thelevel_waitingnotification kind. - [webapp] A result in the prep sheet's Rules & bestiary and Wiki drawers
can be read before it is inserted. Its title opens the entry in the
drawer, in the results' place — Table mode's own Find preview: a wiki page
as the GM reads it, a creature as its block, a rule as its text — with
Insert still there and "Back to results". Escape goes back too, and focus
returns to the result it came from. It works the same in the phone's Tools
sheet (IS-472). - [webapp] The campaign search has a page of results, and a rule you find opens
where you are (#1033). The results under the search box now show three of each
kind — sessions, people, wiki, rules — then "All 14 in Wiki" and "See all 23
results", which lead to a page listing every match, grouped the same way, with a
row of links to narrow it to one kind. Pressing Enter in the box goes there too.
Until now a kind stopped at ten with no way to the rest, and "Rules · 10" could be
hiding more. The page has no search box of its own: the one in the bar is still
the only one. A GM gets the same "Look inside transcripts" switch on the page, and
a player still never sees a GM's material there. Choosing a rule used to do
nothing; it now opens the rule in place — its text, or a creature's block — and
closing it puts you back on the result you chose. The wiki's own search box works
the same way. A kind with more than 200 matches lists the first 200 and says how
many there are. - [repo] The dev demo seed has lane 8's wiki and lane 10's bot tests. Both
parties and who has been in them, reputation, downtime, custody and an
unidentified ring; the 27 quests as pages with givers, rewards and
objectives; mid-text:::gm,:::forand:::partyblocks and pages for
every audience; six handouts with their files and reveals; private page
pictures; three atlases with maps drawn and tiled at seed time, markers of
every kind and the party's trail; players' suggestions (the Tuesday Tide is
now "Players suggest, I decide"); the link index and Recent changes; and six
finished bot tests with what they spent. Files go under
media/private/, only after the commit. The map tiling task's cutting is
now a purecut_tilesthe seed calls directly (#734, #736). - [webapp] An article's read view, History, More, Recent changes and Deleted
articles (lane 8 PR P2, #734; WIKI-3, WIKI-5, WIKI-6, WIKI-17, WIKI-18).- An article (
/wiki/:entryId) renders the body throughWikiText
(PR P1) with the server's own?as_user=for a GM's "See it as…"; a page
outside the reader's audience says only "This page isn't shared with you"
(W3). Relationships, appearances, mentioned-in, references and a timeline
follow, with a margin for the picture, facts, a stat block, and (an item
page) who has had it. - History (
/wiki/:entryId/history) restores through the server's own
POST …/versions/{id}/restore, replacing the old article page's plain
PATCH, which never called it. - The ⋯ More menu: adding to a session's shelf, the three LLM drafts,
Convert the stats, merging a page into this one, and deleting with an
undo — replacing the lastalert()-shaped delete in the product. - Recent changes (
/wiki/recent) and Deleted articles
(/wiki/deleted, GM only) round out the wiki's four remaining P1/P2
surfaces; the front page's "All recent changes" now leads to the former. - Lane 7's Read · Talk · History tab bar is kept; History is now a plain
link to its own page rather than an inline panel. - The read half of
WikiArticle.jsxis deleted; its edit half stays at
/wiki/:entryId/edituntil PR 3 rebuilds the editor. - Frontend only: no backend or bot change.
- An article (
- [webapp] A draft, Suggested wiki updates and Wiki health on the new wiki
(lane 8 page P4, #734; WIKI-9, WIKI-10, WIKI-16).- A draft shows the page beside the draft with each stage in plain
words, typed stats, Send it back and Throw it away. A player's
suggestion opens here too, compared line by line with the page as she
reads it, never the GM's text. - Suggested wiki updates shows where each session's suggestions stand,
then the pages and relationships waiting, one action at a time. - Wiki health lists wanted pages, unlinked mentions, orphans, possible
duplicates and stale pages, each with its fix. - The Contents gains Wiki health for a GM, and Suggested wiki updates shows
its count.
- A draft shows the page beside the draft with each stage in plain
- [webapp] The wiki's editor, a new page and Settings → Wiki, rebuilt on lane 8's
text (WIKI-7, WIKI-8, CAMP-14, D72, #734). Write on the left and the page on the
right (Write | Preview on a phone), with a toolbar,[[autocomplete, and ◐, which
opens the audience picker and writes a:::gm,:::partyor:::forblock for
whoever was chosen. One Save sends the text, the fields, the facts and the timeline
together; the text is kept on the device as it is typed, per person. A save that
started from an older version says who saved since and offers merge by hand, use
theirs or keep mine. One heading's section can be edited on its own. Pasting a
picture uploads it to the page's private files; pasting from a document brings its
headings, bold and lists across as Markdown. A GM sets who sees the whole page
(D166). Relationships, "Who plays them", the picture, the stat block and links you
could add moved in from the old edit page. A player's save is only ever her own
copy of the page, edited: the blocks she can't see are never in the editor, and
the server puts them back where they were. A new page starts from its kind's
template; where players suggest, it goes to the GM's queue. A name two pages
share opens a page listing both (D72), and "See it as…" can now pick a character.
Settings → Wiki sets who may edit, what each kind of page starts with, and the
reputation levels.WikiArticle.jsx,WikiNewEntry.jsxandWikiSearch.jsxare
gone. - [webapp] History per reader, Recent changes, Wiki health, merging two pages,
personal notes, and canon in a block (lane 8 wave 2 PR 2b, #734; IS-051,
IS-055 to IS-060, IS-105, IS-147, IS-148, D1, D69, D197, D219). Server only.- History (
GET /lore/{id}/versions) opens to anyone who may edit the page,
not only the GM and the primary owner, and drops a version that changed
nothing this reader could see. Restoring an earlier version widens the same
way: a character's own player may restore her own page, safely — her restore
is merged against the live body, never overwriting a block she can't see. - Recent changes (
GET /wiki/recent) is one feed — made, saved, deleted,
put back, renamed, merged — filtered so nobody learns that something they
cannot see has changed. Migration8c3d4e5f6a7baddswiki_events. - Wiki health (
GET /wiki/health, GM-only): wanted links, unlinked
mentions, possible duplicates, orphans and stale pages. - Merging two pages (IS-060): "Merge or Keep both" starts the existing
LLM-merge draft (POST /lore/{id}/merge); approving it now also moves the
other page's aliases, links, relationships, references, appearances, owners,
personal notes and Discussions threads onto the survivor, and trashes it. - Personal notes (IS-105,
lore_personal_notes): a player's own notes on a
page, read by the writer and every GM (D197), and never AI context. - Canon answers wrap in a block (IS-148, D219) instead of being refused: a
party or private answer is written as:::partyor:::for, naming a player
as well as a character, pre-filled from the thread's or post's own audience.
A new page made this way starts at that same audience, so its title doesn't
leak to someone who can't read the one block on it. Every canon write adds a
decided infootnote automatically, and a backfill adds one to every post
already made canon. - The NPC tracker folds into the wiki (D1): the migration makes a People
page for any tracked NPC with no page of that name, carrying its session
appearances. The old tracker stays working for one more release. - The 60 s wait before wiki proposals land is now usually instant: each
match pass fires consolidation itself once every category is in, rather than
waiting for the 60 s backstop — which still runs, and now waits longer for a
slow local model before giving up.
- History (
- [webapp] The wiki's front page and a group, rebuilt (lane 8 PR P1, #734;
WIKI-1, WIKI-2, D60).- The front page (
/wiki) leads with a search box scoped to the wiki and
the rules, then "Recently revealed or updated" (the top of Recent changes,
D60), "People you've met" (appearances the reader may see, most recent
session first), and Browse by group. A GM's copy also carries "Waiting for
you" (the top of Suggested wiki updates) and Wiki health's counts. - A group (
/wiki/g/:group) lists a group's pages A to Z, narrowed by
type with a chip when it holds more than one — People, Places, Groups and
Things are now Contents entries; Events and Lore stay reachable only from
Browse by group.WikiFront.jsxand its test are deleted. WikiText(src/wiki/): the parser and renderer for the wiki's block
syntax (:::gm,:::for,:::party, footnotes,[[links]]) that PR P2
wires into the article page — built here, tested, and not yet on any page.- Frontend only: no backend or bot change.
- The front page (
- [webapp] Handouts, private files, and rules' "Make it ours" (lane 8 wave 2
PR 5, #734, §3.8, §3.10; W7, W8). Server and the bot's own side; the pages
follow.- A handout is a
documentwiki page with a file (lore_attachments): PDFs
are validated by magic bytes with their page count read, images re-encoded
through the existing picture pipeline. Migration8a7b8c9d0e1f, on top of
lane 8's quest pages. - Private files stay private (W7). A file narrower than the table no
longer lives at a guessable, cached-forever address:GET /api/campaigns/{id}/files/{attachment_id}decides on every request whether
the caller may have it — the page's own audience, or a live reveal's — and
answers withX-Accel-Redirectto a newinternalnginx location that
refuses a direct request. - Reveals carry an audience, a page and an optional session (W8).
shelf_revealsgainslore_entry_id,piece_label(for a torn letter
shown in pieces), and lane 7's audience columns;session_idis now
nullable, so a handout can be shown between sessions. A handout's own
audience is the union of its live reveals. handout_shown, a bell kind ([wiki]category) and a new, additive bot
event: the bot fetches the file withGET /api/bot/files/{id}and posts it
to the campaign's Discord channel, or DMs it to each named person instead of
posting to the channel.BOT_CONTRACT_VERSIONstays 2.- "Make it ours" (
POST /api/reference/{id}/make-ours): a rules-pack
entry becomes a GM-only campaign page that keeps a link back
(source_reference_entry_id), so the rules chapter needs no endpoint of its
own.
- A handout is a
- [webapp] Handouts and Rules & bestiary, two new chapters of the wiki
(lane 8 page P6, #734; WIKI-14, WIKI-15).- Handouts (
/wiki/handouts): a GM's handouts by session with who each
went to, the ones not shown yet first as GM prep, and a reveal outside
play as "Between sessions". Show it picks who (the audience picker,
characters first), posts to Discord unless switched off (D61), says
when, and names the piece of a letter shown in parts; Take it back
uses the server's own revoke route. A player reads what was shown to
her, by session, "Just you" on hers alone, and may add one (just her and
the GMs) where the campaign lets her write pages. - A handout's own page shows its file: an image, a PDF previewed in the
browser's viewer with "Download the PDF" beside it (download only on a
phone), or plain text. Every file is read through the authorised files
route, never a/media/address. - Rules & bestiary (
/wiki/rules): search the campaign's loaded rules,
each hit tagged as rules; a rule opens with its source, a creature as a
stat block, and a GM can Make it ours. The licence line for each
loaded pack sits at the foot; with no pack loaded the page says where a
GM loads one. - Server (#916): a live reveal that covers a player opens that
handout's page to her to read (rendered for her, so GM blocks never
reach her), and nothing else — not the wiki's lists, search or any
write; taking it back closes it again.GET /api/campaigns/{cid}/handouts
lists the handouts shown to the reader, grouped by session. A player's
own handout page is hers and the GMs'. A page made with "Make it ours"
says which rules entry it came from. - Both chapters join the Contents' wiki group.
- Handouts (
- [webapp] The wiki's links, references and names, one Save, and who may edit
(lane 8 wave 1 PR 2a, #734, #779; IS-033, IS-036, IS-037, IS-042, IS-046, IS-048,
IS-049, IS-053, IS-054, IS-085, IS-088, IS-399; D72). Server only; the pages follow.- Links are records. Every
[[link]]and footnote in a page's text is written
tolore_links/lore_referenceson each save (and backfilled by migration
8b2c3d4e5f6a), each with the block it sits in, read per reader: "Mentioned in"
never names a page, or a link inside a block, the reader can't see. An article
now says what each name it links means to its reader (a page, the
disambiguation page, plain words for a page they can't see, or wanted), and
lists the footnotes they may open (https, a thread or post, a moment in a
session, a rule), numbered for them. The server never fetches a URL. - Names (D72). A name another page has is a 409 that says what to call the
two ("Kettlebridge (town)", "Kettlebridge (boat)"); the bare name then opens a
disambiguation page (GET /lore/by-name/{name}), unless a GM points it at one
page. A renamed page keeps its old name as another name, and can rewrite the
links that use it; a renamed character keeps their:::forblocks. - One Save. A save sends the
version_idit started from; if someone saved
since, it is a 409 carrying the page as it now is, and nothing is saved. A
phone can save one heading's section (GET /lore/{id}/sections/{n}). - Who may edit (Settings → Wiki): only the GMs, players their own pages (the
default, and today's behaviour — now including pages they made, which they may
start), players anything, or players suggest. A suggestion — a new page, an
edit, a relationship — waits in the GMs' queue and is merged as its writer, so
nothing they couldn't see is touched. - Relationships have an audience of their own and may start and end in a
session. Templates per kind and the reputation levels (Pathfinder's
seven to start) are the campaign's to change; exports carry all of it.
- Links are records. Every
- [webapp] The map, a new chapter of the wiki (lane 8 page P7, #734; WIKI-11).
- The map (
/wiki/map) draws each map over its private tiles, with the
atlas's levels as crumbs, filters by kind, the party's trail (a stop per
place, its sessions and days on the road) and "The party is here". A
marker opens its own map, travels a link, or opens its page; a GM reveals
a GM-only marker to the players, with an undo. - A player adds her own markers, for herself and the GMs, the party, or the
table (IS-076). A GM adds a map, as its own atlas or opened from a marker,
and where the party ended a session. - Every marker is also listed under the map, and the map pans with the arrow
keys and zooms with its buttons. - The wiki's front page says where the party is (IS-124), and an article
says where its page is on the map (IS-102, IS-087).
- The map (
- [webapp] The party, what it carries, where it stands and what it did between
sessions (lane 8 wave 2 PR 3, #734; IS-107 to IS-122, IS-088, IS-116, IS-206,
IS-218 to IS-221; D168, D198, D12). Server only; the party page follows.- Parties. Every campaign has a party, and may have more (
/parties), with
who has been in each over time: joined and left at a session, with why
(left, retired, died, captured, swapped), and companions, hirelings, mounts
and vehicles as pages. Swapping characters keeps the record in step. Migration
8d4e5f6a7b8cmakes each campaign's party and places every character at the
first session they were played in. - "The party" names one party (D168). Threads, posts, pages, relationships,
facts and blocks can be for a party by name (:::party The Tuesday Watch);
everything already for "the party" is the first party, so with one party
nothing changes, and a renamed party keeps its blocks. - Items belong to characters (D198): a line is held by a character, the
party's pool, an NPC or a place, can link its own page, and records who has
had it. An unidentified item is named as the party knows it everywhere a
player reads it, until a GM identifies it. Existing lines, XP and gold went
to the character each player was playing in that session. - Removing a line, an award or a movement is undone (D12), not confirmed.
- Where the party stands with each organisation, changed at a session, on
the campaign's levels; and downtime, logged by the player and approved by
the GM, with Pathfinder's activities to pick from.
- Parties. Every campaign has a party, and may have more (
- [webapp] The party and the quest log, as wiki pages (lane 8 PR P5, #734;
WIKI-12, WIKI-13).- The party (
/wiki/party): each party's home base, fund, XP pool,
members with pronouns and companions, and active quests; What we carry
(notable items, then the ledger, filtered by holder; add, give, change);
XP and gold side by side with each character's total; downtime with
Pathfinder's activities to pick from and a GM's Approve; Who has been in
it, current then former, with moving a character to another party and
why; and Where we stand in the margin. Removing a line is undone, never
confirmed (D12). A player adds to the ledger only where the campaign
allows it and changes only her own lines (D6); an unplaced row counts in
the main party (D236). - Quests (
/wiki/quests): Rumours waiting for the GM first, then each
status, with its colour, a failed quest struck through, when it started,
and "The quest log did not load" rather than an empty list. A new quest is
a new page, opened in its editor. - The Contents gains Quests and The party under The wiki; "Quests & loot"
goes./campaigns/:id/managenow redirects to the party page, and the
old campaign page is deleted withLootLedger,QuestLogand the
components only it used. An ownership offer is answered with Accept and
Decline on This week's banner itself. - Frontend only: no backend or bot change.
- The party (
- [webapp] Quests are wiki pages now (lane 8 wave 2 PR 4, #734, §3.7; D196).
Server only; the quest page follows.- Every existing
plot_threadsrow becomes a page of kindquest, keeping its
title, status, sessions and reward note: the GM's private notes move into a
:::gmblock at the top of the body, andplayer_visiblebecomes the page's
own audience —tablewhen it was visible,gmwhen it wasn't. Migrations
8e5f6a7b8c9dand8f6a7b8c9d0e. - A rumour may be about no quest yet (D196/W10). Confirming one offers
"Make it a quest," which creates the page and posts it under whoever started
the rumour. - The bot's
/quests, the pre-session reminder's open threads, Prep's context,
the shelf's open loops and the Quests drawer's quick add all read quest pages
now;GET/POST/PATCH/DELETE .../planning/threadsandGET .../questsare
removed in favour ofGET/POST .../lore/quests, filtered per reader like any
other page. The bot's response shape is unchanged — no contract bump.
- Every existing
- [webapp] "Test the bot", a GM's way to check the recording pipeline
before the evening without a real session (lane 10, PR 7a, #736, D124).
Settings → Recording, retention & AI gains a launch (30 seconds, stoppable
early, keep-or-delete after), the running stages, the result — per-stage
timing, the transcript, the summary, and a plain sentence naming who can
fix a failure — and the kept test runs, with a confirmed delete (D12). The
session page offers a link to it while Start is refused for being early
(D149, D222). - [webapp] Two public pages for an email's links, the notification grid's
Discord DM column, and the installed app's update and offline notices
(lane 10 PR 7b, #736)./email/stopand/email/answerwork signed out
(D154, D155): stopping a kind of email needs one click and never reveals
more than the token names, and answering "Are you in?" records the RSVP
through the same writer the session page uses. The grid's DM column shows
"Discord can't reach you" when the bot's last message failed, without
hiding the switch (HOME-6), and the star-and-wish ask is its own indented
line under Sessions (D232). The installed app now offers a quiet "Quest
Board has been updated" notice instead of reloading silently (D13), says
so while offline, and no longer locks to portrait orientation. - [webapp] Email grew a one-click stop, answering "Are you in?" without signing in,
and a proper recap timing (D154, D155, D162, O7, O8, O15, #736). Every outbound
email now goes through one template, withList-Unsubscribe/List-Unsubscribe-Post
headers (RFC 8058) and a footer link to/email/stop(POST /api/email/stop, with an
undo). Session-confirmed and reminder emails carry "I'm in / Maybe / I can't" buttons
that record an RSVP through/email/answer, good until the session starts. Calendar
invites carry the session number inSUMMARYand aURL:back to it, no longer ask
for a reply (RSVP=FALSE— nothing read the old one), and put a cancellation's reason
at the top ofDESCRIPTION. The post-session recap now sends when the GM approves the
summary, not the moment it is first written, and never twice. A developer-only
GET /api/dev/email-preview/{kind}(behindENVIRONMENT=development) renders every
template from invented fixture data, for reviewing them without real mail. - [webapp] The star-and-wish ask is its own row under Sessions in notification
settings (D232, O29, #736). It used to share the "Sessions" cell; existing
preferences carry over as the new row's starting value (migration10d4e5f6a7b8). - [repo] Journeys J2 and J4b cover lane 10 (#736). J2 asserts what reaches
the bot — a vote's number and closing time, the confirmation's number, a ❔ from
Discord showing on the session page, the cancel reason, and a Discord DM with the
bell's line — and what reaches Mailpit: the confirmation to the player's own
address, the time in her zone, the unsubscribe headers, answering from the email,
and Stop → Undo → Stop. The new J4b tests the bot from Settings → Recording and
checks nothing else happened, and that a recording whose announcement lands
nowhere still records and says so (D171). The fake bot hands a test's
generated near-silent WAVs over as the real bot does. J2's bell check no longer
depends on how many rows earlier runs added (#887). - [repo] The dev demo seed has character sheets and stat blocks (lane 11,
wave A). Every sheet is made through the sheet services the routes call,
from the mini character packs loaded as campaign packs. The Amberwake Accord
(and its GM copy): Ossian Vale exactly as drawn, Wren's Spells page and focus
pool, Bastian, Sister Quill, the owner's Hollis (Level & XP locked by the
GM, "just you and the GM" campaign notes, and a row from the disabled Salt
Crown pack that keeps its name and numbers), retired Aldous and Hesper's old
sheet; conditions and hurt HP on two; every old stat block converted, Odile
Marsh filled out to the drawing, one block partly shown to the party, one
wholly the GMs', one projected from Hesper's sheet; a rules creature on
Session 24's shelf with its own hit points; histories of 3 to 6 versions.
And a new thin 5e campaign, Hollow & Ash, where the owner plays Tam
Ridley (Fighter 3, HP 31 from rolls of 8 and 7) with four more characters
and two blocks. - [webapp] Sheets reach Admin, the rules, Prep and Members & characters (#737).
Admin → Content packs lists every sheet definition on the instance — built
in, from a pack, or made from a pack's older stat blocks — with its version,
whether it is active or deprecated, how many sheets are on it and how its
fixtures did; a pack's row names its own definitions and which sheets are
still on an older version. Loading a pack that carries definitions shows the
validator's report for each: activated or not, every error (with a "did you
mean" where there is one), every warning, and which fixtures failed and why.
One error stops that definition only; the pack's content still goes in, and
a campaign's own pack says its definitions are not used. An open rules
result has Add to a sheet ▾, listing the characters and blocks the reader
may edit that take it — a player's own character, a GM's every sheet — and
puts the entry on with where it came from; a reader with nothing to edit
gets no menu. Instant NPC can Save as a page, with a block: a new NPC
page with a block drafted for the campaign's game, waiting in Suggested wiki
updates to be checked (no button where the game has no block to draft).
Members & characters has Open the sheet on a character whose page has
one, and the members list names each character'ssheet_id. - [webapp] Level up on the sheet, with XP from the campaign's ledger (#737).
A character's XP is now every XP award to them, or to their party, since
their sheet was made. When it reaches the next level the sheet's header says
"Level 7 is waiting", with Level up and Not yet; a GM can level any character
at any time. Settings → General chooses whether the campaign levels by XP or
by milestone; a milestone campaign shows no XP and its GM says when. Level up
walks the level in the system's own order: hit points (Pathfinder's worked
out from the level; 5e's rolled on the class die or taken as the fixed value,
the die kept with the sheet and the roll standing), the class's own features,
each feat from the loaded content (one that can't be taken yet is shown,
disabled, with why), a skill increase as far as the level allows, attribute
boosts, and 5e's improvement (+2 to one score or +1 to two, none past 20) or a
feat. What changes lists every number that moves, before and after, and why.
Reach level n keeps the sheet as it was as a version first, so it can be put
back, and adds "Ossian Vale reached level 7" to the campaign's milestones, on
the last session played. - [repo] J7, the character journey, and the rest of the demo's sheets
(lane 11, wave B).make journeysnow walks a character from the first
step to level 2, on desktop, a phone and the keyboard alone, with an
accessibility scan at every page: Priya starts a Pathfinder character from
Members & characters (ancestry, background and class from the loaded
content, attributes by boosts, nothing rolled), leaves it and carries on,
and finishes it; Dana shows Odile Marsh's block to the table by field, and
Priya is handed the shown lines and no others at the table, on the page and
in the answer behind it; Dana awards XP, the sheet says "Level 2 is
waiting", and Priya levels up through What changes to Reach level 2, which
keeps a version first and adds the milestone. The journeys' seed gains a
campaign for it, with the sample character pack imported through the GM's
own import. The dev demo seed gains: "Level 7 is waiting" on Ossian Vale's
sheet, reached through the XP ledger; Tam Ridley's third level taken on the
sheet, with its hit-point die kept and "Tam Ridley reached level 3" in the
campaign's history; and two characters begun and left, waiting on Members as
"Carry on with …" — one with its 4d6 kept, the lowest die of each struck. - [webapp] D&D 5e character sheets and stat blocks, built in (SRD 5.2.1).
The character sheet works out ability modifiers, saving throws, the
proficiency bonus by level, all eighteen skills with proficiency or
expertise and Passive Perception, Armor Class from the armor and shield
worn (medium armor capping Dex at 2), hit points from each level's rolled
or fixed hit die plus Constitution, Hit Point Dice, death saves, weapon
attacks and damage, and spellcasting (save DC, spell attack, slots for full
and half casters, Pact Magic). Exhaustion and the conditions that stop a
creature moving change the numbers they affect. Creation (standard array,
27-point buy, 4d6 drop the lowest), levels 1–20 with the Ability Score
Improvement or a feat at 4, 8, 12, 16 and 19, short and long rests, and
the stat block in the SRD's order are all defined. Old 5e stat blocks map
onto the new block. Includes material from the SRD 5.2.1 under CC BY 4.0,
credited on the sheet. Nothing on screen yet (#737). - [webapp] The block on its page, Show the table by field, Make a block, and the block at the table (#737).
An NPC's or a creature's page with a sheet draws its block in the game's own
order under the page's lead. The GM sees every line and which the party can
read — a GM-only line has the GM's rule down its side, with a legend — and
has Show the table…, Edit the block, Put on the shelf and Export… beside it.
Show the table is a checklist by the game's groups (name, level and traits;
perception and senses; languages; skills; AC, saves and HP; strikes by name
only, or with their attack and damage; abilities…); GM notes are never
offered. What is ticked becomes the party's for good, on the page, in search
and on the table, and unticking a group or Take it back hides it again.
While a session is being played it is also shown at the table at once — the
party's lines on the players' screens and, if asked, in the campaign's
Discord channel — and Take it back takes that showing down, as a handout's
is, telling the other GMs. That showing opens nothing else: a GM-only NPC's
page stays the GMs'. A GM can make a block from a character's sheet (Make a
block…): a new NPC page, the GMs' alone, with a copy of the character as it
is now that later changes to the sheet do not touch. At the table, Find says
which pages have a block, and a wiki card or the preview draws the block with
its hit points (−5 / −1 / +) and conditions changed from the card with no
version made; a monster from the rules keeps its own hit points and
conditions on its card for the session. Showing a card with a block to the
table now asks what to show, by field. - [webapp] Rules content now carries the numbers a sheet reads. Every
reference entry has structuredfieldsper kind — an item's Bulk or weight,
a weapon's damage and traits, armour's AC and Dex cap, a class's hit die, a
condition's modifiers — filled when a Foundry PF2e or Open5e pack is
imported, or written directly in a Quest Board pack (checked per kind; ones
that don't fit are kept as data with a warning). PF2e ancestries and
heritages, and 5e species, import as their own kinds. The starter-pack
scripts now build the Player Core's classes, ancestries, heritages,
backgrounds, feats and equipment (ORC, setting prose stripped) and the
SRD 5.2.1 only for 5e; publishing them is a release step
(docs/OPERATIONS.md, "Building the starter packs"). A small mechanics-only
character pack per system backs the tests, the journeys and the demo
(#737). - [webapp] The engine that will work out character sheets and stat blocks.
A sheet's definition (its fields, the sums behind them, the bonuses and
penalties that apply) is now checked and evaluated on the server: every
number comes with the named terms it was added up from, bonuses of the same
kind stack the way the game says, conditions and equipped items apply
themselves, and a definition that refers to a field that does not exist is
refused with a "did you mean" suggestion. Ships with a generic sheet for any
game without its own, and a shared set of test cases the browser will be
held to as well. Nothing on screen yet (#737). - [webapp] The sheet at the table: hit points, conditions, spells, rests, and adding from the loaded content (#737).
A character's player and the GM change the hot half straight from the sheet,
on a phone too, with no version made: hit points with −5 / −1 / + and their
temporary points, hero points, dying and wounded as tracks to press, and
conditions as chips with − and + (Frightened 1), an "until" note (Off-guard
until the start of your turn) and Add a condition; every figure a condition
changes shows the penalty in its terms. A caster's spells have Cast, which
spends the rank's slot (a focus point for a focus spell) and marks a prepared
spell expended, and the expended mark takes it back; Refocus sits by the focus
points. Rest… in the header lists the game's own rests and says what each
gives back before it is taken: Pathfinder's daily preparations (hit points by
Constitution and level, every spell slot and focus point, fatigued, drained
and doomed eased) and Refocus, which is off for a character with no focus
points and says why; 5e's short rest spends the Hit Point Dice the table
rolled (the page rolls nothing; Constitution is added for each die), and the
long rest gives back every hit point and every Hit Point Die (SRD 5.2.1) and
takes off a level of Exhaustion. Hero points come back to 1 when a session
starts being played, once per session. While a sheet is open it picks up what
the other side changed within a few seconds. Editing a sheet now offers Add
from the loaded content — search, the entry's card (damage, traits, category,
bulk, price, and where it is from with its licence), then how many and where —
and each row's own quantity, where it is, invested or equipped, prepared, and
Remove; a row whose pack has gone keeps its name and says where it came from. - [webapp] The sheet engine runs in the browser too. The same sums the
server works out for a character sheet or stat block can now be worked out
in the page while someone types, so numbers will update as you edit; the
server's answer still replaces them on every save. The browser's copy is
held to the very same shared test cases as the server's, down to how it
rounds, orders text and prints large numbers. It can also show dice the
server rolled (which dice were kept and which dropped); the browser never
rolls. Nothing on screen yet (#737). - [webapp] Sheets can name their numbers the way the page shows them.
A line in the working-out under a number can now take its name from the
sheet ("Dex 4" rather than "Attribute", "Prof 8" with "trained" beside
it), a bonus or penalty can name itself ("Frightened 1"), and a sheet can
pick one name out of a list ("Chain mail 16", "d10 → 8, 7"). A sheet
definition that misspells a function now gets a "did you mean". Also, a
malformed item row or an impossible rank no longer stops the whole sheet
from being worked out (#737). - [webapp] A content pack can carry sheet definitions (pack format v2). A
.qbpackmay listsheets/*.json(andsheets/fixtures/*.json) undersheet_definitionsin its manifest. An administrator's pack has each definition checked (unknown names come back with a did-you-mean, fixtures are run) and activated only if it passes; a broken one is refused while the pack's content still imports, and a GM's campaign pack keeps its content but has its definitions ignored, with a note. The import response carries the per-definition report,GET /api/admin/sheet-definitionslists every definition, disabling or removing a pack deprecates its definitions (sheets on them stay readable), export writes them back, and a pack'ssystem.jsonmay now update a game system that same pack introduced. - [webapp] Pathfinder 2e character sheets and creature blocks, built in.
The Remaster character sheet is defined in full: attributes with boosts,
Armor Class with the armor's Dex cap, saves, Perception and class DC by
proficiency rank, Hit Points from ancestry, class and Constitution, dying,
wounded and Hero Points, all sixteen skills and Lore with the armor check
penalty, Strikes worked out from the weapons carried (attack, the second and
third attack, damage), feats by level, Bulk and encumbrance, invested items,
a spells page for casters with slots by rank and Focus Points, and each
condition's real effect (frightened, sickened, clumsy, enfeebled, drained,
stupefied, fatigued, off-guard, prone, unconscious, encumbered). The
creature block follows Monster Core's order, takes over today's stat blocks
field for field, and can be made from a character. Both carry the ORC
License notice and no setting names. Nothing on screen yet (#737). - [webapp] Character sheets and stat blocks are kept on the server. A
player character's page, an NPC's or a creature's can now carry a sheet,
made with the game's own definition (or a generic one for any game without,
and for a campaign with no system). Every value is checked against the
definition as it is saved; each field can be shown to the table, the party,
just its player and the GMs, or chosen people, and a character's player
always reads the whole of their own sheet. A GM can lock part of a
character's sheet. Autosaves that cross another window's edit are refused
with the sheet as it now stands, edits keep a history (one entry per person
per ten minutes of editing) that can be put back, and hit points and
conditions change at the table without filling that history. Rows added
from loaded rules keep their numbers if the pack is later taken down. The
page's stat block is written from the sheet, and every reader gets only the
lines they may see. A sheet exports as Quest Board JSON. Nothing on screen
yet (#737). - [webapp] Edit a character's or a creature's sheet, and it saves itself (#737).
Edit turns a sheet's source fields into inputs of the right kind (numbers,
text, choices that also take a name of your own, ranks as pips you press,
rows you add and remove on the generic sheet) while what is worked out stays
text and moves as you type. The header says where the sheet stands (Saving…,
Saved at, Not saved with Try again) and there is no Save button; what you
typed is kept on this device until it lands. If somebody else saved first, the
sheet says so and shows both versions of each field before you keep yours or
take theirs. The GM can lock a field or a group, and a locked one reads as
such to the player; each group that has its own audience can be set to who
sees it. Every save is a version, and any of them can be put back from
Versions. A person's or a creature's page with no sheet has Make a sheet, for
the GM and for a character's own player. Also: a page with no sheet no longer
ends its crumb on a Sheet that is not there, printing leaves the header
controls off the paper, a private group's pill names the player by their first
name, and a derivation whose constant is labelled with its own figure prints
it once. - [webapp] A character's or a creature's sheet, to read: Open the sheet from its page (#737).
The sheet is a document of its own with its pages (Statistics, Abilities &
inventory, Notes & actions, and Spells for a caster) in the Contents and, on a
phone, in the Contents sheet. Every figure is shown once with what it is made
of beneath it, on tap on a phone; ranks, hit points, dying, wounded and hero
points, strikes worked out from the inventory, feats by level, the inventory
with its bulk and where each thing is, spells by rank with how many slots are
left, and the character's own actions with their cost and full text are all
drawn from the system's definition, so Pathfinder 2e, D&D 5e and the generic
sheet are one page. An item whose pack has gone says so and keeps its last
numbers. What the page itself says (appearance, backstory) is shown with Edit
on the page; a group a player keeps for themselves and the GM wears the GM
colour. A reader who may not change the sheet is told so quietly. Export
gives Quest Board JSON, and Print the sheet prints every page. A page that is
not a person or a creature says it has no sheet. Read only: editing comes
next. - [webapp] The design system gains the character sheet's pieces: Derivation, RankPips, Track, Resource, Dice, StepStrip, WhatChanges and Picker, each with its states on
/design(#737, #364). - [webapp] Start a character, in the game system's own steps (#737).
Members & characters has a Start a character for you, and for the GM beside
each player. A Pathfinder 2e character walks Concept, Ancestry, Background,
Class, Attributes, Skills, Equipment and Details; a D&D 5e one walks Class,
Origin, Ability scores, Alignment and Details. Pathfinder attributes are
boosts by source, with the cap and how many free boosts are left. 5e ability
scores come from the standard array (drag a number to an ability, or tap both
to place or swap them), from point buy with its 27 points and the cost of each
score, or from rolling 4d6 and dropping the lowest, with every die shown, the
dropped one struck through, and no second roll where the method allows none.
The dice are rolled by the server and kept with the sheet. Next says why it
is held back, each step saves itself, and Finish makes the character's page at
once, with its prose sections named and empty, and the sheet is the player's.
A character left half-made is kept for 30 days as "Carry on with …" on Members
& characters (and on its page), and Open the sheet appears beside a character
that has one. - [docs] Lane 10's drawings, for review (#736, #824): the prototype's lane 8 page draws Test the
bot (D124, D149, D150), the two pages an email opens (D154, D155), Discord DMs on the notification
grid (D151, D161), the voice channel and "Where" (D159), and a recording announcement nobody could
post (D171), and brings its older stills into line with D96, D97, D123 and D151–D163. The owner's
answers to its questions are recorded as D221–D232, and Settings → Discord is redrawn with pickers
for the server, channel and voice channel (D226). Catalogue rows IS-443 to IS-458;
coverage/lane8.pyand the CAMP-12, CAMP-13, SESS-2, TABLE-4 and HOME-6 scripts classify them. - [bot] The bot says it can run Test the bot (D124, #824, #864).
GET /identity's
featureslistsrecord_testbesidedm, so Quest Board can tell a bot too old for
Test the bot before a GM presses the button (additive; bot contract unchanged). - [webapp] Admin's recordings table says when each recording goes. "Kept 90
days, as the campaign asks: until 23 November", or whose setting it is when
that is the instance's or its cap. The date is the one the nightly clean-up
acts on, taken from the same function;GET /api/admin/recordingsgains
audio_expires_at,audio_retention_mode,audio_retention_days,
audio_retention_sourceandcontent_approved(#872). - [webapp][bot] Taking a handout back deletes its Discord post or DMs (D157,
D238, #896). The handout path never recorded where it landed, so its own
Undo neither told anyone nor deleted anything — this closes both gaps.- A handout reveal now records where it went: the campaign's channel post
reuses lane 10'sshelf_reveals.discord_message_id/discord_channel_id
(POST /api/bot/reveals/{id}/message), and achosenaudience's DMs each
get a row in a newshelf_reveal_messagestable via a new, additive
POST /api/bot/reveals/{id}/dm-message. Migration8c9d0e1f2a3b, on top
of lane 8's map. The reveal is now committed before the bot is asked to
post it, so the bot's report of where it posted can never beat the row. - A handout's Undo and a shelf card's take-back are now one code path
(shelf_service.take_back): stamp it, tell the campaign's other GMs
(D238), commit, then publishreveal_taken_backcarrying every recorded
post or DM asextra.messages, so the bot deletes each — best-effort,
silent on a message already gone. Lane 10's single-post fields stay in the
event for an older bot.BOT_CONTRACT_VERSIONstays 2. - Handout embeds are ported to
faces.handout, the bot's one post shape
(EXT-2), with a golden row intest_faces.pyand a dev-preview specimen.
- A handout reveal now records where it went: the campaign's channel post
- [webapp] A quest can be changed (D242, #925). A quest's page shows its status, main or side, who gave it, the reward, the sessions it started and ended in, and its objectives; a GM edits all of these, with an objective kept for the GMs never shown to players. Changing the status puts a line on the quest's timeline. A GM confirms a rumour as a quest, or marks it not canon, from the quest log.
- [repo] A shared Claude Code setup for tiered delegation (#935). Specialist
agents in.claude/agents/:qb-implementerandqb-frontend(Sonnet,
medium effort),qb-preflightandqb-render-check(Sonnet, low), and
qb-test-runnerandqb-ci-triage(Haiku, low). Skills:/qb-test,
/qb-ci-log,/qb-render,/qb-preflightand/qb-deploy-dev. Workflows:
/qb-verify,/qb-build-sliceand/qb-review. The local CI recipes now
build test images from the Dockerfile'sbackend-devandbot-devtargets,
so they can no longer go stale. - [repo] The dev demo seed writes a GM copy of The Amberwake Accord. A fifth
campaign, "The Amberwake Accord (GM copy)", mirrors the original row for row
under its own ids, with the seats swapped: the owner's account owns and runs
it and is credited with everything Dana did as GM, and Dana plays the
owner's character. Every GM-only surface can now be reviewed on Amberwake's
data. Its GM's bell also has a session short of players and a recording
about to be deleted. The same builder writes both (engine.Variant), and
--resetrebuilds the copy with the rest (#951). - [webapp] A Test-the-bot run's AI usage now counts in the campaign's usage and credits like any other run, marked as a test (D227).
session_usage.session_idis nullable; a test row carriesrecording_test_idandpurpose="test"instead, and Admin → AI usage & speed shows it as "Test the bot". - [webapp] Settings → Discord chooses its server, channel and voice channel from
pickers, once Discord is linked (D226, D159). The bot answers two new internal
routes — the Discord servers it shares with a given user, and the text/voice
channels it can see in one of them — and the backend asks them only for the
signed-in GM's own verified Discord link, never a Discord id from the client. A
GM with no verified link, whose bot can't be reached, or who just prefers to, keeps
typing IDs, exactly as before. - [bot]
GET /discord/guildsandGET /discord/channels, bot-auth and additive
(BOT_CONTRACT_VERSIONstays 2): list a user's shared servers, and the channels the
bot can see in one of them. Both re-check that the given user is actually a member
of the guild in question before answering.
Changed
- [webapp] A wiki page in the bin now travels in a campaign's export, and
an import puts it straight into the bin (#1009). Since #990 a binned page
was left out of the file, so moving a campaign ended the days a GM has to
restore it (30 unless the instance says otherwise). It is now carried in
data.json(a newwiki_binsection; export schema 16) with the time it
was binned, and an import writes it already binned: for the GMs only until
one restores it, never live for a moment, and with the days it had left
rather than a fresh count. What stood
beside it comes back as it stood (its aliases, owners, relationships, name
targets, picture, shelf cards and prep items, the thread about it), so
Restore brings back what it would have in the old campaign. What was shown
at the table stays shown until a GM takes it back, as it does where it came
from: a card shown from the page, and the page's own reveal when it was
shown in a session, are still listed under that session for the people they
were shown to, by the title they were shown with; a player's handouts list
leaves the page out until it is restored. A binned page is in nothing a person reads:
no file underwiki/, no count inREADME.md, its picture under
media/bin/lore/. A page already past the bin's days when the file is
imported is deleted by the next daily purge, as it would have been where
it came from, so restore it as soon as you have imported. Every other bin still
stays behind, and an archive from before this release imports as it did. A
Quest Board older than this one refuses a schema 16 file by its version;
this one now says "made by a newer Quest Board … update, then import it
again" when it meets a file newer than it reads. A fact or a timeline row
that names a page further down the file now keeps its link on import, where
it used to lose it. Seedocs/API.md. - [webapp] Shortening how long a campaign keeps transcripts is an ordinary
confirmation now (#1010). It was the serious kind, with a red button, from
when the older transcripts were destroyed that night. Since #680 they go to a
bin and can be restored, so the question keeps its words and its offer to
export the campaign first, and loses the red. - [webapp] The footer is at the bottom of the page, not a strip pinned under it.
The version and the project, Help, Accessibility, Privacy and Licences
links used to sit under the page's scroll area at every width, taking two
lines of a phone's screen for good and cutting off the end of long lists.
They are now the last thing in the page itself, at the window's bottom on a
short page and reached by scrolling on a long one, beside the Contents
rather than under it (#1020). - [webapp] The party page shows XP and gold as their figures, with a small
History beside each. History opens every entry, newest first, in a dialog
("XP history" / "Gold history"), with the same Remove and Undo; the page no
longer lists them all (#1020). - [webapp] The "Quest Board" wordmark in the app bar is now a link to Home.
Inside a campaign it was only a picture of the name; it now takes you back
out to Home from every page of the signed-in app, and is named "Quest Board,
home" for a screen reader (#1020). - [repo] The Claude Code setup is now a thin layer over a shared, user-level set (#1037).
Quest Board's test recipes, preflight checks, release rules, deploy facts and UI
settings moved into a## Project profilesection ofCLAUDE.md./qb-test,
/qb-ci-log,/qb-preflight,/qb-deploy-devand/qb-renderkeep their names
but delegate to the global skills and agents, and/qb-verify,/qb-reviewand
/qb-build-slicedelegate to the global workflows. Theqb-test-runner,
qb-ci-triage,qb-preflightandqb-render-checkagents are gone, replaced by
the globaltest-runner,ci-triage,preflightandrender-check. The pull
request template now asks for achangelog.d/fragment instead of an edit to
CHANGELOG.md. - [repo] Prereleases are cut for the dev server, and the Quest Board hosts
run the published images. AvX.Y.Z-rcNtag now publishes release notes
rendered from the pending changelog fragments (changelog.py render, which
consumes nothing), and its images are:X.Y.Z-rcNonly. A committed
docker-compose.images.ymlpins the six app services to the release's images
and removes theirbuild:; its tags must equalAPP_VERSION, which
check_version_sync.pynow enforces. A host that adopts it sets
COMPOSE_FILE=docker-compose.yml:docker-compose.images.ymlin its.envand
updates by checking out a release tag. A self-hoster who builds from source
sets nothing and is unaffected.docs/OPERATIONS.mdcovers cutting a
prerelease, the image update, rollback and production updates (#1043). - [webapp] Cutting a recording into speech spans no longer starts ffmpeg
once per span (#684). With silence trimming on — and always on a managed
provider with a per-request size limit — every speaker's track is cut into
its speech spans before transcription, and each span was its own ffmpeg
process: around 1,300 of them for a three-hour, five-speaker session, most
of whose time was process start-up. The spans of a track are now cut
twenty-five to a process. The files are byte for byte what they were, so
transcripts, timestamps and the counts on a summarisation run are unchanged;
on the 30-minute test scene the cutting went from 208 processes and about 17
seconds to 11 and about 2. If a batch fails, its spans are cut one at a time
exactly as before and the worker log says so once
(… in one ffmpeg run failed … cutting them one at a time instead). ffmpeg
briefly uses a little more memory for a batch than for a single cut —
roughly 65 to 80 MB against 56 MB; the batch size was chosen to keep that
small in a worker limited to 1 GB. Nothing to configure. - [repo] The synthetic-session guard tests take about half as long (#684).
Besides the batched cuts above, the guards no longer re-runsilencedetect
over a track ffmpeg has already scanned unchanged (test-only; a rewritten or
copied track is always scanned again), and a new guard compares the batched
cuts of the scene's tracks with single cuts, byte for byte, in the CI job
that is not allowed to skip. - [webapp] Anyone who may edit a wiki page directly can upload or remove its
picture (D234, #734). It follows the campaign's Who may edit: a player's own
pages by default, any page they can see under "players anything", none under
"suggest" (D233). Setting a picture from a web address stays the GMs', since it
makes the server fetch something. - [webapp] Admin's Recording & retention, Backup, Import and Audit log are
rebuilt on their own pages (lane 9 page 4b, #735). Recording & retention
gains the default/cap split (D24) with IS-311's confirm before a tighter
cap catches a campaign's sessions, and says D179's retention promises
correctly. Backup queues without asking first (D12), polls while a run is
in progress, and no longer claims uploaded images are left out, since
D143 already put them in. Import a campaign now shares its form with
Home's New campaign → Import a file. The audit log filters by name
rather than id, groups "What happened" into categories, and reads its
detail as labelled lines with the record as stored one click further. - [webapp] Admin's AI models, AI usage & speed, Discord, Email and
Instance health are rebuilt on their own pages (lane 9 page 4c, #735).
The old 49-field Bot Settings tab splits into AI models (the capability
panel, the transcriber and summariser, the summary and wiki modes, and
who is using their own) and Discord (the bot's address and its key,
with IS-394's identity check replacing the token field per D139).
AI usage & speed gets an address of its own for the hardware preflight
and the usage report. Email's test now sends what is typed, saved or
not, and answers with the SMTP server's own reply instead of queuing.
Instance health has a screen for the first time: the AI queue's
free/busy/stuck verdict, polled every 10 seconds, and which secrets
this server can read. Also fixes a date column (Joined, Last here,
Started, When) wrapping mid-date on Users, Campaigns and Audit at 390px. - [webapp] Admin's Content packs, Content notices and Plans & entitlements
are rebuilt, and the legacy Admin page is gone (lane 9 page 4d, #735).
Content packs takes lane 3's kit pieces rather than the old Tailwind
component, and adds starter packs and an instance pack's own default
("Every member", AD-120). Content notices gets a screen for the first
time: where a rightsholder writes, filing and filtering notices, and
acting on one (turning a pack off, or removing it or just the records
named, all or nothing). Plans & entitlements shows the four prices
Plan & billing already reads, hosted servers only.pages/Admin.jsx
and its last two orphaned panels (AdminAiUsage,AdminHardwareProfile)
are deleted. Also fixes two dialogs (making an administrator, handing
the server or a campaign on) that drew no buttons at all, and a narrow
screen squeezing a table's columns instead of letting it scroll. - [bot] Commands, and a private way to raise a concern (lane 10, EXT-1, #736).
/recapnow also takes a session's number (/recap 13), resolved to the right
campaign in a server that binds more than one (O9);/next,/quests,/history
and/askresolve the same way, and every one of them offers acampaign:picker
when it needs to ask./historyand/asknow answer privately,/historylists
numbers instead of session IDs, and/asknames the sessions its answer was drawn
from — worked out by Quest Board, never the model (O16)./nextshows RSVP counts.
/noteand/help's words follow D97/D106. A new/concernopens a private
Discord form, in a server or a DM, that reaches only the campaign's GMs through the
same anonymous queue as the web's Table rules & safety flag (O13, D160) — nothing it
carries is ever echoed into a channel, logged, or stored with a sender. All
additive:BOT_CONTRACT_VERSIONstays at 2. - [webapp] The server side of the above (#736).
GET /api/bot/guilds/{id}/sessions/by-number/{n}resolves a session by number within a
guild's campaign;channel_id/campaign_idare new optional parameters on
/next-session,/quests,/sessions/listand/sessions/askfor the same
resolution, returning 409 with the campaign names when a server holds more than one
and neither narrows it./sessions/ask's response gainssources. A newPOST /api/bot/safety/submissionsis/concern's door into the existing, sender-less
safety queue, kept out of the access log the same way the web route is. No
migration. - [bot] Every Discord post has one shape (lane 10, EXT-2, #736). A coloured edge
for its kind (six tones that read on Discord's light and dark themes), a title in words
with the kind first and the session by number ("On: Session 14", "Cancelled: Session
14", "Recording saved · Session 14"), the facts as fields — When, Where (the
campaign's voice channel, D159) and Who's in — one link back to Quest Board and
none on a server with no public address, and Discord timestamps throughout. All built
in one place,questboard_bot/faces.py. A time added to an open vote is now also
posted on its own ("A time was added · Session 14"); a cancellation says who and why
(D101); the approved summary is the only summary post and says who approved it (D96);
no post promises a summary "when it is ready" any more, and asession_summarised
event still queued from before D96 is dropped rather than posted. The "Recording
stopped" line is gone: the table hears once, after the handoff. The bot's status reads
"Recording · /help" while any recording runs (D158).tests/test_faces.pypins every
face, andpython -m questboard_bot.preview --channel <id> --yesposts every one,
from invented examples, into a channel of the developer's own test server (D164, O17). - [webapp] Bot events carry what the posts need (lane 10, #736). Every session event
addsnumberandvoice_channel_id; the poll addscloses_at, the reminderrsvp,
the cancellationcancelled_by, the played postduration_s, the approval
approved_by, and a slate change says whether a time wasaddedand by whom. The
bot's/api/bot/sessions/{id}/timeslotsaddsnumber. All additive; the bot contract
stays at 2. - [bot] Reactions and taking things back (lane 10, EXT-3, #736). Reacting to a
confirmed session gains ❔ ("I might make it", D50) alongside ✅ and ❌, with mutual
exclusion between the three enforced on the message (Manage Messages permitting); the
confirmed post's footer says so. Voting on a proposed session now names the voter's
whole slate in the acknowledgement ("A and C work for you"), not only the option that
just changed. Taking a revealed card back down in Quest Board (O10) now deletes the
bot's own Discord post too, and tells the campaign's other GMs (reveal_taken_back, a
new bell kind). All additive:BOT_CONTRACT_VERSIONstays at 2. - [webapp] The reveal message, and its removal (#736).
POST /api/bot/reveals/{id}/messagerecords where ashelf_revealembed landed
(shelf_reveals.discord_message_id/discord_channel_id, migration
10b2c3d4e5f6);revoke_revealnow publishesreveal_taken_backfor the bot to
delete it, and notifies the campaign's GMs other than the one who took it back.
BotAttendanceRequest.attendedis now optional alongside a newrsvpfield
(yes/maybe/no); the vote endpoint's reply carries the voter'syour_answers. - [webapp] The D&D 5e sheet names what it adds up, as drawn. Each number's
working now reads "Chain mail 16 + Shield 2", "10 + 2 × (d10 → 8, 7) +
Con 2 × 3", "Str 3 + Prof 2" and "2 + ⌊(3 − 1) / 4⌋" instead of generic
labels, and penalties say their level ("Exhaustion 2") or their cause
("Chain mail, needs Str 13") (#737). - [webapp] Existing stat blocks become blocks on the new sheets, automatically.
On upgrade, every person's and creature's stat block turns into a creature
sheet for its game — Pathfinder 2e, D&D 5e, a game added by a content pack,
or the generic stat block when its game is gone — keeping who could see it:
a block everyone could read stays readable by everyone at the table, and a
GM-only or player-private one stays with the GMs. Nothing is lost: the old
block is kept with the sheet, a value that doesn't fit the new sheet is kept
aside rather than dropped, and the page's block is rebuilt the first time
it's opened. The stat block editor on a page, the importer, "Stat block" and
"Convert stats" all work on the sheet now; a drafted stat block fills the
new block's fields and, once approved, lands on the page's sheet instead of
in its text. Administrators can runpython -m app.sheets.rebuild_projections
(with--dry-runto see what it would do); the worker also does it once
when it starts (#737). - [docs] The lane 9 drawing gains Actions and Rest… for the sheet build (#737).
Notes & actions lists a character's actions with their cost and full text, and
the sheet header gains Rest… (D209); the drawing also drops its explanatory
lines, the recap promise and the Foundry export (D208, D210, D200). The
catalogue gains IS-459 to IS-462 and the survey is committed as
docs/design/lane11-build-contract.md. - [webapp] The old stat block editor is gone; stat blocks are edited on the sheet.
On an NPC's or a creature's page, the editor now shows "Open the sheet" when
the page has one and "Make a sheet" when it does not, in place of the form of
fields. Saving a page with astatsvalue is refused with "Stats are edited
on the sheet." Pages still show their stat block, and importing a campaign
still brings stat blocks in (#737). - [webapp] Pathfinder 2e sheets read the way the game prints them. The
sums under a number now name the attribute they use ("Dex 4", "Wis 4") for
class DC, spell DC and attack, and Strikes; Hit Points read "Ancestry 8 +
6 levels 60 (Class 8 + Con 2 each)"; and a condition that has a value says
it ("Frightened 1", "Stupefied 2", "Drained 1"). The numbers themselves are
unchanged (#737). - [webapp] Admin follow-ups and the demo seed's own surfaces (lane 9 PR 6, #879).
GET /admin/settings/bot/identitypasses through the bot's own reason for
a disabled Discord connection, so the Discord section says "Discord
refused the token" separately from "No Discord token set" instead of one
generic message. Instance health's "Campaigns' own keys (N)" line is real:
the secrets audit now counts every campaign's own transcriber/summariser
key and how many this host'sSECRET_KEYcan no longer read.AdminAi
andAdminRecordingregain the test coverage the legacy Admin page's
deletion (#735) didn't carry over — gap severity ordering, a provider
guessed from the address, the retention form's defaults and caps together,
and the wiki pipeline's mode change and error states. - [repo] The demo seed gains two thin campaigns purely for Admin's own
pages (#879, D145). The Salt Crown (archived) and Brinewatch (a
one-shot, binned), both owned and GM'd by an invented person who has not
signed in since — a stale owner, a failed recording, a transcript kept
"for good" for the retention cap-confirm to name, a second campaign pack
disabled after a content notice, and nine more audit-log events. Nobody
plays these two tables; seewebapp/backend/demo/README.mdfor why they
stay that thin. - [webapp] The email answer page now says when a session has started or been cancelled, and links to it; the Notifications page says which of Discord's two refusals stopped your DMs.
Answering a started or cancelled session from an email is refused (409) instead of reading as an expired link. - [webapp] Awarding XP is the GMs' alone (D241). The campaign's "players can add loot" setting still lets players add items and gold, but no longer XP; removing or restoring an award is the GMs' too.
- [repo] The backend test suite is much faster. The local-Whisper
/healthprobe is stubbed by default (tests that exercise it opt in with@pytest.mark.real_asr_probe), removing the ~8 s DNS-plus-timeout wait from about thirty recording-pipeline tests; a test network guard now fails any test that resolves an unmocked hostname (opt out with@pytest.mark.allow_network); and the synthetic-session harness (@pytest.mark.synthetic) is deselected from the backend CI job'spytest -m "not synthetic"since its own job already runs it. Full run: ~1,061 s before, ~536 s after (about 9 min). - [repo] CI's two slowest jobs now run in parallel pieces (#939).
- Vitest was running on one worker. CI jobs get 2 CPUs, and vitest's default is CPUs − 1, so the whole suite took 1,603 s. The frontend job is now two shards, each with
--maxWorkers=2. Lint, the build and the audit run once, in shard 1. - The journeys were running one project after another. They're now a matrix of three jobs (desktop, phone, keyboard), each with its own stack. The one-worker constraint (per-IP login rate limit, one shared seeded database) is per stack, so no spec changes.
- Three prep-sheet test waits no longer fail under load. Their explicit
timeout: 4000was shorter than the suite-wide 5 sasyncUtilTimeout. The two that wait on the 1 s autosave now wait 8 s.
- Vitest was running on one worker. CI jobs get 2 CPUs, and vitest's default is CPUs − 1, so the whole suite took 1,603 s. The frontend job is now two shards, each with
- [webapp] Session emails no longer repeat "Quest Board" as a heading under the brand line; they open with the news itself, as drawn (EX-040).
- [webapp] The design system reference page,
/design, now ships in production
builds, for server administrators only. Anyone else, signed in or not, gets
the same "not found" page as any unknown address. Development builds still
open it to everyone. - [webapp] Cards on the GM's table board are packed, and a new one lands beside
the last card on screen. A card nobody has moved used to take a slot by its
place in the whole shelf, so a shelf whose cards had mostly been placed by hand
drew a new one far down the board with empty slots above it. Unplaced cards now
fill the first free spots around the ones the GM placed (which never move), and
a note or a card added from Find goes next to the last card in view — to its
right, else under it — never on top of another card. - [repo] Changelog entries are now fragments in
changelog.d/, gathered
intoCHANGELOG.mdat release. Every PR used to add its bullet
directly at the top of[Unreleased]'s section lists, which clashed
between PRs merged close together. Now each PR adds a small file under
changelog.d/instead;make changelog(scripts/changelog.py assemble)
gathers them at release time, andmake changelog-check(also run by CI)
validates them. Seechangelog.d/README.md.
Fixed
- [webapp] Level up no longer offers another class's feat as if it could be taken. At a class feat step a Fighter was listed a rogue's feat, because feats carry their class as a trait rather than a prerequisite. It is shown disabled with the reason ("A rogue feat."), the way a feat with an unmet prerequisite is, and applying the level with it is refused. A feat can still be typed by name (D261, #1008).
- [webapp] A temporary hit point figure typed straight after a sheet opens is no longer put back to the old number. The box reset itself to the server's figure on mount, which could run after an early edit (#1017).
- [webapp] An article's text no longer sits against the rule under its
tabs. The Read tab now leaves the same gap below the rule as the other
pages, for a player's view and a GM's, at every width (#1020). - [webapp] On
/design, pressing anything no longer blanks the open panels.
The bell, menus, account menu and search drawn open closed on a press
anywhere else on the page, or on Escape, and nothing brought them back; the
ones drawn open now stay open (#1020). - [webapp] A page shown to you as a handout no longer logs a 404 for its map
markers. "On the map" asked about a page the reader may open through a
reveal, and the route only knew the page's own audience; it now answers with
the markers she can see, usually none (#1020). - [webapp] Opening a notification from the bell now marks it read. Following a
row's link (a click, Enter, or a new tab) did not clear it in a browser,
because the frame closed the panel before the row heard the click; the row
now marks itself first. The tick beside a row still marks it without opening
it (#1020). - [webapp] The page a player meets on a GM's prep address reads as
English. It said prep "is where each session is got ready"; it now says
"Prep is for the GMs of this campaign: it is where a GM gets each session
ready." Planning's own version of the line is reworded the same way
(#1020). - [webapp] A scene can be deleted from the prep sheet, and a card's "File it
in" menu there can be read and used. The sheet's Shelf section could make
and rename a scene but not delete one; each scene now has Delete beside
Rename, the same delete Table mode's Scenes panel has: at once, with an Undo,
the scene's cards staying on the shelf under "No scene yet" until the undo
files them back. The menu on a shelf card was drawn inside its list and cut
off at the list's edge, so only its heading and "No scene" could be seen; it
now hangs clear of the list, and it offers every scene with the card's own
marked, where it used to leave that one out (#1020). - [webapp] A search puts the thing you named first. Searching the wiki for
"Odile" listed Bastian Crowe, whose page mentions her, above Odile Marsh:
a wiki search had no ranking at all and answered in the wiki's browsing
order (kind, then title), where a character comes before an NPC. Every
search now follows one rule, written once on the server
(app/services/search_rank.py): the name that is the query, then a name
that starts with it, then a name with a word that starts with it, then the
same for its aliases, then everything that only mentions it — each of those
in the order the search had before. Case, accents and extra spaces don't
count. It applies to the app bar's search, the wiki's own, Table mode's
Find, the prep sheet's Rules & bestiary and Wiki drawers and its article
pickers, and the rules search, and it is applied before any limit, so the
exact match is never the row a cap drops. A player's results are ranked by
the names a player knows: an unidentified item by what the party calls it,
and a page they can't see is not there to be ranked. A rules record is also
now found by the start of any word in its name ("stalk" finds Reed Stalker). - [webapp] The wiki front page's results open under its search box. They
hung from the far right of the page's main column, away from what was
typed: the box is the app bar's search component, whose results hang from
the right edge of a box that, on this page, was as wide as the column. The
box is now up to 32rem wide and the results start at its left edge at the
same width, on a phone too. - [webapp] The last results in a prep-sheet drawer are no longer cut off by
the footer. The tool margin beside the sheet was sized from the window
(100dvh - 6rem), but it sticks inside the page's scroller, which is the
window less the app bar, any banner and the footer — so its last 36px or
more sat behind the footer, where neither its own scroll nor the page's
could reach. It is now sized from the scroller itself, measured, so it holds
whatever the bar and the footer do. - [webapp] A result found by its own name no longer says the name twice.
"Odile Marsh" sat over "npc · Odile Marsh". The line under it now says how
the page starts — in the text that reader may read — or, for a session, how
its summary starts; with nothing to say it is just the kind or the date. - [webapp] Table mode's Find no longer scrolls sideways. A row was as wide
as its whole snippet (778px in a 590px list), so the list scrolled across
and the snippet was cut at the list's edge. Rows now fit the list, the title
keeps its room and the snippet ends in an ellipsis. - [webapp] In the prep sheet's lookup drawers, "In the sheet" stays on one
line on a phone, and a preview that opens is brought into view so its
Insert is not below the fold of the Tools sheet. - [webapp] A GM can find the sweep, and the table, from a session that has
been played. "Sweep the shelf" was only at the foot of the Review tab's
checklist, and nothing on a played session's page led back to its table. A
GM now has both under the tabs on Read as well as on Review: Sweep the
shelf while any card from the session is still on its shelf, and Open
the table always — the evening's board, its scenes and what was shown are
still there. In Table mode the sweep's button is called Sweep the shelf…
too, in the bar and at the foot of the phone view, where it used to say
"End of session…". Closing a sweep opened from the session page takes
?sweep=1off the address, so reloading or going Back does not open it
again (#1020). - [webapp] Showing something to the table from a GM's phone no longer turns
large. A card put on stage on a phone was set in the desktop stage's
display type — everything a size up, its first line at heading size — when
only the title had been brought down for a phone; the phone's stage is now at
the phone's size, as drawn. "Show the table" opened a dialog whose field was
13px, which makes iOS Safari zoom the whole page in as the field takes focus;
on a phone the field is 16px. A long session or card name wraps instead of
pushing the page sideways, the bar's clock is the phone's size, and every
button on the phone view and in its dialogs is at least 44px, including
Leave, the stage's ×, ‹ and ›, and the sweep's rows (#1020). - [webapp] A thread marks the posts that were unread when you opened it.
Each carries a dot and the word "New" (not colour alone), with a "New since
you were last here" line above the first; they stay marked for the visit
and are read on the next. The thread's read answer gainsis_newon each
post andread_throughon the thread, both additive (#1020). - [webapp] The wiki's front page lists Quests under Browse by group.
Every entry type had a row there but a quest's, which was only reachable
from the Contents' quest log; the row opens it and counts the quest pages
the reader can see (#1020). - [webapp] The Contents' wiki chapters now include Events and Lore.
They were reachable only from the front page's Browse by group, so the
sidebar and the phone's Contents looked as if categories were missing; both
now list all six groups in Browse by group's order (#1020). - [webapp] Back and forward return to where you were on pages that load their content after the page opens. A session's Review tab and Admin → Audit log came back short, or at the top, because the position was set before the page was tall enough; it is now kept trying until the page can reach it, and stops the moment you scroll yourself (#1023).
- [webapp] The notifications panel no longer scrolls inside itself. Its hidden "unread" labels were not contained by the list, which made the panel taller than its box; the heading could end up half cut off at the top (#1023).
- [webapp] The wiki editor's Save row stays at the bottom of the window all the way down the page. It used to leave the screen once the reader scrolled past it to the stat block, picture and relationships; it is now the last thing in the editor and settles at the end of the page, above the footer (#1023).
- [webapp] The page no longer lets the whole window scroll under the app frame, and the bell and Contents buttons are 44 pixels wide on a phone. A visually hidden label far down a long page could make the window itself scrollable; the page's own scroller now contains it. The footer's links no longer leave a dangling "·" at the end of a wrapped line (#1023).
- [webapp] A menu no longer hangs off the side of a phone screen. A menu
hangs from one edge of the button that opens it, which is right only while
that button sits near that edge of the page. A wiki article's "See it as…"
wraps to the left on a phone, and its menu ran 30px off the screen with its
names cut short. Every menu now checks where it has opened and moves back
inside the page, and is never wider than the screen (#1029). - [webapp] Choosing a feat or a class from the loaded content no longer
loses the list while it searches. Level up's feat picker and Start a
character's "Choose from the loaded content" emptied their list for the
length of every search, said "Nothing matches" until the answer came, and
took a row out from under whoever had just reached it with the keyboard. The
results on screen now stay until the new ones arrive (#1030). - [webapp] Coming back to a discussion just as it finishes loading now
refreshes it. A thread, a list of threads and a Talk tab's count read
again when the window regains focus. A focus arriving in the instant the
page finished loading was dropped, because the page had not yet been told it
was loaded; the next one worked. It also made one test fail now and then on a
busy build machine (#1036). - [webapp] A flaky Preview test waits for the scroll it asserts (#1039).
The test for bringing the preview panel into view a second time checked the
call on the line after the entry's text appeared, which on a busy CI runner
is before the effect that makes the call has run. Test only. - [webapp] A card's notes for tonight are no longer lost when the card
closes mid-sentence. The notes save a second and a half after you stop
typing. Closing the opened card, or the stage moving to another card, inside
that moment threw away what had just been typed. A save still waiting is now
sent as the field goes (#536). - [webapp] A character can no longer be given a name that another character
is "heard as". Listing "Wyatt" as a misspelling of Idani and then naming a
second character Wyatt was accepted, and from then on that misspelling
silently stopped being corrected in transcripts, with nothing to say why.
Creating or renaming a character, finishing a sheet, or assigning an imported
member to a name like that is now refused, saying whose misspelling it is. A
campaign already in that state is unaffected: no transcript was ever
rewritten wrongly, and its characters can still be edited (#634). - [webapp] Shortening how long transcripts are kept no longer destroys them
on the next nightly run (#680). A transcript past its campaign's window
now goes to a bin first, as a recording does, and is deleted for good only
once the bin's grace has passed — the same number of days the bin keeps
audio, never fewer than seven. While it is in the bin nobody can read or
search it, it is in no export and is sent to no model; the session's summary
and highlights are untouched. The campaign's GMs are told three days before
it is deleted (one notification when a session's recording and transcript
are due together; nothing new is posted to Discord), and a GM can restore it
from the session's Review tab. Lengthening the window again lets every
transcript it no longer reaches back out of the bin on the next run, so a
window shortened by mistake is undone by putting it back. Erasing a member
still reaches a transcript in the bin. Settings → Recording, retention & AI
now says where the transcripts go and for how long, instead of "There is no
bin behind it". Upgrading runs one migration
(sessions.transcript_trashed_at,sessions.transcript_deletion_warned_at);
a transcript the old sweep already deleted is not brought back by it. See
docs/OPERATIONS.md→ "How retention removes recordings and transcripts". - [webapp] A player's Handouts page no longer breaks when a handout was shown to her with no file of its own; it is listed as a page.
- [webapp] The party page's character links are underlined, not told apart from the text around them by colour alone (WCAG 1.4.1).
- [webapp] Maps are cut into tiles. The worker mounted the media volume
read-only, so every map's tiling failed on every install. It is writable
now; a customiseddocker-compose.override.ymlthat redefines the worker's
volumes must drop:rofrommedia:/app/media. A failed map now says why in
words rather than showing the server's error and file paths. - [webapp] A party's trail stays in its own campaign. Its routes did not
check the party's campaign, so a member of one campaign could read another
campaign's trail by its id, and a GM could add stops to it. A stop on a map a
player can't see no longer tells her where it is. - [webapp] Creating an atlas, a map or a marker no longer fails with a
server error. - [webapp] The first person to sign in to a new server is its owner as well as its administrator, so Admin's "Run by" line and hand-the-server-on work from the first day. The J1 journey now walks Admin's leftovers, Backup and the not-an-administrator stop in all three projects.
- [webapp] Fixed the Notifications grid's narrow-screen note always saying "always comes to the bell" even for the star-and-wish row, which shows on Home instead — it now names any such exception.
- [webapp] A mail app's own Unsubscribe button now stops the emails it is on.
TheList-Unsubscribeheader named the "Stop these emails" page, which answers
the one-click POST (RFC 8058) with a 405, so pressing Unsubscribe in Gmail or
Apple Mail stopped nothing. It now namesPOST /api/email/stop/one-click; opening
that address instead shows the page, which asks first. - [webapp] A sheet no longer says "Someone else changed this sheet" about
your own save (#737). Leaving a sheet, or a character being started, in
the moment its changes were being saved kept a copy of them on the device;
coming back, that copy was sent again and refused as a change made
elsewhere. What the server already holds is now let go of, and only what is
really still unsaved is put back. - [webapp] A character who has left the party stops collecting its XP
(#737). A retired character's sheet went on counting every award to the
party, and said a level was waiting. Their own awards are still theirs. - [webapp] Each "Choose from the loaded content" on Start a character says
what it chooses (#737). The Ancestry step had three buttons with one
name; a screen reader now hears which is the ancestry's, the heritage's
and the ancestry feat's. - [webapp] A failed recording's retention clock no longer restarts when its
session is written to (#798). A session whose processing only ever failed
counted its audio retention fromupdated_at, which moves on any write to
the row, so erasing a member from it (the erasure note) or editing its title
kept the audio for another full window. It now counts from the new
write-oncesessions.audio_received_at, stamped when the bot's audio is
first accepted. Migrationa798c0d1e2f3adds the column and backfills
existing failed runs fromupdated_at, so nothing is caught earlier or
later on upgrade day. The campaign export and import carry the new field. - [webapp] A campaign's export now carries its prep sheets, story arcs and
the shelf, and an import puts them back (#830). A GM who exported a
campaign to move it, or to keep a copy, lost their prep and their table
setup. The file (schema 15) now holds each session's sheet, beats, plan and
items (For later included), the arcs, and the shelf's scenes, pins, cards and
reveals, each section marked as the GMs'. A pin rides the later sheets and
shelves as it did, and a card taken off a shelf stays off it. A reveal comes
back no wider than it was shown (one for named people becomes the GMs'), and
one taken back comes back taken back. Discord message ids and Workbench runs
do not travel: a generated or a rules card reads from what it kept. Older
files import as before. Seedocs/API.md. - [webapp] A paused or unreachable Redis no longer hangs the request that
touches it, andnotify()stops trying recipients once the broker is down.
Every async and sync Redis client the app builds now has a 2 s connect timeout and a 10 s
reply timeout (the Celery side already had its own), built in one place,
app/redis_pool.py; the one client that waits on pub/sub (the live
recording-status stream) keeps no read timeout. A dead broker used to cost one
publish timeout per recipient; the first connection failure now skips the
rest of that batch (the in-app notifications are still written). (#841) - [webapp] Notification emails are sent again (#865, #736). The delivery task
read its SMTP settings on a second event loop inside its own, which cannot start,
so every per-person notification email since #655 was logged as "Failed to load
SMTP config" and dropped — including, once lane 10 moved them onto that path, a
group without Discord's time votes and confirmations (D153). The task now reads
the settings in its own session and sends off the loop. - [webapp] A phone's Contents no longer names Admin twice. With Home's
new "This instance" group on screen (lane 9 page 4a), the account rows
below it dropped their own Admin link rather than repeat the door under
the same name. - [repo] Two backend tests stop leaking rows into later test files (#869).
test_recap_emaildeletes the SMTP config row rather than nulling it, so it no
longer breakstest_admin's test-email tests when it runs first, and
test_backup_offsitecleans up the BackupLog rows its task tests commit. - [webapp] The Review checklist's "Recap email" row is truthful before approval (#871,
D162). Since the recap now fires on approval rather than at processing time, the row had
no record to read yet and showed a bare "No record". It now reads a server-computed
forecast — "To four people, once you approve" when the campaign has recaps on and someone
would get one, or "Skipped" with why (recaps off, or nobody on the attendance list has
opted in) — the same check the send itself makes, made read-only. - [webapp] Admin → Campaigns lists the campaigns in the bin, dimmed, with the day
each goes for good, as the drawing does. The page never asked the server for
them.GET /api/admin/campaignsgainspurge_at. Its Hand it on… dialog also
hands on to the member it shows selected, where it sent an empty choice unless
the list was touched first. A server with no campaigns says so (#872). - [webapp] Admin → Campaigns' "Keeps its own" column says what a campaign keeps
("Audio 90 days", "Transcripts for good") instead of always "Its own settings",
and a failed recording's error no longer ends in ".”." (#872). - [webapp] Admin's Audit, Users and Campaigns tables scroll sideways on a phone
instead of wrapping every cell over several lines. Each table has a width of
its own, dates and short cells stay on one line, and the scrolling box is
named and reachable by keyboard (#872). - The GM who confirms a session, books one directly or moves it is no longer sent a bell row and an email about it; the rest of the table still is (#887).
- [webapp] A page's picture is as private as the page (#894). Pictures were stored publicly, so a GM-only page's picture could be opened by anyone with its address. They are stored privately now and served only to people who can read the page. A picture uploaded before this moves to the private store the first time it is shown, and its old address no longer works. The backend's own media mount (used where nginx isn't in front of it) no longer serves private files either.
- [webapp] A player's wiki save can no longer rename an item that isn't
identified yet (#912, IS-122). A player only ever reads the party's name
for such an item, never its real title, soPATCH .../lore/{id}now keeps
the real title silently if she sends back what she was shown, and refuses
the save with a 403 if she sends anything else — a GM's rename still works. - [repo] Fixed every upload failing on a fresh install: the backend image now creates
/app/mediaowned by its own user, so a newmediavolume is writable (#914). - Choosing people to show something to, then clicking the next control in the dialog (such as "Send it to them on Discord"), now does both; the first click is no longer lost (#918).
- A session page no longer flips back to what it said before a GM's action (Withdraw, say) when a background refresh that left earlier lands late (#932).
- A slow answer can no longer overwrite a newer one anywhere the app re-reads while it waits: the Review tab, the vote results fold, and the Try again buttons (#941).
- [webapp] A raw
DELETE FROM campaignsno longer fails when a quest
objective was done in one of the campaign's sessions (#943). The objective
was reached by a cascade through its quest and a SET NULL through the
session in one statement; its quest foreign key is now deferred to commit.
The demo seed's delete-order workaround for it is gone. - [webapp] The demo seed's played sessions load again (#978). The seed wrote each played session's characters into
canonical_names, which holds the name generator's picks, so every one of them failed with "This session could not be loaded"; a test now builds every seeded session's page as its GM and as a player. - [webapp] One star, one wish asks about every campaign that played, at
once. A player in two campaigns that both played last night was asked
about one, and the other appeared only after answering and reloading,
which read as the same question twice. Home now shows one card per
campaign, each naming its campaign when there is more than one, and
GET /api/me/star-wishreturnsprompts(withpromptkept as the
first of them) (#980). - [webapp] Campaign Settings → Recording's "Test the bot" box has room inside
its border. Its text and buttons ran right up against the edge; the box now
has the same inner padding as the AI panel above it. - [webapp] The models that write about people are now told their pronouns, so a saved recap no longer has to guess. The session summary, its event log and key moments, the wiki suggestions,
/ask, the wiki's draft tools and one-line page summaries, and the GM's prep tools each get the pronouns of the people they can name, and only of those who have set some: players' (from their account), their characters', and those on wiki pages the table can read. A player's and their character's are sent as two separately labelled lists and never stand in for each other, and the model is told to use a name or singular they for anyone not listed rather than guess. Nothing is copied: they are read each time a prompt is built, so clearing them, leaving a campaign or deleting an account takes them out of every later prompt. Pronouns are personal data and now go to the configured model along with the names and transcript that already did;/privacyanddocs/OPERATIONS.md("What the LLM is sent") say so. (#999, finishes #653) - [webapp] A tap on a phone no longer goes missing while the page is still
scrolling. The page's scroller hadscroll-behavior: smooth, which
animated every scroll — the browser's own scroll to whatever took focus, a
restored position, the scroll to a button about to be pressed — and a tap's
click, which a phone sends a moment after the finger lifts, landed on
whatever had slid under it by then. It was J5's intermittent phone-only
failure (Strike, Reword and "Accurate" not taking). Scrolls are now
immediate; a jump you ask for (a Contents entry, an event's "at 1:09:12")
still glides, unless your system asks for reduced motion. - A scheduling test no longer fails on Tuesdays: it put a session past the 14-day RSVP horizon on that one weekday.
- [webapp] The web fonts are back. No production build had shipped a
font file since the design system moved to self-hosted fontsource faces, so
every page rendered in the browser's fallback sans-serif: Tailwind's PostCSS
pass lost the path Vite needed to find the font files, and Vite only warns
about that. The faces are now imported from JavaScript, and Vite emits them
with hashed names (none inlined, which the CSP would refuse).npm run build
now fails if anyurl()in the built CSS points at a file that is not there,
and nginx answers a missing/assets/file with a 404 instead of the app's
index.html(#891). - The wiki no longer names the demo campaign to everyone: its front page, a group and Recent changes said "the Amber Coast" and "Dana" in every campaign. They say "the GMs" now (D174).
- A wiki change reads "Dana started it", not "Dana new page it", on the front page and in Recent changes.
Security
- [webapp] The add-member and attach-to-an-account search needs two letters
before it finds anyone (#702). One letter, or an empty query, used to list a
slice of every account on the instance whichever scope was set; it now answers
nothing, and the page does not ask until two letters are typed.%and_
in a search are plain characters rather than wildcards. - [webapp] An unidentified item's true name no longer reaches players
outside the wiki (#877). A page with a name the party knows it by ("A
tarnished silver ring") showed that name on every wiki read, but not
everywhere a page is named. A player could read the real one in the
page's History, in the bell line, email and Discord post for a handout
shown to them, in a reveal's own title, in "another page is already
called that" when they guessed it, in a campaign timeline row with no
words of its own, in the quest log, the bot's/quests, the pre-session
reminder's open threads and the session page's open loops, in "Add to a
sheet", in the name a new character sheet starts with, in "… reached
level 7" and in a rest's "… has no hit dice". The wiki's list was also
sorted by the real name. Each now uses the name the reader knows; a GM
still reads the real one in their own views, and words written once for
everyone (a reveal, a Discord post, a milestone) carry the party's name.
A new test lists every read of a title in the server and fails on one
nobody has classified (tests/wiki/test_title_readers.py). - pypdf, which reads the page count of uploaded handout PDFs, is on 6.19.0 rather than 5.1.0, clearing the parser advisories pip-audit reports against it (#930).
- [bot] pyjwt 2.13.0 → 2.14.0, clearing ten advisories (CVE-2026-101917, CVE-2026-102265 to 102274) against the version the bot pulled in through redis.
- [frontend] Dev tooling: brace-expansion and fast-uri patch bumps, clearing four advisories (GHSA-q2hr-2g5m-vwhr, GHSA-qhr7-859c-m2p7, GHSA-6j4f-fj2g-mc7p, GHSA-hrr3-gc8f-f4qj) in build-time dependencies; nothing shipped to the browser changes.
- [webapp] A wiki page in the bin is no longer in a campaign's export, so
an import can no longer bring it back live (#990). The export wrote a
binned page like any other, and the import has no way to put a page in the
bin, so it came back as a live page with the audience it last had: a player
of the new campaign could read what the GM had taken away. A soft-deleted
row now stays behind, with everything in the file that hangs off it — the
page's Markdown file and picture, its aliases, owners, name targets and
relationships, any reveal of it, and the wiki cards, pins and prep items
that carry a copy of it. A row that pointed at it keeps its own words and
loses the link. The import also skips any row a file marks as deleted
rather than importing it live. Nothing that is only ended is left out: a
cancelled session, a hidden post, a reveal taken back and a card taken off
a shelf travel as before. An archive made before this release may still
contain binned pages, which import live — export the campaign again
instead of importing an old file. Seedocs/API.md. - [webapp] The draft tools (rephrase, expand backstory, promote, stat block, merge) and the relationship-proposal prompt now show the model an unidentified item by the name the table knows it by, so a model can no longer write its true name into text meant for the table. What a GM reviews and what an approval writes back are unchanged, and an approved proposal never renames the page to its player title. A proposal about an unidentified item now finds its page by the table's name instead of arriving as a new page (two items sharing a player title are not guessed between). (#993, follows #877)
- [bot] pyjwt 2.14.0 → 2.15.0, clearing CVE-2026-101918 against the version the bot pulls in through redis; and [webapp] serialize-javascript 7.1.1 → 7.1.2 in the frontend's lockfile (GHSA-gfhx-hw2g-v5hg), a build-time dependency that is not shipped (#995).
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- [webapp] The bell reaches older notifications with "Show older". The
-
v4.4.1
StableAll checks were successfulCI / Bot/backend version sync (push) Successful in 1m0sRelease / Create Forgejo release (push) Successful in 14sCI / Backend lint (ruff) (push) Successful in 1m20sCI / Docker image build (push) Successful in 34sRelease / Build and push versioned images (push) Successful in 1m5sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 2m16sCI / Bot tests and audit (push) Successful in 3m34sCI / Frontend tests, audit, and build (push) Successful in 4m11sCI / Synthetic session harness (no GPU, no LLM) (push) Successful in 12m46sCI / Backend migration, tests, and audit (push) Successful in 14m39sreleased this
2026-09-12 03:27:06 +00:00 | 2304 commits to main since this releaseFixed
- [webapp] A session's quote board is no longer built from only part of the
session (#613, #614). A long session is read by the AI in windows, and how
much text fits in one window was estimated with a character count tuned for
ordinary prose. A transcript is not ordinary prose — every line carries a
timestamp, a speaker's name, and often half a sentence — so the estimate ran
around 60% under, and a window could be refused by the AI server as too big
to read. A refused window was then dropped with nothing in the log but the
wordRuntimeError. For the quote board that was worse than it sounds: a
partial set of quotes replaced the previous complete one, so a board could
end up holding only the last twenty minutes of an evening, with the good
version already deleted. Now a refusal teaches Quest Board how far its
estimate was off for that server, the window is split smaller and asked
again once, and a quote board is replaced only when every window came back.
The same recovery covers the two other places a long session is read in
windows: the events behind a summary, and the scan that suggests wiki
updates. Nothing needs configuring — the correction is measured from the
refusal itself, is not written into your settings, and sessions that already
summarised correctly send exactly the same windows they did before.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- [webapp] A session's quote board is no longer built from only part of the
-
v4.4.0
StableAll checks were successfulCI / Backend lint (ruff) (push) Successful in 35sCI / Docker image build (push) Successful in 34sRelease / Create Forgejo release (push) Successful in 32sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m0sCI / Bot/backend version sync (push) Successful in 1m10sRelease / Build and push versioned images (push) Successful in 1m6sCI / Bot tests and audit (push) Successful in 1m56sCI / Frontend tests, audit, and build (push) Successful in 3m16sCI / Backend migration, tests, and audit (push) Successful in 7m54sCI / Synthetic session harness (no GPU, no LLM) (push) Successful in 17m35sreleased this
2026-09-12 00:01:27 +00:00 | 2308 commits to main since this releaseAdded
-
[webapp] Wiki suggestions wait until you approve the summary, and your
corrections clear them (#610). Suggested wiki updates used to be made once,
automatically, the moment a session finished transcribing — before anyone had
read a word of it. They are built from the summary and the transcript and
never from the events, so nothing you struck, corrected or relabelled could
reach them: a GM who misspoke "You're in Ironroot Hold" could correct the
event and watch the wiki go on proposing Ironroot Hold as a place the party
had arrived at. Now nothing is suggested until you press Approve summary,
which is the moment you have decided the session's record is right; and any
regenerate, restore or hand-edit throws away the suggestions you have not
acted on yet, putting the button back to Suggest wiki updates. Wiki
entries you already approved are never touched by that. If you want
suggestions before approving, the button works exactly as it always did. -
[webapp] Tell the summary what is story, and what it must not leave out
(#607). Two more corrections in "Everything that happened", beside Strike and
Correct. Mark as table note moves an event the model mistook for story —
a rules explanation filed as a reveal — into the folded table notes, where it
is never narrated; a table note that was really part of the story gets Mark
as story, with the kind you pick (other, unless you choose). Emphasise
marks an event the summary must include: it is written in on top of the
events the summary is sampled from on a long session, never in place of one,
and the writing step is told it may not be left out. As before, everything is
held on the page — several corrections on one event if they go together, each
undoable — until Regenerate summary. Afterwards the new summary's events
show a star on what you emphasised and "was: reveal" on what you relabelled,
and Un-emphasise takes a star off. Players see the result — a relabelled
table note folded like any other, an ordinary row for one brought into the
story — and never the markers. -
[webapp] When the summary is wrong because the table said something wrong,
you can fix the event and get a new summary (#424). Some mistakes are true
to the transcript, so nothing can catch them: the GM says "You're in Ironroot
Hold", meaning "you were", the event checks out because it was said, and every
summary since puts the party in Ironroot Hold. Under the summary, in
"Everything that happened", a GM can now Strike an event or Correct its
wording, see the pending changes marked, undo any of them, and then
Regenerate summary. Only the writing step runs again — no transcription,
no re-reading of the session — so it takes about as long as the summary's
last step did. The events the model found are kept exactly as it found them;
the corrected set is recorded alongside them, with the original wording, so a
corrected event shows "Corrected by GM" and what it used to say, and a struck
one stays visible to you, struck through. Players see the corrected text and
never see a struck event. Regenerating replaces the current summary, hand edits
included, so the old one is kept and Restore the summary from before puts
it back. An approved summary can be regenerated just as it can be edited, but
the copy already posted to Discord is not changed — withdraw approval and
approve again to repost. If the regenerate fails, the summary is left as it
was, the reason is shown, and your corrections stay on screen to try again. -
[webapp] A short summary you will actually read, and underneath it,
everything that happened (#568). Long sessions were being summarised badly in
two different directions, and both were the same argument: the summariser is
asked for three to six paragraphs and told to represent every verified event,
and on a real 59-event session the local model resolved that by quietly leaving
twelve of them out, while the hosted models resolved it by writing ten thousand
characters nobody skims. So the summary is now written from up to thirty-six
events (three to seven paragraphs' worth; see #600), sampled across the whole
evening rather than off the top of it, in the order they happened (see #589) —
and every verified event, however many
there are, is listed in full under it: "Everything that happened (59 events)",
collapsed until you want it, then one timestamped line each saying when, who,
and what. That list is built from the checked events themselves rather than
written by a model, so nothing in it is a guess and nothing can be dropped from
it, and it is there for your players too, exactly like the summary it sits
under. Events the validator could not confirm are still yours alone, with the
reason, where they always were. A session under thirty-six events reads exactly
as it did before this release. -
[webapp] You can tell Quest Board what the transcriber calls your
characters, and it will stop calling them that (#579). Every character now
has an "Also heard as" list — the names the transcript gets wrong for them —
and after transcription each whole-word match is snapped back to the real
name, possessives included ("Vic's" becomes "Viq's") and without touching a
word that merely starts the same way ("Victor" is left alone). This is not
another attempt to persuade the decoder: the campaign's names already go to
Whisper as an initial prompt and, since #577, optionally as hotwords, and on
the session that prompted this "Viq" still came back as Vic, Vick or Nick in
all 22 mentions, because a speech model will not overturn a common English
word the audio genuinely matches. So the correction is stated by you and
applied deterministically, to every speaker's track — players say each other's
characters' names constantly, and a line that says "Vic" cannot support a
summary beat about Viq. The listed misspellings also go to the transcriber
alongside the real names, which sounds backwards and is the point: a
consistently wrong spelling is one that can be corrected, and three different
wrong ones are not. Quest Board refuses an entry that is another character's
name, and each run records what it corrected asname_snapson the
summarisation run, so a name that never needed fixing is an entry you can
delete (migration6b7c8d9e0f1a). -
[webapp] The campaign's names can be sent to the transcriber as
hotwords
instead of an initial prompt (#577). The bundled WhisperX server takes the
same list of proper nouns two ways — Whisper'sinitial_prompt, a sentence
the decoder is told it has just transcribed, and faster-whisper'shotwords,
the dedicated bias for exactly this job — and they share one slot in the
prompt, so exactly one is ever sent. Which works better is a question about
your audio, your model and your campaign's names, so it is now a switch:
QB_ASR_VOCABULARY_STYLE=hotwords(ornone, the free control arm, which
does not even query the wiki). Unset isinitial_promptand sends
byte-identical requests to every release before this one, so nothing moves
until you move it. Asking for hotwords from a server that does not declare
them falls back to the prompt and warns on every capability probe rather than
running your comparison on the arm you were trying to leave. Each run records
the style it used in the new nullablesummarisation_runs.vocabulary_style
(migration4f5a6b7c8d9e), exposed asvocabulary_styleon the
summarisation-run responses, so the comparison can be read back from the
database instead of from a memory of when the setting changed. -
[webapp] When a rightsholder writes in, there is now somewhere to put it
and something to do about it (#555). An administrator can record a notice —
who wrote in, how to reach them, what they claim, what they say it is about,
when it arrived, and your own notes — and then act on it across everything on
the instance in one place: disable or remove a pack loaded for the whole
instance, a pack in any campaign including one you did not import, or
individual records inside either. Disabling is immediate and reversible and
takes the content out of every search on the next query; removal is final.
Whatever you do, the person who imported the content is told on the pack
itself, in a message naming the notice and quoting the affirmation they
agreed to at import — the version on their own pack row, not today's wording
— and every action is written to the audit log with the notice id, so the log
can still answer "why did this go?" after the pack itself no longer exists. A
batch of actions is all-or-nothing. There is also one operator-set contact
address for notices, readable by any signed-in user so a licences page can
print it. Quest Board decides none of this. Nothing is taken down
automatically, no notice moves its own status, no counter-notice reinstates
anything, and there is no repeat-infringer logic — whether a claim is valid
and what you are obliged to do about it are yours, and the software only
records and applies what you chose. New migration2d3e4f5a6b7cadds
content_notices; no existing table changes. What an operator actually does
with all this is written up indocs/OPERATIONS.md→ "Handling a
rightsholder notice", and the endpoints are indocs/API.md. -
[webapp] Import a Foundry VTT compendium export (#553). Upload the folder,
a zip of it, or the single JSON array people share — Quest Board reads the
documents either way. Pathfinder 2e and D&D 5e are mapped in full, so their
creatures arrive as stat blocks rather than as prose, and a system Quest Board
has no map for still imports as searchable text with itssystemblock kept
intact for export. HTML descriptions become Markdown, and an actor's own items
become the strikes and abilities in its entry. Foundry keeps a document's
publication and licence in four different places depending on whether it is an
item or an actor and whether the export predates the Pathfinder remaster; all
four are read, because a creature that imports with no licence is the one
failure a content pack cannot afford. -
[webapp] Import the D&D 5e homebrew you already have, from 5etools or
Dungeon Master's Vault (#553). Two more readers for the content-pack
importer, so people with homebrew in the two places most of it lives do not
have to convert it by hand. A 5etools homebrew.jsonbrings in its
monsters as full stat blocks — armour class, hit points, speed, challenge
rating, ability scores, size and type — along with its spells, items,
conditions, feats, traps, variant rules, classes and backgrounds; 5etools'
inline markup is rendered to readable text rather than left as{@hit 7}, so
what you search is what you would read. A.orcbrewexport from Dungeon
Master's Vault or OrcPub2 brings in its monsters, spells, magic items, feats
and character options, computes a monster's hit points from its hit dice when
the file does not state them, and takes the pack's name from the homebrew pack
the entries belong to, since the format has no header to read it from.
Neither format records a licence unless the file itself declares one —
Quest Board does not guess on your behalf. Content in either file that
Quest Board does not recognise is imported as searchable text with the
original kept for export, and it tells you what it did rather than dropping
it quietly; anything it genuinely could not read is reported as a warning on
the import, alongside the ones the importer itself raises. Adds one backend
dependency,edn-format, for reading.orcbrew. -
[webapp] Import Open5e data, and build the two starter packs we are allowed
to publish (#553). The last of the five readers takes an Open5e v2 export in
whichever shape you saved it — a single API page, a plain array of records, or
a folder or zip with one file per resource — and brings in its creatures as
full stat blocks alongside its spells, conditions, rules, items, feats and
character options. An Open5e record carries no licence of its own: the licence
belongs to the document it came from, so the reader looks each one up. The
two SRDs and the Black Flag reference document are CC-BY-4.0, the Kobold Press
books and the other known third-party documents are OGL 1.0a, and a document
Quest Board does not recognise gets no licence at all rather than a guess — it
still imports, and the exporter withholds it. Alongside the reader, two build
scripts underscripts/content/produce the starter packs an administrator
can fetch on request:build_5e_starter.pytakes an Open5e export and keeps
only thesrd-2014andsrd-2024documents, andbuild_pf2e_starter.py
takes a Foundry Pathfinder 2e checkout and keeps only the records the ORC
License covers, dropping creature flavour text because that is where the
Reserved Material the licence excludes lives. Each writes a.qbpackcarrying
the notice its licence requires — the SRD's Creative Commons attribution to
Wizards of the Coast, or the ORC Notice with Paizo Inc. named as the licensor
of the products the pack drew on — and aSOURCES.mdbeside it listing the
documents, the counts, and everything withheld with the reason.
Operators: these packs are published as release assets and fetched on
request, never bundled in the image, so an operator who wants neither ships
neither. Each build is pinned to an upstream commit and is byte-identical when
re-run from the same source, so a content update arrives as a diff someone can
read rather than as an opaque new blob. -
[webapp] Load the rules and bestiary content you already have, and look it
up without leaving Quest Board (#553). Quest Board still ships no game
content of its own — bundling a monster manual would make every operator a
distributor of it. Instead there is now an importer: an administrator loads a
pack for the whole instance, a GM loads one for a single campaign, and either
affirms at the point of import that they are entitled to. Quest Board records
who affirmed what and when, shows the licence notice the pack carries,
searches it locally, and exports back only the records whose licence actually
permits it — the rest are listed in the export with the reason they were
withheld. This first slice reads Quest Board's own format in three shapes: a
.qbpackarchive, a plain JSON array of records, and a CSV you can write in a
spreadsheet (kind,name,level,traits,summary,body). A campaign's own version
of a creature ranks above the instance-wide one of the same name and both are
shown, each saying which pack it came from; a GM's records stay GM-only until
they say otherwise. Operators: an administrator can disable any pack on
the instance — a campaign's included — which removes it from every search
immediately while keeping the rows, and can remove it outright; both actions,
and every import and export, are written to the audit log. Descriptions
imported as HTML are sanitised before they are stored, so nothing executable
survives an import. New migration1c2d3e4f5a6baddscontent_packsand
reference_entries; the backend gains two dependencies,nh3and
markdownify. Full format reference indocs/CONTENT-PACKS.md. The screen
that drives it is the next entry; the player-facing search lands with the
v4.4.0 interface work, and the API is documented indocs/API.md. -
[webapp] A screen for loading content packs, and for the promise you make
when you do (#553). Administrators get a Content packs tab in Admin for
the packs the whole instance can search; a GM gets a Content packs panel
on their campaign page for that campaign's own. Both take a file or a URL,
let you name the game system and the format (or leave it to detect), and say
who imported each pack, when, what is in it, and under what licence — with
the licence's own notice one click away, because that text is attribution the
licence requires be carried. The affirmation you agree to is fetched from the
server and shown word for word rather than copied into the app, the box is
unticked again after every import because affirming is per pack and not per
visit, and if the wording changes while the page is open the server sends the
new text back and the box asks again. A pack too large to import in one
request reports its progress with a clock on it and updates itself when it
finishes; warnings from an import are shown rather than counted. Export is a
download link, and removing a pack asks first and names how many records go
with it. Operators: disabling and re-enabling a pack stay administrator
actions on both scopes — a GM can see that one of their packs was disabled
and why, and can remove it, but putting it back is the host's call, since
disabling is how a rightsholder's notice gets answered.
Fixed
-
[webapp] Suggested wiki updates no longer arrive in duplicate (#609).
Every suggestion for a session was written twice — twelve suggestions meaning
twenty-four rows to read and dismiss — whenever the matching step took longer
than a minute, which with a self-hosted model is every time. The step that
writes the suggestions waits for the earlier phases by asking to be retried,
and that request was being caught and turned into a second retry with the
same delay, so two identical copies of the job woke together and each wrote a
full set. The retry is now passed through untouched, and the write takes a
lock and recognises when its twin has already finished, so a repeated
delivery does nothing at all. Existing duplicates are not cleaned up
automatically: press Look for wiki updates again on an affected session
and its suggestions are replaced with one clean set. -
[webapp] A long session's summary no longer spends its spare room on the
start of the evening, and talk that is not about the game stays out of it
(#603). Since #589 the summary takes one event from every stretch of the
session that has any, and because some stretches are quiet there are usually
a few places left over. Those went to whatever was left in the order it
happened, so always to the start of the evening and never to a busy ending.
They are now shared out in proportion to how much each stretch still has to
offer: a stretch where a lot happened gets more of them, wherever it falls,
and a quiet one keeps its one. A busy stretch's events are chosen from across
it rather than from its first few seconds, and the summary is now always
written from the session's last event as well as its first, so its closing
paragraph is about how the evening actually ended. On the real 54-event
session behind this fix, that moves three of the thirty-six places into the
second half of the evening and brings back both of the late events the old
choice left out. It is a better sample, not a guarantee about any single
event, and every event is still listed in full under the summary. Separately, the summariser was
told to leave out talk that is not about the game and recorded it anyway, as
if it were story — a conversation about another video game arrived as an
event, and a player's pun reached the prose. It now labels that talk as table
admin, the same as the rules calls in #596, so it is kept in the event list,
folded away, and never written into the summary. -
[webapp] A summary no longer ends on a "What's next?" with nothing after
it (#595). Every summary prompt asked the model to "end with a short
'What's next?' hook", and Gemma 4 26B took the quoted phrase as the thing to
write: both reruns of one session finished on a bareWhat's next?line and
stopped, which reads as a summary cut off mid-thought. The prompts now ask for
what that last paragraph should say — one or two sentences on what the party
is left facing or has decided to do — and if a model still ends on the
heading alone, the heading is removed before the summary is saved (and before
any "part of the session could not be summarised" note is placed, so the note
never lands in the wrong spot because of a heading that was about to go). A
hook with something after it, or "what's next" anywhere in the prose, is left
exactly as written. Each run records whether this happened ashook_dropped,
so whether the new wording holds for a given model is visible across runs. -
[webapp] The same event, word for word, no longer appears twice in a
session's events (#588, in part). On one real session "Clio discovers the
water is much deeper…" was listed twice at 00:04:56, and two more events were
doubled the same way, because deduplication only merged copies that cited the
same transcript line for the same person, and these copies had drifted apart
on one or the other. Two events with the same start time and the same words —
ignoring capitals, spacing and punctuation at either end — are now one; the
first is kept, unless only the later copy passed the transcript check. Nothing
cleverer than that: two differently worded descriptions of one moment, or one
moment split into two events, are still left alone, because telling those
apart needs judgement and #588 stays open for it. Each run now records how
many duplicates deduplication removed asbeats_collapsed(migration
8d9e0f1a2b3c, which also addshook_dropped). -
[webapp] The GM running the table is no longer written up as the story
(#596). Replaying a real session, several of the "events" the summariser
confirmed were the GM running the game: fixing a character's position on the
map, moving another to the upper layer, explaining what a thresher is (twice),
asking everyone for their backstories, three on which skill reading a journal
needed, and three on adding a skill to a character sheet. Each one passed the
checks, because the GM really did say it, so they filled up the event list,
took places among the thirty events the summary is written from, and one
reached the prose as "the group received an explanation regarding what a
thresher is". Telling the model to leave such lines out had not worked on
that model, so now it labels them instead, as table admin, and the label
does the rest: table admin is never written into the summary and never counts
against it, and in "Everything that happened" it sits behind one quiet line —
"9 table notes" — that opens them in place, each marked as such. A line
the model labels as table admin by mistake is therefore still on the page,
one click away, not lost. The same goes for the GM answering a question about
something earlier (#584), which is now labelled rather than dropped. Each run
also logs how many events look like table admin but were not labelled that
way, which is how we will know whether a given model is using the label. -
[webapp] A long session's summary no longer skips the start of the session
(#589). The cap added in #568 keeps the summary to thirty events and picked
which thirty by kind, preferring reveals, decisions, fights and travel over
"social" and "other". On the first real session it ran on, that cut the
session's own opening: the party's long rest, waking up somewhere they had not
fallen asleep, and working out that the caverns had shifted were all labelled
"other" by the extractor, so each of them lost its place to a reveal from
forty minutes later, and the summary began instead with one character walking
down a ramp. Fifty of that session's eighty-one confirmed events — 62% of
it — sat in the losing tier, because six labels borrowed from combat and
intrigue do not describe a session spent exploring. So the thirty are now
chosen by when they happened: the session is cut into thirty equal stretches
of time, every stretch that contains anything contributes an event, and the
labels only decide which event comes out of a stretch. A summary chosen this
way can still give a moment less weight than you would have; it can no longer
leave out the part of the evening that set everything else up. Sessions with
fewer than thirty events read exactly as they did before, and every confirmed
event is still listed in full underneath the summary either way. -
[webapp] The summariser no longer takes the GM answering a question as
something that just happened (#584). On one session the party was thrown
clean across the map by shifting caverns, and every summariser tried on
it — a 9B model, a 26B one, a frontier hosted one — put them back in the
place they had been thrown out of. All three were repeating the same
extracted event, and that event had passed every check: the GM really had
said "you're in ", a few turns into the players asking where they had
been before the caves. The words were spoken and the speaker spoke them,
which is all the validator can decide from the text; the tense was answering
a question about the past, which it cannot. So the extractor is now told
that a GM line answering a player's question — about earlier events, the
rules, the map or a name — is context rather than something happening now,
and may only be recorded as an event when the lines around it show the
fiction changing. Nothing is rejected on a guess about tense: a beat whose
only evidence is a short GM reply just after a player's question is counted
in the logs so we can tell whether the rule is working, and still goes into
your summary, because a heuristic sure enough to delete a real event from a
session is not one anybody has measured yet. That counting was itself blind to
start with, and found nothing on the very session it was written for: it was
reading the shortened transcript the summariser is prompted from, in which a
run of lines from one speaker is merged into a single line — so a GM answering
in three short bursts looked like one long line of narration, and "short reply
just after a question" could never match it. It now reads the lines as they
were actually spoken, and the counts mean what they say. -
[webapp] The provider concurrency cap now actually applies to the work that
happens after a session (#573). The cap from #356 exists so that a burst of
finished sessions costs latency rather than failures, and the post-session
fan-out — storyline, title suggestions, lore proposals, highlights — was
running entirely outside it. Every LLM call those tasks made logged "Could not
reach Redis to take a llm slot … (RuntimeError: Event loop is closed); running
without a concurrency cap for this call", and then did exactly that. Redis was
fine; the connection to it was not. A Celery worker runs each task in its own
asyncio.run(...), an async Redis connection belongs to the event loop it was
opened on, and the pool holding it was shared for the life of the process — so
from the second task onwards the worker was asking a closed loop for a slot.
Because the cap fails open by design, nothing looked broken: a self-hosted
llama.cpp simply took more concurrent requests than it declares and slowed all
of them down together, which is the failure #356 was written to prevent.
Connection pools are now kept per event loop, and a closed loop's pool is
dropped — the same answerapp.database.task_sessionalready gives for the
database. The one other async pool shared across the process, behind the bot
event stream, is fixed with it: its two beat tasks would have failed more
quietly still, since the unread-trim check reads any error as "no stream yet"
and reports no data loss. -
[webapp] Session summaries are told the truth about the order events
happened in (#566). Quest Board extracts the events of a session more than
once — a second pass reliably finds things the first missed — and then hands
the merged list to the model that writes your summary, with the instruction
that it is "already in chronological order". It was not: each pass was sorted
on its own and the passes were then stacked one after the other, so the list
read as the whole session followed by the bits found on the second look. Every
summary was affected, because every run takes at least two passes. The list is
now put in time order at the door the compose step cannot go round, so the
order the model is told it has is the order it gets — and the summary follows
the session rather than the order the events happened to be noticed in. -
[webapp] The quote board reads the whole session again, instead of
whatever fitted (#575). Picking the memorable lines out of a session was the
one step that still sent the entire transcript in a single prompt, however
long the session was. On a real one that came to about 31,000 tokens against a
22,900-token budget, and the log said as much before sending it anyway — so a
local server that quietly drops the front of an over-long prompt answered from
the tail of the session, without its instructions, and the quotes came back
thin with nothing to explain why. The transcript is now split into windows
that fit, exactly as the summariser's own reading of it already was, and each
window is asked separately. The number of quotes you asked for is still the
number you get: they are taken a few from each part of the session rather than
all from the first, so a four-hour game no longer produces a quote board made
entirely of its first twenty minutes, and "key moments" are capped for the
session instead of per call. One window failing no longer loses the rest of
the board. Nothing changes for a session that already fitted in one prompt. -
[webapp] A part of the session the summariser could not read is retried,
and then said out loud (#572). Quest Board reads a session for events in
consecutive windows, and a window whose call to the model failed was skipped:
the summary simply did not cover that stretch, and the only trace was a line
in the worker log that ended with the provider's error message — which for a
local llama.cpp router is the empty string, so it readBeat extraction failed for window 4/5 (pass 1):and stopped there. On the session that prompted
this, window 4 of 5 was the payoff, it failed on every pass, and the summary
covered none of it with nothing anywhere to say so. Three changes, all in the
direction of saying what happened: the log now names the exception's type and
its repr, and describes "the model returned nothing" in different words from
"the call failed"; a window that fails is asked once more before it is given
up on, since these failures come and go per window; and if it still returns
nothing on every pass, the summary itself ends with a plain sentence naming
the missing range — "Part of the session (from 00:41 to 00:52) could not be
summarised because the summariser returned nothing for it; the transcript is
complete" — placed before the "What's next?" hook. A quiet window that the
model read and found nothing in is not a failure and says nothing. Which
windows were lost is also recorded on the summarisation run aswindows_failed
(migration7c8d9e0f1a2b), so the run history can be asked how often this
happens on a given endpoint. -
[webapp] The check that watches for invented people stopped reporting
"Fortunately" as one (#567). After writing a summary Quest Board reads it
back and looks for anyone the verified events, the speakers and the characters
cannot account for — a cheap guard against a model adding a person nobody
played. It decided what a name was by capitalisation, so every sentence that
opened with an ordinary word was a person: real runs reported "Early",
"Fortunately", "Undeterred", "Continuing", "Ahead", "Getting", "Whatever",
"Among" and "Rounding". English capitalises the first word of every sentence
whether or not it is a name, so that position is now treated as saying
nothing: an opening word counts only if it is a name Quest Board already
knows, or if the summary uses the same word capitalised mid-sentence, where
grammar does not explain it. A genuinely invented name still gets reported the
moment the prose mentions it anywhere but a sentence's first word — which
composed prose does, because it introduces a person and then talks about them.
This has always been a log line and never changed or withheld a summary; what
changes is that the log line means something again. -
[webapp] Transcripts no longer contain the things Whisper says about
silence (#569). Speech recognition writes text for near-silent audio, and it
writes the same things every time: television caption credits ("Sous-titrage
ST' 501", "Closed Captioning by …", "Red Bee Media"), a bare "Thank you.", a
lone full stop, one word looped a dozen times, a fragment in a language nobody
at the table speaks, and — because Quest Board tells the model your campaign's
proper nouns before it listens (#355) — one of those nouns handed straight back
to us. One real six-speaker session carried 143 of these among 1,778 lines.
They cost tokens in the summariser's context, they were cited as evidence for
events that never happened, and a model that reads "Thank you." seventy-eight
times writes about how polite everybody was. A filter now drops them before
the transcript is assembled, matching whole lines only — "Thank you." goes,
"Thank you, Kira, that was genuinely heroic" stays — and never a name a person
might have said on its own, so "Wilhelmina?" survives while "Office of
Prophetic Fulfillment" does not. What it removed is counted per rule on the
session's summarisation run, so a GM can see that lines were taken out and how
many; the lines themselves are never logged. Migration3e4f5a6b7c8dadds one
nullable column tosummarisation_runs. -
[webapp] Pages served beside the app on the same hostname no longer get
bounced to the dashboard (#535). The installed app's service worker answered
every navigation on its hostname with the app shell, so a page the reverse
proxy serves under a path of its own, such as the design-review feedback site
at/feedback/, loaded as Quest Board and was sent to the dashboard by the
router, in any browser that had visited the app before. The worker now leaves
/feedback/alone, uncached, as it already did for the API and the sign-in
flow. Browsers that hold the old worker pick up the new one on their next
visit and are right after one reload. -
[webapp] Editing a summary and then clicking away no longer loses what you
wrote (#464). Correcting a session's summary or transcript and then heading
off somewhere else — the nav bar, a campaign link, the browser's Back button —
discarded every word of the correction without a murmur. Closing the tab or
reloading had asked first since #410, but a click inside Quest Board never
leaves the page as far as the browser is concerned, so nothing was there to
ask. Leaving a half-written edit now stops and asks whether to discard it, and
staying put leaves the text exactly where it was. Nothing changes if you have
saved, or if you opened the editor and typed nothing. -
[webapp] "Saved" now stays on screen under a reviewed stat block (#182).
Correcting a proposed stat value on a wiki draft autosaves it, and the little
status line beside the stat block was supposed to say so. It did — for a
single frame, because the same code that noticed the form was no longer
unsaved immediately reset the line back to blank. In practice the autosave
looked like it had done nothing, which is the one thing a silent background
save must never look like. The line now reads "Saved" until the next edit.
Internal (no operator-visible change)
-
[repo] The v4.4.0 design record and a self-hosted feedback site (#361,
#362, #535).docs/design/v4.4.0-concepts/holds the brief's sources, the
surface inventory, the wireframe kit and the sixteen concept fragments that
build the checkpoint A review pages.tools/feedback-site/is a small,
separate container (FastAPI, a JSON-file store) that serves the players' and
GMs' review pages with a form and an admin readout, so beta testers without
accounts can answer. It shares nothing with Quest Board and is not deployed
with it. -
[webapp] Backend support for an admin view of every campaign on the
instance (#437).GET /api/admin/campaignsreturns each campaign's
ownership, member/session counts, Discord routing flags and retention
overrides regardless of the caller's memberships — metadata only, no campaign
content. The Admin screen that uses it lands later in the milestone. -
[webapp] Adopted the React Compiler lint rules from
eslint-plugin-react-hooksv7 (#182). The frontend enabled two of the
plugin's rules by hand and deliberately skipped itsrecommendedpreset,
which in v7 also brings the compiler's own checks — purity, refs,
immutability, set-state-in-effect and a dozen more. The preset is now on in
full, and the forty violations it found across twenty-two files are fixed
rather than suppressed: mostly loading and "stale" flags that an effect
raised on its way past, rewritten as values derived during render from what
the data in hand was fetched for. There are noeslint-disablecomments for
any of it. Running the compiler itself over the build remains a separate
decision that this change does not make.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v4.3.0
StableAll checks were successfulCI / Bot/backend version sync (push) Successful in 42sRelease / Create Forgejo release (push) Successful in 12sCI / Backend lint (ruff) (push) Successful in 55sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m29sCI / Frontend tests, audit, and build (push) Successful in 2m16sCI / Bot tests and audit (push) Successful in 2m36sCI / Docker image build (push) Successful in 4m13sRelease / Build and push versioned images (push) Successful in 3m53sCI / Backend migration, tests, and audit (push) Successful in 10m13sCI / Synthetic session harness (no GPU, no LLM) (push) Successful in 14m47sreleased this
2026-09-06 09:51:10 +00:00 | 2430 commits to main since this releaseProduct 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, astatefield onlink-status, areasonfield 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_VERSIONis 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/apiis 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/htmlresponses 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>/recordingaddress 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/nextexisted was to be
told by somebody who already knew./helpnow 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/helpdid 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/…/recordingaddress 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'ssession_prepandnpc_voiceat 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
/editURL 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/linkagain, got the same silence, and went
round a loop that/linkcould never resolve, because the fix was
/unlinkon 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 startknows 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 —/nextshows one session
and omits its id,/historylists 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 stopno 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 stopinsisting 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 stopanswers 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,/questsand/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/askon 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
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v4.2.3
StableAll checks were successfulCI / Docker image build (push) Successful in 24sCI / Backend lint (ruff) (push) Successful in 36sCI / Bot/backend version sync (push) Successful in 38sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 50sRelease / Create Forgejo release (push) Successful in 14sCI / Bot tests and audit (push) Successful in 1m36sCI / Frontend tests, audit, and build (push) Successful in 1m44sRelease / Build and push versioned images (push) Successful in 4m52sCI / Backend migration, tests, and audit (push) Successful in 6m46sCI / Synthetic session harness (no GPU, no LLM) (push) Successful in 17m32sreleased this
2026-09-06 02:20:47 +00:00 | 2488 commits to main since this releaseThe first slice of v4.3.0, shipped early because three of these are things the Discord bot was telling your players that were not true. A failed summary is no longer announced as ready, every notification now links back to the session it is about, the bot no longer asks Discord for a permission it never used, and a campaign you archive stops scheduling sessions and pinging people about them. Plus three small web fixes: players can name their own character, wiki articles are readable on a phone, and an unreachable duplicate page is gone.
No migration. The database schema is unchanged since v4.2.0 (
e9fa0b1c2d3eremains the head).Bot API contract v1, unchanged. The notification payload gained an optional link field; an older bot ignores it and a newer bot renders without it.
BOT_EXPECTED_APP_VERSIONis bumped so the bot's startup log matches.One Discord setting you can now turn off. The bot no longer needs the "Message Content Intent" toggle in the Discord Developer Portal. Leaving it on is harmless; turning it off is one fewer privileged permission.
No new settings. Nothing to add to
.env.Fixed
-
[webapp] The wiki article page no longer squeezes the article text into a
sliver on a phone (#376). The infobox now stacks below the article instead
of sitting beside it below thelgbreakpoint, so the article body gets the
full screen width on narrow screens. -
[webapp] Archiving a campaign now makes it go quiet (#479). Archiving was
a display state and nothing more: it changed which list a campaign appeared
in, and every scheduled task went on treating it as fully live. The root was
not the reminders — it was session creation. The hourly materialiser selected
active recurring series with no campaign check at all, so an archived campaign
with a weekly game kept minting confirmed sessions 28 days ahead, every hour,
and the reminder poller then correctly reminded a group that had stopped
playing about every one of them. A campaign a GM had explicitly retired could
therefore be the noisiest thing on a Discord server. Materialisation now skips
archived campaigns (and trashed ones, which had the same hole), and the five
notifications that fire with nobody in the loop — the session reminder, the
at-risk warning, the three-day vote nudge, the recap email and the
end-of-session attendance post — are suppressed for a campaign that is
archived, each logging the campaign id once so an operator can see why nothing
was sent. Nothing else changes: an archived campaign still transcribes and
summarises its recordings, still writes journal entries, still warns before
retention deletes its audio, and still answers a vote or a "reveal to Discord"
that somebody actually asked for — the line is unprompted, not
Discord-facing, because swallowing a button press would be a worse bug than
the one being fixed. Future sessions that a series already materialised are
kept, and archiving is never refused because of them: deleting them would
destroy rows a group may have notes and attendance on, and refusing would make
archiving fail on exactly the campaigns it exists for. They simply stop
speaking, and the archive response now returns how many just went quiet.
Un-archiving resumes everything, materialisation included. -
[bot] A failed transcription no longer announces itself as "Session Summary
Ready" (#372). When summarisation failed, the backend sent the same
session_summarisedevent with an error instead of a summary, and the bot
posted the success embed anyway: a green "Session Summary Ready" headline over
the body "No summary was generated.", followed by 👍/⚠️/❌ reactions inviting
the table to rate a summary that did not exist. The failure now gets its own
red notice that names the session, says the recording was kept so it can be
run again, and points a GM at the retry on the session page — with no feedback
reactions, because there is nothing to rate. The underlying error goes to the
bot's log rather than the channel. -
[bot/webapp] Every Discord notification now links back to Quest Board
(#391). Twelve of the thirteen notification embeds named a session and then
left you to go and find it; only the vote reminder carried a link, and the
summary embed's footer said "Full transcript available to GMs on Quest Board"
without being clickable, because Discord embed footers cannot hold links. The
backend now attaches the session's URL to every notification it sends, and
each embed renders it as an "Open in Quest Board" field — the summary's
signpost among them, moved out of the footer so it can actually be followed.
Nothing changes for an instance with noAPP_URLset: the field is omitted
rather than filled with a link that goes nowhere. -
[bot] The bot no longer asks Discord for the privileged Message Content
intent (#394). It never read message text — every command is a slash
command — but requesting the intent meant a self-hoster who did not tick
"Message Content Intent" in the Discord Developer Portal got a bot that
refused to connect and, under Compose's restart policy, restarted for ever.
The intent is gone, so that toggle is no longer needed; Server Members
Intent is still required and is what per-speaker recording attribution
depends on. If a privileged intent ever is missing, the bot now says which
one and where to enable it, once, and keeps serving/healthinstead of
crash-looping..env.exampleand the Discord setup docs have been corrected —
they told you to enable Message Content and called Server Members optional,
which was backwards.
Internal (no operator-visible change)
- Deleted the orphaned
CampaignNotespage and its unused/campaigns/:id/notes
route (#384). It was linked from nowhere and duplicated the campaign journal
already shown on the campaign page itself; the "back to dashboard" pattern it
was cited as an example of still exists inCampaignHighlights.jsx. - Added regression tests locking in that a player already sees an actionable
"+ Add character" button on their own row when they have no character yet
(#383). Investigation found the underlying behaviour was already fixed as a
side effect of the multi-character work in #330; no production code changed.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v4.2.2
StableAll checks were successfulRelease / Create Forgejo release (push) Successful in 12sCI / Bot/backend version sync (push) Successful in 45sCI / Backend lint (ruff) (push) Successful in 54sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m28sCI / Frontend tests, audit, and build (push) Successful in 1m55sCI / Bot tests and audit (push) Successful in 2m9sCI / Docker image build (push) Successful in 4m11sRelease / Build and push versioned images (push) Successful in 4m25sCI / Backend migration, tests, and audit (push) Successful in 9m59sCI / Synthetic session harness (no GPU, no LLM) (push) Successful in 14m56sreleased this
2026-09-05 17:26:13 +00:00 | 2503 commits to main since this releasev4.2.1's headline fix did not hold on the bundled stack, so this patch follows within the hour. The hardware preflight now measures a reasoning model's throughput from the tokens it actually produced, whether or not it finished its answer, and reports a profile band on the default self-hosted configuration.
No migration. The database schema is unchanged since v4.2.0 (
e9fa0b1c2d3eremains the head).Bot API contract v1, unchanged.
BOT_EXPECTED_APP_VERSIONis bumped so the bot's startup log matches.No new settings. If you deployed v4.2.1 and the preflight panel still said "Not measured" for the LLM, upgrade and re-run the preflight; that is the whole of the change. If you skipped v4.2.1, its notes still apply.
Fixed
- [webapp] The hardware preflight now measures a reasoning model whether or
not it finishes its answer (#507). v4.2.1 gave a thinking model a bigger
output budget on the theory that it would finish inside one. On the bundled
llama.cpp stack serving qwen3.5 — the default self-hosted configuration — it
did not: the model thought for the whole budget, 2048 tokens in 61 seconds,
and the panel reported "Not measured" again. Raising the cap further is the
wrong lever, because a budget large enough for a workstation GPU to finish
inside is a timeout on a CPU box. So the probe stopped asking the model to
finish. 2048 tokens in 61 seconds is 33.6 tokens per second, which is the
only number the preflight exists to obtain, and it is now read off the
generation that ran out of room instead of being thrown away with it. A
deployment running a local reasoning model gets a generation rate, a profile
band and an expected session time, with the panel saying that the rate came
from a generation that never reached a visible answer. An endpoint that
reports no tokens at all is still reported as broken.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- [webapp] The hardware preflight now measures a reasoning model whether or
-
v4.2.1
StableAll checks were successfulCI / Bot/backend version sync (push) Successful in 43sCI / Backend lint (ruff) (push) Successful in 54sRelease / Create Forgejo release (push) Successful in 12sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m25sCI / Frontend tests, audit, and build (push) Successful in 1m51sCI / Bot tests and audit (push) Successful in 2m9sCI / Docker image build (push) Successful in 4m30sRelease / Build and push versioned images (push) Successful in 4m41sCI / Backend migration, tests, and audit (push) Successful in 9m24sCI / Synthetic session harness (no GPU, no LLM) (push) Successful in 15m14sreleased this
2026-09-05 16:26:30 +00:00 | 2510 commits to main since this releaseSuperseded by v4.2.2. The preflight fix below did not hold on the bundled llama.cpp stack: a reasoning model thinks for as many tokens as it is given, so the larger budget still produced no visible answer and the panel still read "Not measured" (#507). v4.2.2 measures throughput from the tokens the model did produce instead. The other two fixes in this release are good; upgrade straight to v4.2.2.
A patch on v4.2.0 from the first live validation of the release: the hardware preflight now measures reasoning models instead of reporting "Not measured", usage rows name the ASR model that actually served the run, and the synthetic-session harness scores its in-character assembly against the right denominator.
No migration. The database schema is unchanged from v4.2.0 (
e9fa0b1c2d3eremains the head).Bot API contract v1, unchanged. A v4.2.0 bot works with a v4.2.1 backend and vice versa;
BOT_EXPECTED_APP_VERSIONis bumped so the bot's startup log matches.No new settings. Nothing to add to
.env. If your preflight panel showed "Not measured" for the LLM on a self-hosted reasoning model, re-run the preflight after upgrading; nothing else is required.Fixed
-
[webapp] The hardware preflight now measures deployments running a
reasoning model, instead of reporting "Not measured" (#507). Admin → AI →
"Run preflight" asked for at most 128 tokens of prose, which is not a short
answer for a thinking model — it is no answer at all, because the hidden
reasoning spends the whole budget before the first visible word. On the
bundled llama.cpp stack serving qwen3.5, which is the default self-hosted
configuration, the probe failed every time: no generation rate, no expected
session time, and a "Not measured" label beside an endpoint that summarises
whole sessions perfectly well. A provider that declares reasoning is now
probed with a budget it can answer in, and one that turns out to need it
anyway — an OpenAI o-series model, or Ollama serving qwen3 — is retried once
with the larger budget, with the panel saying why that measurement took
longer. The reported rate still counts the reasoning tokens, because a real
summarisation run pays for them too. -
[webapp] Admin → AI Usage now says which model transcribed a session
(#508). If you run the bundled transcription server, the model column was
empty on every run: the server picks its model at startup and takes no model
parameter, so there was nothing configured to record — while the
summarisation half happily named its model. That left the one question worth
asking of a self-hosted box unanswerable, because the model does change
under you when somebody redeploys it with a smaller one, and nothing in the
history said when. Quest Board already asks the server what it is running
(that is how it knows whether your server can do word timings); it now
records the answer alongside the run and shows it beside the transcription
time. A model you have set by hand still wins, an endpoint that never
answered is recorded as unknown rather than blank, and a run on your own
hardware is still costed in compute seconds rather than dollars. Older rows
keep their empty column — the model a session used last month is not
recoverable now. -
[webapp] "No vocabulary hints" is now reported as a medium-impact gap, not
a low one (#506). The capability panel ranked it below "the provider was
guessed from the URL", which told operators the wrong thing to fix first: the
measurement in v4.2.0 put the campaign vocabulary at roughly ten to
twenty-five points of proper-noun recall on identical audio, at no cost to
the rest of the words — the largest single effect anything in that release
measured. The wording now says what you actually lose, which is your own
characters' and places' names coming back mis-heard and spelled differently
every time. Nothing about the pipeline changed; if you run the bundled
transcription server, the fix is still to update it.
Internal (no operator-visible change)
- The synthetic-session harness scored marker detection for its
ic_only
assembly over the whole script's 54 marker occurrences, four of which are
only ever spoken in out-of-character lines that assembly does not render —
so that leg understated itself by counting markers no transcriber could have
returned (#506). Each assembly is now scored against its own script, and the
excluded occurrences are reported next to the count.fullis unchanged.
Committed calibration figures are annotated rather than restated: re-deriving
them needs a rerun on the GPU box. - The same harness seeded its Discord links unverified, and attendance
resolution deliberately ignores unclaimed links, so a live run reported all
five known players as unmatched strangers and never reached the
uncaptured-member check the fixture exists to exercise (#506). The links are
seeded verified now,seed_db --verifychecks that they are, and the
expected attendance outcome of a live run is written down.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v4.2.0
StableAll checks were successfulCI / Backend lint (ruff) (push) Successful in 27sCI / Bot/backend version sync (push) Successful in 22sRelease / Create Forgejo release (push) Successful in 12sCI / Bot tests and audit (push) Successful in 1m26sRelease / Build and push versioned images (push) Successful in 46sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 2m1sCI / Frontend tests, audit, and build (push) Successful in 2m42sCI / Docker image build (push) Successful in 4m24sCI / Backend migration, tests, and audit (push) Successful in 9m55sCI / Synthetic session harness (no GPU, no LLM) (push) Successful in 14m20sreleased this
2026-09-05 06:22:55 +00:00 | 2527 commits to main since this releaseAI Provider Abstraction & BYO-AI. No commitment to any single transcription or
language-model provider: a normalised contract with swappable adapters, a
campaign can bring its own providers, self-hosting is a first-class, measured
target on both GPU and CPU, and the pipeline tells you what your configuration
cannot do instead of quietly working around it.⚠️ Five migrations —
a5b6c7d8e9fa(provenance on summary runs),
b6c7d8e9fa0b(per-session usage and cost),c7d8e9fa0b1c(per-word timings
on transcript segments),d8e9fa0b1c2d(per-campaign AI settings) and
e9fa0b1c2d3e(resolved provider identity on usage rows). All are additive —
new tables or nullable columns, nothing rewritten, nothing backfilled — and
each was verified to apply, roll back and re-apply cleanly. For a minor
upgrade this size take a snapshot first:make migrate-safedoes the
preflight, backup and migrate in one step.The bot contract is unchanged (still v1) and nothing under
/api/bot/*
changed shape, so the backend and bot images can be upgraded independently in
either order.⚠️ If you run the bundled transcription server, update it. The word
timings and the campaign-vocabulary hint in this release need its API v2;
against an older server Quest Board keeps working exactly as before and the
new "What your AI configuration can and cannot do" panel tells you to update.
The server lives outside this repository; see OPERATIONS → "Upgrading the
bundled transcription server".Six new optional
.envsettings, all with safe defaults:
QB_LLM_CONCURRENCY_LIMIT,QB_ASR_CONCURRENCY_LIMIT,QB_AI_PROFILE,
QB_LLM_TIMEOUT_SECONDS,QB_LLM_PROSE_TIMEOUT_SECONDS,
QB_ASR_TIMEOUT_SECONDS. Leave them unset unless OPERATIONS tells you
otherwise; a CPU-only deployment should pinQB_AI_PROFILE=cpu.The repository now carries ~9 MB of invented audio — a committed
synthetic 30-minute session that exercises transcription end to end in CI.
No real voices, no real content; seeCLAUDE.mdfor the carve-out.Added
-
[webapp] Each group can now bring its own AI provider, and pay its own
bill (#128). One additive migration (d8e9fa0b1c2d) adds a new
campaign_ai_settingstable; nothing existing is touched, nothing is
backfilled, and every campaign keeps using the instance's providers until
someone changes that. Transcription and summarisation have always been one
endpoint each for the whole instance, which is right when you host one group
and awkward the moment you host several — "whose OpenAI bill is this" is a
question those groups need to be able to answer separately. Admins get one new
switch in Admin → Bot Settings, off by default: "Let campaigns bring their
own AI providers". With it on, each campaign's settings page grows a panel
where its GM can point transcription and summarisation at their own endpoints
and keys. The two sides are independent, so "our own key for summaries, your
Whisper box for transcription" works; leaving either blank keeps using yours.
Keys are encrypted at rest with the same key as your own instance secrets and
are never shown again, to anyone. Every URL goes through the same checks an
admin's does, so a GM cannot aim the instance at an address on your private
network. Turning the switch back off makes every override on the instance
inert immediately without deleting any of them — switch it on again and
each group has exactly what it had. Two things worth knowing before you enable
it: a group pointing at a hosted service is uploading their session audio to
that service under their contract rather than yours, and a campaign whose own
endpoint is misconfigured fails visibly rather than quietly borrowing yours.
See "Per-campaign AI providers (bring your own)" indocs/OPERATIONS.md. -
[webapp] Quote timestamps now point at the word, not at the sentence — if
your transcription server is current (#352). One additive migration
(c7d8e9fa0b1c) adds a nullablewordscolumn totranscript_segments;
nothing existing is touched and nothing is backfilled. The bundled
transcription server used to return one time per sentence, so a quote
captioned "[01:12:40]" was really "somewhere in the twenty seconds that
started at 01:12:40" — which is fine for a one-line reply and useless for a
line thirty seconds into somebody's monologue. The current server aligns
every word, and Quest Board now reads those timings and anchors each quote to
the word it actually begins on. Alignment is not perfect and is not pretended
to be: a word the server could not place is positioned between its neighbours
and marked as an estimate, and a quote anchored to an estimated word keeps
the old sentence-level timestamp rather than a prettier invented one.
Because two versions of that server exist and you update it on your own
schedule, Quest Board now asks it what it can do — one cachedGET /health
— instead of assuming. On an older server everything behaves exactly as it
did before, with one deliberate change: it is now reported as taking no
vocabulary hints, because it accepts the campaign-names field and silently
ignores it, and Admin → Bot Settings tells you that updating the server is
the fix. Word timings and vocabulary bias apply to new sessions only —
nothing is reprocessed, and older transcripts keep their sentence-level
times. See OPERATIONS.md → "Upgrading the bundled transcription server". -
[webapp] A busy evening now makes you wait instead of making you retry
(#356). Groups play on the same two evenings, so a week of recordings lands
within a few hours of each other — against one transcriber that does one
session at a time and one AI endpoint that does two requests at a time.
Quest Board now knows those limits and queues behind them instead of pushing
past them, so a Saturday backlog costs you some patience rather than a
failed session with a rate-limit error in it. If a service turns a request
away anyway — a shared GPU box, or a hosted provider's own limit — that
session goes back in the queue and waits as long as the service asked,
instead of failing with something you could not have fixed. While a session is waiting its
page says so — "Queued behind 3 other sessions — about 20 minutes" — rather
than showing the same silent "Processing…" a genuinely stuck session shows,
which is what has been sending GMs to the Retry button and putting a second
copy of the same recording into the same queue./askand the Test LLM
button never wait: if the queue is full they say so immediately, and say
that your endpoint is not the problem. Admins get the whole picture at
GET /api/admin/ai/queue. The defaults suit the deployments this ships
with; if you run llama.cpp with--parallel 4, or several transcription
replicas behind one URL, two new optional settings say so —
QB_LLM_CONCURRENCY_LIMITandQB_ASR_CONCURRENCY_LIMITin.env. No
migration, and nothing to change if you leave them alone. -
[webapp] Transcription is now tested end to end against a session that
never happened (#433). Recording, transcribing and attributing a session is
the part of Quest Board with the most ways to go quietly wrong, and until now
it was only ever tested against mocks — nothing checked that a real speech
server, given six speakers talking over each other for half an hour, gave
every line back to the right person. There is now a complete invented session
in the repository: six speaker tracks of computer-generated speech, thirty
minutes, every word written for the test, and every check that guards your
recordings watched actually firing against it. No real voices and no real
session content are involved, and none ever will be. This costs the
repository about 9 MB of audio; the reasoning for allowing that at all, and
the rules it lives under, are inCLAUDE.md. -
[webapp] Transcription is now told your campaign's names before it
listens (#355). Your party's character names, your wiki's people, places,
factions and artifacts, your NPC cards and any alternate names you have
recorded are handed to the speech model as a hint at the start of every
recording it transcribes. Invented names are the one thing a general-purpose
speech model has never heard, and they are also the words that matter most in
a session — and once a name is wrong in the transcript, nothing further down
can fix it, because the summary, the highlights and the wiki suggestions all
read the transcript rather than the audio. You should see fewer sessions
where a character's name is spelled three different ways, or split into two
ordinary words. Nothing to configure, and nothing changes for a campaign with
an empty wiki. The list is rebuilt from your wiki each time a recording is
processed, so a name you add tonight is in effect for tonight's session, and
a name you retire stops being suggested. Entries in the wiki trash and
suggestions the bot has made but you have not accepted are deliberately left
out. -
[webapp] Admin → Bot Settings now tells you what your AI providers cannot
do, instead of quietly working around it (#358). One additive migration
(a5b6c7d8e9fa) adds a nullable column tosummarisation_runs; nothing is
backfilled and nothing existing changes. A new panel, "What your AI
configuration can and cannot do", names the resolved provider on each side and
lists every gap in plain language with the consequence you would otherwise
just experience: an endpoint that cannot enforce a JSON schema (the big one —
events can be dropped or mis-attributed from a summary with nothing failing),
a context window too small to hold a session in one prompt and how many passes
it will take instead, a provider that does not report prompt tokens so an
over-long transcript cannot be detected, an endpoint that silently discards
the start of an over-long prompt, a provider guessed from its URL rather
than configured, and transcription without word-level timestamps so quote
timings are estimated. The "Test connection" buttons report the same thing
alongside reachability, because an endpoint can answer in 300 ms and still be
unable to do any of this. Quest Board has known all of it since v4.1.1 and
told nobody, which meant a limitation of the endpoint you picked read as a
limitation of Quest Board. Summaries also now record the gaps that applied
when they were written — shown on the session as "Summarised under a limited
configuration" — and a run on a provider that cannot enforce a schema is
marked degraded up front rather than only when its output came back visibly
broken. -
[webapp] You can now have OpenAI or Groq transcribe your sessions instead
of the Whisper server you run yourself (#128, #350). No migration, and
nothing changes unless you choose it — the bundled server stays the default
and stays a fully tested path.Admin → Bot Settingsgains a transcription
provider select and a model field alongside the endpoint URL; point it at
https://api.groq.com/openai/v1orhttps://api.openai.com/v1and leave the
model blank for that host's default, or name any other server that speaks
the same API. Read this part before you switch: with a managed provider,
every per-speaker track of every recording is uploaded to a third party. If
you self-host Quest Board precisely so your table's recordings stay on your
own machine, this setting undoes that and no amount of accuracy is worth it.
If that is not why you self-host, what you get is worth having: word-level
timings that are measured rather than estimated across each sentence, so
jumping to a quote lands on the word rather than near it; no GPU to own or
keep running; and a three-hour session transcribed in minutes on Groq's turbo
model. The "What your AI configuration can and cannot do" panel makes the
difference concrete — the bundled server lists two gaps there and this lists
none. Both services cap one request at 25 MB, which is under fourteen minutes
of audio, so tracks are cut into speech spans and stitched back onto the
session timeline automatically — including when you have silence trimming
switched off, because otherwise every track would be refused unsent. A chunk
that is refused anyway is counted, and a track that loses more than a quarter
of itself fails the run rather than handing you a transcript quietly missing
a third of the evening. See OPERATIONS.md → "Choosing a transcription
provider". -
[webapp] Quest Board now checks its own AI providers once a month, so a
dead endpoint is found before a session needs it (#359). Every AI call in
the product happens because a GM asked for something, which means a Whisper
server that stopped answering in April is discovered by a failed
transcription in July — with the recording already made. A new monthly
background task transcribes one second of silence through whichever ASR you
have configured and asks your LLM to reply with "OK", then records
{ran_at, asr_ok, llm_ok, asr_provider, llm_provider, error}under the
provider_canarykey inapp_settings, logging a warning when either side
fails. It costs a second of audio and a handful of tokens a month; it grades
nothing, retries nothing, and treats "not configured" as a blank rather than
a fault. There is no admin panel for it yet — read it with thepsql
one-liner in OPERATIONS.md → Alerting. Alongside it, every AI provider
adapter now has to pass a shared conformance suite in CI, which is what keeps
"swap your AI provider without a rewrite" true as the code moves, and keeps
the self-hosted fallback a tested path rather than a hopeful one. -
[webapp] Quest Board now records what each session costs to process, in
dollars or in GPU minutes depending on what you run (#357). One additive
migration (b6c7d8e9fa0b) adds a newsession_usagetable; nothing
existing is touched and nothing is backfilled, because there is nothing to
backfill from — none of this was ever recorded. A new panel in Admin → Bot
Settings shows the last 30 days by campaign, by month, and run by run: how
many hours of per-speaker audio went in, how much of it the silence pre-pass
removed before transcription, how long each phase took, how many tokens the
model read and wrote, and what it is estimated to have cost. Every processing
run gets a row, including the ones that failed — transcription is the
expensive half and it finishes long before most failures do, so a run that
transcribed three hours and then fell over has already been paid for, and a
retry is a second row rather than a replacement for the first. Dollar
figures are estimates from published list rates on a stated date, marked as
such on every number: no provider tells us what it actually charged, and a
model our rate table has never heard of gets no figure rather than a
plausible-looking guess. If you self-host your transcription or your LLM there
is no bill to estimate, so those runs show the compute time they occupied
instead — which is the number to size a GPU against, and the honest answer
where "$0.00" would imply the run was free. Nothing to configure; it starts
recording on the next session you process. -
[webapp] Admin → Bot Settings can now measure your hardware, and the docs
say what to expect from three named profiles (#353, #354). No migration.
A new panel, "Hardware profile", has a Run preflight button: it makes one
health round trip to your transcription endpoint and one short generation
against your LLM, times them, and reports what it found — a detected profile,
tokens per second, transcription speed relative to real time, the gaps in your
configuration, and roughly how long a 3.5-hour, five-player session will
take on your hardware. Until now the only way to learn that was to record
one and wait. The result is stored with the date it was taken, so the panel
says "as measured on …" rather than pretending to be live, and the
transcription speed is read from sessions you have actually processed rather
than from a synthetic clip — a deployment that has processed nothing sees
"not yet measured" instead of an invented number.POSTandGET /api/admin/ai/preflightare the same thing as JSON. docs/OPERATIONS.md →
"Hardware profiles" now carries a table of three profiles — CPU-only, modest
GPU, workstation-class GPU — with the model, the hardware, the expected
processing time and the measured accuracy scores for each, including the
command that produced every number, plus
docs/examples/docker-compose.cpu-llm.yml: a complete, runnable CPU-only LLM
service to drop into the stack if you have no GPU.
Changed
- [webapp] Running without a GPU is now a supported configuration rather than
a slower version of one (#353). No migration. Every timeout in the AI
pipeline used to be a fixed number chosen against a GPU — five minutes for a
structured call, ten for a transcription track, thirty seconds for the "Test
connection" button. On CPU-only hardware those are not slow budgets, they are
impossible ones, so a self-hoster without a GPU saw failures rather than
patience, with a recording already made and nothing naming the cause. A new
QB_AI_PROFILEsetting (auto,gpuorcpu) scales all of them together,
andQB_LLM_TIMEOUT_SECONDS,QB_LLM_PROSE_TIMEOUT_SECONDSand
QB_ASR_TIMEOUT_SECONDSset any of the three directly if you have measured
your own endpoint — a value you write down is used exactly as written. The
Celery time limits are derived from those timeouts rather than configured
beside them, so the limit can never be shorter than the provider calls it is
supposed to contain. Nothing changes for an existing deployment: the
default isauto, which is the GPU timings unless the new preflight has
measured your box as CPU-class, and on the GPU profile the task limits are the
same 6 hours they have always been. Two smaller consequences worth knowing:
the monthly provider health check and the admin "Test connection" button now
scale too, because a thirty-second cap on a CPU endpoint could only ever
report a working provider as broken.
Fixed
- [webapp] A paid transcription service was being reported as free compute
(#502). One additive migration (e9fa0b1c2d3e) adds three nullable
columns tosession_usage; nothing existing is touched and nothing is
backfilled. Admin → AI Usage recorded the provider you configured rather
than the one that ran — and "Detect from the URL" is the default, which
stores nothing at all. So if you had pasted an OpenAI or Groq endpoint and
left that select alone, every one of those runs was filed as self-hosted:
the page showed GPU seconds and no dollars for sessions you were being
invoiced for, which is the exact opposite of what it is for. Runs now record
the transcription service, model and endpoint they actually used, so a
managed one is counted as managed from here on. Older rows are left as they
are — the endpoint a session used six weeks ago is not recoverable now, and
guessing it would be inventing history rather than repairing it. Alongside
that: managed transcription can be priced at all for the first time (OpenAI
and Groq list rates, per model — the Groq figure is a floor, because their
ten-second minimum per request is not visible from what is recorded), the
rate table has learned the Claude 5 models the documentation recommends
instead of showing no cost for them, andmake conformance-livecan now be
pointed at a managed transcription service rather than only at the bundled
one.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v4.1.2
StableAll checks were successfulRelease / Create Forgejo release (push) Successful in 13sCI / Bot/backend version sync (push) Successful in 43sCI / Backend lint (ruff) (push) Successful in 51sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m17sCI / Frontend tests, audit, and build (push) Successful in 1m54sCI / Bot tests and audit (push) Successful in 2m12sCI / Docker image build (push) Successful in 4m51sRelease / Build and push versioned images (push) Successful in 4m40sCI / Backend migration, tests, and audit (push) Successful in 7m13sreleased this
2026-09-05 01:25:00 +00:00 | 2628 commits to main since this releaseThe AI provider you chose in v4.1.1 is now honoured everywhere, including the
session summary itself. Upgrade straight past v4.1.1 if you have not already.No migrations. Nothing to rehearse, nothing to back out.
The bot contract is unchanged (still v1), and nothing under
/api/bot/*
changed at all, so backend and bot images can be upgraded independently in
either order.Under the hood every generation path now carries the configured LLM as one
object instead of five loose parameters (#484). That is invisible to operators
and changes no behaviour; it is what made the fix below a one-place change.Fixed
- [webapp] The AI provider you chose is now used for session summaries and
for the Test button, not just for everything else (#488). The provider
select added in v4.1.1 was honoured when generating structured content and
answering/ask, but two paths went on detecting the provider from the
endpoint URL and ignoring your choice: writing the session summary itself,
and Admin → Bot Settings' "Test LLM" button. This only affects you if you set
the provider to something the URL does not already give away — typically a
gateway or reverse proxy on your own domain — in which case summaries were
being sent to the wrong service with credentials in the wrong header, usually
failing outright, and the Test button reported on a connection nothing else
in the product would have made. Nothing changes if you left the provider on
"Detect from the URL".
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- [webapp] The AI provider you chose is now used for session summaries and