feat: production hardening — background fights, queue tuning, CORS, cycling taglines

- Background fight loop: mock bots auto-fight every ~45s for site activity
  - Quiet hours (2-8 UTC) run 4x slower, jitter prevents robotic timing
  - Configurable via FIGHT_LOOP_ENABLED, FIGHT_LOOP_INTERVAL_MS
- Queue timeout: 30s in prod (was 3s), configurable via QUEUE_TIMEOUT_MS
- CORS: env-configurable via CORS_ORIGIN (default '*')
- Homepage: 40 cycling taglines with typewriter effect
- docker-compose: document all new env vars

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 11:04:32 +00:00
co-authored by Claude Opus 4.6
parent 6bf9fe27b3
commit 3e74712c44
6 changed files with 276 additions and 15 deletions
+3 -1
View File
@@ -21,7 +21,9 @@ app.onError((err, c) => {
})
app.use('*', logger())
app.use('/api/*', cors({ origin: '*' }))
// CORS: lock down in production, allow all in dev
const allowedOrigin = process.env.CORS_ORIGIN || '*'
app.use('/api/*', cors({ origin: allowedOrigin }))
// Rate limit all POST endpoints (60/min per IP)
app.use('/api/*', rateLimit(60_000, 60))
+101
View File
@@ -0,0 +1,101 @@
import { db, schema } from '../db/index.js'
import { runMockFight } from './mock.js'
// Background fight loop keeps mock bots fighting so the site always has fresh content.
// Configurable via env:
// FIGHT_LOOP_ENABLED=true (default: true in production, false in dev)
// FIGHT_LOOP_INTERVAL_MS=45000 (default: 45s between fights)
// FIGHT_LOOP_QUIET_HOURS=2-8 (default: 2am-8am UTC, slower fights)
const isProduction = process.env.NODE_ENV === 'production'
const enabled = process.env.FIGHT_LOOP_ENABLED
? process.env.FIGHT_LOOP_ENABLED === 'true'
: isProduction
const intervalMs = Number(process.env.FIGHT_LOOP_INTERVAL_MS) || 45_000
const quietStart = Number(process.env.FIGHT_LOOP_QUIET_START) || 2
const quietEnd = Number(process.env.FIGHT_LOOP_QUIET_END) || 8
let running = false
export function startBackgroundFights() {
if (!enabled) {
console.log('[background] fight loop disabled (set FIGHT_LOOP_ENABLED=true to enable)')
return
}
if (running) return
running = true
console.log(`[background] fight loop started — interval ${intervalMs}ms, quiet hours ${quietStart}-${quietEnd} UTC`)
loop().catch(err => {
console.error('[background] fight loop crashed:', err)
running = false
})
}
export function stopBackgroundFights() {
running = false
}
async function loop() {
// Small delay on startup to let everything settle
await sleep(5_000)
while (running) {
try {
await runOneBackgroundFight()
} catch (err) {
console.error('[background] fight error:', err)
}
// During quiet hours, fights run 4x slower
const hour = new Date().getUTCHours()
const isQuiet = quietStart < quietEnd
? hour >= quietStart && hour < quietEnd
: hour >= quietStart || hour < quietEnd
const delay = isQuiet ? intervalMs * 4 : intervalMs
// Add jitter (±25%) so fights don't feel robotic
const jitter = delay * (0.75 + Math.random() * 0.5)
await sleep(jitter)
}
console.log('[background] fight loop stopped')
}
async function runOneBackgroundFight() {
// Pick two mock bots with ELO-weighted matchmaking
const allBots = await db.select({
id: schema.bots.id,
webhookUrl: schema.bots.webhookUrl,
eloRating: schema.bots.eloRating,
name: schema.bots.name,
}).from(schema.bots)
const mockBots = allBots.filter(b => b.webhookUrl.startsWith('http://mock.local'))
if (mockBots.length < 2) return
// Mixed matchmaking: 70% close ELO, 30% wild card
const isWild = Math.random() < 0.3
const botA = mockBots[Math.floor(Math.random() * mockBots.length)]
const others = mockBots.filter(b => b.id !== botA.id)
let botB: typeof botA
if (isWild) {
botB = others[Math.floor(Math.random() * others.length)]
} else {
others.sort((a, b) => {
const diffA = Math.abs(a.eloRating - botA.eloRating) + Math.random() * 150
const diffB = Math.abs(b.eloRating - botA.eloRating) + Math.random() * 150
return diffA - diffB
})
botB = others[0]
}
const fightId = await runMockFight(botA.id, botB.id)
console.log(`[background] ${botA.name} vs ${botB.name} => fight ${fightId}`)
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
+2 -1
View File
@@ -17,7 +17,8 @@ interface QueueEntry {
const waitingQueue: QueueEntry[] = []
// How long a bot waits before getting matched against a mock bot
const QUEUE_TIMEOUT_MS = 3_000
// In production, give real bots time to match (30s). In dev, fall back fast (3s).
const QUEUE_TIMEOUT_MS = Number(process.env.QUEUE_TIMEOUT_MS) || (process.env.NODE_ENV === 'production' ? 30_000 : 3_000)
// Post-fight cooldown tracking
const fightCooldowns = new Map<string, number>()
+4
View File
@@ -2,6 +2,7 @@ import { serve } from '@hono/node-server'
import { app } from './app.js'
import { runMigrations } from './db/startup.js'
import { seedMockBots } from './engine/mock.js'
import { startBackgroundFights } from './engine/background.js'
// Run migrations and seed mock bots before starting the server
runMigrations()
@@ -11,4 +12,7 @@ const port = Number(process.env.PORT) || 9100
serve({ fetch: app.fetch, port }, () => {
console.log(`[botfights] server listening on http://localhost:${port}`)
// Start background fight loop so the site always has fresh activity
startBackgroundFights()
})