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).
123 lines
4.1 KiB
TypeScript
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);
|
|
}
|