Batched remaining harness tasks (27-30, 33):
Task 27 — Artifact capture on failure: screenshots, HTML snapshots,
game state JSON, and console error tails are captured into
tests/soak/artifacts/<run-id>/ when a scenario throws. Successful
runs get a summary.json. Old runs (>7d) are pruned on startup.
Task 28 — Graceful shutdown: first SIGINT/SIGTERM flips the abort
signal (scenarios finish current turn then unwind). 10s after, a
hard-kill fires if cleanup hangs. Double Ctrl-C = immediate exit.
Exit codes: 0 success, 1 errors, 2 interrupted.
Task 29 — Periodic health probes: every 30s GET /health against the
target server. Three consecutive failures abort the run with
health_fatal, preventing staging outages from being misattributed
to harness bugs. Corrected endpoint from /api/health to /health
per server/routers/health.py.
Task 30 — Smoke test script: tests/soak/scripts/smoke.sh, a 60s
end-to-end canary that health-probes the target, seeds if needed,
and runs one minimal populate game.
Task 33 — Version bump to v3.3.4: both index.html footers (was
v3.1.6), new footer added to admin.html (had none), pyproject.toml.
Also fixes discovered during stress testing:
- SessionPool sets baseURL on all contexts so relative goto('/')
resolves correctly between games (was "invalid URL" error)
- RoomCoordinator key is now unique per game-start (Date.now
suffix) so Deferred promises don't carry stale room codes from
previous games
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
- 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>
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>
Enables public beta signup metering: DAILY_OPEN_SIGNUPS env var controls
how many users can register without an invite code per day (0=disabled,
-1=unlimited, N=daily cap). Invite codes always bypass the limit.
Also adds per-IP signup throttling (DAILY_SIGNUPS_PER_IP, default 3/day)
and fail-closed rate limiting on auth endpoints when Redis is down.
Client dynamically fetches /api/auth/signup-info to show invite field
as optional with remaining slots when open signups are enabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update version across pyproject.toml, FastAPI app, HTML footers,
and V3.17 doc. Mark kicked-ball bug as resolved in docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Base .golfer-container rule was after the mobile @media override,
clobbering it. Moved base rule before the media query. Landscape
gets -2px (snug), mobile gets 12px.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reduce golfer-container margin from 10px to 4px in landscape (2-row)
mode while keeping 10px on mobile. Swap bottom suits to checkerboard
pattern: club/diamond on top, heart/spade on bottom.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap golfer+ball in a positioned container so the ball is absolutely
anchored to the golfer's front foot, independent of inline flow/viewport.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rearrange golf ball SVG suits from single row to 2x2 grid
- Add even spacing between logo, golfer, and title in mobile view
- Remove row-gap between logo row and title row in landscape view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 749px media query was triggering at mid-range widths, collapsing
the two-row logo+title into a single line. Fix by:
- Using inline-grid on h1 for bulletproof two-row layout
- Lowering single-line breakpoint from 749px to 500px
- Widening lobby container to 550px for title to fit naturally
- Constraining game controls to 400px max-width
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change .logo-row from inline-block to block so the golf ball logo
always left-aligns flush with the G, regardless of viewport width.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The h1 shrinks to its widest child (GolfCards.club), centers via
margin auto, and text-align left aligns both lines within it.
No breakpoint-dependent transforms needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logo and title naturally left-align within the centered lobby box.
Mobile (<480px) gets text-align center + inline display for single line.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cut lastPlayPause to 2s and increase shake interval by 80% (3s→5.4s)
so the draw/discard nudge feels less nagging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Increase post_draw_settle timing (1.1s→1.3s) and swap retry delay
(100ms→350ms) to prevent draw animation from being cut short by the
arriving swap animation. Also make CPU go-out decisions consider
opponent scores and avoid swapping high cards (8+) into hidden slots.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tighten padding, gaps, card sizes, and margins across all scoresheet
elements so the full modal fits without scrolling on most viewports.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues fixed:
1. renderGame() was called before the lastPlayPause delay, causing the
board to jump to final card positions while the swap animation was
still visually playing. Moved renderGame() to after the wait+pause.
2. When the local player makes the final play, their swap animation
defers the round_over game_state to pendingGameState. The deferred
state bypassed the round-end intercept, so preRevealState was never
set — causing the scoresheet to appear immediately without the
reveal animation. Now completeSwapAnimation checks for round_over
transitions and sets preRevealState. Also added a wait loop in
runRoundEndReveal for robustness.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Force discard pile DOM update before draw animation starts to prevent
stale card display when previous swap animation blocked renderGame.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move CPU +/- buttons inline into Players header row with "CPU:" label
- Tighten vertical spacing for mobile stacked layout (≤700px)
- Fit Decks/Holes/Card Backs settings in single row on mobile
- Reduce room code banner and auth bar edge margins
- Consistent spacing across all stacked viewport widths
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Style the flip overlay's front face to match player hand cards (gradient
background, proper border/shadow) instead of using generic card-front
styles. Hide the underlying card during the animation so the green table
shows through the flip rather than a white card peeking behind it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The turn pulse shake was targeting .discard-stack, which is an ancestor of
#held-card-floating. A CSS transform on any ancestor breaks position:fixed,
causing the held card to render far from the deck area. Now target #discard
directly instead.
Also fix duplicate getCardPointValue methods — the 3-arg scoring version
shadowed the 1-arg tooltip version, leaving cardValues undefined on hover.
Add staging deploy script (rsync working tree, no git pull needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>