Files
botfights/.claude/plans/breezy-questing-dove.md
T
DorianandClaude Opus 4.6 610e799605 feat: SSE live fight spectating with spectator count
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>
2026-03-08 19:46:39 +00:00

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 secureHeaders from hono/secure-headers with 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 bodyLimit from hono/body-limit — 256KB max for API requests

1.3 Hide error details in production — DONE

File: server/src/app.ts

  • onError returns 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.tsgetActiveFighterCount()
  • server/src/engine/human-responses.tsclearAllPending()

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 sqlite from db/index.js

3.2 releasePayment reverts DB state — DONE

File: server/src/engine/payments.ts

  • Now also reverts status from consumed back to confirmed in DB

Phase 4: Live Round-by-Round TUI — DONE

4.1 onRoundComplete callback — DONE

File: server/src/engine/fight-loop.ts

  • New onRoundComplete option, subscribes to fightEvents.onAll() for round_end events

4.2 TUI wired to round updates — DONE

File: server/src/fight-loop-cli.ts

  • onRoundComplete updates state.currentFight HP/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 defender
  • satoshiTornado — 14-24 ₿ spiral in tightening vortex around defender, then explode
  • hodlWave — wall of ₿ letters (4-6 rows x 7-10 cols) advances like a tidal wave
  • bitcoinBarrage — rapid-fire stream of 20-35 spinning ₿ from the laptop
  • All added to CREATOR_MOVES array and choreographyMap

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)

  • morphRevert now calls attacker.use(k.sprite(originalSpriteKey)) to swap back to Creator
  • Added await on applyCreatorOmniMorph call (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

  1. pnpm build — verify no type errors (both server and frontend pass)
  2. Production server: curl -I /api/health — X-Frame-Options, X-Content-Type-Options, CSP headers present
  3. Body limit: oversized POST returns 413
  4. Error hiding: bad endpoint returns "Internal server error" (not stack trace)
  5. Graceful shutdown: kill -TERM shows drain log
  6. TUI: pnpm fight-loop --max=3 shows live round-by-round HP updates
  7. Payment race: concurrent ranked joins with same paymentId — only 1 succeeds
  8. Ranked fights: no more "Missing pubkey" error in production
  9. Creator fights: omni-morph visually transforms into random archetypes (sprite swaps)
  10. Creator fights: ₿ letters orbit persistently, new swarm attacks fire regularly