• v4.7.0 d01ef4ee2f

    v4.7.0
    All checks were successful
    CI / Backend lint (ruff) (push) Successful in 35s
    CI / Does this change touch the recording path? (push) Successful in 17s
    CI / Feedback site tests (push) Successful in 53s
    CI / Which journeys does this change need? (push) Successful in 18s
    CI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m11s
    CI / Bot/backend version sync (push) Successful in 1m7s
    CI / Bot tests and audit (push) Successful in 2m18s
    Release / Create Forgejo release (push) Successful in 17s
    Release / Build and push versioned images (push) Successful in 3m28s
    CI / Frontend (shard 2/2) (push) Successful in 9m57s
    CI / Frontend (shard 1/2) (push) Successful in 11m25s
    CI / Docker image build (push) Successful in 11m26s
    CI / Synthetic session harness (no GPU, no LLM) (push) Successful in 8m44s
    CI / End-to-end journeys (keyboard) (push) Successful in 11m56s
    CI / End-to-end journeys (phone) (push) Successful in 11m39s
    CI / End-to-end journeys (desktop) (push) Successful in 12m23s
    CI / Backend migration, tests, and audit (push) Successful in 40m44s
    Stable

    claude-bot released this 2026-11-05 22:50:57 +00:00 | 86 commits to main since this release

    Browse the Rules. The loaded rules content can be browsed, not only searched:
    by kind in a campaign, in a library per game system, with each system's own
    filters, and in place where it is used (the sheet's pickers, Prep's drawer and
    Find at the table). Also: copies of a creature on the shelf, the wiki page's
    infobox with suggested and known facts, facts the session may have changed
    (suggested by the campaign's own AI, for the GM to use), names without the AI
    in Prep, PostgreSQL 18, and the fixes and dependency updates since 4.6.2.
    Tried on the dev server as 4.7.0-rc1 to -rc4.

    Upgrading from 4.6.2

    • PostgreSQL 16 to 18, once, before starting 4.7.0 (#1245): make backup-now, copy the newest dump and .env off the host, git checkout v4.7.0 && docker compose pull, then make pg-upgrade and read the per-table
      counts it prints, then docker compose up -d. The pg-upgrade-guard service
      stops the stack until the upgrade has been done. See OPERATIONS.md,
      "Upgrading PostgreSQL 16 → 18".
    • Five database migrations (b1242c0d1e2f, d1321c0e5a7b, e1279a0b1c2d,
      f1280a0b1c2d, a1257b0c1d2e), applied by migrate on up.
    • BOT_CONTRACT_VERSION is unchanged (2): the bot and backend images can
      be upgraded one at a time.
    • Optional, once, after the upgrade: suggest facts for the sessions already
      processed: docker compose exec -T worker celery -A app.tasks.celery_app call app.tasks.fact_tasks.suggest_fact_changes_backfill. Nothing is shown to
      players, and nothing is changed until a GM uses a suggestion.

    Added

    • [webapp] Browse the rules: the reference content can be read through, not only searched (#1240).
      The rules page lists the kinds of record a game system has (spells, creatures, feats, equipment and so on,
      in the system's order, each with how many there are), and opens a kind as an A-Z list in numbered pages that
      reaches every record, narrowed by level, by pack and by a search. Only what the reader may already see is
      listed or counted: disabled packs, other campaigns' packs and, for players, GM-only records are left out.
    • [webapp] The rules library: anyone signed in can browse the rules loaded on this server, outside any campaign. GET /api/reference/library lists each game system that has an active server pack with records, and the existing kinds, page and entry routes, called with no campaign, give a server-wide view of server packs only (never a campaign's). GM-only records show to admins and to anyone who runs a live campaign of that game system; everyone else's counts leave them out. An admin can browse one pack, switched off or not, GM-only records included, at /api/admin/content-packs/{id}/browse/kinds, /page and /{entry_id}.
    • [webapp] Browse the rules by each game system's own filters. A PF2e spell has tradition, rank, cantrip, focus and rarity; a creature size, traits and rarity; an item price (in gp), Bulk, category, traits and rarity; a feat type and class or ancestry. A 5e spell has school, class, ritual and concentration; a creature CR, type and size; an item category (rarity and attunement appear once a pack has them). Each choice shows how many records it would leave, counted with the other filters applied, and a kind can sort by level, rank, price or CR as well as A-Z. The filters are declared per system in app/sheets/builtin/<system>/browse.json, not in code. GET /api/game-systems/{key}/reference/filters (and the admin pack's /browse/filters) returns them with their counts, and /reference/page takes f_<key> and sort. A new reference_entries.facets column holds the filter values; the worker fills it at start after the upgrade, with no manual step (python -m app.cli refill_facets does the same by hand). Migration b1242c0d1e2f.
    • [webapp] Table mode's Find can browse the rules (#1243). "Browse the rules ›" in Find opens the kinds and a kind's list inside the same dialog; choosing a row opens it in the table's preview, where it can be added to the shelf. "‹ Back to the search" and Escape return to the search as it was.
    • [webapp] Prep's Rules & bestiary drawer can browse. A Search | Browse switch shows the campaign's kinds with counts, then a kind as a filtered, paged list; each row previews and inserts like a search result. Creatures open at the party's range (PF2e level -2 to +3, 5e CR a quarter of the level to level + 2), taken from the active player characters' sheet levels. (#1243)
    • [bot] The "Played" post in the campaign's Discord channel now asks the
      players for one star and one wish (#1255).
      When a session completes and
      Quest Board is asking anyone about it, the post the bot already makes gains a
      line asking for a star and a wish, saying when the ask closes, and its link
      goes to Home, where they are answered. It is one line in a post that goes out
      anyway, once: the channel is asked, not each player, and nothing brings it
      back. Nothing about the session's content reaches Discord.
    • [webapp] The Names drawer in Prep draws names from a built-in generator instead of asking an AI. Forty-four styles (the peoples, places by size and kind, groups, ships, items, books, battles and festivals), ten names at a time, free and instant, in a campaign with no AI as well. A campaign is never offered the same name twice, and never a name its wiki already holds; a style that names a place can use one you give it. Describe, the one line under a name, still asks the campaign's AI and is offered only when there is one. The ready-made name prefetch and its Redis cache are gone. (#1257)
    • [webapp] A wiki page's margin is one box with suggested facts. A page starts with the facts its type usually has in the campaign's system (Pathfinder 2e, D&D 5e or Generic), empty and never shown until filled; where Quest Board already knows a value (First met and Last seen from the sessions, a character's player and class from the campaign and the sheet, a place's region from its map, a creature's level from the rules entry it is based on) readers see it until a GM types their own. A page can show a map as its picture, with a caption, and a GM can remove a suggested fact for good. New columns lore_entries.image_caption, .image_map_id and .facts_dismissed (one migration); GET /api/campaigns/{id}/wiki/suggested-facts; the article read gains image_caption, image_map and facts. The campaign file carries the caption, the removed facts and the page's map.
    • [webapp] Facts the session may have changed. After a session is summarised, the campaign's own LLM is asked, page by page, whether a status, ruler, leader, owner or allegiance changed. Each suggestion carries a quote found in the transcript that itself says the change (it names the new value and a word for it, such as "rules", "owns" or "died"), with the speaker and time of the line it came from, and waits for a GM to use it, change it or turn it down; nothing is shown to players. New table fact_suggestions (migration f1280a0b1c2d) and a task to run once after the upgrade for sessions already processed: celery -A app.tasks.celery_app call app.tasks.fact_tasks.suggest_fact_changes_backfill.
    • [webapp] Two of the same creature can be on a session's shelf (#1321). Adding a rules entry that is already there with another: true makes a numbered copy with its own HP; a pin carries the first card forward and copies stay with their session. Cards carry copy_number, and a rules card copies.
    • [repo] The synthetic-session harness runs leg 3 in one command, and can
      send its vocabulary as hotwords as well as initial_prompt (#506).

      python -m evals.synthetic_session summary-leg goes audio, ASR, merged
      transcript, LLM summary, scores through the same summarisation and scoring
      code leg 2 uses, and reports leg 3 beside leg 2 (--baseline); --provider stub runs it offline. asr-leg and summary-leg take
      --vocabulary-mechanism {initial_prompt,hotwords,both}, which sets the
      product's own QB_ASR_VOCABULARY_STYLE switch for the run without touching
      the provider code. The README records the measured matrix.

    Changed

    • [webapp] New campaign → "The setup of one of yours" now opens the full copy dialog.
      It is the same one as Settings → Archive → "Start a new campaign from this one":
      choose the pages, maps, lines and veils, packs and people to take, with the
      scheduling and reminder settings always copied. The old settings-only copy
      on POST /api/campaigns (template: {"from_campaign_id": …}) is removed.
    • [webapp] The character sheet's pickers open on what fits the character.
      GET /api/campaigns/{cid}/sheets/{sid}/picker and .../options now answer
      fits (the browse filters that match the character at that step, declared as
      fits rules in each system's browse.json), fits_label, filters, sorts
      and total, and take stage 3's f_<key>, sort, page, per_page and
      all=1; with any of those the "belongs to this character" narrowing is
      skipped. .../options now answers {results, …} instead of a bare list.
    • [repo] PostgreSQL moves from 16 to 18, and an existing install must run
      make pg-upgrade before it starts the new release.
      The 18 image keeps its
      data in a new place, so the database lives in a new volume
      (postgres18_data) and the old one cannot simply be reused: pointing 18 at
      it would start an empty database. make pg-upgrade (from the new release's
      checkout, before docker compose up -d) backs up, dumps the old database
      with its own PostgreSQL 16 tools, restores it into the new volume, and checks
      every table's row count and the migration revision. It never removes the old
      volume, which stays as the rollback and archive; the pre-upgrade dump is
      written to ./backups. A new pg-upgrade-guard service stops the stack, with
      a message naming make pg-upgrade, if the old volume holds a database and the
      new one is empty, so a plain docker compose up -d cannot give you an empty
      database. A fresh install and an already-upgraded install start as usual. The
      backend image's pg_dump client moves to 18 with it, so make migrate-safe
      refuses to back up against a 16 server until the upgrade is done. Renovate
      now sees the PG_MAJOR in the Dockerfile and moves every PostgreSQL pin in
      one pull request. See docs/OPERATIONS.md, "Upgrading PostgreSQL 16 to 18"
      (#1245, supersedes #627).
    • [webapp] Campaign names reach the transcriber as hotwords by default. On the bundled WhisperX server, the campaign's vocabulary list now goes in hotwords rather than initial_prompt when the server declares support; on a 100-name list this got 89–90% of names right against 80–82% (#506). A server without hotwords gets the prompt, as before, quietly. Set QB_ASR_VOCABULARY_STYLE=initial_prompt to keep the old behaviour.
    • [repo] Dependencies brought up to date for 4.7.0: React and React DOM 19.3.1, authlib 1.9.1, eslint 10.13.0, @axe-core/playwright 4.14.0 (with the accessible-name fixes its stricter check found, #1244), and Mailpit 1.32 in the end-to-end stack.
    • [webapp] [bot] "GM only" is now "GMs only" everywhere it is shown. The
      wiki's front page and recent changes, the campaign search, a hidden page's
      screen-reader label, the table's Find, character sheets, Prep's lookups,
      the copy dialog, content-pack visibility and the bot's /help all say it
      the way the map and the rules already did (owner's decision, 30 Oct).

    Fixed

    • [webapp] Two reads no longer write. The character-creations list
      ("Carry on with ...") only hides creations left untouched for 30 days instead
      of deleting them while it lists, and a page's picture stored publicly before
      #894 is served where it is instead of being moved on read. A daily
      background task now deletes the stale creations, and another moves the old
      pictures into the private store; both are safe to repeat. The only
      remaining GETs that write are the two sign-in callbacks and the content-pack
      exports (#1206).
    • [webapp] Screen readers hear what is on screen. Prep's "File it" / "Move" button and the storyline's chapter links now have accessible names that contain their visible words, which axe 4.14's label-content-name-mismatch rule checks (#1229). The scene button reads "File it in a scene: …" or "Move to a scene: …".
    • [webapp] Fetching a starter content pack no longer freezes the server, and a busy server no longer signs you out. Reading and normalising a pack ran on the web server's event loop, so fetching two packs at once could miss the health check and get both workers restarted; the work now runs in a thread. The sign-in check also treats only a 401 as "signed out": an error or dropped connection from /api/me keeps you waiting and retries instead of showing the sign-in page. (#1239)
    • [webapp] Prep → Names: a long "In the wiki as …" after saving no longer squeezes the name into one letter a line. The name keeps at least 45% of the row, and the receipt wraps in its own column.
    • [webapp] Admin → Content packs reads properly. A pack's contents say "9 ancestries", not "ancestrys"; its version no longer gains a stray "v" ("pf2e-8.6.1", not "vpf2e-8.6.1"); the licence's terms link is underlined, not told apart by colour alone; and on a phone the app bar's home link is named by the "QB" it shows (#1302).
    • [webapp] A moment without Redis now answers "try again" (503), not a server error (#1326). Found by the live checks (#1246): a Redis restart turned signed-in requests into 500s for a fraction of a second. Nobody was signed out then, and nobody is now. The manual healthcheck checklist in OPERATIONS.md also had a worker step that couldn't work; it now stops the process from the host.
    Downloads