Commit Graph

269 Commits

Author SHA1 Message Date
adlee-was-taken
d3b468575b feat(soak): per-room watchdog + heartbeat wiring + multi-game lobby fix
Watchdog class with 4 Vitest tests (27 total now), wired into
ctx.heartbeat in the runner. One watchdog per room with a 60s
timeout; firing logs an error, marks the room's dashboard tile
as errored, and triggers the abort signal so the scenario unwinds.
Watchdogs are explicitly stopped in the runner's finally block
so pending timers don't keep the node process alive on exit.

Also fixes a multi-game bug discovered during stress scenario
verification: after a game ends sessions stay parked on the
game_over screen, which hides the lobby and makes a subsequent
#create-room-btn click time out. runOneMultiplayerGame now
navigates every session to / before each game — localStorage
auth persists so nothing re-logs in.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:52:49 -04:00
adlee-was-taken
921a6ad984 feat(soak): stress scenario with chaos injection
Rapid short games with a parallel chaos loop that has a 5% per-turn
chance of firing one of:
  - rapid_clicks: 5 quick clicks at the player's own cards
  - tab_blur:    window blur/focus event pair
  - brief_offline: 300ms network outage via context.setOffline

Chaos counts roll up into ScenarioResult.customMetrics.chaos_fired.

Important detail: chaos loop has a 3-second initial delay so room
creation, joiners, and game start can complete without interference.
Chaos during lobby setup (especially brief_offline) was causing
#create-room-btn to go unstable.

Verified: stress smoke with --games-per-room=3, 4 accounts + 1 CPU,
first game completed with 37 turns and chaos events fired across all
three event types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:49:30 -04:00
adlee-was-taken
c307027dd0 feat(soak): --watch=tiled launches N headed host windows
SessionPool accepts headedHostCount; when > 0 it launches a second
Chromium in headed mode and creates the first N sessions in it.
Joiners (sessions N..count) stay headless in the main browser.

Each headed context gets a 960×900 viewport — tall enough to show
the full game table (deck + opponent row + own 2×3 card grid +
status area) without clipping. Horizontal tiling still fits two
windows side-by-side on a 1920-wide display.

window.moveTo is kept as a best-effort tile-placement hint, but
viewport from newContext() is what actually sizes the window
(window.resizeTo is a no-op on modern Chromium / Wayland).

Verified: 1-room tiled run plays a full game cleanly; 2-room
parallel tiled had one window get closed mid-run, which is
consistent with a user manually dismissing a window — tiled mode
is a best-effort hands-on debugging aid, not an automation mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:01:21 -04:00
adlee-was-taken
21fe53eaf7 feat(soak): click-to-watch live video via CDP screencast
Runner creates a Screencaster before sessions are acquired, then
wires its start/stop into DashboardServer.onStartStream / onStopStream
after sessions exist (the handlers close over a sessionsByKey map).
Clicking a player tile in the dashboard starts a CDP screencast on
that session's page, forwards JPEG frames as WS "frame" messages.
Closing the modal (or disconnecting the WS) stops all screencasts.

Verified end-to-end: programmatically connected WS, sent start_stream,
received 5 frames (13.7KB each), sent stop_stream, screencast_stopped
log line fired, run completed cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:54:52 -04:00
adlee-was-taken
34ce7d1d32 feat(soak): Screencaster — CDP Page.startScreencast wrapper
Attach/detach CDP sessions per Playwright Page, start/stop JPEG
screencasts with configurable quality and frame rate, forward each
frame to a callback. Used by the dashboard for click-to-watch
live video (wired in Task 23).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 19:06:17 -04:00
adlee-was-taken
796de876b7 feat(soak): wire --watch=dashboard in runner
Starts DashboardServer on port 7777 (or --dashboard-port), uses its
reporter as ctx.dashboard, auto-opens the URL via xdg-open/open/start.
Cleans up on exit. WS client connections logged as info events so
you can see when the browser attaches.

