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
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:
@@ -11,7 +11,31 @@ const listQuerySchema = z.object({
|
||||
});
|
||||
|
||||
export function registerAnomalyRoutes(app: FastifyInstance) {
|
||||
// List anomalies
|
||||
// List anomalies (console route: GET /api/v1/cost/anomalies)
|
||||
app.get('/api/v1/cost/anomalies', async (req, reply) => {
|
||||
const query = listQuerySchema.parse(req.query);
|
||||
const tenantId = (req as any).tenantId;
|
||||
const offset = (query.page - 1) * query.limit;
|
||||
|
||||
const result = await withTenant(tenantId, async (client) => {
|
||||
let sql = 'SELECT * FROM anomalies WHERE 1=1';
|
||||
const params: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (query.status) { sql += ` AND status = $${idx++}`; params.push(query.status); }
|
||||
if (query.account_id) { sql += ` AND account_id = $${idx++}`; params.push(query.account_id); }
|
||||
if (query.min_score) { sql += ` AND score >= $${idx++}`; params.push(query.min_score); }
|
||||
|
||||
sql += ` ORDER BY detected_at DESC LIMIT $${idx++} OFFSET $${idx++}`;
|
||||
params.push(query.limit, offset);
|
||||
|
||||
return client.query(sql, params);
|
||||
});
|
||||
|
||||
return { anomalies: result.rows, page: query.page, limit: query.limit };
|
||||
});
|
||||
|
||||
// List anomalies (legacy route)
|
||||
app.get('/api/v1/anomalies', async (req, reply) => {
|
||||
const query = listQuerySchema.parse(req.query);
|
||||
const tenantId = (req as any).tenantId;
|
||||
@@ -35,7 +59,18 @@ export function registerAnomalyRoutes(app: FastifyInstance) {
|
||||
return { anomalies: result.rows, page: query.page, limit: query.limit };
|
||||
});
|
||||
|
||||
// Acknowledge anomaly
|
||||
// Acknowledge anomaly (console route: POST /api/v1/cost/anomalies/:id/acknowledge)
|
||||
app.post('/api/v1/cost/anomalies/:id/acknowledge', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const tenantId = (req as any).tenantId;
|
||||
|
||||
await withTenant(tenantId, async (client) => {
|
||||
await client.query("UPDATE anomalies SET status = 'acknowledged' WHERE id = $1 AND status = 'open'", [id]);
|
||||
});
|
||||
return { status: 'acknowledged' };
|
||||
});
|
||||
|
||||
// Acknowledge anomaly (legacy route)
|
||||
app.post('/api/v1/anomalies/:id/acknowledge', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const tenantId = (req as any).tenantId;
|
||||
@@ -72,7 +107,33 @@ export function registerAnomalyRoutes(app: FastifyInstance) {
|
||||
return { status: 'expected' };
|
||||
});
|
||||
|
||||
// Dashboard summary
|
||||
// Cost summary (console route: GET /api/v1/cost/summary)
|
||||
app.get('/api/v1/cost/summary', async (req, reply) => {
|
||||
const tenantId = (req as any).tenantId;
|
||||
|
||||
const result = await withTenant(tenantId, async (client) => {
|
||||
const openCount = await client.query("SELECT COUNT(*)::int as count FROM anomalies WHERE status = 'open'");
|
||||
const topResources = await client.query(`
|
||||
SELECT resource_type, COUNT(*)::int as anomaly_count, AVG(score)::numeric(5,2) as avg_score
|
||||
FROM anomalies WHERE status = 'open'
|
||||
GROUP BY resource_type ORDER BY anomaly_count DESC LIMIT 10
|
||||
`);
|
||||
const recentTrend = await client.query(`
|
||||
SELECT date_trunc('hour', detected_at) as hour, COUNT(*)::int as count
|
||||
FROM anomalies WHERE detected_at > now() - interval '24 hours'
|
||||
GROUP BY hour ORDER BY hour
|
||||
`);
|
||||
return {
|
||||
open_anomalies: openCount.rows[0]?.count ?? 0,
|
||||
top_resources: topResources.rows,
|
||||
hourly_trend: recentTrend.rows,
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// Dashboard summary (legacy route)
|
||||
app.get('/api/v1/dashboard', async (req, reply) => {
|
||||
const tenantId = (req as any).tenantId;
|
||||
|
||||
|
||||
@@ -2,7 +2,58 @@ import type { FastifyInstance } from 'fastify';
|
||||
import { withTenant } from '../data/db.js';
|
||||
|
||||
export function registerBaselineRoutes(app: FastifyInstance) {
|
||||
// List baselines
|
||||
// List baselines (console route: GET /api/v1/cost/baselines)
|
||||
app.get('/api/v1/cost/baselines', async (req, reply) => {
|
||||
const tenantId = (req as any).tenantId;
|
||||
|
||||
const result = await withTenant(tenantId, async (client) => {
|
||||
return client.query(`
|
||||
SELECT id, account_id, resource_type, welford_count as sample_count,
|
||||
welford_mean::numeric(12,4) as mean,
|
||||
CASE WHEN welford_count > 1
|
||||
THEN sqrt(welford_m2 / welford_count)::numeric(12,4)
|
||||
ELSE 0 END as stddev,
|
||||
updated_at
|
||||
FROM baselines ORDER BY account_id, resource_type
|
||||
`);
|
||||
});
|
||||
|
||||
return { baselines: result.rows };
|
||||
});
|
||||
|
||||
// Update baseline by id (console route: PUT /api/v1/cost/baselines/:id)
|
||||
app.put('/api/v1/cost/baselines/:id', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const tenantId = (req as any).tenantId;
|
||||
const body = req.body as { welford_mean?: number; welford_m2?: number; welford_count?: number };
|
||||
|
||||
const result = await withTenant(tenantId, async (client) => {
|
||||
const existing = await client.query('SELECT * FROM baselines WHERE id = $1', [id]);
|
||||
if (!existing.rows[0]) return null;
|
||||
|
||||
const updates: string[] = [];
|
||||
const params: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.welford_mean !== undefined) { updates.push(`welford_mean = $${idx++}`); params.push(body.welford_mean); }
|
||||
if (body.welford_m2 !== undefined) { updates.push(`welford_m2 = $${idx++}`); params.push(body.welford_m2); }
|
||||
if (body.welford_count !== undefined) { updates.push(`welford_count = $${idx++}`); params.push(body.welford_count); }
|
||||
updates.push(`updated_at = now()`);
|
||||
updates.push(`version = version + 1`);
|
||||
|
||||
params.push(id);
|
||||
const { rows } = await client.query(
|
||||
`UPDATE baselines SET ${updates.join(', ')} WHERE id = $${idx++} RETURNING *`,
|
||||
params,
|
||||
);
|
||||
return rows[0];
|
||||
});
|
||||
|
||||
if (!result) return reply.status(404).send({ error: 'Baseline not found' });
|
||||
return { baseline: result };
|
||||
});
|
||||
|
||||
// List baselines (legacy route)
|
||||
app.get('/api/v1/baselines', async (req, reply) => {
|
||||
const tenantId = (req as any).tenantId;
|
||||
|
||||
@@ -21,7 +72,7 @@ export function registerBaselineRoutes(app: FastifyInstance) {
|
||||
return { baselines: result.rows };
|
||||
});
|
||||
|
||||
// Reset baseline for a specific resource
|
||||
// Reset baseline for a specific resource (legacy route)
|
||||
app.delete('/api/v1/baselines/:accountId/:resourceType', async (req, reply) => {
|
||||
const { accountId, resourceType } = req.params as { accountId: string; resourceType: string };
|
||||
const tenantId = (req as any).tenantId;
|
||||
|
||||
@@ -1,11 +1,71 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { withTenant } from '../data/db.js';
|
||||
import { GovernanceEngine } from '../governance/engine.js';
|
||||
|
||||
const engine = new GovernanceEngine();
|
||||
|
||||
const governanceRuleSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
rule_type: z.string(),
|
||||
threshold: z.number().optional(),
|
||||
action: z.string().default('alert'),
|
||||
enabled: z.boolean().default(true),
|
||||
config: z.record(z.unknown()).default({}),
|
||||
});
|
||||
|
||||
export function registerGovernanceRoutes(app: FastifyInstance) {
|
||||
// Get governance status
|
||||
// Get governance status (console route: GET /api/v1/cost/governance)
|
||||
app.get('/api/v1/cost/governance', async (req, reply) => {
|
||||
const tenantId = (req as any).tenantId;
|
||||
|
||||
const result = await withTenant(tenantId, async (client) => {
|
||||
return client.query('SELECT governance_mode, governance_started_at FROM tenants WHERE id = $1', [tenantId]);
|
||||
});
|
||||
|
||||
const tenant = result.rows[0];
|
||||
if (!tenant) return reply.status(404).send({ error: 'Tenant not found' });
|
||||
|
||||
const daysInMode = Math.floor((Date.now() - new Date(tenant.governance_started_at).getTime()) / (86400 * 1000));
|
||||
|
||||
return {
|
||||
mode: tenant.governance_mode,
|
||||
days_in_mode: daysInMode,
|
||||
started_at: tenant.governance_started_at,
|
||||
};
|
||||
});
|
||||
|
||||
// Create governance rule (console route: POST /api/v1/cost/governance)
|
||||
app.post('/api/v1/cost/governance', async (req, reply) => {
|
||||
const tenantId = (req as any).tenantId;
|
||||
const body = governanceRuleSchema.parse(req.body);
|
||||
|
||||
const result = await withTenant(tenantId, async (client) => {
|
||||
const { rows } = await client.query(
|
||||
`INSERT INTO governance_rules (tenant_id, name, rule_type, threshold, action, enabled, config)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[tenantId, body.name, body.rule_type, body.threshold, body.action, body.enabled, JSON.stringify(body.config)],
|
||||
);
|
||||
return rows[0];
|
||||
});
|
||||
|
||||
return reply.status(201).send({ rule: result });
|
||||
});
|
||||
|
||||
// Delete governance rule (console route: DELETE /api/v1/cost/governance/:id)
|
||||
app.delete('/api/v1/cost/governance/:id', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const tenantId = (req as any).tenantId;
|
||||
|
||||
await withTenant(tenantId, async (client) => {
|
||||
await client.query('DELETE FROM governance_rules WHERE id = $1 AND tenant_id = $2', [id, tenantId]);
|
||||
});
|
||||
|
||||
return { status: 'deleted', id };
|
||||
});
|
||||
|
||||
// Get governance status (legacy route)
|
||||
app.get('/api/v1/governance', async (req, reply) => {
|
||||
const tenantId = (req as any).tenantId;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user