feat(rails): GET /api/transitions + /api/escalations for SIEM dashboard
This commit is contained in:
@@ -68,7 +68,9 @@ const server = createServer(async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const req2 = { ...parsed.data, agentName: parsed.data.agentName || AGENT_NAME };
|
||||
// Force agentName to this sister's identity (env), not whatever rails sent.
|
||||
// The stage info is preserved separately in parsed.data.stage.
|
||||
const req2 = { ...parsed.data, agentName: AGENT_NAME };
|
||||
const railsClient = new RailsClient(req2.railsApiUrl);
|
||||
|
||||
log("info", "invoke.start", {
|
||||
|
||||
@@ -172,6 +172,71 @@ function mergeContextIntoSnapshot(
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
export async function listTransitions(opts?: {
|
||||
pipelineId?: string;
|
||||
eventType?: string;
|
||||
limit?: number;
|
||||
}): Promise<
|
||||
Array<{
|
||||
id: number;
|
||||
pipelineId: string;
|
||||
fromState: string;
|
||||
toState: string;
|
||||
eventType: string;
|
||||
timestamp: Date;
|
||||
}>
|
||||
> {
|
||||
const prisma = getPrisma();
|
||||
const where: { pipelineId?: string; eventType?: string } = {};
|
||||
if (opts?.pipelineId) where.pipelineId = opts.pipelineId;
|
||||
if (opts?.eventType) where.eventType = opts.eventType;
|
||||
|
||||
return prisma.stateTransition.findMany({
|
||||
where,
|
||||
orderBy: { timestamp: "desc" },
|
||||
take: opts?.limit ?? 100,
|
||||
select: {
|
||||
id: true,
|
||||
pipelineId: true,
|
||||
fromState: true,
|
||||
toState: true,
|
||||
eventType: true,
|
||||
timestamp: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function listEscalations(opts?: {
|
||||
pipelineId?: string;
|
||||
resolved?: boolean;
|
||||
limit?: number;
|
||||
}): Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
pipelineId: string;
|
||||
reason: string;
|
||||
errorCategory: string;
|
||||
stage: string;
|
||||
attempts: number;
|
||||
contextSnapshot: string;
|
||||
resolvedAt: Date | null;
|
||||
resolution: string | null;
|
||||
createdAt: Date;
|
||||
}>
|
||||
> {
|
||||
const prisma = getPrisma();
|
||||
const where: { pipelineId?: string; resolvedAt?: null | { not: null } } = {};
|
||||
if (opts?.pipelineId) where.pipelineId = opts.pipelineId;
|
||||
if (opts?.resolved === false) where.resolvedAt = null;
|
||||
if (opts?.resolved === true) where.resolvedAt = { not: null };
|
||||
|
||||
return prisma.escalation.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: opts?.limit ?? 50,
|
||||
});
|
||||
}
|
||||
|
||||
export async function listPipelines(opts?: {
|
||||
state?: PipelineState;
|
||||
limit?: number;
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
createPipeline,
|
||||
getPipelineState,
|
||||
listPipelines,
|
||||
listTransitions,
|
||||
listEscalations,
|
||||
sendEvent,
|
||||
} from "../orchestrator/persist.js";
|
||||
import { runPipeline } from "../orchestrator/runner.js";
|
||||
@@ -201,6 +203,32 @@ export async function startHttpServer(opts: ServerOpts): Promise<{
|
||||
return sendJson(res, 200, detail);
|
||||
}
|
||||
|
||||
// ── State transitions (SIEM-style log) ──
|
||||
if (method === "GET" && path === "/api/transitions") {
|
||||
const limit = parseInt(url.searchParams.get("limit") ?? "100", 10);
|
||||
const pid = url.searchParams.get("pipelineId") ?? undefined;
|
||||
const eventType = url.searchParams.get("eventType") ?? undefined;
|
||||
const transitions = await listTransitions({
|
||||
...(pid !== undefined && { pipelineId: pid }),
|
||||
...(eventType !== undefined && { eventType }),
|
||||
limit,
|
||||
});
|
||||
return sendJson(res, 200, { transitions });
|
||||
}
|
||||
|
||||
// ── Escalations ──
|
||||
if (method === "GET" && path === "/api/escalations") {
|
||||
const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
|
||||
const pid = url.searchParams.get("pipelineId") ?? undefined;
|
||||
const resolvedQ = url.searchParams.get("resolved");
|
||||
const opts: { pipelineId?: string; resolved?: boolean; limit: number } = { limit };
|
||||
if (pid !== undefined) opts.pipelineId = pid;
|
||||
if (resolvedQ === "true") opts.resolved = true;
|
||||
else if (resolvedQ === "false") opts.resolved = false;
|
||||
const escalations = await listEscalations(opts);
|
||||
return sendJson(res, 200, { escalations });
|
||||
}
|
||||
|
||||
return sendJson(res, 404, { error: "not_found", path });
|
||||
} catch (err) {
|
||||
log.error(
|
||||
|
||||
Reference in New Issue
Block a user