-
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