fix: early name validation, fighter glow, reduced visual noise

- Add /api/auth/check-name endpoint for early name availability check
- JoinBoutPage checks name availability at naming step (not after webhook)
- FightCardPage: replace poster containers with circular glow behind sprites
- Reduce synthwave grid and CRT overlay opacity for cleaner backgrounds
- BotProfilePage: webhook management, owner-only fields
- useNostr: normalize bot data, guard auto-restore, clearAllState helper
- bots.ts: expose owner-only webhook info on stats endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 11:03:16 +00:00
co-authored by Claude Opus 4.6
parent 2d8cdcc60a
commit 9a11c48487
7 changed files with 405 additions and 120 deletions
+154 -52
View File
@@ -9,7 +9,7 @@ import type { SpriteCustomization } from '../game/sprites'
const route = useRoute()
const router = useRouter()
const { bot: nostrBot, isLoggedIn, logout, updateCustomization } = useNostr()
const { bot: nostrBot, pubkey, isLoggedIn, logout, updateCustomization, updateWebhook } = useNostr()
const botName = route.params.name as string
interface BotCustomization {
@@ -34,6 +34,7 @@ interface BotStats {
winStreak: number
bestStreak: number
tier: number
isActive: boolean
tierName: string
tierColor: string
winRate: number
@@ -44,6 +45,10 @@ interface BotStats {
satsWagered: number
hasWallet: boolean
createdAt: string
// Owner-only webhook fields
webhookUrl?: string
consecutiveErrors?: number
isHuman?: boolean
recentFights: {
id: string
opponent: string
@@ -86,6 +91,15 @@ const showCustomize = ref(false)
const isSaving = ref(false)
const custError = ref('')
// Webhook management
const showWebhook = ref(false)
const webhookInput = ref('')
const isTestingWebhook = ref(false)
const isUpdatingWebhook = ref(false)
const webhookTestResult = ref<{ reachable: boolean; validResponse: boolean; latencyMs: number; error?: string; message?: string } | null>(null)
const webhookError = ref('')
const webhookSuccess = ref('')
const ARCHETYPES = [
'standard', 'lobster', 'sheep', 'cyborg', 'blob', 'tank', 'dog', 'cat',
'cactus', 'pizza', 'mushroom', 'shark', 'penguin', 'octopus', 'skeleton',
@@ -210,9 +224,49 @@ async function saveCustomization() {
isSaving.value = false
}
async function testWebhook() {
if (!stats.value || isTestingWebhook.value) return
isTestingWebhook.value = true
webhookTestResult.value = null
webhookError.value = ''
try {
const res = await fetch(`/api/bots/${encodeURIComponent(botName)}/test`, { method: 'POST' })
webhookTestResult.value = await res.json()
if (webhookTestResult.value?.validResponse && stats.value) {
stats.value = { ...stats.value, consecutiveErrors: 0, isActive: true }
}
} catch {
webhookError.value = 'Network error testing webhook.'
}
isTestingWebhook.value = false
}
async function saveWebhook() {
const url = webhookInput.value.trim()
if (!url || isUpdatingWebhook.value) return
try { new URL(url) } catch { webhookError.value = 'Invalid URL.'; return }
isUpdatingWebhook.value = true
webhookError.value = ''
webhookSuccess.value = ''
try {
await updateWebhook(url)
webhookSuccess.value = 'Webhook updated and verified.'
if (stats.value) {
stats.value = { ...stats.value, webhookUrl: url, consecutiveErrors: 0 }
}
webhookInput.value = ''
} catch (err) {
webhookError.value = err instanceof Error ? err.message : 'Update failed.'
}
isUpdatingWebhook.value = false
}
onMounted(async () => {
try {
const res = await fetch(`/api/bots/${encodeURIComponent(botName)}/stats`)
const pk = pubkey.value || ''
const qs = pk ? `?pubkey=${encodeURIComponent(pk)}` : ''
const res = await fetch(`/api/bots/${encodeURIComponent(botName)}/stats${qs}`)
if (res.ok) stats.value = await res.json()
else loadError.value = `Failed to load bot (${res.status})`
} catch (err) {
@@ -317,42 +371,23 @@ const tierClass = (t: number) => `tier-${t}`
class="drop-shadow-[0_0_20px_rgba(0,255,255,0.3)]"
/>
</div>
<!-- Bot player: human controller + wire + bot sprite -->
<div v-else class="flex items-end justify-center gap-0 mb-4 relative">
<!-- Bot player: human controller + bot sprite -->
<div v-else class="flex items-end justify-center gap-2 mb-4">
<HumanPreview
:seed="stats.avatarSeed || stats.name"
:archetype="stats.archetype || 'standard'"
:size="150"
:win-rate="(stats.winRate || 0) / 100"
anim="idle"
class="drop-shadow-[0_0_12px_rgba(0,0,0,0.6)] shrink-0 relative z-10"
class="drop-shadow-[0_0_12px_rgba(0,0,0,0.6)] shrink-0"
/>
<!-- Wire from gamepad to bot with electricity -->
<svg class="absolute bottom-8 left-0 w-full h-24 z-0 pointer-events-none" preserveAspectRatio="none">
<defs>
<linearGradient id="wire-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#444455"/>
<stop offset="50%" stop-color="#555566"/>
<stop offset="100%" stop-color="#444455"/>
</linearGradient>
<filter id="elec-glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<path d="M 30% 85% Q 45% 40%, 55% 55% T 78% 30%" fill="none" stroke="url(#wire-grad)" stroke-width="3" stroke-linecap="round"/>
<path class="elec-pulse-1" d="M 30% 85% Q 45% 40%, 55% 55% T 78% 30%" fill="none" stroke="#00f0ff" stroke-width="1.5" stroke-linecap="round" filter="url(#elec-glow)" stroke-dasharray="8 20" opacity="0.8"/>
<path class="elec-pulse-2" d="M 30% 85% Q 45% 40%, 55% 55% T 78% 30%" fill="none" stroke="#ff2d7b" stroke-width="1" stroke-linecap="round" filter="url(#elec-glow)" stroke-dasharray="5 25" opacity="0.6"/>
<circle class="elec-spark-1" cx="45%" cy="55%" r="2" fill="#00f0ff" filter="url(#elec-glow)" opacity="0"/>
<circle class="elec-spark-2" cx="62%" cy="45%" r="2" fill="#ff2d7b" filter="url(#elec-glow)" opacity="0"/>
</svg>
<SpritePreview
:seed="stats.avatarSeed || stats.name"
:archetype="stats.archetype"
:tier="stats.tier"
:size="140"
:customization="stats.customization || undefined"
class="drop-shadow-[0_0_20px_var(--glow)] relative z-10"
class="drop-shadow-[0_0_20px_var(--glow)]"
:style="{ '--glow': stats.tierColor + '80' } as any"
/>
</div>
@@ -561,6 +596,100 @@ const tierClass = (t: number) => `tier-${t}`
</button>
</div>
<!-- Webhook management (owner only, bots only) -->
<div v-if="isOwner && stats.archetype !== 'human' && !stats.isHuman" class="mt-4">
<!-- Deactivated warning -->
<div v-if="stats.isActive === false" class="p-3 border-2 border-ko/40 bg-ko/5 mb-3">
<p class="font-display font-bold text-xs tracking-wider text-ko mb-1">BOT DEACTIVATED</p>
<p class="font-mono text-[10px] text-text-muted">
Too many webhook errors. Test your webhook to reactivate.
</p>
</div>
<button
class="w-full py-2 border border-border text-text-secondary font-display font-bold text-[10px]
tracking-wider hover:border-neon-cyan/40 hover:text-neon-cyan transition-all text-center"
@click="showWebhook = !showWebhook"
>
{{ showWebhook ? 'HIDE' : 'WEBHOOK' }} SETTINGS
</button>
<div v-if="showWebhook" class="mt-3 border border-border bg-surface-raised/60 p-4 space-y-3">
<!-- Current webhook info -->
<div>
<p class="font-display text-[9px] text-text-muted tracking-wider mb-1">CURRENT WEBHOOK</p>
<div class="flex items-center gap-2">
<span
class="w-2 h-2 rounded-full flex-shrink-0"
:class="(stats.consecutiveErrors ?? 0) === 0 ? 'bg-neon-green' : 'bg-ko'"
/>
<p class="font-mono text-[10px] text-text-secondary break-all flex-1">
{{ stats.webhookUrl || '—' }}
</p>
</div>
<p v-if="(stats.consecutiveErrors ?? 0) > 0" class="font-mono text-[10px] text-ko mt-1">
{{ stats.consecutiveErrors }} consecutive errors
</p>
</div>
<!-- Test button -->
<button
class="w-full py-2 bg-neon-cyan/10 border border-neon-cyan/40 text-neon-cyan
font-display font-bold text-[10px] tracking-wider
hover:bg-neon-cyan/20 transition-all
disabled:opacity-30 disabled:cursor-not-allowed
flex items-center justify-center gap-2"
:disabled="isTestingWebhook"
@click="testWebhook"
>
<span v-if="isTestingWebhook" class="w-3 h-3 border-2 border-neon-cyan/30 border-t-neon-cyan rounded-full animate-spin" />
{{ isTestingWebhook ? 'TESTING...' : 'TEST WEBHOOK' }}
</button>
<!-- Test result -->
<div v-if="webhookTestResult" class="p-2.5 border text-[10px] font-mono"
:class="webhookTestResult.validResponse
? 'border-neon-green/30 bg-neon-green/5 text-neon-green'
: 'border-ko/30 bg-ko/5 text-ko'">
<p class="font-display font-bold text-xs tracking-wider mb-1">
{{ webhookTestResult.validResponse ? 'PASSED' : 'FAILED' }}
</p>
<p>{{ webhookTestResult.message || webhookTestResult.error }}</p>
<p v-if="webhookTestResult.latencyMs" class="text-text-muted mt-0.5">
Latency: {{ webhookTestResult.latencyMs }}ms
</p>
</div>
<!-- Update webhook URL -->
<div class="border-t border-border/50 pt-3">
<p class="font-display text-[9px] text-text-muted tracking-wider mb-1">UPDATE WEBHOOK URL</p>
<div class="flex gap-2">
<input
v-model="webhookInput"
type="url"
placeholder="https://your-bot.example.com"
class="flex-1 px-2 py-1.5 bg-black/30 border border-border font-mono text-[10px] text-text-primary
placeholder-text-muted/50 focus:outline-none focus:border-neon-cyan/50"
@keyup.enter="saveWebhook"
/>
<button
class="px-3 py-1.5 bg-neon-cyan/10 border border-neon-cyan/40 text-neon-cyan
font-display font-bold text-[10px] tracking-wider
hover:bg-neon-cyan/20 transition-all
disabled:opacity-30 disabled:cursor-not-allowed"
:disabled="!webhookInput.trim() || isUpdatingWebhook"
@click="saveWebhook"
>
{{ isUpdatingWebhook ? '...' : 'UPDATE' }}
</button>
</div>
</div>
<p v-if="webhookError" class="font-mono text-[10px] text-ko">{{ webhookError }}</p>
<p v-if="webhookSuccess" class="font-mono text-[10px] text-neon-green">{{ webhookSuccess }}</p>
</div>
</div>
<!-- Sign out (only if owner) -->
<div v-if="isOwner" class="mt-3 text-center flex-shrink-0">
<button
@@ -608,30 +737,3 @@ const tierClass = (t: number) => `tier-${t}`
</div>
</template>
<style scoped>
/* Electricity pulses along wire */
.elec-pulse-1 {
animation: elec-flow 1.2s linear infinite;
}
.elec-pulse-2 {
animation: elec-flow 0.9s linear infinite reverse;
}
@keyframes elec-flow {
0% { stroke-dashoffset: 0; opacity: 0.9; }
50% { opacity: 0.4; }
100% { stroke-dashoffset: -56; opacity: 0.9; }
}
/* Spark flashes at wire midpoints */
.elec-spark-1 {
animation: spark-flash 0.8s ease-in-out infinite;
}
.elec-spark-2 {
animation: spark-flash 1.1s ease-in-out infinite 0.4s;
}
@keyframes spark-flash {
0%, 70%, 100% { opacity: 0; r: 1; }
75% { opacity: 1; r: 4; }
85% { opacity: 0.6; r: 2; }
}
</style>