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).
This commit is contained in:
122
apps/control/src/routes/reports.ts
Normal file
122
apps/control/src/routes/reports.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user