Enable real-time fight spectating for all live fights (not just human fights). Multiple spectators can watch simultaneously via SSE. Spectator count is tracked per-fight and broadcast with every SSE event. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
6.6 KiB
Production Hardening v2 — Final Status
Context
The original hardening plan (greedy-skipping-lollipop.md) was 95% complete. A full security audit confirmed the fight engine, scoring, auth, SSRF, rate limiting, and webhook systems were solid. This session addressed remaining production gaps AND added Creator ₿ animation/morph enhancements AND fixed a production ranked-fight bug.
Phase 1: Security Headers & Error Hardening — DONE
1.1 Security headers middleware — DONE
File: server/src/app.ts
- Added
secureHeadersfromhono/secure-headerswith CSP (production only), X-Frame-Options, X-Content-Type-Options, HSTS, Referrer-Policy
1.2 Request body size limit — DONE
File: server/src/app.ts
- Added
bodyLimitfromhono/body-limit— 256KB max for API requests
1.3 Hide error details in production — DONE
File: server/src/app.ts
onErrorreturns generic "Internal server error" in production, full message in dev
Phase 2: Graceful Shutdown & Env Validation — DONE
2.1 Graceful shutdown handler — DONE
File: server/src/index.ts
- SIGTERM/SIGINT handlers wait 10s for in-flight fights, clear pending human challenges, then exit
2.2 Export helpers for shutdown — DONE
server/src/engine/orchestrator.ts—getActiveFighterCount()server/src/engine/human-responses.ts—clearAllPending()
2.3 Stricter env validation — DONE
File: server/src/index.ts
- Warns if CORS_ORIGIN not set in production
Phase 3: Payment Race Condition Fix — DONE
3.1 Atomic consumePaymentForQueue — DONE
File: server/src/engine/payments.ts
- Replaced check-then-set with atomic
UPDATE ... WHERE(single SQL statement) - Imported raw
sqlitefrom db/index.js
3.2 releasePayment reverts DB state — DONE
File: server/src/engine/payments.ts
- Now also reverts
statusfromconsumedback toconfirmedin DB
Phase 4: Live Round-by-Round TUI — DONE
4.1 onRoundComplete callback — DONE
File: server/src/engine/fight-loop.ts
- New
onRoundCompleteoption, subscribes tofightEvents.onAll()forround_endevents
4.2 TUI wired to round updates — DONE
File: server/src/fight-loop-cli.ts
onRoundCompleteupdatesstate.currentFightHP/round/challengeType and re-renders
Phase 5: Creator ₿ Animation Enhancements — DONE
5.1 Persistent orbiting ₿ letters on entrance — DONE
File: frontend/src/game/FightScene.ts (Creator entrance)
- Replaced 6 plain circle dots with 10 ₿ text letters in 2 counter-rotating rings
- Varied sizes (7-14px), 5 colors, gentle rocking animation
- Visible for the entire fight
5.2 Upgraded omni-morph ₿ orbits — DONE
File: frontend/src/game/FightScene.ts (applyCreatorOmniMorph)
- Increased from 4 to 8 ₿ in dual rings (inner 5, outer counter-rotating 3)
- Varied sizes (10-18px), 3 colors, wobble animation
5.3 bitcoinRain uses ₿ text — DONE
File: frontend/src/game/FightScene.ts (bitcoinRain)
- Changed from circles to actual ₿ text with 3 colors, varied sizes (6-18px), spinning as they fall
5.4 Four new ₿ swarm choreographies — DONE
File: frontend/src/game/FightScene.ts
bitcoinSwarm— 18-30 ₿ letters zigzag from Creator to defendersatoshiTornado— 14-24 ₿ spiral in tightening vortex around defender, then explodehodlWave— wall of ₿ letters (4-6 rows x 7-10 cols) advances like a tidal wavebitcoinBarrage— rapid-fire stream of 20-35 spinning ₿ from the laptop- All added to
CREATOR_MOVESarray andchoreographyMap
Phase 6: Creator Omni-Morph Visual Fix — DONE
6.1 Sprite swap on morph — DONE
File: frontend/src/game/FightScene.ts (applyCreatorOmniMorph)
- Was: only added overlays (aura, ₿, color tint) — sprite stayed as Creator
- Now: generates a full sprite sheet for the picked archetype via
generateSpriteSheet(seed, tier, colors, archetypeOverride) - Loads it as a Kaplay sprite and swaps with
fighter.use(k.sprite(morphKey)) - Function changed from sync to
async - Unique sprite keys via incrementing counter prevent collisions
6.2 Sprite revert after attack — DONE
File: frontend/src/game/FightScene.ts (morph trigger in playRound)
morphRevertnow callsattacker.use(k.sprite(originalSpriteKey))to swap back to Creator- Added
awaitonapplyCreatorOmniMorphcall (now async)
Phase 7: Production Ranked Fight Fix — DONE
7.1 Missing pubkey in ranked endpoints — DONE
File: frontend/src/composables/useWallet.ts
payEntryFee()now sends{ botId, pubkey: pubkey.value }to/api/payments/create-invoice
File: frontend/src/pages/JoinBoutPage.vue
fightRanked()now sends{ paymentId, pubkey: pubkey.value }to/api/queue/join-ranked
Root cause: Server requires pubkey in production for ownership verification, but frontend wasn't sending it. Worked in dev because dev mode skips pubkey checks.
All Files Modified
| File | Phase | Changes |
|---|---|---|
server/src/app.ts |
1 | +secureHeaders, +bodyLimit, error hiding |
server/src/index.ts |
2 | +graceful shutdown, +env validation |
server/src/engine/orchestrator.ts |
2 | +getActiveFighterCount() |
server/src/engine/human-responses.ts |
2 | +clearAllPending() |
server/src/engine/payments.ts |
3 | Atomic consume, releasePayment DB revert |
server/src/engine/fight-loop.ts |
4 | +onRoundComplete via fightEvents |
server/src/fight-loop-cli.ts |
4 | Wire onRoundComplete to TUI |
frontend/src/game/FightScene.ts |
5,6 | ₿ animations, omni-morph sprite swap, 4 new choreographies |
frontend/src/composables/useWallet.ts |
7 | +pubkey in create-invoice |
frontend/src/pages/JoinBoutPage.vue |
7 | +pubkey in join-ranked |
frontend/src/game/sprites/archetypes/the_creator.ts |
— | No changes (reference only) |
Verification
pnpm build— verify no type errors (both server and frontend pass)- Production server:
curl -I /api/health— X-Frame-Options, X-Content-Type-Options, CSP headers present - Body limit: oversized POST returns 413
- Error hiding: bad endpoint returns "Internal server error" (not stack trace)
- Graceful shutdown:
kill -TERMshows drain log - TUI:
pnpm fight-loop --max=3shows live round-by-round HP updates - Payment race: concurrent ranked joins with same paymentId — only 1 succeeds
- Ranked fights: no more "Missing pubkey" error in production
- Creator fights: omni-morph visually transforms into random archetypes (sprite swaps)
- Creator fights: ₿ letters orbit persistently, new swarm attacks fire regularly