feat: move creator pubkey to env, fix mobile TTS + signer, button loaders
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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
dd3cbdae7f
commit
6dc50f5d5d
@@ -306,14 +306,18 @@ async function showHitText(text: string, color: string, x: number) {
|
||||
}
|
||||
|
||||
function addRoundToLog(round: FightRound, stagger: boolean): Promise<void> {
|
||||
// Hide human player's typed answer from the battle log
|
||||
const isAHuman = props.fight.botA?.archetype === 'human'
|
||||
const isBHuman = props.fight.botB?.archetype === 'human'
|
||||
|
||||
if (!stagger) {
|
||||
const challenge = JSON.parse(round.challengeData)
|
||||
logItems.value.push(
|
||||
{ type: 'header', round: round.roundNumber, text: `ROUND ${round.roundNumber}: ${challengeLabel(round.challengeType)}`, color: 'neon-purple' },
|
||||
{ type: 'prompt', round: round.roundNumber, text: challenge.displayPrompt || challenge.prompt, color: 'text-muted' },
|
||||
{ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse || '[NO RESPONSE]'} (${round.botATimeMs}ms)`, color: 'neon-cyan' },
|
||||
{ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse || '[NO RESPONSE]'} (${round.botBTimeMs}ms)`, color: 'neon-pink' },
|
||||
)
|
||||
if (!isAHuman) logItems.value.push({ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse || '[NO RESPONSE]'} (${round.botATimeMs}ms)`, color: 'neon-cyan' })
|
||||
if (!isBHuman) logItems.value.push({ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse || '[NO RESPONSE]'} (${round.botBTimeMs}ms)`, color: 'neon-pink' })
|
||||
if (round.narration) logItems.value.push({ type: 'narration', round: round.roundNumber, text: `>> ${round.narration}`, color: 'neon-yellow' })
|
||||
const winner = round.winnerId === props.fight.botA?.id ? props.fight.botA?.name : round.winnerId === props.fight.botB?.id ? props.fight.botB?.name : 'DRAW'
|
||||
logItems.value.push({ type: 'result', round: round.roundNumber, text: `${winner} wins round! (${round.botAScore} vs ${round.botBScore})`, color: 'text-secondary' })
|
||||
@@ -326,14 +330,18 @@ function addRoundToLog(round: FightRound, stagger: boolean): Promise<void> {
|
||||
scrollLog(); await sleep(150)
|
||||
logItems.value.push({ type: 'prompt', round: round.roundNumber, text: challenge.displayPrompt || challenge.prompt, color: 'text-muted' })
|
||||
scrollLog(); await sleep(200)
|
||||
logItems.value.push({ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse || '[NO RESPONSE]'}`, color: 'neon-cyan' })
|
||||
scrollLog(); await sleep(150)
|
||||
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botATimeMs}ms | Score: ${round.botAScore}`, color: 'text-muted' })
|
||||
scrollLog(); await sleep(150)
|
||||
logItems.value.push({ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse || '[NO RESPONSE]'}`, color: 'neon-pink' })
|
||||
scrollLog(); await sleep(150)
|
||||
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botBTimeMs}ms | Score: ${round.botBScore}`, color: 'text-muted' })
|
||||
scrollLog()
|
||||
if (!isAHuman) {
|
||||
logItems.value.push({ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse || '[NO RESPONSE]'}`, color: 'neon-cyan' })
|
||||
scrollLog(); await sleep(150)
|
||||
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botATimeMs}ms | Score: ${round.botAScore}`, color: 'text-muted' })
|
||||
scrollLog(); await sleep(150)
|
||||
}
|
||||
if (!isBHuman) {
|
||||
logItems.value.push({ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse || '[NO RESPONSE]'}`, color: 'neon-pink' })
|
||||
scrollLog(); await sleep(150)
|
||||
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botBTimeMs}ms | Score: ${round.botBScore}`, color: 'text-muted' })
|
||||
scrollLog()
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useNostr } from '../composables/useNostr'
|
||||
|
||||
const CREATOR_PUBKEY = 'da5e0c1b646bdb13c2300f805b0ca3e5afe5b052c594ce78bac8978d21c3fa39'
|
||||
|
||||
interface Stats {
|
||||
uptime: number
|
||||
rssBytes: number
|
||||
@@ -51,7 +49,7 @@ interface Fight {
|
||||
const { pubkey } = useNostr()
|
||||
const router = useRouter()
|
||||
|
||||
const isAuthorized = computed(() => pubkey.value === CREATOR_PUBKEY)
|
||||
const isAuthorized = ref(false)
|
||||
const stats = ref<Stats | null>(null)
|
||||
const bots = ref<Bot[]>([])
|
||||
const fights = ref<Fight[]>([])
|
||||
@@ -119,11 +117,15 @@ function botName(id: string): string {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!isAuthorized.value) {
|
||||
router.replace('/')
|
||||
return
|
||||
}
|
||||
await Promise.all([fetchStats(), fetchBots(), fetchFights()])
|
||||
if (!pubkey.value) { router.replace('/'); return }
|
||||
// Check authorization via API — no hardcoded pubkeys in frontend
|
||||
try {
|
||||
const res = await fetch('/api/admin/stats', { headers: headers() })
|
||||
if (!res.ok) { router.replace('/'); return }
|
||||
stats.value = await res.json()
|
||||
isAuthorized.value = true
|
||||
} catch { router.replace('/'); return }
|
||||
await Promise.all([fetchBots(), fetchFights()])
|
||||
isLoading.value = false
|
||||
refreshTimer = setInterval(fetchStats, 15_000)
|
||||
})
|
||||
|
||||
@@ -156,7 +156,13 @@ async function initLiveScene() {
|
||||
liveScene.startMusic()
|
||||
}
|
||||
announceDeepIntro()
|
||||
await liveScene.playEntrance()
|
||||
try {
|
||||
await liveScene.playEntrance()
|
||||
} catch (err) {
|
||||
console.warn('[FightPage] entrance failed:', err)
|
||||
}
|
||||
// Safety: ensure fighters are visible after entrance (prevents invisible characters on mobile)
|
||||
liveScene._resetPositions()
|
||||
}
|
||||
|
||||
liveLogItems.value.push(
|
||||
|
||||
Reference in New Issue
Block a user