LimitRequestBodyMiddleware (pure ASGI, registered outermost) rejects request
bodies larger than PIRATS_MAX_BODY_BYTES (default 1 MiB) before they're buffered
into memory: it checks the declared Content-Length first, then counts the bytes
actually streamed so a chunked/length-omitting client can't bypass the header
check. Exposed as services.pirats.maxBodyBytes and documented in the README.
Tested in isolation (Content-Length fast path + streamed path) and through the
real app.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Player-authored free text is scrubbed at the API boundary before storage via a
new validation.py: drop control characters and unpaired surrogates, trim
whitespace, and cap length (names 120, other text 2000). Applied to crew/player
names, the character sheet, like/hate answers, techniques, recruit techniques,
the renamed Name, the Gat description, and challenge stakes. Values matched
against stored text (swap offers, face-card assignments) and identifiers/enums
are left untouched so they still compare equal.
Constrained set-role to the two real roles so it can't stash an arbitrary string
in the column. Audited the rest: queries are parameterized by SQLModel (the lone
f-string SQL in database.py is the hardcoded legacy-migration list, no user
input); objective-type and rollback writes already whitelist their keys; no
endpoint accepts a generic (field, value) write.
Tests cover the sanitizer (control chars, surrogates, emoji, caps) and the
create/join HTTP path end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New maintenance.py runs a background asyncio loop (started from the app
lifespan) that purges games which either finished more than PIRATS_PURGE_
FINISHED_DAYS (default 14) ago or have had no activity for PIRATS_PURGE_
INACTIVE_DAYS (default 30). "Activity" is a game's most-recent GameEvent
timestamp; games with no events are undatable and left alone.
Each purge is logged with the crew name, uuid, reason, idle days and an
estimated byte footprint; every run that removes anything VACUUMs the SQLite
file and logs the disk space actually reclaimed. Child rows go via the ORM
cascade already declared on Game's relationships.
Configurable via PIRATS_PURGE_ENABLED / _INTERVAL_HOURS / _FINISHED_DAYS /
_INACTIVE_DAYS, exposed as services.pirats.purge.* in the NixOS module and
documented in the README. Unit test covers the selection logic and cascade;
smoke-tested the VACUUM/reclaim path against a file DB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add configure_logging() (main.py): always logs to stderr, plus a rotating
file when PIRATS_LOG_FILE is set; level from PIRATS_LOG_LEVEL (default INFO).
Called from both the CLI entrypoint and the app lifespan (idempotent), which
also logs startup/migration/shutdown.
Targeted server-side events for ops/debugging (not every game event): game
created, player joined, player removed (kick/leave), game ended.
NixOS service: new logFile (/var/log/pirats/pirats.log) and logLevel options,
wired to the env vars; LogsDirectory=pirats makes the path writable under the
sandboxed DynamicUser. README documents the env vars/options.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Crew roster bubbles: explicit text color + opaque distinct surface so
they're legible and stand out from the page in both themes (the bug was
the <button> defaulting to black-on-dark in dark mode).
- End-Scene enforcement: unless Dev Mode is on, a scene can't end until
every living Pi-Rat has faced a Deep Challenge or the Obstacle List is
empty. end_scene_and_transition now returns (ok, msg) and the route
surfaces a 400; the Deep sees a ✓/○ challenge-progress badge on each
Pi-Rat bubble. 3 new tests cover the gate.
- Hand panel: dropped the how-to-play blurb and the title in favor of a
low-profile "Your Hand" label.
- Bump VERSION to 8 with changelog entry; check off TODO.md Polish items.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two scene-layout tweaks to the inline (third-column) Event Log:
- It can now be collapsed to a fixed corner button (✕ in its header) to declutter,
and reopened from that button. ScenePhase owns `logOpen` and reflows the grid to
two columns while collapsed; EventLog dispatches `collapse`.
- The panel now pins at top:3.5rem (matching .dashboard-container's top padding)
with max-height calc(100vh - 4.5rem), so its bottom stays on-screen at any scroll
position and .log-content scrolls internally — previously the bottom sat ~24px
below the fold at the top of the page.
Verified across Pi-Rat and Deep views, scroll extremes, and 3-col / 1-col widths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per playtest feedback:
- Drop the per-challenge 'Plays:' line — redundant with the card column and the
event log (also removes the now-unused chPlays const).
- Drop the 'Attempted by … drag a card … assist' how-to paragraph; players know
the basics, and the challenge header already names who's challenged.
- Replace the 'Red/Black Obstacle · match to draw' tag with the original card in
the title (e.g. '5♣ — Loitering Mermaid'), and color the title by suit. The
left border already marks color; the dead .obstacle-color-tag CSS is removed.
Suit glyphs in the title use plain text symbols (not SUIT_EMOJI) so they take
the theme-aware suit color and stay readable on the dark panel. Verified in
light and dark.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Cheesy Heist walkthrough was a wall of text. Add a card-state diagram at
each step (after the draw, and after each of the three Challenges) showing both
Obstacle columns as they grow — reusing the same overlapping-column visual as
the in-app obstacle display: original card at the back with a gold ring, plays
stacked in front (green = beat the difficulty, red = didn't), newest in full
view. Self-contained CSS in rules.html; card faces stay light parchment in both
themes. Verified in light/dark and on mobile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the big-original-card + row-of-minis layout with a single vertical
column: the original sits at the back (accent ring), each play stacks in front
with a heavy negative margin so only its rank+suit peeks out, and the latest
card is fully visible at the bottom. Played cards keep a green/red success ring;
the ChallengePanel's per-play list still credits each player.
Top-aligns the Current Difficulty / Successes boxes so they no longer stretch to
the now-taller card column. Matches the rulebook's column card layout and is far
more compact. Verified in the ChallengePanel, the obstacle list, and on mobile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Start tracking a version number and a player-facing changelog, surfaced from
the ☰ menu via a new About item.
- frontend/src/lib/changelog.js: VERSION (bump every commit) + CHANGELOG.
- AboutModal.svelte: shows the version and changelog using the shared modal CSS;
closes via ×, backdrop click, or Escape.
- CornerMenu.svelte: new 'ℹ️ About' menu item, modal rendered outside the menu's
stacking context so it overlays correctly on every page.
- AGENTS.md: documents the bump-every-commit / log-only-user-facing convention.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>