Verified: 2-account populate run with --watch=dashboard serves the
static page on :7777, accepts WS connections, cleanly shuts down
when the run completes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 19:05:44 -04:00
adlee-was-taken
a35e789eb9 feat(soak): dashboard status grid UI
Static HTML page served by DashboardServer. Renders the 2×2 room
grid with progress bars and player tiles, subscribes to WS events,
updates tiles live. Click-to-watch modal is wired but receives
frames once the CDP screencaster ships in Task 22.

Adds escapeHtml() on all user-controlled strings (roomId, player
key) — not strictly needed for our trusted bot traffic but cheap
XSS hardening against future scenarios that accept user input.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:55:21 -04:00
adlee-was-taken
9d1d4f899b feat(soak): DashboardServer — vanilla http + ws
Serves one static HTML page, accepts WS connections, broadcasts
room_state/log/metric messages to all clients. Replays current
state to late-joining clients so refreshing the dashboard during
a run shows the right grid. Exposes a reporter() method that
returns a DashboardReporter scenarios can call without knowing
about sockets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:53:59 -04:00
adlee-was-taken
d1688aae0b feat(soak): runner.ts end-to-end with --watch=none
First full end-to-end milestone: parses CLI, builds SessionPool +
RoomCoordinator, loads a scenario by name, runs it, reports results,
cleans up. Watch modes other than "none" log a warning and fall back
until Tasks 19-24 implement them.

Smoke test passed against local dev:
  bun run soak -- --scenario=populate --accounts=2 --rooms=1
    --cpus-per-room=0 --games-per-room=1 --holes=1 --watch=none
  → Games completed: 1, Errors: 0, Duration: 78.2s, exit 0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:52:08 -04:00
adlee-was-taken
a6a276b509 fix(bot): GolfBot handles authenticated sessions + num-decks stepper
Two small fixes to tests/e2e/bot/golf-bot.ts needed to run the bot
from the soak harness with authenticated accounts:

1. createGame and joinGame now check whether #player-name is
   visible before filling it. Authenticated sessions hide that
   input (the server uses the logged-in username); guest sessions
   still fill it as before. Existing e2e tests behave identically
   since they register guests who always see the input.

2. startGame's 'decks' option was calling selectOption on
   #num-decks, which is a hidden input inside a stepper widget,
   not a <select>. Replaced with stepper-click logic that
   increments/decrements until the hidden input matches the
   target value.

End-to-end verified via the soak runner: 2 authenticated sessions
played a complete 1-hole game against local dev, 26 turns, exit 0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:52:07 -04:00
adlee-was-taken
6df81e6f8d feat(soak): CLI parsing + config precedence
parseArgs pulls --scenario/--rooms/--watch/etc from argv,
mergeConfig layers scenarioDefaults → env → CLI so CLI flags
always win. 12 Vitest unit tests cover both parse happy/edge
paths and the 4-way merge precedence matrix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:25:04 -04:00
adlee-was-taken
2c20b6c7b5 feat(soak): populate scenario + scenario registry
Partitions sessions into N rooms, runs gamesPerRoom games per room
in parallel via Promise.allSettled so a failure in one room never
unwinds the others. Errors roll up into ScenarioResult.errors.

Verified via tsx: listScenarios() returns [populate], getScenario()
resolves by name and returns undefined for unknown names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:23:56 -04:00
adlee-was-taken
722934bdf2 feat(soak): shared runOneMultiplayerGame helper
Encapsulates the host-creates/joiners-join/loop-until-done flow so
populate and stress scenarios don't duplicate it. Honors abort
signal and a max-duration timeout, heartbeats on every turn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:23:00 -04:00
adlee-was-taken
2a86b3cc54 feat(soak): scripts/seed-accounts.ts CLI wrapper
Thin standalone entry for pre-seeding N accounts before the first
harness run. Wraps SessionPool.seed and writes .env.stresstest.

