- Backend: - Migration 032: agent_gov_level, status, incidents, permissions tables - Domain types for governance layer - Permission Engine with all governance checks - Governance Service (promote/demote/roles) - Revocation Service (revoke/suspend/reinstate) - Audit Service (events filtering and stats) - Incidents Service (create/assign/escalate/resolve) - REST API routes for governance, audit, incidents - Frontend: - TypeScript types for governance - API clients for governance, audit, incidents - GovernanceLevelBadge component - CityGovernancePanel component - AuditDashboard component - IncidentsList component with detail modal Based on: Agent_Governance_Protocol_v1.md
294 lines
7.7 KiB
TypeScript
294 lines
7.7 KiB
TypeScript
/**
|
|
* Incidents Routes
|
|
* Based on: docs/foundation/Agent_Governance_Protocol_v1.md
|
|
*/
|
|
|
|
import { Router, Request, Response } from 'express';
|
|
import { incidentsService } from '../services/governance/incidents.service';
|
|
import {
|
|
IncidentStatus,
|
|
IncidentPriority,
|
|
EscalationLevel,
|
|
TargetScopeType,
|
|
} from '../domain/governance/types';
|
|
import { logger } from '../infra/logger/logger';
|
|
|
|
const router = Router();
|
|
|
|
/**
|
|
* POST /api/v1/incidents
|
|
* Create a new incident
|
|
*/
|
|
router.post('/', async (req: Request, res: Response) => {
|
|
try {
|
|
const {
|
|
createdByDaisId,
|
|
targetScopeType,
|
|
targetScopeId,
|
|
priority,
|
|
title,
|
|
description,
|
|
metadata,
|
|
} = req.body;
|
|
|
|
if (!createdByDaisId || !targetScopeType || !targetScopeId || !title) {
|
|
return res.status(400).json({
|
|
error: 'Missing required fields: createdByDaisId, targetScopeType, targetScopeId, title',
|
|
});
|
|
}
|
|
|
|
const incident = await incidentsService.createIncident({
|
|
createdByDaisId,
|
|
targetScopeType: targetScopeType as TargetScopeType,
|
|
targetScopeId,
|
|
priority: priority as IncidentPriority | undefined,
|
|
title,
|
|
description,
|
|
metadata,
|
|
});
|
|
|
|
res.status(201).json(incident);
|
|
} catch (error) {
|
|
logger.error('Error creating incident', error);
|
|
res.status(500).json({ error: 'Failed to create incident' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/incidents
|
|
* List incidents with filters
|
|
*/
|
|
router.get('/', async (req: Request, res: Response) => {
|
|
try {
|
|
const {
|
|
status,
|
|
priority,
|
|
escalationLevel,
|
|
targetScopeType,
|
|
targetScopeId,
|
|
assignedToDaisId,
|
|
limit,
|
|
offset,
|
|
} = req.query;
|
|
|
|
const result = await incidentsService.listIncidents({
|
|
status: status as IncidentStatus | undefined,
|
|
priority: priority as IncidentPriority | undefined,
|
|
escalationLevel: escalationLevel as EscalationLevel | undefined,
|
|
targetScopeType: targetScopeType as TargetScopeType | undefined,
|
|
targetScopeId: targetScopeId as string | undefined,
|
|
assignedToDaisId: assignedToDaisId as string | undefined,
|
|
limit: limit ? parseInt(limit as string, 10) : 50,
|
|
offset: offset ? parseInt(offset as string, 10) : 0,
|
|
});
|
|
|
|
res.json(result);
|
|
} catch (error) {
|
|
logger.error('Error listing incidents', error);
|
|
res.status(500).json({ error: 'Failed to list incidents' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/incidents/count
|
|
* Get open incidents count by level
|
|
*/
|
|
router.get('/count', async (_req: Request, res: Response) => {
|
|
try {
|
|
const counts = await incidentsService.getOpenIncidentsCount();
|
|
res.json(counts);
|
|
} catch (error) {
|
|
logger.error('Error getting incidents count', error);
|
|
res.status(500).json({ error: 'Failed to get incidents count' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/incidents/:id
|
|
* Get incident by ID
|
|
*/
|
|
router.get('/:id', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const incident = await incidentsService.getIncident(id);
|
|
|
|
if (!incident) {
|
|
return res.status(404).json({ error: 'Incident not found' });
|
|
}
|
|
|
|
res.json(incident);
|
|
} catch (error) {
|
|
logger.error('Error getting incident', error);
|
|
res.status(500).json({ error: 'Failed to get incident' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/incidents/:id/history
|
|
* Get incident history
|
|
*/
|
|
router.get('/:id/history', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const history = await incidentsService.getIncidentHistory(id);
|
|
res.json(history);
|
|
} catch (error) {
|
|
logger.error('Error getting incident history', error);
|
|
res.status(500).json({ error: 'Failed to get incident history' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v1/incidents/:id/assign
|
|
* Assign incident to an agent
|
|
*/
|
|
router.post('/:id/assign', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { assignedToDaisId, actorDaisId } = req.body;
|
|
|
|
if (!assignedToDaisId || !actorDaisId) {
|
|
return res.status(400).json({
|
|
error: 'Missing required fields: assignedToDaisId, actorDaisId',
|
|
});
|
|
}
|
|
|
|
const result = await incidentsService.assignIncident({
|
|
incidentId: id,
|
|
assignedToDaisId,
|
|
actorDaisId,
|
|
});
|
|
|
|
if (!result.success) {
|
|
return res.status(400).json({ error: result.error });
|
|
}
|
|
|
|
res.json({ success: true, message: 'Incident assigned' });
|
|
} catch (error) {
|
|
logger.error('Error assigning incident', error);
|
|
res.status(500).json({ error: 'Failed to assign incident' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v1/incidents/:id/escalate
|
|
* Escalate incident to higher level
|
|
*/
|
|
router.post('/:id/escalate', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { newLevel, actorDaisId, reason } = req.body;
|
|
|
|
if (!newLevel || !actorDaisId) {
|
|
return res.status(400).json({
|
|
error: 'Missing required fields: newLevel, actorDaisId',
|
|
});
|
|
}
|
|
|
|
const result = await incidentsService.escalateIncident({
|
|
incidentId: id,
|
|
newLevel: newLevel as EscalationLevel,
|
|
actorDaisId,
|
|
reason,
|
|
});
|
|
|
|
if (!result.success) {
|
|
return res.status(400).json({ error: result.error });
|
|
}
|
|
|
|
res.json({ success: true, message: `Incident escalated to ${newLevel}` });
|
|
} catch (error) {
|
|
logger.error('Error escalating incident', error);
|
|
res.status(500).json({ error: 'Failed to escalate incident' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v1/incidents/:id/resolve
|
|
* Resolve incident
|
|
*/
|
|
router.post('/:id/resolve', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { resolution, actorDaisId } = req.body;
|
|
|
|
if (!resolution || !actorDaisId) {
|
|
return res.status(400).json({
|
|
error: 'Missing required fields: resolution, actorDaisId',
|
|
});
|
|
}
|
|
|
|
const result = await incidentsService.resolveIncident({
|
|
incidentId: id,
|
|
resolution,
|
|
actorDaisId,
|
|
});
|
|
|
|
if (!result.success) {
|
|
return res.status(400).json({ error: result.error });
|
|
}
|
|
|
|
res.json({ success: true, message: 'Incident resolved' });
|
|
} catch (error) {
|
|
logger.error('Error resolving incident', error);
|
|
res.status(500).json({ error: 'Failed to resolve incident' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v1/incidents/:id/close
|
|
* Close incident
|
|
*/
|
|
router.post('/:id/close', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { actorDaisId } = req.body;
|
|
|
|
if (!actorDaisId) {
|
|
return res.status(400).json({ error: 'Missing required field: actorDaisId' });
|
|
}
|
|
|
|
const result = await incidentsService.closeIncident(id, actorDaisId);
|
|
|
|
if (!result.success) {
|
|
return res.status(400).json({ error: result.error });
|
|
}
|
|
|
|
res.json({ success: true, message: 'Incident closed' });
|
|
} catch (error) {
|
|
logger.error('Error closing incident', error);
|
|
res.status(500).json({ error: 'Failed to close incident' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v1/incidents/:id/comment
|
|
* Add comment to incident
|
|
*/
|
|
router.post('/:id/comment', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { actorDaisId, comment } = req.body;
|
|
|
|
if (!actorDaisId || !comment) {
|
|
return res.status(400).json({
|
|
error: 'Missing required fields: actorDaisId, comment',
|
|
});
|
|
}
|
|
|
|
const result = await incidentsService.addComment(id, actorDaisId, comment);
|
|
|
|
if (!result.success) {
|
|
return res.status(400).json({ error: result.error });
|
|
}
|
|
|
|
res.json({ success: true, message: 'Comment added' });
|
|
} catch (error) {
|
|
logger.error('Error adding comment to incident', error);
|
|
res.status(500).json({ error: 'Failed to add comment' });
|
|
}
|
|
});
|
|
|
|
export default router;
|
|
|