TS134 linhas3.988 bytes
apps/worker/src/routes/premiacao.ts
SHA-256
b8d5f7dd5272e7d2f8adba61363f65aee88c73b8f8dbd7c57f809a3696681457
Somente leiturafonte-e58fe007b6ee
import { Hono } from 'hono'
import type { Env } from '../types'
import {
apurarPremiacao,
getSnapshotFixado,
setSnapshotFixado,
PremiacaoIndisponivel,
MIN_LINHAS_SNAPSHOT,
MESES_MIN_VITORIA,
REGIOES,
} from '../services/premiacao'
import { listSnapshots } from '../services/ranking'
const premiacao = new Hono<{ Bindings: Env }>()
function exigeAdmin(c: { req: { header: (k: string) => string | undefined }; env: Env }) {
const secret = c.req.header('X-Admin-Secret')
return Boolean(secret) && secret === c.env.ADMIN_SECRET
}
premiacao.get('/', async (c) => {
try {
const resultado = await apurarPremiacao(c.env)
// Resultado congelado não muda até alguém repinar: pode cachear por mais tempo.
const maxAge = resultado.congelado ? 86400 : 3600
return c.json(resultado, 200, {
'Cache-Control': `public, max-age=${maxAge}, stale-while-revalidate=600`,
})
} catch (err) {
if (err instanceof PremiacaoIndisponivel) {
// 503: a apuração recusa em vez de publicar prêmio sobre dado parcial.
return c.json(
{ error: 'premiacao indisponivel', motivo: err.motivo, detalhe: err.detalhe },
503,
)
}
console.error('[premiacao] erro:', err)
return c.json({ error: 'apuracao falhou', detalhe: String(err) }, 500)
}
})
/** Metadados estáveis: regiões, piso de sanidade e snapshot em uso. */
premiacao.get('/meta', async (c) => {
const fixado = await getSnapshotFixado(c.env)
return c.json(
{
edicao: 'leg57',
legislatura: 57,
congelado: fixado !== null,
snapshot: fixado,
mesesMinimosVitoria: MESES_MIN_VITORIA,
minLinhasSnapshot: MIN_LINHAS_SNAPSHOT,
regioes: Object.entries(REGIOES).map(([id, r]) => ({
id,
nome: r.nome,
ufs: r.ufs,
})),
},
200,
{ 'Cache-Control': 'public, max-age=3600' },
)
})
/** Admin: lista snapshots disponíveis para congelar. */
premiacao.get('/snapshots', async (c) => {
if (!exigeAdmin(c)) return c.json({ error: 'unauthorized' }, 401)
const snapshots = await listSnapshots(c.env)
const fixado = await getSnapshotFixado(c.env)
return c.json({
fixado,
minLinhasSnapshot: MIN_LINHAS_SNAPSHOT,
snapshots: snapshots.map((s) => ({
...s,
apto: s.total >= MIN_LINHAS_SNAPSHOT,
emUso: s.computedAt === fixado,
})),
})
})
/**
* Admin: fixa o snapshot da apuração.
*
* Sem `computed_at`, congela o snapshot mais recente que passe no piso de
* sanidade. Recusa snapshots inexistentes ou truncados — é exatamente o caso
* que o piso existe para barrar.
*/
premiacao.post('/congelar', async (c) => {
if (!exigeAdmin(c)) return c.json({ error: 'unauthorized' }, 401)
const snapshots = await listSnapshots(c.env)
if (snapshots.length === 0) {
return c.json({ error: 'nenhum snapshot disponível' }, 503)
}
const pedido = c.req.query('computed_at')
const alvo = pedido
? snapshots.find((s) => s.computedAt === pedido)
: snapshots.find((s) => s.total >= MIN_LINHAS_SNAPSHOT)
if (!alvo) {
return c.json(
{
error: pedido
? `snapshot ${pedido} não existe`
: `nenhum snapshot com ao menos ${MIN_LINHAS_SNAPSHOT} linhas`,
disponiveis: snapshots.slice(0, 10),
},
404,
)
}
if (alvo.total < MIN_LINHAS_SNAPSHOT) {
return c.json(
{
error: 'snapshot incompleto',
detalhe: `${alvo.computedAt} tem ${alvo.total} linhas, mínimo ${MIN_LINHAS_SNAPSHOT}`,
},
422,
)
}
await setSnapshotFixado(c.env, alvo.computedAt)
return c.json({ ok: true, congelado: true, snapshot: alvo.computedAt, total: alvo.total })
})
/** Admin: volta a apurar sobre o snapshot mais recente. */
premiacao.post('/descongelar', async (c) => {
if (!exigeAdmin(c)) return c.json({ error: 'unauthorized' }, 401)
await setSnapshotFixado(c.env, null)
return c.json({ ok: true, congelado: false })
})
export { premiacao }