Files
boocode/apps/control/src/routes/reports.ts
indifferentketchup b18de2a331 chore: snapshot working tree - pty_exited notifications + in-flight inference WIP
feat(booterm): structured pty_exited WS notifications. Plan-validated, impl-validated, code-reviewed green (contracts build clean, contracts test 29/29, booterm + web typecheck clean).

wip: in-progress inference/provider refactor (agents.ts, provider.ts, new llama-providers.ts, removed llama-args-validator), plus arena, dispatcher, compaction, schema changes.

openspec: pty-exit-notifications complete; x-agent-flags planned (not yet implemented).
2026-06-14 12:48:47 +00:00

123 lines
4.1 KiB
TypeScript

import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyBaseLogger } from 'fastify';
import type { Sql } from '../db.js';
import { generateReport, runReportSchedulerTick } from '../services/reports.js';
import { jsonbObject } from '../services/jsonb.js';
/**
* P6.2: Reports tab API + scheduled digest.
*
* GET /api/reports — list generated reports (newest first)
* GET /api/reports/:id — single report (markdown + stats)
* POST /api/reports/generate — manually trigger a digest now
* GET /api/reports/schedule — current schedule meta
* POST /api/reports/schedule — update schedule meta {interval, enabled}
*/
export function registerReportRoutes(app: FastifyInstance, sql: Sql): void {
app.get('/api/reports', async (_req: FastifyRequest, reply: FastifyReply) => {
const rows = await sql<{
id: string;
kind: string;
interval: string;
period_start: string;
period_end: string;
created_at: string;
}[]>`
SELECT id, kind, interval, period_start, period_end, created_at
FROM control_reports
ORDER BY created_at DESC
LIMIT 100
`;
return reply.send({
reports: rows.map((r) => ({
id: r.id,
kind: r.kind,
interval: r.interval,
periodStart: r.period_start,
periodEnd: r.period_end,
createdAt: r.created_at,
})),
});
});
app.get('/api/reports/:id', async (req: FastifyRequest, reply: FastifyReply) => {
const { id } = req.params as { id: string };
const rows = await sql<{
id: string;
kind: string;
interval: string;
period_start: string;
period_end: string;
markdown: string;
stats: unknown;
created_at: string;
}[]>`
SELECT id, kind, interval, period_start, period_end, markdown, stats, created_at
FROM control_reports WHERE id = ${id}
`;
if (rows.length === 0) {
return reply.status(404).send({ error: 'report not found' });
}
const r = rows[0]!;
return reply.send({
id: r.id,
kind: r.kind,
interval: r.interval,
periodStart: r.period_start,
periodEnd: r.period_end,
markdown: r.markdown,
stats: jsonbObject(r.stats),
createdAt: r.created_at,
});
});
app.post('/api/reports/generate', async (req: FastifyRequest, reply: FastifyReply) => {
const body = (req.body as Record<string, unknown>) ?? {};
const interval = body.interval === 'weekly' ? 'weekly' : 'daily';
const id = await generateReport(sql, interval);
return reply.status(201).send({ id });
});
app.get('/api/reports/schedule', async (_req: FastifyRequest, reply: FastifyReply) => {
const rows = await sql<{ interval: string; enabled: boolean; last_run_at: string | null }[]>`
SELECT interval, enabled, last_run_at FROM control_schedule_meta WHERE name = 'report-digest'
`;
const m = rows[0];
return reply.send({
interval: m?.interval ?? 'daily',
enabled: m?.enabled ?? true,
lastRunAt: m?.last_run_at ?? null,
});
});
app.post('/api/reports/schedule', async (req: FastifyRequest, reply: FastifyReply) => {
const body = (req.body as Record<string, unknown>) ?? {};
const interval = body.interval === 'weekly' ? 'weekly' : 'daily';
const enabled = body.enabled !== false;
await sql`
UPDATE control_schedule_meta
SET interval = ${interval}, enabled = ${enabled}
WHERE name = 'report-digest'
`;
return reply.send({ interval, enabled });
});
}
/**
* Start the in-process report scheduler: an immediate catch-up tick on boot,
* then hourly. Returns a stop function for onClose.
*/
export function startReportScheduler(sql: Sql, log: FastifyBaseLogger): () => void {
const tick = async () => {
try {
const result = await runReportSchedulerTick(sql);
if (result.ran) log.info({ reportId: result.reportId }, 'reports: digest generated');
} catch (err) {
log.warn({ err: (err as Error).message }, 'reports: scheduler tick failed');
}
};
// Catch-up on boot.
void tick();
const timer = setInterval(tick, 3600_000); // hourly
return () => clearInterval(timer);
}