fix: align backend API routes with console frontend contract
Some checks failed
CI — P3 Alert / test (push) Successful in 34s
CI — P4 Portal / test (push) Successful in 37s
CI — P5 Cost / test (push) Successful in 35s
CI — P6 Run / saas (push) Successful in 33s
CI — P5 Cost / build-push (push) Failing after 5s
CI — P6 Run / build-push (push) Failing after 4s
CI — P2 Drift (Go + Node) / agent (push) Successful in 1m5s
CI — P2 Drift (Go + Node) / saas (push) Successful in 37s
CI — P2 Drift (Go + Node) / build-push (push) Failing after 16s
CI — P3 Alert / build-push (push) Failing after 14s
CI — P4 Portal / build-push (push) Failing after 27s

This commit is contained in:
Max
2026-03-03 06:09:41 +00:00
parent 76715d169e
commit 47a64d53fd
8 changed files with 394 additions and 20 deletions

View File

@@ -10,11 +10,15 @@ const listQuerySchema = z.object({
service: z.string().optional(),
});
const statusUpdateSchema = z.object({
status: z.enum(['acknowledged', 'resolved', 'suppressed']),
});
export function registerIncidentRoutes(app: FastifyInstance) {
// List incidents
app.get('/api/v1/incidents', async (req, reply) => {
const query = listQuerySchema.parse(req.query);
const tenantId = (req as any).tenantId; // Set by auth middleware
const tenantId = (req as any).tenantId;
const offset = (query.page - 1) * query.limit;
const result = await withTenant(tenantId, async (client) => {
@@ -50,7 +54,24 @@ export function registerIncidentRoutes(app: FastifyInstance) {
return result;
});
// Acknowledge incident
// Unified status update (console route: POST /api/v1/incidents/:id/status)
app.post('/api/v1/incidents/:id/status', async (req, reply) => {
const { id } = req.params as { id: string };
const { status } = statusUpdateSchema.parse(req.body);
const tenantId = (req as any).tenantId;
await withTenant(tenantId, async (client) => {
if (status === 'resolved') {
await client.query("UPDATE incidents SET status = 'resolved', resolved_at = now() WHERE id = $1", [id]);
} else {
await client.query('UPDATE incidents SET status = $1 WHERE id = $2', [status, id]);
}
});
return { status };
});
// Acknowledge incident (legacy route)
app.post('/api/v1/incidents/:id/acknowledge', async (req, reply) => {
const { id } = req.params as { id: string };
const tenantId = (req as any).tenantId;
@@ -62,7 +83,7 @@ export function registerIncidentRoutes(app: FastifyInstance) {
return { status: 'acknowledged' };
});
// Resolve incident
// Resolve incident (legacy route)
app.post('/api/v1/incidents/:id/resolve', async (req, reply) => {
const { id } = req.params as { id: string };
const tenantId = (req as any).tenantId;
@@ -74,7 +95,7 @@ export function registerIncidentRoutes(app: FastifyInstance) {
return { status: 'resolved' };
});
// Suppress incident (snooze)
// Suppress incident (legacy route)
app.post('/api/v1/incidents/:id/suppress', async (req, reply) => {
const { id } = req.params as { id: string };
const tenantId = (req as any).tenantId;
@@ -86,7 +107,25 @@ export function registerIncidentRoutes(app: FastifyInstance) {
return { status: 'suppressed' };
});
// Dashboard summary
// Dashboard summary (console route: GET /api/v1/incidents/summary)
app.get('/api/v1/incidents/summary', async (req, reply) => {
const tenantId = (req as any).tenantId;
const result = await withTenant(tenantId, async (client) => {
const counts = await client.query(`
SELECT status, severity, COUNT(*)::int as count
FROM incidents
WHERE created_at > now() - interval '24 hours'
GROUP BY status, severity
`);
const total = await client.query(`SELECT COUNT(*)::int as total FROM incidents WHERE status = 'open'`);
return { breakdown: counts.rows, open_total: total.rows[0]?.total ?? 0 };
});
return result;
});
// Dashboard summary (legacy route)
app.get('/api/v1/summary', async (req, reply) => {
const tenantId = (req as any).tenantId;

View File

@@ -6,7 +6,7 @@ import { withTenant } from '../data/db.js';
const logger = pino({ name: 'api-notifications' });
const notifConfigSchema = z.object({
channel: z.enum(['slack', 'email', 'webhook']),
channel: z.enum(['slack', 'email', 'webhook']).optional(),
config: z.object({
slack_webhook_url: z.string().url().optional(),
email_to: z.string().email().optional(),
@@ -17,7 +17,39 @@ const notifConfigSchema = z.object({
});
export function registerNotificationRoutes(app: FastifyInstance) {
// List notification configs
// Get all notification configs (console route: GET /api/v1/notifications/config)
app.get('/api/v1/notifications/config', async (req, reply) => {
const tenantId = (req as any).tenantId;
const result = await withTenant(tenantId, async (client) => {
return client.query('SELECT * FROM notification_configs ORDER BY channel');
});
return { configs: result.rows };
});
// Update all notification configs (console route: PUT /api/v1/notifications/config)
app.put('/api/v1/notifications/config', async (req, reply) => {
const tenantId = (req as any).tenantId;
const body = req.body as { configs?: Array<{ channel: string; config: any; min_severity?: string; enabled?: boolean }> };
if (body.configs && Array.isArray(body.configs)) {
await withTenant(tenantId, async (client) => {
for (const cfg of body.configs!) {
const parsed = notifConfigSchema.parse(cfg);
await client.query(
`INSERT INTO notification_configs (tenant_id, channel, config, min_severity, enabled)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (tenant_id, channel) DO UPDATE SET config = $3, min_severity = $4, enabled = $5`,
[tenantId, cfg.channel, JSON.stringify(parsed.config), parsed.min_severity, parsed.enabled],
);
}
});
}
logger.info({ tenantId }, 'Notification configs updated via bulk');
return { status: 'updated' };
});
// List notification configs (legacy route)
app.get('/api/v1/notifications', async (req, reply) => {
const tenantId = (req as any).tenantId;
const result = await withTenant(tenantId, async (client) => {
@@ -26,7 +58,7 @@ export function registerNotificationRoutes(app: FastifyInstance) {
return { configs: result.rows };
});
// Upsert notification config
// Upsert notification config (legacy route)
app.put('/api/v1/notifications/:channel', async (req, reply) => {
const { channel } = req.params as { channel: string };
const body = notifConfigSchema.parse({ ...req.body as any, channel });