-
v4.1.0
StableAll checks were successfulCI / Backend lint (ruff) (push) Successful in 46sCI / Docker image build (push) Successful in 9sRelease / Create Forgejo release (push) Successful in 12sCI / Bot/backend version sync (push) Successful in 41sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m42sRelease / Build and push versioned images (push) Successful in 54sCI / Frontend tests, audit, and build (push) Successful in 2m15sCI / Bot tests and audit (push) Successful in 2m32sCI / Backend migration, tests, and audit (push) Successful in 7m37sreleased this
2026-09-01 18:11:30 +00:00 | 336 commits to main since this releaseData Durability & Recovery. Nothing you have made can be destroyed by a single
click, a crashed worker, or a background job you never saw run.⚠️ Two migrations —
e3f4a5b6c7d9(wiki entry soft delete) and
f4a5b6c7d8ea(campaign soft delete). Both add nullable columns and partial
indexes only; no data is rewritten, and both were verified to apply, roll back
and re-apply cleanly. For a major upgrade take a snapshot first —make migrate-safedoes the preflight, backup and migrate in one step.The bot contract is unchanged (still v1), so you can upgrade the backend and
bot images independently if you need to. Everything added to/api/bot/*this
release is optional on both sides.⚠️ Deleted things now occupy disk for 30 days. Campaigns and wiki entries go
to a trash rather than being removed immediately, and are purged by a daily job
once the grace period elapses. The window is enforced with a 7-day floor in
code — configuring a shorter one is clamped up, deliberately, because these are
artefacts nobody can reconstruct.New optional setting:
RESTART_POLICYin.env. Leave it unset unless
something else on your host owns boot ordering; see.env.example.Added
-
[webapp] The container restart policy can now be set per host. A new
optionalRESTART_POLICYin.env. Left unset it behaves exactly as before
— the stack comes back on its own after a reboot — so no existing install
needs to change anything.Set it to
on-failureonly if something else on your host is responsible for
starting stacks in order, such as a boot sequencer pacing many Compose stacks
across shared disks. It still restarts containers that crash; it only stops
Docker bringing the stack up by itself when the daemon starts. -
[webapp] Deleting a campaign can now be undone, and is much harder to do by
accident. Deleting a campaign removed everything in it — every session,
transcript, summary, wiki entry, note and ledger entry — immediately and
permanently, and the only thing standing in the way was a browser dialog
that any GM on the campaign could dismiss on reflex. There was no undo, no
pause, and nothing suggesting you take a copy first.Deleting now asks you to type the campaign's name, offers to export the
campaign before you go ahead, and puts it in a trash for 30 days rather than
destroying it. Deleted campaigns appear at the bottom of your dashboard
with a countdown, and restoring one brings back everything exactly as it was.
Nothing is deleted for real until the 30 days are up.Archiving is unchanged and still means what it always did — a campaign moved
out of the active list that you can still read, browse and export. -
[webapp] Deleting a wiki entry can now be undone. Deleting an entry used
to remove it permanently and immediately — along with its entire edit
history, which went in the same instant. A GM who deleted what looked like a
duplicate, and realised afterwards it was the real entry with months of lore
in it, had nothing at all to go back to.Deleted entries now sit in a trash for 30 days, where a GM can put them back
with everything intact. Admin → your campaign's wiki trash lists what is
in there and when each one stops being recoverable. -
[webapp] A wiki entry can be rolled back to an earlier version. The edit
history has always been visible and never usable: recovering an older draft
meant copying it out of the history view and pasting it back in by hand, and
the sidebar and timeline fields could not realistically be restored at all.
There is now a restore button on each version.Restoring saves the current state first, so if you restore the wrong version
you can simply restore your way back.
Changed
-
[bot] Recordings now take a sixth of the disk space they used to. The bot
was storing audio in Discord's format and converting it after the session
ended. It now converts as it records, so a six-hour session occupies about
0.7 GB per speaker instead of 4.2 GB, and there is no conversion step to wait
through when you stop recording.Audio quality is unchanged — the conversion filters the audio properly on the
way down, the same as the old post-processing step did. Recordings made by an
older version are still readable, and are converted the way they always were.
Added
-
[webapp] Backups can now leave the machine they are protecting. Until now
the scheduled backup wrote a database dump to a folder on the same host and
disk as the database itself. Losing that host — a failed disk, a
deprovisioned server, ransomware — took the database, the recordings, and
every backup of the database at the same moment.Set an off-host destination in Admin → Backups and each backup is copied
there as well. It uses rclone, so the same setting works for S3, Backblaze,
a NAS over SFTP, and most other storage; your storage credentials live in
rclone's own configuration and are never stored by Quest Board.A backup that cannot be copied off-host is now reported as failed, even
though the dump itself worked. A backup sitting on the disk it was supposed
to protect is not a backup, and calling it a success is how an instance finds
out it has none at the worst possible moment. The local copy is still kept. -
[webapp] Recordings can be included in backups. A database dump cannot
contain audio files. Switch on Include audio recordings and each backup
gains a compressed archive of them.It is off by default because recordings are far larger than a dump — check
your disk space before enabling it. But if you keep recordings at all, you
probably want it: a session whose transcription failed has no transcript, so
its audio is the only record that the session happened.
Fixed
-
[webapp] Granting admin by email no longer crashes when you have two
accounts. Accounts are identified by which login provider you came from,
not by your email address, so the same person can hold more than one — most
commonly after copying a production database onto a test machine, where your
test login is a separate account that was never made an admin.make set-admin EMAIL=…used to fail with a raw database error in exactly
that situation. It now lists the matching accounts, shows which one already
has admin, and tells you how to pick the right one. Nothing is changed until
you do. The operations guide has a section on this, since it looks like lost
access when it is not. -
[docs] The operations guide told you to leave recordings out of your
backups. It described the audio volume as scratch space that "does not need
to survive a restore". That stopped being true some time ago — recordings
stay until a GM approves them, a campaign can be set to keep them forever,
and a failed transcription leaves the audio as the only copy of anything.
Anyone who followed that advice would have lost every recording on the
instance during a restore. The guide now describes what the volume actually
holds, and how to back it up and restore it.
Changed
-
[webapp] Build tooling:
@vitejs/plugin-react6.0.5 → 6.1.1. Purely a
build-time dependency; nothing about the app itself changes.The update had been stuck since it was first offered, because 6.1.1 declares
an optional peer whose own optional peer wants Babel 8 while the project is
on Babel 7 — enough for npm to refuse the update even though none of those
packages are actually installed. A singleoverridesentry stops npm chasing
that chain, which unblocks it without dragging in 61 unrelated dependency
bumps as the alternative would have.
Fixed
-
[bot] A recording interrupted by a crash now finishes itself on the next
start. If the bot went down mid-session, the audio it had already captured
was left as raw working files, and there was no way to get it to Quest Board
short of someone converting it by hand — the earlier fix stopped that audio
being deleted, but stopped there. A recording that was interrupted an hour
into a four-hour session sat on disk indefinitely.On its next start the bot now converts what it captured and hands it over for
transcription, and the session appears as normal. Nothing is reconstructed:
the tracks were already recorded against a single session clock, so the
timings and who-said-what are the real ones. Speakers are named the way they
are on any other recording — server nickname first, so a player whose
nickname is their character's name still shows up as that character.It errs towards leaving the audio alone. If it cannot reach Quest Board, or
the session's group has no Discord server attached, it changes nothing and
tries again next start. If the session already has a transcript it keeps the
take but will not overwrite what is there.
Added
-
Recordings left behind by a deleted campaign are now reclaimed. Deleting
a campaign removed its sessions but left every recording on disk, and nothing
ever cleaned them up — one sweep only looks at sessions that still exist, and
the other treats a handed-off recording as permanently off-limits. On a busy
instance that is storage that only ever grows.A daily job now removes recording folders that no session refers to, after
giving each a week's grace, and records what it removed in the audit log.It is deliberately cautious about it: it only ever touches folders it
recognises as its own, it leaves anything recent alone, and if the session
list comes back empty it refuses to do anything at all — an instance with
recordings on disk and no sessions has a database problem, and treating that
as "everything is an orphan" would delete every recording on the volume.
Added
-
The bot event queue's size is configurable, and reports whether it is big
enough. Notifications for the bot wait in a queue capped at 10,000 — enough
for one group, but every group on an instance shares it. A bot down long
enough for its unread notifications to age out loses them; the queue is what
makes delivery reliable, so there is no catching up afterwards.BOT_EVENTS_MAXLENnow sets that cap, and a new
bot_events_trimmed_unread_totalmetric reports whether notifications are
actually being lost — so the number can be sized from evidence instead of
guessed at. It reads nothing rather than zero when there is not yet enough
data to tell, since "nothing to measure" should not look like "nothing
wrong".
Fixed
-
Unsaved writing is much harder to lose. Three surfaces could discard
work with no warning:- The Session Shelf's tonight-notes — the field you type into during
play — only saved when it lost focus. Closing the tab with the cursor
still in it lost everything since the last time you clicked away. It now
saves as you type, a moment after you stop. - The Beat Planner discarded your draft when you switched sessions in
its own dropdown. It now asks first. - Session summary and transcript editors, the Beat Planner and the Shelf
all now warn before a tab close or reload with unsaved text.
Note that the browser shows its own generic "Leave site?" wording for that
warning — it does not let an app supply the message. Warning on in-app
navigation needs a change to how the app routes pages and is tracked
separately. - The Session Shelf's tonight-notes — the field you type into during
Fixed
-
[bot] A vote that doesn't reach Quest Board now tells you. Reacting to a
poll in Discord leaves your emoji on the message whether or not the vote was
actually recorded — so a brief backend outage during a voting window meant
several people believed they had voted when they hadn't. Voting would then
close on the tally that happened to arrive, confirm a time, and nobody would
ever learn a vote had gone missing.A reaction that fails to register now gets you a DM saying so and what to do,
while there is still time to fix it. If your DMs are closed, the bot marks the
message with ❗ instead. Successful reactions stay silent.The same applies to RSVPs and summary feedback, and to reactions on a poll
Quest Board has stopped tracking (an old poll, or a bot restart) — previously
indistinguishable from reacting to any other message, and equally silent.
Fixed
-
A departing member's shared notes are kept when someone else has already
left. Deleting an account anonymises what you wrote onto a shared "Deleted
user" record. If a second person later deleted their account and had written
on the same session, their note collided with the first one and was thrown
away instead — so the campaign quietly lost one person's writing, in a way
indistinguishable from it never having been written.Both accounts' notes are now kept together on that session.
Votes and attendance still cannot be merged — one vote per person is the
point of the rule, and there is only ever one "Deleted user" — so a second
departure genuinely does reduce those tallies by one. That was already true;
what is new is that it is now counted and recorded in the audit log rather
than happening silently.
Fixed
-
The campaign journal works for campaigns with more than one GM. It
errored outright for every member of a co-GM campaign — the page looked as
though everyone's notes had vanished, when in fact it was crashing while
loading them. Co-GMs are supported everywhere else in the app; this one query
assumed there was exactly one.Both GMs' public notes now appear, each labelled with who wrote it, so two
people's notes under one "GM Notes" heading no longer read as one person
contradicting themselves.
Fixed
- A queue hiccup no longer leaves lore generation stuck at "queued…". The
session was marked queued before the job was actually handed to the queue, so
a momentary blip left it claiming work that nobody would ever pick up — and
because it looked busy, re-running it was refused. The failure is now
reported straight away, as a failure, with the re-run available immediately
rather than after a wait.
Added
-
You are told before a recording is deleted for good. Audio reaches
deletion through the trash, and until now nothing announced that the clock
was running — the countdown existed only in the admin console, so noticing
required knowing to go and look there.Quest Board now posts to your campaign's Discord channel a few days out,
saying which recording, exactly when, and how to keep it. Once per trip
through the trash, not daily: a message that arrives every morning is one you
stop reading. The same countdown appears on the session page itself.It says explicitly when a session's transcription failed, because that is
the case where the recording is the only copy of anything — no transcript was
ever produced — and losing it loses the session rather than just its media.
Fixed
-
"Append to beat notes" no longer wipes them. The Workbench's session-prep
tool composed its append against a field the campaign session list has never
returned, so the existing notes always read as empty — the generated sheet
replaced whatever the GM had written, and the tool reported "Appended to
beat notes." A GM with an evening of hand-written prep lost it, and was told
the opposite. There is no undo for beat notes.The same root cause left the beat-notes editor opening blank no matter
what was stored, so typing there and saving replaced the stored notes too.Beat notes now have a read path and both places use it. If the current notes
cannot be loaded, appending fails and says so rather than composing against
nothing — a button labelled Append must never be able to replace. The notes
stay GM-only: the read is gated the same way the transcript is, so a player
cannot see the GM's plan for the evening. -
Nothing the app generates will overwrite what you wrote. Two paths did,
both silently, and neither keeps history — so the work was not merely
replaced, it was gone.The campaign storyline was rebuilt from raw session summaries every time
any session in the campaign was transcribed, re-transcribed, or had its
summary touched. An hour spent turning AI summaries into a readable chronicle
survived until the next recording. A hand-edited storyline is now left alone;
the "Regenerate" button still rebuilds it, because that is you asking.Picking a canonical name — settling what to call a tavern — re-summarised
the whole session and replaced the summary with it. A GM who had corrected
the AI's mistakes by hand got those mistakes back, from an action that only
claimed to name something. Naming no longer touches the summary at all; the
name still applies to everything generated afterwards, which is what it was
for.The two compounded: one canonical-name pick could destroy a corrected summary
and the campaign chronicle, because the first rewrite triggered the second.
Fixed
-
Two people editing the same thing no longer silently erase each other.
Notes, summaries and planning notes were saved last-write-wins with no check
of any kind, so whoever clicked Save second won and the other edit was gone —
no error, no warning, nothing in a log.The case that actually bit: you open a session on a laptop, type notes from
your phone over Discord all evening (which correctly appends them), then click
Save on the laptop out of habit at the end of the night. The laptop still held
the note as it looked before the session, and saving replaced the whole
evening with it.Saves now carry the version they were based on. If the thing changed
underneath you, the save is refused and you are told to reload rather than
finding out later that something is missing. Nothing is auto-merged and
nothing you typed is thrown away — you choose what to keep.Appending instead of replacing was the other option and is worse for a text
editor: fixing a typo would duplicate the whole note rather than correct it.
Refusing the write loses nothing either way.
Added
-
Four actions that could only be taken once can now be taken back.
Marking a session complete, cancelling one, approving a summary and having a
wiki draft stall were all one-way doors: no endpoint could reverse them, so a
mis-click or a lost worker left editing the database by hand as the only
repair.- Undo complete and Reinstate return a session to where it was. A
cancelled session goes back to confirmed if it still holds a time and to
proposed if it does not. Both are recorded in the audit log, because they
rewrite something players and Discord have already seen. - Withdraw approval lets a GM correct a summary and publish the
correction. Approving is what posts the summary to Discord, and it could
only happen once — so an error spotted after approval was permanent in the
channel no matter how many times the text was fixed in the app. Quotes are
unpublished from players while the approval is withdrawn, mirroring what
approving did. - A wiki draft stuck generating can be released without discarding it.
Discarding was the only exit and it deleted the draft outright, taking
every inline edit and every round of iteration with it. Releasing marks the
draft failed and changes nothing else, which is enough — iterating,
approving and discarding are all reachable from there. Guarded so a draft
that may still be running is left alone.
What none of these do is pretend the side effects never happened. A Discord
message already delivered is not recalled, shelf notes already merged into a
wiki entry's GM notes stay merged, and withdrawing an approval does not pull
the audio back out of the trash — that grace window is deliberately bounded
(see the retention change in v4.0.1) and an approve/withdraw cycle must not
be able to extend it. - Undo complete and Reinstate return a session to where it was. A
Fixed
-
[webapp] A stalled lore run no longer hides its own retry button. The
re-run action was hidden for every in-progress state, so a run whose worker
died left the session showing "Extracting lore candidates…" with nothing to
click. Once a run has gone half an hour without any sign of progress, the
action comes back, labelled so it is clear the run is being replaced rather
than queued alongside. A run that is merely slow keeps reporting progress at
each stage and is left alone.Closing the same hole from the other side: the endpoint behind that button
refused a second run only while the first was queued, not while it was
extracting or matching. Triggering it during either of those stages started a
second extraction over the same transcript — duplicate proposals, and
double the tokens. Nothing hit it in practice only because the button was
hidden, which is exactly the rule being relaxed here, so both halves ship
together. -
[webapp] A restart during processing no longer strands a session forever.
Celery acknowledged a job the moment it was received, so a worker that died
mid-run took the job with it — nothing was redelivered and nothing was logged.
The session had already been flipped to "Processing…" on intake, and both
retry buttons refuse to act on a session in that state, so the only way out
was an UPDATE against the database by hand. An ordinary
docker compose up -d --buildduring a three-hour transcription was enough to
cause it.Work is now acknowledged on completion, so the queue redelivers whatever a
dying worker was holding. Two safeguards ship with it, because redelivery
alone would turn a job that kills its worker into a job that kills every
worker in turn: a six-hour limit (above the six-hour recording maximum, so a
legitimate full-length transcription is never cut off) converts that into an
ordinary failure, and each worker now reserves one long job at a time rather
than a batch.An hourly watchdog is the backstop for anything redelivery cannot reach.
A job with no progress for eight hours is marked failed with a plain
explanation, which puts the retry button back within reach; audio, lore
extraction, entry drafts and Workbench generations are all covered. Nothing is
deleted, the audio path is left intact, and a job still legitimately running is
left alone. Transient network failures during transcription now retry twice on
their own before any of this applies. -
[bot] The bot no longer deletes recordings it told you were saved. Its
startup cleanup removed any session directory that was unmarked and more than
an hour old, and the "this is safe" marker was only written after a
successful upload. So a failed upload, a crash around the moment of upload, a
crash mid-recording, or the backend declining to overwrite an existing
transcript all left a real recording looking like abandoned scratch — and the
next restart destroyed it. In two of those cases the bot had just told the GM
in the channel that the recording was safe.Deletion now requires proof there is nothing to lose: anything holding a
non-empty track is kept, marked or not, and reported in the log instead. A
recording whose upload failed is also retried automatically on the next
start, replaying the original request rather than a reconstruction, so it
usually repairs itself without anyone noticing.Raw capture files from a crash mid-session are likewise kept rather than
deleted. They still need finalising by hand — the log names the path — but
they are no longer thrown away.
Changed
-
Scheduled database backups are now on by default, and land somewhere you
can see. They shipped disabled, which meant an instance only had backups if
its operator went looking for the setting — production ran for months with no
scheduled dump ever taken. They now run nightly out of the box, writing to
./backupsbeside yourdocker-compose.ymlinstead of a Docker named
volume buried under/var/lib/docker. That is the difference between a file
your own backup tooling already sweeps up and one it never sees. An install
that has deliberately turned backups off keeps that choice.Upgrading: your existing dumps are still in the
quest-board_backups
volume. Nothing moves or deletes them —docs/OPERATIONS.mdhas a one-liner
to copy them across when you are ready.Still not covered: the media and audio volumes, and any off-host copy. A
backup on the same disk survives a bad migration, not a dead machine. Point
your own tooling at./backupsin the meantime.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-