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>
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>
Returning players are the common case, so surface their saved crews first.
The two glass panels had no margin between them; add spacing on the
Rejoin box so it sits clear of the Muster box below it.
Also tidies the TODO 'Polish' section (renamed from 'UI Polish'; drops the
now-committed Rank-3 bonus 'Gameplay gaps' entry).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the roster banner and the standalone character-sheet panel with
a left-hand column of crew bubbles. Each bubble shows the player's role
icon, name, rank, current hand size, and a 🛞 for the Captain; clicking
one pops out that player's character sheet as a modal.
- New CrewColumn.svelte: bubbles (Pi-Rats first, Deep last) + an
Objectives button that opens a crew-objectives popup.
- CharacterSheet.svelte is now a target-driven modal: description,
likes/hates, and personal objectives for anyone; secret J/Q/K only on
your own sheet; the Duel UI (no "Challenge whom?" — target is fixed)
only when a living Pi-Rat views another living Pi-Rat.
- ScenePhase.svelte → 3-column layout (crew | obstacles | hand/deep);
crew column collapses to a horizontal strip on narrow screens.
- Shared .modal-backdrop/.modal-box/.modal-close moved into components.css;
removed the now-dead roster-banner and captain-badge styles.
Backend unchanged — the state blob already carries hands, roles, ranks,
objectives, and the captain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
During between_scenes there's now just one centered panel with the
voting controls, nomination progress, and Crew Rank Ledger. The separate
"Resting Deep Upkeep" panel (which only said hand refresh would happen
later) is rendered solely during deep_upkeep, where the actual discard/
redraw happens. Moved the rank ledger into the voting panel and added a
.between-grid.single layout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirrors the name modal: earning the Gat objective sets
Player.needs_gat_description, and GatModal.svelte (rendered by Dashboard
across all phases) prompts for a one-of-a-kind Gat description, stored in
Player.gat_description and shown on the character sheet. Like a stolen
Name, the Gat's description travels with it through Gat Tax transfers.
- Player.gat_description / needs_gat_description (+ migration)
- toggle_objective personal_1 sets/clears the prompt
- crud_challenge gat-tax paths move the description with the Gat
- set-gat-description route; CharacterSheet displays it
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the Event Log is collapsed, a freshly-arrived event now flashes as
a toast anchored above the button, then animates down and fades into it.
Suppressed on the initial state load and on rollback reseeds so it only
fires for genuinely new events; clicking the toast opens the log.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Admin page's back button now returns to this browser's own Pi-Rat
dashboard for the game (looked up from the saved sessions), labelled
"← Back to Game". Falls back to the join page only when this browser has
no saved session for the game.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completing Crew Objective 1 (Steal a Ship) no longer anoints the
highest-ranked Pi-Rat as Captain (nor vacates the seat when the ship is
lost). Captaincy is now earned: the crew chooses one via the Captaincy
UI / set_captain and Crew Objective 2. Decoupled captaincy from crew_1
in toggle_objective, dropped the now-unused random import, and updated
test_captaincy_and_hand_sizes accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Deep now states what happens on success and on failure as two
separate fields when calling a Challenge, instead of one free-text
stakes blob. ChallengePanel shows them as "✅ On success" / "❌ On
failure"; the event log lists both.
- Challenge.stakes_success / stakes_failure (+ migration); the old
single `stakes` column is kept (vestigial) so in-scene rollback
snapshots taken before the upgrade still deserialize, and ChallengePanel
falls back to it for any pre-existing challenge.
- create_challenge / route / DeepControlPanel updated; tests adjusted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The post-voting (deep_upkeep) screen now names who won the rank-up vote
and their new rank, instead of just "Voting complete!". Adds
Game.last_rank_up_player_id (+ migration), set when votes are tallied
and cleared when the next scene begins; falls back to a "no one ranked
up" message when there were no votes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After a resting Deep player confirms their hand refresh, replace the
discard/keep drag editor with a confirmation panel showing the freshly
drawn hand and a "Waiting for the rest of the crew" message, instead of
re-rendering the editable drag UI (which made it look like nothing
happened). Also stops nagging "please complete your Hand Refresh" once
they're done. Visible whenever they remain in deep_upkeep waiting on
other resting Deep players.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An admin can now opt into "teaching mode" from the lobby, seeding
themselves as the Rank-3 player who must play the Deep in the first
scene (instead of leaving it to the random starting-rank roll), so they
can run the table for new crews.
- Game.teaching_deep_player_id (+ Alembic migration)
- maybe_finish_assign_techniques honors it before rolling ranks, so both
the normal flow and the dev-mode skip path respect it
- Admin-gated POST .../teaching-mode toggle (pre-rank phases only)
- Lobby checkbox UI, greyed out if another admin already volunteered
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking Revise on basic records, techniques, or J/Q/K assignment now
offers a Cancel button that throws away any edits made since Revise and
restores the previously-saved values. Basic-records cancel is a pure
client-side toggle; techniques/face cancel re-submit a snapshot taken at
Revise time (since Revise immediately unsubmits/unassigns server-side).
Also added Cancel to the recruit-creation basic-records revise.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>