fetchNostrProfile now uses Promise.allSettled to query all relays
concurrently. Aggregates results with latest-created_at-wins strategy
instead of stopping at the first relay that responds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Module re-evaluation during HMR reset autoRestoreRan to false, causing
duplicate auth-restore API calls. Now persists flag on globalThis.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
useNostr (9), useFightCache (5), useOnlineStatus (4) composable tests.
Added fake-indexeddb dev dependency for IDB tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Catches runtime errors in child components, displays user-friendly
error message with reload button. Wired into App.vue wrapping
router-view. Tests verify error capture and button rendering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Timeout and WebSocket errors reject with proper Error objects
- Caller catches and falls through to poll-based confirmation
- Preimage undefined check prevents calling confirm with no preimage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feedbackTimer, timerHandle, and pollHandle are all cleared in
onUnmounted. Test confirms cleanup pattern works correctly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 silent catch handlers replaced with descriptive console.warn logging
across 6 frontend files. No silent error swallowing remains.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SSE now always attempts reconnection when fight isn't finished,
regardless of isLive.value. Uses exponential backoff (1s, 2s, 4s,
max 8s). Moved sseRetries to outer scope to persist across reconnects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Frontend: vitest.config.ts with vue plugin + jsdom, dummy component test
- Server: in-memory SQLite test DB factory + Hono testClient helper + smoke test
- CI: add pnpm audit and server coverage threshold steps
- Root: vitest workspace config for multi-project test discovery
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scoring: both-wrong is now a DRAW — equal scores (3/3), no winner,
symmetric minimal damage. Garbage answers no longer beat reasonable
ones just by being faster. Both-wrong narrations reflect the draw.
Entrance: removed duplicate announceDeepIntro() call from FightViewer
(was already called inside playEntrance). Removed _resetPositions()
after entrance (entrance already places fighters at home positions,
the extra reset caused a visible snap/reset).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merged bot-setup and choose-connection into one actionable step.
Users now see mode picker, download button for the correct guide
(BOTFIGHTS-WEBHOOK.md or BOTFIGHTS-POLLING.md), and safety info
all on one screen instead of two filler steps with no actions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
buildNip98Token checked window.nostr before secretKeyHex, so
"Generate New Identity" would sign with the browser extension's key
after saving. Now explicit keys always take priority over extensions.
Also made setup flow mode-aware: webhook users get BOTFIGHTS-WEBHOOK.md,
polling users get BOTFIGHTS-POLLING.md with matching copy prompts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Allow huggingface.co in CSP connect-src (fixes Kokoro TTS model download)
- Add registerSW.js route (fixes PWA service worker 404)
- Add _resetPositions() safety after entrance (fixes invisible fighters)
- Fight end sequence works without canvas scene (KO/overlays/log always play)
- Pre-fight instructions in battle log for human players
- NIP-55 visibility sync and cleanup handlers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When window.nostr isn't available (common on mobile Chrome where
extensions can't inject), fall back to NIP-55 nostrsigner: intent
URIs. This opens Amber/Primal directly to sign a NIP-98 event,
then redirects back with the signed event for JWT authentication.
- Build nostrsigner: URI with unsigned NIP-98 event + callback URL
- Process NIP-55 callback on page mount (extract signed event from URL)
- Auto-detect Android to show "SIGN IN WITH AMBER / PRIMAL" label
- Reduced window.nostr polling from 3s to 2s before NIP-55 fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Security:
- Move CREATOR_PUBKEY from hardcoded constant to BOTFIGHTS_CREATOR_PUBKEYS
env var. Shared isCreatorPubkey() in constants.ts used by auth, admin,
tournaments. Frontend checks authorization via API, not client-side.
Mobile fixes:
- Nostr signer: poll for window.nostr up to 3s (Amber injects late).
- TTS: auto-unlock AudioContext on first user interaction via
installAutoUnlock() on fight page mount.
UX:
- Add loading spinners to "I BUILD BOTS" and "I FIGHT MYSELF" buttons.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Nostr signer: poll for window.nostr up to 3s on mobile (Amber injects
late). Both login() and handleSignerLogin() now wait before failing.
- Mobile TTS: install global one-time click/touch/keydown handler to
auto-unlock AudioContext when fight pages mount. Previously only
triggered by explicit sound toggle, so mobile TTS silently failed.
- Add loading spinners to "I BUILD BOTS" and "I FIGHT MYSELF" buttons.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add sfxModem() — synthesized 56k handshake sound with carrier tones,
data burst, and chirps. Plays instead of TTS for code_golf/hack_battle
rounds and code-detected answers.
- Fix entrance voice overlap: remove duplicate announceDeepIntro() from
robe entrance, add cancelPrevious to entrance-specific voice calls
(girlfriend, bouncer, shopping cart, spotlight, creator) so they
cleanly replace the global intro instead of overlapping.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove .slice(0, 120) truncation from battle log entries and
.slice(0, 60) from speech bubble calls. Increase bubble limits
to 200 chars, 24 chars/line, 8 lines so responses display fully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On narrow viewports (<600px), fighters were oversized due to the
desktop scale formula. Use reduced base (1.1) and tier (0.2) scale
factors on mobile while keeping desktop unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix invisible characters in human mode: init live scene BEFORE starting
challenge polling so entrance plays before first question appears
- Cap creative writing timer to 10s for multiple choice (just tapping buttons)
- Fix TTS reliability: precache priority phrases (Round 1-7, Fight!, K.O.)
all at once instead of in slow batches; prevent duplicate precache runs
- Add 10 vibe-coded narrations (~20% chance): "I was vibe coded into existence"
- Add 15 sweary narrations (~30% chance): raw unhinged fight commentary
- Add sweary draw and retro narrations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace insecure raw-pubkey auth with cryptographic NIP-98 signed
requests and server-issued JWT sessions. Logout now fully clears
all state including nsec. Add yellow "Use Nostr Signer" button
for Amber/NIP-07 remote signers.
- Server: JWT middleware (HMAC-SHA256, 24h expiry), NIP-98 verification
- Server: POST /api/auth/nostr/session endpoint
- Frontend: NIP-98 token builder + authFetch wrapper with JWT Bearer
- Frontend: All authenticated API calls use authFetch
- Security: logout clears JWT, pubkey, bot, nsec, and profile pic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Practice button to BotProfilePage for quick sparring
- Fix rate limiter bug: all rateLimit() instances shared one counter map,
causing global and per-route limits to corrupt each other. Each limiter
now gets its own isolated map.
- Replace 8-digit hex colors (#ffd70066) with rgba() in sprite rendering
for mobile browser compatibility (iOS Safari renders them as black boxes)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shows a one-time warning when users first click "Fight For Sats" letting
them know it's vibe coded and to only use small sats amounts. Acknowledged
state persists in localStorage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Mobile nav menu now overlays content (absolute positioning) instead of
pushing it down
- Speech bubbles stay visible for minimum 400ms even when TTS resolves
instantly or fails
- kokoroPlayCached checks audio cache and loads static files even when
Kokoro worker hasn't loaded — fixes TTS not playing on production
- CORS_ORIGIN env now supports comma-separated origins
- Rename "VIDEO REPLAY" to play icon + "REPLAY"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Guard initScene() with initializingScene flag to prevent concurrent calls
- Don't auto-reinit scene on WebGL context restore during active replay
- Block initScene() after component is destroyed
- Static audio files now play even before Kokoro worker loads
- kokoroSpeak/kokoroSpeakAsync check cache before requiring worker ready
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
71 static voice files (9.6MB) for round calls, intros, hype lines,
and challenge announces. These play instantly from file instead of
running Kokoro TTS generation in the browser. Dynamic content
(questions, answers, narrations) still uses Kokoro.
Includes Node.js generation script (scripts/generate-voice-files.mjs)
and modified tts.ts to check static file cache before worker generation.
Static audio works even before Kokoro model finishes loading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reduces worker generation timeout from 10s to 4s and adds 2s race on
awaitReady so slow Kokoro generation falls back to Web Speech API
quickly instead of stalling the fight.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds await-then-play pattern: audio is pre-generated and cached before
visuals appear, so log text + speech bubble + mouth animation + voice
all fire in the same frame. Prefetches both answers during question
playback for instant transitions. Adds hideSpeechBubble() to dismiss
bubbles when voice ends instead of fixed 5s timer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Track active fight IDs per bot in orchestrator (Set → Map)
- Return fightId in "already in fight" error responses (409)
- Frontend shows "REJOIN FIGHT" link instead of generic error
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Listen for webglcontextlost/restored events on the fight canvas.
Show "recovering" overlay on context loss, re-init scene on restore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Don't reset isJoining flags on successful navigation (component unmounts)
- Add onBeforeRouteLeave guard to clean up polling and rate limit timers
- Use handleError with rate limit countdown for all error paths
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Warn before navigating away from active fight (beforeRouteLeave)
- Re-poll challenge state when tab becomes visible (visibilitychange)
- Prevents silent forfeit on back-button and stale timer after tab switch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sprite generation fallback now preserves archetype and customization
instead of silently reverting to a random character.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- BUG-1: Prevent double-tap by locking phase before async submit
- BUG-2: Submit timeout notification to server when timer expires
- BUG-3: Distinct "TIME'S UP!" visual vs "ANSWER SUBMITTED"
- BUG-4: Track consecutive poll failures, show connection lost banner
- BUG-5: Add A-D / 1-4 keyboard shortcuts for MC choices
- BUG-6: Use choice text as v-for key instead of array index
- BUG-7: Deadline-based timer (250ms tick) prevents drift
- BUG-8: Validate choice is in current choices before submit
- BUG-9: Submit empty timeout instead of random choice on expiry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Creative challenges (roast_battle, creative_writing, meme_war,
code_golf, wrestling_match) now auto-generate multiple choice options
from per-type response pools: 1 good answer + 3 weaker distractors.
The free text input UI is commented out but preserved for future use.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>