import express, { type Express } from 'express'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import type { StatePayload, SyncEventPayload } from '../shared/types.js'; import type { Db } from './db.js'; import { rootDir } from './config.js'; import { addClient } from './sse.js'; import { buildFailureReport } from './failures.js'; export interface RouteDeps { db: Db; triggerSync: () => Promise; } export function buildSyncPayload(db: Db, changedPrIds: string[] = [], newEventCount = 0): SyncEventPayload { return { lastSyncAt: db.getMeta('last_sync_at'), error: db.getMeta('last_sync_error'), changedPrIds, newEventCount, unreadCount: db.unreadCounts().total, }; } export function createApp({ db, triggerSync }: RouteDeps): Express { const app = express(); app.use(express.json()); app.get('/api/state', (_req, res) => { const { byPr, total } = db.unreadCounts(); const payload: StatePayload = { prs: db.getOpenSnapshots(), lastSyncAt: db.getMeta('last_sync_at'), lastSyncError: db.getMeta('last_sync_error'), unreadCountsByPr: byPr, unreadTotal: total, failuresByPr: db.failureSummaries(), }; res.json(payload); }); app.get('/api/prs/:id/failures', (req, res) => { res.json(db.failureReportRows(req.params.id).map(buildFailureReport)); }); app.get('/api/timeline', (req, res) => { const limit = Math.min(Number(req.query.limit) || 50, 200); const before = typeof req.query.before === 'string' ? req.query.before : undefined; const unreadOnly = req.query.unread === '1'; const prId = typeof req.query.pr === 'string' ? req.query.pr : undefined; res.json(db.timeline({ limit, before, unreadOnly, prId })); }); app.post('/api/timeline/:id/read', (req, res) => { db.markRead(req.params.id, true, new Date().toISOString()); res.json({ ok: true }); }); app.post('/api/timeline/:id/unread', (req, res) => { db.markRead(req.params.id, false, new Date().toISOString()); res.json({ ok: true }); }); app.post('/api/timeline/read-all', (req, res) => { const before = typeof req.body?.before === 'string' ? req.body.before : undefined; const changed = db.markAllRead(new Date().toISOString(), before); res.json({ ok: true, changed }); }); app.post('/api/prs/:id/read-all', (req, res) => { const changed = db.markAllReadForPr(req.params.id, new Date().toISOString()); res.json({ ok: true, changed }); }); app.post('/api/sync', (_req, res) => { void triggerSync(); res.json({ ok: true }); }); app.get('/api/events', (_req, res) => { addClient(res, buildSyncPayload(db)); }); const webDist = join(rootDir, 'web', 'dist'); if (existsSync(webDist)) { app.use(express.static(webDist)); app.get(/^\/(?!api\/).*/, (_req, res) => { res.sendFile(join(webDist, 'index.html')); }); } return app; }