End-to-end verified: ran against local dev with --count=4, all 4
accounts landed in the DB with is_test_account=TRUE, cred file
written with correct format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:22:10 -04:00
adlee-was-taken
3bc0270eb9 feat(soak): SessionPool — seed, login, acquire contexts
Owns BrowserContexts, seeds via POST /api/auth/register with the
invite code on cold start, warm-starts via localStorage injection of
the cached JWT, falls back to POST /api/auth/login if the token is
rejected. Exposes acquire(n) for scenarios.

Infrastructure changes needed to import the real GolfBot class from
tests/e2e/bot/ without the Task-10 structural-interface workaround:
 - Add @playwright/test as devDep so value-imports in e2e/bot/*.ts
   resolve at runtime (Page/Locator/expect are pulled even as types)
 - Remove rootDir from tsconfig so TS follows cross-package imports;
   add a paths entry so TS can resolve @playwright/test from the soak
   package's node_modules when compiling files under tests/e2e/bot
 - Drop the local GolfBot structural interface + its placeholder
   GamePhase/StartGameOptions/TurnResult types; re-export the real
   class from types.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:19:39 -04:00
adlee-was-taken
066e482f06 feat(soak): structured JSONL logger with child contexts
Single file, no transport, writes one JSON line per call to stdout.
Child loggers inherit parent meta so scenarios can bind room/game
context once and forget about it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:12:27 -04:00
adlee-was-taken
02642840da feat(soak): RoomCoordinator with host→joiners handoff
Lazy Deferred per roomId with a timeout on await. Lets concurrent
joiner sessions block until their host announces the room code
without polling or page scraping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:11:05 -04:00
adlee-was-taken
1565046ab7 feat(soak): core types + Deferred primitive
Establishes the Scenario/Session/Logger/DashboardReporter contracts
the rest of the harness builds on. Deferred is the building block
for RoomCoordinator's host→joiners handoff.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:09:36 -04:00
adlee-was-taken
5478a4299e feat(soak): scaffold tests/soak package
Placeholder runner, tsconfig with @bot alias to tests/e2e/bot,
gitignored .env.stresstest + artifacts. Real behavior follows
in Task 10 onward.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 08:19:09 -04:00
adlee-was-taken
835a79cc0f docs: soak harness bring-up steps
Documents the one-time UPDATE invite_codes SET marks_as_test = TRUE
step required before running tests/soak against each environment,
schema verification queries, and the expected filter behavior post-run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 01:23:13 -04:00
adlee-was-taken
983518e93d feat(admin): visible Test/Test-seed badges + filter toggle
Users table shows [Test] next to soak-harness accounts, invite codes
list shows [Test-seed] next to codes that flag new accounts as test,
and a new "Include test accounts" checkbox lets admins hide bot
traffic from the user list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 01:20:09 -04:00
adlee-was-taken
917ef2a239 feat(server): admin users list surfaces is_test_account
UserDetails carries the new column, search_users selects and
optionally filters on it, and the /api/admin/users route accepts
?include_test=false to hide soak-harness accounts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 01:13:41 -04:00
adlee-was-taken
b5a25b4ae5 feat(server): stats queries support include_test filter
Leaderboard and rank queries take an optional include_test param
(default false). Real users never see soak-harness traffic unless
they explicitly opt in via ?include_test=true.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:33:38 -04:00
adlee-was-taken
0891e6c979 feat(server): register flow flags accounts from test-seed invites
When a user registers with an invite_code whose marks_as_test=TRUE,
their users_v2.is_test_account is set to TRUE. Normal invite codes
and invite-less signups are unaffected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:25:00 -04:00
adlee-was-taken
1f20ac9535 feat(server): expose marks_as_test on InviteCode
Adds the field to the dataclass, SELECT list in get_invite_codes,
and a new get_invite_code_details helper that the register flow
will use to discover whether an invite should flag new accounts
as test accounts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:11:34 -04:00
adlee-was-taken
8e23adee14 feat(server): propagate is_test_account through User model & store
User dataclass, create_user, and all SELECT lists now round-trip the
new column. Value is always FALSE until Task 4 wires the register
flow to the invite code's marks_as_test flag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:01:53 -04:00
adlee-was-taken
3817566ed5 docs: rename test-account index to match users_v2 convention
Post-review fix for Task 1: code reviewer flagged that
idx_users_v2_is_test_account didn't match the idx_users_<suffix>
convention used by every other index in user_store.py. The
implementation commit (d163675) was amended to use
idx_users_test_account; this commit updates the plan and spec
docs so they stay in sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:52:55 -04:00
adlee-was-taken
d16367582c feat(server): add is_test_account + marks_as_test schema
New columns support separating soak-harness test traffic from real
user traffic in stats queries. Rebuilds leaderboard_overall matview
to include is_test_account so the fast path stays filterable.

Migration is idempotent via DO $$ / IF NOT EXISTS blocks inside
SCHEMA_SQL, which runs on every server startup — same mechanism
every existing post-v1 column migration uses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:52:18 -04:00
adlee-was-taken
e8051b256b docs(plan): harden soak-harness schema migration for deploy
Makes the deployment path explicit in Task 1: traces the existing
lifespan → get_user_store → initialize_schema → conn.execute(SCHEMA_SQL)
flow, notes that the DO $$/IF NOT EXISTS pattern is the same one
every post-v1 column migration uses, and explains why rollback is
safe (additive changes only).

Adds two new verification steps to Task 1:
 - Step 7: post-deploy psql checks against staging
 - Step 8: same against production

Adds a "Post-deploy schema verification" block to CHECKLIST.md so
the schema state is verified after every server restart against
each target environment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:40:28 -04:00
adlee-was-taken
cf916d7bc3 docs: implementation plan for multiplayer soak harness
33-task TDD plan across 11 phases implementing the soak & UX test
harness design. Server-side schema/filter/admin changes ship first
(independent), then the tests/soak/ TypeScript runner builds up
incrementally — first milestone is a --watch=none smoke run against
local dev after Task 18, then dashboard, live video, tiled mode,
stress scenario, failure handling, and staging bring-up. Final
task bumps project version to v3.3.4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:37:15 -04:00
adlee-was-taken
97036be319 docs: multiplayer soak & UX test harness design
Design for a standalone Playwright-based soak runner that drives 16
authenticated browser sessions across 4 concurrent rooms to populate
staging scoreboards and hunt stability bugs. Architected as a
pluggable scenario harness so future UX test scenarios (reconnect,
invite flow, admin workflows, mobile) slot in cleanly.

Also gitignores .superpowers/ (brainstorming session artifacts).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:03:28 -04:00
adlee-was-taken
52d7118c33 Add favicon and prod deploy script
Some checks failed
Build & Deploy Staging / build-and-deploy (release) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.3.3 v3.3.4
2026-04-07 20:37:36 -04:00
adlee-was-taken
d7631ec671 Fix CI: remove checkout step, runner can't resolve gitea hostname
All checks were successful
Build & Deploy Staging / build-and-deploy (release) Successful in 1m27s
The build happens on the staging server via SSH, not in the runner
container, so checkout is unnecessary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.3.2
2026-04-07 19:51:54 -04:00
adlee-was-taken
f6eeaed97d Fix CI/CD: use SSH-based build instead of Docker-in-Docker
Some checks failed
Build & Deploy Staging / build-and-deploy (release) Failing after 30s
act_runner doesn't reliably support docker/build-push-action.
Build the image on the staging server and push to registry from
there instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.3.1
2026-04-07 19:49:35 -04:00
adlee-was-taken
ef54ac201a Add invite request system and Gitea Actions CI/CD pipeline
Some checks failed
Build & Deploy Staging / build (release) Waiting to run
Build & Deploy Staging / deploy (release) Has been cancelled
Invite request feature:
- Public form to request an invite when INVITE_REQUEST_ENABLED=true
- Stores requests in new invite_requests DB table
- Emails admins on new request, emails requester on approve/deny
- Admin panel tab to review, approve, and deny requests
- Approval auto-creates invite code and sends signup link

CI/CD pipeline:
- Build & push Docker image to Gitea registry on release
- Auto-deploy to staging with health check
- Manual workflow_dispatch for production deploys

Also includes client layout/sizing improvements for card grid
and opponent spacing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.3.0
2026-04-07 19:38:52 -04:00
adlee-was-taken
0c0588f920 TUI: add double-escape quit and minor layout tweaks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:38:38 -04:00
adlee-was-taken
420928f11e Reconcile docker-compose.prod.yml drift with droplet (INC-0001/AI-10)
The droplet copy of this file had drifted from the repo across
multiple sessions. This commit brings the repo in line with what
is actually running in production.

Changes landing from the droplet:
- Add restart: unless-stopped to app, postgres, and redis services
  (the deploy.restart_policy block was swarm-only and silently
  ignored by compose v2; top-level restart: is the correct form)
- Drop the dead deploy.replicas: 1 and deploy.restart_policy:
  subfields from the app service (swarm-only noise)
- Remove the traefik: service block entirely; Traefik now runs as
  its own stack at /opt/traefik from the adlee-traefik repo
- Remove the letsencrypt: volume declaration (owned by adlee-traefik
  now via the external golfgame_letsencrypt volume)
- Change web network from "driver: bridge" to
  "name: traefik_web, external: true" so this stack attaches to the
  shared ingress network created by the traefik stack

This closes INC-0001/AI-10 and unblocks future deploys of this repo.

The other uncommitted client/*, tui_client/* changes in the working
tree are unrelated and intentionally left alone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:27:14 -04:00
adlee-was-taken
bc9445f06e Relicense project under GPL-3.0-or-later
Replaces the previous MIT license with GPL-3.0-or-later. Adds the full
GPL-3.0 license text at LICENSE, updates pyproject.toml metadata and
classifier, updates the README, and adds SPDX-License-Identifier headers
to all first-party server Python and client JavaScript sources.
Third-party anime.min.js is left untouched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:26:06 -04:00
adlee-was-taken
ea34ddf8e4 Fix swap animation stutter and remove 1s server-side dead delay
- Remove unused card_revealed broadcast + 1s asyncio.sleep in swap handler
  (client never handled this message, causing pure dead wait before game_state)
- Defer swap-out (opacity:0) on hand cards to onStart callback so overlay
  covers the card before hiding it — eliminates visual gap for all players
- Defer heldCardFloating visibility hide to onStart — held card stays visible
  until animation overlay replaces it
- Thread onStart callback through animateUnifiedSwap → _runUnifiedSwap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:47:26 -05:00
adlee-was-taken
5408867921 Harden .gitignore and add detect-secrets baseline
Add 19 missing secret file patterns to .gitignore (.env.* variants,
private keys, certificates, credentials, SSH keys). Add detect-secrets
baseline for pre-commit hook secret scanning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:47:02 -05:00
adlee-was-taken
a8b521f7f7 Fix two production crashes and bump to v3.2.0
1. Fix IndexError in current_player() when player leaves mid-game
   - remove_player() now adjusts current_player_index after popping
   - current_player() has safety bounds check as defensive fallback

2. Fix AssertionError in StaticFiles catching WebSocket upgrades
   - Wrap static file mount to reject non-HTTP requests gracefully
   - Starlette's StaticFiles asserts scope["type"] == "http"

Both crashes were observed in production on 2026-02-28 during a
multi-player session. The IndexError cascaded into reconnection
attempts that hit the StaticFiles assertion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:30:08 -05:00
adlee-was-taken
7f0f580631 Add client-side card reveal before swap and YOUR TURN badge update
Reveal face-down cards briefly (1s) before swap completes, using
client-side state diffing instead of a separate server message.
Local player reveals use existing card data; opponent reveals use
server-sent card_revealed as a fallback. Defers incoming game_state
updates during the reveal window to prevent overwrites.

Also update YOUR TURN badge to cyan with suit symbols.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:35:49 -05:00
adlee-was-taken
215849703c Add inline comments across client and server codebase
Full-codebase commenting pass focused on the tricky, fragile, and
non-obvious spots: animation coordination flags in app.js, AI decision
safety checks in ai.py, scoring evaluation order in game.py, animation
engine magic numbers in card-animations.js, and server infrastructure
coupling in main.py/handlers.py/room.py. No logic changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 22:17:19 -05:00
adlee-was-taken
72eab2c811 TUI visual polish: felt table, status bar, scoreboard delay
- Dark green felt background for game screen
- Status bar: dark brown bg with amber text instead of blue
- YOUR TURN badge: green bg with white text instead of bright gold
- 3s delay before hole-complete scoreboard overlay
- Dealer indicator changed from Ⓓ to (D)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 21:56:01 -05:00
adlee-was-taken
dfb3397dcb Overhaul TUI navigation, quit handling, and scoreboard tags
- Replace [esc][esc] quit with [q] quit globally (immediate on login,
  confirmation prompt elsewhere)
- [esc] is now consistently "back": signup→login, lobby→log out (with
  confirm), in-room host→leave (with confirm), in-room guest→leave
- Extract ConfirmScreen to shared screens/confirm.py
- Move dealer Ⓓ indicator to bottom-left corner of player box border
- Scoreboard now tags OUT (went out first) and  (lowest score)
- Send finisher_id and player id in round_over server message
- Room code moved inside in-room section with amber border
- Lobby title uses branded 🏌️ GolfCards.club ♠♥♣♦
- Amber borders and dark green backgrounds on login/lobby containers
- Deck preview renders actual card-back shapes (▓▒▓/▒▓▒)
- Help/standings panels close only with [esc], hint updated
- Game footer: s[⇥]andings [h]elp on left, [q]uit on right

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 21:41:45 -05:00
adlee-was-taken
b1d3aa7b77 Add session persistence, splash screen, and TUI polish
Save JWT token to ~/.config/golfcards/session.json after login so
subsequent launches skip the login screen when the session is still
valid. A new splash screen shows the token check status (SUCCESS /
NONE FOUND / EXPIRED) before routing to lobby or login.

Also: move OUT indicator to player box bottom border, remove checkmark,
center scoreboard overlay, use alternating shade blocks (▓▒▓/▒▓▒) for
card backs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 19:35:03 -05:00
adlee-was-taken
67d06d9799 Mark stale games as abandoned in DB during cleanup and on startup
- Periodic room cleanup now updates games_v2 status to 'abandoned'
- Server startup marks all orphaned active games as abandoned
- Prevents stale games from accumulating in the admin portal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:25:03 -05:00
adlee-was-taken
82aa3dfb3e Add auto-cleanup of stale game rooms after 5 minutes of inactivity
Rooms that sit idle (no player actions or CPU turns) for longer than
ROOM_IDLE_TIMEOUT_SECONDS (default 300s) are now automatically cleaned
up: CPU tasks cancelled, players notified with room_expired, WebSockets
closed, and room removed from memory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:17:57 -05:00
adlee-was-taken
7001232658 Add single-escape navigation: back from signup/lobby, leave room
- Single Esc: goes back one step (signup→login, lobby→connect, room→lobby)
- Double Esc: still quits the app
- Footer bar shows [Esc] Back and [Esc][Esc] Quit hints on all screens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 20:17:10 -05:00
adlee-was-taken
13e98d330a Add TUI signup flow, quit/help/standings modals, and UI refinements
- Add signup with invite code support, remove guest login
- Add quit confirmation (q), help screen (h), standings tab
- Unified footer: [h]elp [q]uit | action text | [tab] standings
- Amber card highlighting persists through entire initial flip phase
- Player box border only highlights on turn (green) or knock (red)
- Play area gold border only during player's actual turn
- Game end returns to lobby create/join instead of login screen
- Lobby reset_to_pre_room for replayability without reconnecting
- Dynamic opponent layout fits all in one row when terminal is wide enough
- Hole emoji () in status bar, branded title with suits on connect screen
- DECK label spacing, Hole terminology in scoreboard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 20:14:04 -05:00