TS146 linhas4.991 bytes
apps/worker/src/routes/ranking.ts
SHA-256
bd4011a69d4ed9b7dac662b4b645a398cda6f2020d3583d137640a57753e0abe
Somente leiturafonte-acf635076da2
import { Hono } from 'hono'
import type { Env, IdsScore } from '../types'
import { getLatestRanking, computeRanking, persistRanking } from '../services/ranking'
import { getAfastadosFull, MESES_MIN_PREMIO } from '../services/legis'
const ranking = new Hono<{ Bindings: Env }>()
type FilterPreset =
| 'geral'
| 'premio'
| 'ativos'
| 'titulares'
| 'suplentes'
| 'recentes'
| 'encerrados'
| 'afastados'
function applyFilterPreset(scores: IdsScore[], filter: FilterPreset): IdsScore[] {
switch (filter) {
// Coorte que concorre ao prêmio da Legislatura 57
case 'premio':
return scores.filter((s) => s.elegivelPremio !== false)
case 'ativos':
return scores.filter((s) => s.emExercicio !== false && s.status !== 'afastado')
case 'encerrados':
return scores.filter((s) => s.emExercicio === false)
case 'titulares':
return scores.filter(
(s) => s.status === 'titular_pleno' || s.status === 'titular_voltou',
)
case 'suplentes':
return scores.filter(
(s) => s.status === 'suplente_efetivo' || s.status === 'suplente_recente',
)
case 'recentes':
return scores.filter((s) => s.status === 'recente' || (s.mesesAtivos ?? 0) < 6)
case 'geral':
default:
return scores
}
}
function ordenarScores(scores: IdsScore[], ordenar: string): IdsScore[] {
const arr = [...scores]
switch (ordenar) {
case 'nome':
return arr.sort((a, b) => a.nome.localeCompare(b.nome, 'pt-BR'))
case 'partido':
return arr.sort((a, b) => a.partido.localeCompare(b.partido) || b.idsTotal - a.idsTotal)
case 'uf':
return arr.sort((a, b) => a.uf.localeCompare(b.uf) || b.idsTotal - a.idsTotal)
case 'meses':
return arr.sort((a, b) => (b.mesesAtivos ?? 0) - (a.mesesAtivos ?? 0))
case 'ids_bruto':
return arr.sort((a, b) => (b.idsTotalBruto ?? 0) - (a.idsTotalBruto ?? 0))
case 'ids':
default:
return arr.sort((a, b) => b.idsTotal - a.idsTotal)
}
}
ranking.get('/', async (c) => {
const filter = (c.req.query('filter') ?? 'geral') as FilterPreset
const status = c.req.query('status')
const partido = c.req.query('partido')
const uf = c.req.query('uf')
const bloco = c.req.query('bloco')
const mesesMin = Number(c.req.query('meses_min') ?? '0')
const ordenar = c.req.query('ordenar') ?? 'ids'
// Aba "Afastados" tem fonte distinta (sem score)
if (filter === 'afastados') {
const afastados = await getAfastadosFull(c.env)
return c.json({
total: afastados.length,
filter,
data: afastados,
}, 200, {
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=600',
})
}
const scores = await getLatestRanking(c.env)
let result = applyFilterPreset(scores, filter)
if (status) result = result.filter((s) => s.status === status)
if (partido) result = result.filter((s) => s.partido === partido.toUpperCase())
if (uf) result = result.filter((s) => s.uf === uf.toUpperCase())
if (bloco) result = result.filter((s) => s.bloco.toLowerCase().includes(bloco.toLowerCase()))
if (mesesMin > 0) result = result.filter((s) => (s.mesesAtivos ?? 0) >= mesesMin)
result = ordenarScores(result, ordenar)
return c.json({
total: result.length,
filter,
ordenar,
computedAt: scores[0]?.senadorCod ? new Date().toISOString() : null,
empty: scores.length === 0,
data: result,
}, 200, {
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=600',
})
})
ranking.get('/meta', async (c) => {
const scores = await getLatestRanking(c.env)
const partidos = [...new Set(scores.map((s) => s.partido).filter(Boolean))].sort()
const ufs = [...new Set(scores.map((s) => s.uf).filter(Boolean))].sort()
const blocos = [...new Set(scores.map((s) => s.bloco).filter(Boolean))].sort()
const statuses = [...new Set(scores.map((s) => s.status).filter(Boolean))].sort() as string[]
return c.json({
partidos, ufs, blocos, statuses,
filtros: [
'geral', 'premio', 'ativos', 'titulares', 'suplentes',
'recentes', 'encerrados', 'afastados',
],
ordenacao: ['ids', 'ids_bruto', 'nome', 'partido', 'uf', 'meses'],
premio: {
legislatura: 57,
mesesMinimos: MESES_MIN_PREMIO,
elegiveis: scores.filter((s) => s.elegivelPremio !== false).length,
total: scores.length,
},
}, 200, {
'Cache-Control': 'public, max-age=3600',
})
})
// Endpoint admin para forçar recálculo (chamado pelo cron ou manualmente)
ranking.post('/recalcular', async (c) => {
const secret = c.req.header('X-Admin-Secret')
if (secret !== c.env.ADMIN_SECRET) {
return c.json({ error: 'unauthorized' }, 401)
}
try {
const scores = await computeRanking(c.env)
await persistRanking(c.env, scores) // síncrono — garante persistência antes de retornar
return c.json({ ok: true, total: scores.length })
} catch (err) {
return c.json({ error: String(err) }, 500)
}
})
export { ranking }