export const dynamic = 'force-dynamic' export const runtime = 'nodejs' import { NextResponse } from 'next/server' import { voiceBus } from '@/lib/voice-bus' import { addTimer, removeTimer, listActive, findByLabel, adjustTimer, Timer } from '@/lib/timers' // Допускаем либо bearer (Python-скрипт), либо auth_token cookie (браузер планшета). // Cookie ↔ middleware нас bypass'ит для этого пути, проверяем вручную присутствие. function authorized(req: Request): boolean { const expected = process.env.VOICE_API_KEY const auth = req.headers.get('authorization') || '' const bearer = auth.replace(/^Bearer\s+/i, '').trim() if (expected && bearer === expected) return true const cookie = req.headers.get('cookie') || '' if (/auth_token=[a-f0-9]{32,}/i.test(cookie)) return true return false } function emit(event: string, payload: Record) { voiceBus.emit('voice', { event, ...payload, timestamp: new Date().toISOString(), }) } export async function GET() { return NextResponse.json({ timers: listActive() }) } export async function POST(req: Request) { if (!authorized(req)) { return NextResponse.json({ error: 'unauthorized' }, { status: 401 }) } const body = await req.json().catch(() => null) if (!body || typeof body.action !== 'string') { return NextResponse.json({ error: 'action required' }, { status: 400 }) } // ─────────── start ─────────── if (body.action === 'start') { const seconds = Number(body.seconds) const label = typeof body.label === 'string' ? body.label.slice(0, 80) : 'Таймер' const agent = body.agent === 'lusya' ? 'lusya' : 'cosmo' if (!Number.isFinite(seconds) || seconds < 1 || seconds > 24 * 3600) { return NextResponse.json({ error: 'seconds must be 1..86400' }, { status: 400 }) } const endsAt = new Date(Date.now() + seconds * 1000).toISOString() const t = addTimer({ label, endsAt, agent }) emit('timer_start', { timer: t }) return NextResponse.json({ timer: t }) } // ─────────── cancel ─────────── if (body.action === 'cancel') { let target: Timer | null = null if (typeof body.id === 'string' && body.id) { target = removeTimer(body.id) } else if (typeof body.label === 'string' && body.label) { const found = findByLabel(body.label) if (found) target = removeTimer(found.id) } else { return NextResponse.json({ error: 'id or label required' }, { status: 400 }) } if (!target) { return NextResponse.json({ cancelled: false, error: 'timer_not_found' }, { status: 404 }) } emit('timer_cancel', { id: target.id, label: target.label }) return NextResponse.json({ cancelled: true, timer: target }) } // ─────────── adjust ─────────── if (body.action === 'adjust') { const delta = Number(body.delta_seconds) if (!Number.isFinite(delta) || delta === 0) { return NextResponse.json({ error: 'delta_seconds must be non-zero number' }, { status: 400 }) } let targetId: string | null = null if (typeof body.id === 'string' && body.id) { targetId = body.id } else if (typeof body.label === 'string' && body.label) { const found = findByLabel(body.label) if (found) targetId = found.id } else { return NextResponse.json({ error: 'id or label required' }, { status: 400 }) } if (!targetId) { return NextResponse.json({ adjusted: false, error: 'timer_not_found' }, { status: 404 }) } const updated = adjustTimer(targetId, delta) if (!updated) { return NextResponse.json({ adjusted: false, error: 'timer_not_found' }, { status: 404 }) } emit('timer_start', { timer: updated }) // переиспользуем event — виджеты перечитают return NextResponse.json({ adjusted: true, timer: updated }) } return NextResponse.json({ error: `unknown action: ${body.action}` }, { status: 400 }) }