• v4.6.0-rc2 94deb061b9

    v4.6.0-rc2
    All checks were successful
    CI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m4s
    Release / Create Forgejo release (push) Successful in 15s
    CI / Which journeys does this change need? (push) Successful in 17s
    CI / Does this change touch the recording path? (push) Successful in 18s
    CI / Bot/backend version sync (push) Successful in 26s
    Release / Build and push versioned images (push) Successful in 26s
    CI / Backend lint (ruff) (push) Successful in 35s
    CI / Feedback site tests (push) Successful in 58s
    CI / Bot tests and audit (push) Successful in 2m18s
    CI / Docker image build (push) Successful in 2m20s
    CI / Frontend (shard 2/2) (push) Successful in 9m58s
    CI / Frontend (shard 1/2) (push) Successful in 12m39s
    CI / Synthetic session harness (no GPU, no LLM) (push) Successful in 19m10s
    CI / End-to-end journeys (phone) (push) Successful in 22m5s
    CI / End-to-end journeys (keyboard) (push) Successful in 24m19s
    CI / End-to-end journeys (desktop) (push) Successful in 24m35s
    CI / Backend migration, tests, and audit (push) Successful in 41m44s
    Pre-release

    claude-bot released this 2026-10-17 21:59:38 +00:00 | 381 commits to main since this release

    Prerelease of 4.6.0 for testing on the dev server; not for production.

    Added

    • [webapp] The screens for bringing a picture in, and for a campaign's files (#1181). The wiki editor offers "Bring it in" for a picture written as an outside address (and from the picture button, by address), rewriting the text to the stored copy, with the server's plain reason when a fetch is refused. Wiki health lists "Pictures still on other sites" with a "Bring it in" and an Undo. Settings → Wiki has "Who can add pictures and files", and the editor hides the picture controls the server would refuse. Settings → Files lists a campaign's stored files as a grid or a table, with previews, filters, the source address, files nothing uses, a viewer with zoom, and Remove (showing the text that would break). Admin → Files lists them across campaigns by size, and Preview and Open are separate, recorded steps; the Privacy page says so.
    • [webapp] Bring a picture in from a web address, and a Files page for each campaign and for admins (#1181). Anyone who can edit a page can have the server download a picture once and store it as an upload (JPEG, PNG, WebP or GIF, metadata stripped, never SVG); the source address and time are kept on the file and shown to GMs and admins only. Downloads count toward the campaign's storage, and are limited to 30 a person and 60 a campaign in ten minutes. A new campaign setting, "Who can add pictures and files" (everyone who can edit, or GMs only), is enforced on every upload route and on bring-it-in. Wiki health gains "Pictures still on other sites", with a "Bring it in" per page that rewrites the text as the GM's own edit. Campaign → Settings → Files lists every stored picture, map image, handout and attachment with previews, who added it, its address and whether anything still uses it, and removes them; orphans are listed apart. Admin → Files lists the same across campaigns by size, list only: opening or previewing another campaign's file is a deliberate act and is written to the audit log.
    • [webapp] A GM can change who was involved when rewording an event. Reword can now also set an event's people (a character, a member, or an NPC page; or nobody, for scenery or a narrated happening). The change is saved with the reword, so it is recorded like one (who and when), survives a regenerate, and the next summary is composed from the corrected people. The event log and the summary's anchors show the corrected people, and the GM still sees who the model named. A GM-only search lists the campaign's characters and members first, then NPC pages.
    • [webapp] The line of the night, on screen. Settings, General has "Line of the night: Chosen by the GM / Voted by the table". On a session's Review tab the GM marks, moves or clears the line of the night on an approved highlight, sees when voting closes, can close it now, and picks after a tie or a vote nobody cast. Players read the line of the night first with a small label, and while voting is open vote for a line (changing or withdrawing the vote), with counts hidden until it closes. The campaign Highlights page marks the lines of the night and can show only those.
    • [webapp] The line of the night. A session can have one highlight marked as its line of the night. A campaign setting (Settings, General) says who picks it: the GM (the default), or the table by vote. In GM mode the GM marks, moves or clears one approved highlight. In vote mode every member, GMs included, votes once and can change the vote; voting opens when the GM approves the summary and closes three days later, or when the GM closes it early. Counts stay hidden until it closes, then the most votes wins, and a tie or no votes falls to the GM. The approved-summary post in Discord names a chosen quote as the line of the night. Exports carry the setting and each session's line of the night; imports accept files without them.
    • [webapp] The Pathfinder 2e (ORC) and D&D 5e SRD 5.2.1 starter packs can be fetched from Admin → Content packs. Each release carries them as assets and the fetch address follows the running version, so a release candidate uses its own release. The reviewed SOURCES.md for each pack is in docs/content-packs/4.6.0/. (#1200)
    • [webapp] Start a new campaign from an existing one, and save a campaign's
      setup as a file (#125).
      A GM of the source campaign (an archived one is
      fine) can copy its settings, game system, calendar, wiki page templates and
      reputation levels, and the wiki pages and maps they tick, into a new campaign
      they own, as a background job like an import. The world starts ticked
      (places, organisations, deities, creatures, artifacts, concepts, historical
      events, documents, NPCs); player characters, quests and pages made from a
      session start unticked. GM-only text stays GM-only, personal notes are never
      copied, links to pages left behind become plain words, everything that points
      at a session is dropped (timeline events keep their in-world dates) and quests
      come back not started. The campaign's own content packs are copied only with
      the current affirmation, and the table's lines and veils only with "Invite the
      same people", which sends each other member an ordinary single-use invitation
      (a GM of the old campaign is invited as a GM); nobody is added directly. A
      setup file is a campaign file with "setup_only": true: "Start from a file"
      is the ordinary import. New routes: GET /api/campaigns/{id}/setup,
      POST /api/campaigns/{id}/setup-file, POST /api/campaigns/{id}/clone.
      One audit entry is written on each campaign.
    • [webapp] "A one-shot" accepts one_shot: true, and is stripped back
      further (#125).
      Its single reminder is now the day before (it was two
      hours), the "who hasn't answered" nudge is off, and once its session has been
      played the GM gets one "Needs you" suggestion to archive it
      (archive_one_shot), answered with
      POST /api/campaigns/{id}/archive-suggestion/dismiss and never repeated.
    • [webapp] "Invite the same people" now sends each person their invitation. Starting a campaign from another with that ticked puts " invited you to ." in each other member's bell, and emails it where their settings say so, with a link to join. Nobody is added until they accept, and a used, expired or withdrawn invitation says so instead of linking (#125).
    • [webapp] The screens for starting a campaign from another (#125). Settings → Archive has "Start a new campaign from this one…" and "Save the setup as a file…", one dialog for both: pick the wiki pages (the world starts ticked), maps, Lines & Veils, the campaign's own packs (after affirming them again), and whether to invite the same people. New campaign has "Start from a file" beside "Import a campaign". Needs you asks "Archive it?" for a played one-shot, the bell shows an invitation to a campaign with its join link, and an invitation link says when it was for a GM.
    • [repo] The in-world calendar engine (#126, step 1; nothing user-facing yet). A calendar definition format (months, festival days and leap days with Gregorian-style rules, an optional "outside the week" flag for festival and leap days, week, moons, era; every entry has a stable id), validated by app/schemas/world_calendar.py; pure arithmetic in app/services/world_calendar.py and, identically, webapp/frontend/src/calendar/engine.js (sort numbers, weekdays, moon phases, session ranges, formatting, and the check and date remapping for a calendar change). One shared file of test cases is run by pytest and vitest; scripts/calendar_cases.py regenerates it from brute-force enumeration (--check verifies it). Four generic presets.
    • [webapp] Date the story: the in-world calendar's screens (#126). Settings gains a Calendar section for GMs: start from one of four generic shapes, copy another campaign's calendar or load a file, then edit the months, festival and leap days (placed "after" a month, with leap rules and an "outside the week" tick), the week, the moons, the era and how a date reads, with a live preview and a check before saving that lists what a change would refuse or how many dates it would move. One date picker serves everything: a month grid with a column for each day of the week, festival days as their own cells, years before year 1 ("200 before DR"), the moons' phases as text, and the arrow, Home, End and Page keys; on the wiki it also offers Day, Month or Year with "About". A GM dates a session from Manage ("In the story…": the day it started, "The next day", hours or days covered, "Ran past midnight"), and the review offers one quiet row, "Where in the story?", with one press to save. Every member sees "In the story: 3–5 Mirtul, 1492 DR" under a dated session's head and on the Sessions list, and the campaign home says where the story has got to. A wiki timeline event can carry a date or a named age ("the Age of Ash", placed by choosing what it comes after), and the Storyline can show the timeline "As we played" or "In the world", with "9 days later" between sessions.
    • [webapp] The in-world calendar's server side (#126; the screens follow). A campaign can have one calendar (GET/PUT/DELETE /api/campaigns/{id}/calendar: every member reads it, only a GM writes), copied in from one of four generic presets (GET /api/calendar-presets), from another campaign the caller GMs, or from a JSON file (/calendar/file). A change is checked first (POST /calendar/check): removing a month or day that a session or wiki event is dated on is refused with the list of what uses it, and every other change moves the stored dates with their entries and recomputes the sort order in the same save. Sessions take an in-world start day and a span in hours or days (under 24 hours stays on the start day, plus "ran past midnight") through the ordinary session update, GM only; the session, the Sessions list and the campaign home carry the formatted range, the moons' phases on the start day, the campaign's "today", and a prefilled start for an undated session. Wiki timeline events take an in-world date at day, month or year precision (with "about"), or a named age with no year, and the Storyline's timeline read model carries each line's in-world label and sort number. The campaign export is now schema 21 and carries the calendar and every date; a version 20 file imports with no calendar. Additive migration a126ca1e0d01.
    • [bot] The approved summary's Discord post says where it was in the story. When the session has in-world dates, the post gains a line "In the story: 3–5 Mirtul, 1492 DR" (additive: extra.world_range, no contract bump). The text comes from names the GM typed into the calendar, so it is reduced to letters, digits, spaces and a few punctuation marks before it is posted.

    Changed

    • [webapp] Fewer read-only pages change anything when they are opened. Making a ready-made name set is now a POST (the GET only shows what is cached), the secrets health report no longer upgrades secrets (that happens at startup), an out-of-date ownership offer is expired by a background task rather than by looking at it, and the sheet pages no longer seed the built-in definitions on read (#1178).
    • [webapp] The last gaps in stars and wishes (#122) and downtime (#124) are
      closed.
      A GM sees the players' stars and wishes on the session's Review
      tab, named only where the player signed. The star-and-wish ask goes only to
      players marked as having attended, once any attendance is recorded. On the
      party page a player edits or removes their own waiting downtime and a GM any
      entry (and can approve it as they save it), with Undo after a removal. Downtime
      totals now count approved days only, show "Days since Session N" per
      character, and keep days still waiting apart as "(+3 waiting)". The downtime
      list's totals gain a pending_days figure, and their days no longer
      includes entries waiting for a GM.
    • [repo] Renovate moves the Playwright image and @playwright/test together, in one PR, so a bump can no longer leave the journeys on an image without the browsers its version needs.

    Fixed

    • [webapp] The page-picture "from a web address" fetch now checks every redirect (#1181). It used to check only the address it was given, then follow redirects freely, so a public address could redirect the server to a private one. Each hop is now checked again, at most five are followed, a connection is made only to the address that was checked, and anything but a public address (carrier-grade NAT, 0.0.0.0, IPv4-mapped IPv6 and the like) is refused.
    • [webapp] Switching a campaign to "Voted by the table" no longer reopens voting on sessions already approved. They keep the GM's pick and show no vote; votes already cast are never deleted. "N still deciding" on This week counts players only, like the "Are you in?" line.
    • [repo] The end-to-end journeys checks are named once each ("End-to-end journeys (desktop)"), not "(desktop) (desktop)". The job name no longer spells the matrix value, which Forgejo appends itself (#1203).
    • [webapp] The hourly import sweep no longer breaks a slow "Start a new campaign from this one". It queued every waiting job as an import, which ended a clone with "uploaded file no longer on server". Jobs now carry a kind (import or clone) and a clone keeps its choices, so the sweep queues the right task. GET /api/campaigns/import/jobs/latest and the job responses say which kind a job is, so the import page can tell them apart (#125).
    • [webapp] Six screen fixes from the 4.6.0-rc1 render check. Settings → Files shows each file as a stacked card on a phone and tells files on one page apart by size and date; the party page's Downtime and "What we carry" stack on a phone; the clone dialog keeps its buttons together in a footer that stays in view; the calendar date picker shows just the year when the era has no label; the review tab no longer treats "no summarisation run yet" as an error.
    • [repo] A prerelease's image job no longer reports a failure after pushing every image. The :latest push was skipped with [ … ] && …, which made the step exit 1 for every prerelease.
    • [webapp] This week no longer counts the GM in "have answered". Under "Are you in?" it now reads "4 of 5 players have answered", "No players have answered yet." when only the GM has, and nothing at a table with no players, the same as the session page. Both pages print one shared line, so they cannot drift apart again. (#1202)

    Security

    • [webapp] The check on addresses the server fetches pictures from now refuses IPv6 forms that hide a private IPv4 address (#1181). NAT64 (64:ff9b::/96, 64:ff9b:1::/48), 6to4 (2002::/16) and Teredo (2001::/32) are blocked outright, and the IPv4 address inside each, and inside an IPv4-mapped one, is checked as well. 64:ff9b::7f00:1 used to pass. The same check serves the page picture's "from a web address" fetch.

    • [webapp] The server no longer decodes a compressed answer when it fetches a picture from a web address (#1181). It asks for the file uncompressed, refuses an answer that says it is compressed (a few KB of gzip could unpack to hundreds of MB before the size limit was checked), and counts the limit on the bytes that arrive. Multicast, site-local and IPv4-compatible addresses are refused as well, and the fetch ignores proxy settings in the environment.

    • [bot] multidict moves to 6.9.1, out of two advisories (PYSEC-2026-4244, CVE-2026-104874) that the daily dependency audit flagged on 15 and 16 Oct.

    Downloads