-
v4.5.0-rc6
Pre-releaseSome checks failedCI / Bot/backend version sync (push) Successful in 26sCI / Does this change touch the recording path? (push) Successful in 26sRelease / Create Forgejo release (push) Successful in 22sCI / Feedback site tests (push) Successful in 47sCI / Backend lint (ruff) (push) Successful in 1m9sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m56sCI / Docker image build (push) Successful in 2m31sRelease / Build and push versioned images (push) Failing after 2m39sCI / Bot tests and audit (push) Successful in 3m15sCI / Synthetic session harness (no GPU, no LLM) (push) Successful in 8m34sCI / Frontend (shard 2/2) (push) Successful in 13m58sCI / End-to-end journeys (phone) (push) Successful in 11m52sCI / End-to-end journeys (keyboard) (push) Successful in 13m2sCI / Frontend (shard 1/2) (push) Successful in 16m47sCI / End-to-end journeys (desktop) (push) Successful in 17m51sCI / Backend migration, tests, and audit (push) Successful in 43m43sreleased this
2026-10-05 08:14:08 +00:00 | 744 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. - [webapp] An imported campaign's rules are linked to their content pack
again (#1070, export schema 19). A rule on the shelf, a pinned rule, a
rule in a prep sheet and a wiki page made from a rule ("Based on …") each
point at a record of a content pack. An export used to carry no part of
that link, so after an import every one of them stood alone: it read from
its own copy and could not open the record. The export now names each
rule's pack and record (the pack's key, game system, version and source
hash; the record's kind and slug; never a database id), and an import
links the rule again where this server has that pack in use and a record
of that name in it, whether the pack is the same version or another. A
rule whose pack is not on this server, or whose record the pack here does
not have, comes over as before, with its own copy and no link. A link is
never made to a pack that is disabled, to one named by a rightsholder's
notice that is received or being reviewed, or to a campaign's own pack; a
reference that is not one is ignored rather than refusing the import. The
import's result screen says how many rules were linked, how many were not
and why, and how many found another version of their pack
(rule_linksin the answer). Load a campaign's content packs on the new
server before importing it: loading one afterwards does not link the
rules, and importing the file again does. A file made before schema 19
imports exactly as it did; a Quest Board older than this one refuses a
schema 19 file and says to update. A character sheet's own rows are
unchanged. Seedocs/API.md, "Rules and their content packs", and
docs/OPERATIONS.md, "Campaign exports and imports: size, time and disk". - [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 campaign's export carries its handout files and its map, and
an import brings them back (export schema 18; #991 part 2). An export
used to carry a page's picture and no other file: a handout came back
without its PDF, and no part of the map travelled at all. Now each page's
attachments travel with the page (a binned page's only in the bin), and the
map travels whole: atlases, maps, markers, the links between them and each
party's trail, with each map's original image. Tiles are not carried; they
are cut again on the server that imports the file. On import every file is
checked as its upload was and stored privately under a new name, every
reference points at the new campaign's own rows, and audiences only narrow:
the table's and the party's are kept, anything else comes back as the GMs',
and a marker narrowed to the GMs is no longer "revealed". An export is
made to fit the server's import limit (Admin → Import a campaign): a file
that would take it past the limit is left out and listed, with its page or
its map, in the archive'sREADME.md, and an import that would unpack to
more than the limit leaves the file out the same way. The import's result
screen lists every file that did not come and why (files_left_outin the
response). A file larger than the server takes for one file of its kind is
left out and listed in the same way, by the export and by the import: a
stored file can be over the cap its own upload passed. An import brings a
picture for at most 200 maps and at most 5,000 attachments, and a page's
picture or an image attachment of more than 4000×3000 pixels is left out
without being decoded (in a file of any version). A file that is not what
it says it is, or an archive entry named as two files, still refuses the
whole import. A refused or failed import now removes the files it had
written; a page's picture used to be left behind in the media store.
A map's picture is checked by the worker after the import commits, because
decoding a large map does not fit in the backend's 512 MB (a 70 MB PNG
peaked at 403 MB); until it has, the map shows no picture. A map picture of
more than 8192×6144 pixels (an 8K export, about 50 million) is not brought
over, and the result screen says so. The worker now handles one map's
picture at a time, an upload's tiles included. Every older file imports as
it did, and a file that says it is older than 18 has its map and its
attachments left unread. No migration,
and no change to the bot. Self-hosters: an import now needs room on
the media volume for the archive's files, and the worker must be running
for an imported map to get its picture: seedocs/OPERATIONS.md,
"Campaign exports and imports: size, time and disk". - [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 GM's table on a phone: Take it back, and a phone held sideways gets the phone view (#1020). On the phone's stage, "Take it back" now sits beside "Show the table" while the card on stage is up on the table, with the same call as the desktop's "Shown to the table" list, a busy state, and Try again if it fails; a stat block shown by field has every showing taken down (D271). The phone view was chosen by width alone, so a phone at 844 x 390 got the desktop board in 390px of height: a window that is narrow, or short on a touch screen, now gets the phone view, and a short desktop window or a tablet on its side keeps the board. Turning the phone keeps the card on stage and any open "Show the table" or sweep (D272).
- [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] A wiki page in a campaign file's bin whose days were already up
now gets seven days from the import, not the next day's purge. Importing
an old archive used to leave such a page for the daily sweep, so a GM had
at most a day to restore it. It now stays in the bin for seven days from
the import, and the bin's list shows that date. A page with any time left
keeps its own day. (#1055) - [webapp] The accessibility statement no longer lists the map's keyboard panning and zooming as a known gap. The owner decided it no longer describes a gap worth listing (D287); the board, the map markers, status changes and long transcripts stay on the list. (#367)
- [webapp] A campaign's owner can read its members' email addresses, as its GMs can. The owner manages who is in the campaign, so an owner who plays rather than runs it is no longer held to their own address only (D285, amending D275). A player who is not the owner still sees only their own, an instance administrator gets nothing new from being one, and a former owner loses it when ownership moves. The Privacy page says so. (#677)
- [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. - [webapp] A campaign's export and import are handled on disk, an import
is read within fixed bounds, and the largest import is a setting, 1 GB
unless changed (#991 part 2, #1054). An import used to hold the whole
upload in memory, up to 100 MB, and readdata.jsonout of it with no cap
on what it unpacked to, so a small file could exhaust the backend's memory.
That read is now bounded, and a file written to be expensive that is inside
the bounds no longer exhausts a backend at its default 512 MB (the dearest
found took about 117 MB while it was parsed). The bounds ondata.jsonare
low for that reason; parsing it in the worker, which would let them rise,
remains (#1054).
The upload is now received into a temporary file as it arrives and read
from there one entry at a time: an entry's declared size is checked before
it is read and its real size counted as it is,data.jsonmay unpack to at
most 8 MB and hold at most 400,000 values, and an archive with an encrypted
entry, two entries of one name, more than 20,000 entries, or an entry
compressed with anything but store or deflate (a bzip2 or LZMA bomb
decompresses a whole block before the size is checked) is refused. Each
refusal is a422that says what was wrong. Imports are also bounded while
in flight: two at a time per worker process, one per user, and an upload
that stalls for two minutes is dropped — so a slow or many-at-once upload
cannot fill the server's disk or memory. An export is built in a
temporary file and downloaded by the browser from a link, with its length,
where the page used to hold the whole zip in memory first. The largest
file an import takes is now set on Admin → Import a campaign: 1 GB by
default, between 100 MB and 4 GB; a file over it is refused with a413
that says so, before it has all arrived. Nothing about what an export
holds has changed (export schema 17), and every older file imports as it
did. If you run a reverse proxy in front of Quest Board, raise its upload
limit forPOST /api/campaigns/import: the bundled nginx sets none on
that route, but yours has its own. Exports and imports now need disk
rather than memory;TRANSFER_SPOOL_DIRsays where. See
docs/OPERATIONS.md, "Campaign imports and your proxy's upload limit" and
"Campaign exports and imports: size, time and disk". - [webapp] A campaign's export now carries each quest's details and each
character sheet and stat block, and an import brings them back (#991,
part 1; export schema 17). A quest used to come back as a plain page: no
status, giver, reward or objectives, and so not in the quest log. A
character or a stat block came back as the block its page printed, with no
sheet behind it. Both now travel on the page's own entry indata.json
(questandsheet), so they go where the page goes, into the bin with a
binned page. A quest keeps its status, giver, reward, sessions, story
arc and objectives; a hidden objective stays hidden. A sheet keeps its
values, its rows, the damage taken and conditions, its locks, its kept
rolls and the day it was made, and a character's player owns it again once
the GM gives their placeholder to their account. Nothing is shown wider
than it was: an objective or a field for the table or the main party comes
back as it was, and one for named people or one person comes back for the
GMs only until a GM shows it again.
A sheet's version history does not travel: the sheet arrives with one
version, "Imported with the campaign", and "Put back" has nothing older to
offer. A character still being started does not travel: its player
starts it again in the new campaign.
A sheet is only ever put under the definition it was made under. The
built-in ones are on every server; for a content pack's, the import looks
for the identical definition and installs nothing, so a sheet whose rules
are not on the new server is left out and its page arrives with its
printed block as before. The import says so: under "Imported …" it lists
each such page with the rules it needs (sheets_left_outin the
response), and once an administrator has loaded those rules as a content
pack, importing the file again brings the sheets. Rows from a pack the new
server lacks read from their kept copy, marked as removed.
XP is what it was. The campaign's parties and who was in them now
travel, and an XP award keeps whose it was and the day it was given, where
an import used to make every award the one party's: with sheets arriving,
that would have counted every award on every sheet and lit "Level n is
waiting" for characters who had earned none of it. An award to a party is
that party's; an award to one character counts for nobody until that
character's placeholder is claimed, and is the character's again then; an
award to somebody or some party the file does not hold stays in the ledger
and counts toward no sheet. A claimed character is in the party it was in,
and one who had left or retired is not put back in the main party. The new
campaign is linked to the same game system and levels the same way.
A quest's file underwiki/gains a## Questsection and a page with a
sheet a## Stat blocksection. A Quest Board older than this one refuses
a schema 17 file by its version. An archive from before this release
imports as it did, with two differences, both refusals or narrowings: a
printed block's line for named people now comes back for the GMs only, and
a page whosestatsis not a stat block (a file edited by hand) is refused
with a message naming the page, where it used to import and then fail each
time the page was opened. Maps and handout files are still to come. See
docs/API.md. - [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. - [repo]
make migrate-safenow fetches the new release's image (pull on an image host, build on a source build) and runs the preflight, backup and migration from it, instead of from the old running container; the docs no longer tell image hosts to avoid it (#1045). - [webapp] Uploads over 1 MB work in a deployed build (#1057).
nginx had no upload limit set, so its default of 1 MB refused every map image, handout file, larger picture and campaign import before Quest Board saw it, with a bare "413" page. The proxy now lets through anything up to just above the largest upload Quest Board accepts, and Quest Board's own limits and messages apply. A self-hoster behind another reverse proxy should check its limit too. - [repo] Fixed uploads failing on an install whose
mediavolume was created before #914: the one-shotmigrateservice now mounts it and hands it to the app user when it is not already owned by it (one cheap check, no walk of the tree on later starts), so an upgrade repairs it by itself (#1064). - [webapp] Files left behind by a campaign that no longer exists are now removed. A backend killed in the middle of a campaign import (a restart, a deploy, the out-of-memory killer) left that campaign's pictures, maps and attachments on disk forever, and permanently deleting a campaign from the trash never removed its files either. A daily task now removes any campaign folder that has no campaign and has not been touched for 24 hours; a campaign in the trash keeps its files so it can be restored, and a permanent delete now removes them once it has committed. (#1075)
- [webapp] A batch of imported maps no longer holds up everything else the
worker does (#1077). An import queued a task for each map. The worker has
two slots and takes maps one at a time, so one slot worked on a map while
the other held the next map's task, waiting: a recording handed off during
an import was not transcribed, and no reminder went out, until the last map
was done. An import now queues one task that takes its maps in turn, and no
map task waits in a slot for another map: it steps aside and comes back.
Each map still shows its picture as soon as it is done. A map uploaded
during an import gets its turn between two of the import's maps. - [webapp] A photo that relies on its EXIF orientation is stored upright (#1079). A phone stores a portrait photo as landscape pixels plus a tag that says which way to turn them, and the picture check stripped the tag without making the turn, so such a photo came out on its side as a page's picture, an image attachment or a map. The turn is now made before the metadata is stripped, for all eight orientations, in a JPEG, a PNG and a WebP, on upload and on import, and a map's width and height are the upright ones. It costs no more memory than any other picture, so the pixel bounds are unchanged and a 12 MP phone photo is taken as before. Pictures already stored are not changed, because their orientation was stripped with the rest of their EXIF: re-upload one that is on its side to fix it.
- [webapp] An imported character sheet's rows find their content pack
again, and a sheet no longer stores ids taken from an import file (#1095,
export schema 20). A row of a sheet or a stat block that came from a pack
(a feat, a dagger), a condition, and the choices creation and a level-up
kept each held the pack's id and the record's. The export wrote those ids
and the import stored them as given, so on another server every such row
read as removed, and the ids in the database were whatever the file said.
(A reader only followed one to a pack in use that was the instance's or
the campaign's own, so this never reached another campaign's content.) The
export now names each record the way the shelf's rules are named (the
pack's key, game system, version and source hash; the record's kind and
slug) and writes no id. An import links each one where this server has the
pack in use and the record in it, under the same rule and in the same pass
as the shelf's rules, and otherwise leaves the row with its own copy: the
sheet works out either way, and with the same pack every number is the one
it was. The import's result screen counts sheet rows with the other rules.
A file made before schema 20 has its sheets' ids dropped on import, so
its rows come in unlinked, including on the server it was exported from,
where they used to be found by id: export the campaign again after
updating to keep them linked. A Quest Board older than this one refuses a
schema 20 file and says to update. Seedocs/API.md, "A sheet's rows and
their content packs". - [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] Links in running text are underlined everywhere, and map markers and stand-alone text buttons are easier to press. One rule in the design system underlines a link inside a paragraph, a hint, a banner or rendered markdown (wiki articles, summaries, Help) in the ink variant of the accent; menus, lists, navigation, cards, the app bar, the footer and buttons are left alone. A map marker keeps its drawn size but its pressable area is 24 by 24 pixels (44 by 44 on touch) around the same point, and where two markers are closer together than that a press goes to the nearer one; a text button that stands alone ("Show more", "Mark all read") is at least that tall; one inside a sentence stays on its line. A marker that has keyboard focus now opens with Enter or Space, which it did not before, and keeps the focus once it is open. The accessibility statement no longer says map markers are small; it says what is left, that two markers drawn very close together overlap where they can be pressed, and how to reach each (D277, D278, #367).
- [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]
members.mdin a campaign's export no longer prints a
character's pronouns as if they were the player's (#991). The line read
"Marcus (Player) — Wren Halloway (ey/em)", and for a member with no
character name the pronouns stood beside the player's name alone. The
character is on a line of its own now, "Character: Wren Halloway (the
character's pronouns: ey/em)". A player's own pronouns were never in the
archive and still are not. - [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 damaged image is refused with a message instead of "Internal server error".
Uploading a picture whose file was cut short or whose image data was broken, as a map, a page's picture or an attached file, passed the checks on its header and then failed while Quest Board re-saved it, so the upload answered with a 500 and no reason. It now answers "This image is damaged or incomplete, so it could not be read." and stores nothing. Sound images are unaffected. - [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. - [repo] A CI run of the journeys removes the images it built. Each run names its compose project after the run, so its images were never reused, and nothing removed them: about ten a project, three projects a run, the Playwright one near 4 GB. They filled the runners' disks, and jobs failed with "no space left on device". The journeys script now takes
QB_E2E_RM_IMAGES=1, which CI sets, and removes them when the stack comes down; a local run keeps its images, since its project name is fixed and the next run reuses them. - 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] A small picture that declares a huge size can no longer end the
backend (#1074). Checking an uploaded picture means decoding it, and
decoding takes memory by the picture's pixels, not by the size of its file.
Nothing bounded the pixels: a 450 KB PNG of 12000×12000 in one flat colour,
well inside the 8 MB cap, took the backend past its 512 MB, so anyone
signed in who could add a picture to a page could stop the server for
everyone. Every picture's pixels are now read from its header and one over
the bound is refused, with a sentence that says its size and the largest
taken, before anything is decoded: a page's picture, an image attachment
and a picture fetched from a web address at 4080×3072 (about 12.5
million pixels, over all its frames), which takes a phone's 12 MP
photograph, and a map at 8192×6144. The same bound now applies to an
import, which was at 4000×3000 and refused an iPhone's 4032×3024. The
check also takes about half the memory it did (a 4032×3024 JPEG took 95 MB
to check and now takes 53 MB), and the backend checks one picture at a
time. What changes for a picture that is within the bound: a WebP is
stored as a JPEG, or as a PNG if it has transparency or was stored without
loss, because encoding a WebP took up to nine times the memory of the
picture itself; and two kinds count for more than their pixels, because
they cost more to check: a WebP four times (the largest taken is 3.13
million pixels, 2040×1536), a CMYK JPEG twice, and a progressive JPEG twice
(so a page picture may be 6.27 million pixels as a progressive JPEG against
12.53 million as a baseline; a map, decoded in the roomier worker, is not
weighted). An animated GIF is
counted by what it costs to write again, six times one frame and a quarter
of all its frames, so that an ordinary one of many small frames is taken
(480×270 may have 362 frames) and one of a few very large frames is not;
it may have 1,000 frames, and is refused if it would be stored at more
than twice the size cap. A GIF whose later frames reach past its own screen
(which made the image library allocate at the larger size), and a JPEG with
more than 32 scans (slow however few its pixels), are refused before any
decode. An upload over the size cap is now refused before it is read into
memory, and the picture decode runs off the event loop so one upload's wait
never freezes the process for every other request. Every scrap of
metadata is stripped on re-encode, on every path — EXIF including a phone
photo's GPS, XMP, ICC profiles, JPEG and GIF comments, PNG text — so a
player's photograph does not carry its location into the campaign.
A map's picture is checked by the worker after the upload has
answered, as an imported map's already was: a legitimate 70 MB map took
more memory to check than the backend has. The map's page waits and shows
the picture when it has passed, or says why it could not be used; a map
that already had a picture keeps it until the new one has passed. API:
POST /api/campaigns/{id}/maps/{map_id}/imagenow answers with the map
"cutting" and without its new picture; seedocs/API.md, "Picture uploads:
what is taken". No migration, and no change to the bot. Self-hosters:
the worker must be running for an uploaded map to get its picture. - [webapp] A picture's metadata can no longer exhaust the server's memory when it is opened (#1087). Opening a picture made the image library parse its metadata in full, and how much work that was is the file's to choose, not the picture's: a 70 kB JPEG whose EXIF directory pointed every entry at the same six megabytes took the open alone to +171 MB, and an APP2 MPF directory to +1.6 GB — before the pixel-count check that guards decoding, and outside the one-picture lock, for anyone who could upload a picture. Every scrap of metadata (EXIF, MPF, ICC, Photoshop, XMP and comment segments in a JPEG; text, ICC and EXIF chunks in a PNG; EXIF, XMP and ICC chunks in a WebP; comment, plain-text and application extensions in a GIF) is now removed from the bytes before the library is handed them, so there is nothing to parse whatever the file claims. Two fixed, bounded JPEG segments are kept because the decoder needs them, not for their metadata: the JFIF
APP0, and an AdobeAPP14, whose transform byte tells YCCK from CMYK and RGB from YCbCr — dropping it would store a Photoshop CMYK photo with the wrong colours. A file built from a vast number of empty segments is refused in a sentence rather than walked to the end; a picture's own image data (a PNG's or WebP's data chunks, which an honest large picture has thousands of) is not counted towards that limit. Honest files — a phone photo with a thumbnail and a maker note, an HDR/MPO, an edited photo with a large XMP block and an ICC profile — are taken as before, and the EXIF orientation (#1079) is still applied, read from the metadata as it is dropped. - [webapp] A member's email address is now sent only to the campaign's GMs and to that member; other players no longer receive it in the members list (#677).
- [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