Version 0.4
This commit is contained in:
380
backend/src/controllers/incidentController.js
Normal file
380
backend/src/controllers/incidentController.js
Normal file
@@ -0,0 +1,380 @@
|
||||
const { getDatabase, runInTransaction } = require('../models/database');
|
||||
const {
|
||||
listIncidentsOrdered,
|
||||
getIncidentById: getIncidentRecordById,
|
||||
listIncidentEndpoints,
|
||||
getLatestIncidentUpdate,
|
||||
listIncidentUpdates,
|
||||
createIncidentRecord,
|
||||
linkIncidentEndpoint,
|
||||
createIncidentUpdate,
|
||||
updateIncidentCore,
|
||||
deleteIncidentLinksExceptSource,
|
||||
deleteAllIncidentLinks,
|
||||
markIncidentResolved,
|
||||
setIncidentAdminManaged,
|
||||
setIncidentStatus,
|
||||
getIncidentUpdateById,
|
||||
reopenIncidentRecord,
|
||||
setIncidentPostMortem,
|
||||
deleteIncidentRecord,
|
||||
} = require('../data/incidentData');
|
||||
const { queueIncidentNotification } = require('../services/notificationService');
|
||||
|
||||
// Helpers
|
||||
|
||||
async function enrichIncident(incident, { includeUpdates = false } = {}) {
|
||||
incident.endpoints = await listIncidentEndpoints(incident.id);
|
||||
|
||||
// Always include the latest update for preview
|
||||
incident.latest_update = await getLatestIncidentUpdate(incident.id);
|
||||
|
||||
if (includeUpdates) {
|
||||
incident.updates = await listIncidentUpdates(incident.id);
|
||||
}
|
||||
|
||||
return incident;
|
||||
}
|
||||
|
||||
// Public
|
||||
|
||||
async function getAllIncidents(req, res) {
|
||||
try {
|
||||
const incidents = await listIncidentsOrdered();
|
||||
|
||||
for (const incident of incidents) {
|
||||
await enrichIncident(incident);
|
||||
}
|
||||
|
||||
res.json(incidents);
|
||||
} catch (error) {
|
||||
console.error('Get incidents error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
async function getIncidentById(req, res) {
|
||||
try {
|
||||
const incident = await getIncidentRecordById(req.params.id);
|
||||
if (!incident) return res.status(404).json({ error: 'Incident not found' });
|
||||
|
||||
await enrichIncident(incident, { includeUpdates: true });
|
||||
res.json(incident);
|
||||
} catch (error) {
|
||||
console.error('Get incident error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
// Admin CRUD
|
||||
|
||||
async function createIncident(req, res) {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
severity = 'degraded',
|
||||
status = 'investigating',
|
||||
source = 'manual',
|
||||
endpoint_ids = [],
|
||||
initial_message,
|
||||
created_by = 'admin'
|
||||
} = req.body;
|
||||
|
||||
if (!title || !severity) {
|
||||
return res.status(400).json({ error: 'Title and severity are required' });
|
||||
}
|
||||
|
||||
const incidentId = await runInTransaction(async () => {
|
||||
const result = await createIncidentRecord({
|
||||
title,
|
||||
description: description || '',
|
||||
severity,
|
||||
status,
|
||||
source,
|
||||
});
|
||||
|
||||
for (const endpointId of endpoint_ids) {
|
||||
await linkIncidentEndpoint(result.lastID, endpointId);
|
||||
}
|
||||
|
||||
if (initial_message) {
|
||||
await createIncidentUpdate(result.lastID, initial_message, status, created_by);
|
||||
}
|
||||
|
||||
return result.lastID;
|
||||
});
|
||||
|
||||
const incident = await getIncidentRecordById(incidentId);
|
||||
await enrichIncident(incident, { includeUpdates: true });
|
||||
await queueIncidentNotification('incident_created', incident.id, initial_message || incident.description || 'New incident created.');
|
||||
res.status(201).json(incident);
|
||||
} catch (error) {
|
||||
console.error('Create incident error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
async function updateIncident(req, res) {
|
||||
try {
|
||||
const { title, description, severity, status, endpoint_ids = [] } = req.body;
|
||||
const existing = await getIncidentRecordById(req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: 'Incident not found' });
|
||||
|
||||
// Mark as admin-managed if a human is editing an auto-created incident
|
||||
const adminManaged = existing.auto_created ? 1 : (existing.admin_managed || 0);
|
||||
|
||||
await runInTransaction(async () => {
|
||||
await updateIncidentCore(req.params.id, {
|
||||
title,
|
||||
description: description || '',
|
||||
severity,
|
||||
status,
|
||||
admin_managed: adminManaged,
|
||||
});
|
||||
|
||||
if (existing.auto_created && existing.source_endpoint_id) {
|
||||
await deleteIncidentLinksExceptSource(req.params.id, existing.source_endpoint_id);
|
||||
for (const endpointId of endpoint_ids) {
|
||||
await linkIncidentEndpoint(req.params.id, endpointId, true);
|
||||
}
|
||||
await linkIncidentEndpoint(req.params.id, existing.source_endpoint_id, true);
|
||||
} else {
|
||||
await deleteAllIncidentLinks(req.params.id);
|
||||
for (const endpointId of endpoint_ids) {
|
||||
await linkIncidentEndpoint(req.params.id, endpointId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const incident = await getIncidentRecordById(req.params.id);
|
||||
await enrichIncident(incident, { includeUpdates: true });
|
||||
await queueIncidentNotification('incident_updated', incident.id, 'Incident details were updated.');
|
||||
res.json(incident);
|
||||
} catch (error) {
|
||||
console.error('Update incident error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveIncident(req, res) {
|
||||
try {
|
||||
const { message, created_by = 'admin' } = req.body;
|
||||
const existing = await getIncidentRecordById(req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: 'Incident not found' });
|
||||
|
||||
// Resolving an auto-incident manually marks it as admin-managed
|
||||
const adminManaged = existing.auto_created ? 1 : (existing.admin_managed || 0);
|
||||
|
||||
const closingMessage = message || 'This incident has been resolved.';
|
||||
await runInTransaction(async () => {
|
||||
await markIncidentResolved(req.params.id, adminManaged);
|
||||
await createIncidentUpdate(req.params.id, closingMessage, 'resolved', created_by);
|
||||
});
|
||||
|
||||
const incident = await getIncidentRecordById(req.params.id);
|
||||
await enrichIncident(incident, { includeUpdates: true });
|
||||
await queueIncidentNotification('incident_resolved', incident.id, closingMessage);
|
||||
res.json(incident);
|
||||
} catch (error) {
|
||||
console.error('Resolve incident error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteIncident(req, res) {
|
||||
try {
|
||||
await deleteIncidentRecord(req.params.id);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete incident error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
// Incident Updates (timeline)
|
||||
|
||||
async function addIncidentUpdate(req, res) {
|
||||
try {
|
||||
const { message, status_label, created_by = 'admin' } = req.body;
|
||||
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: 'Message is required' });
|
||||
}
|
||||
|
||||
const incident = await getIncidentRecordById(req.params.id);
|
||||
if (!incident) return res.status(404).json({ error: 'Incident not found' });
|
||||
|
||||
const updateId = await runInTransaction(async () => {
|
||||
if (incident.auto_created && !incident.admin_managed) {
|
||||
await setIncidentAdminManaged(req.params.id, 1);
|
||||
}
|
||||
|
||||
const result = await createIncidentUpdate(req.params.id, message, status_label || null, created_by);
|
||||
if (status_label) {
|
||||
await setIncidentStatus(req.params.id, status_label);
|
||||
}
|
||||
return result.lastID;
|
||||
});
|
||||
|
||||
const update = await getIncidentUpdateById(updateId);
|
||||
await queueIncidentNotification('incident_updated', req.params.id, message);
|
||||
res.status(201).json(update);
|
||||
} catch (error) {
|
||||
console.error('Add incident update error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
// Maintenance Windows
|
||||
|
||||
async function getAllMaintenance(req, res) {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const windows = await db.all(`
|
||||
SELECT mw.*, e.name AS endpoint_name
|
||||
FROM maintenance_windows mw
|
||||
LEFT JOIN endpoints e ON e.id = mw.endpoint_id
|
||||
ORDER BY mw.start_time DESC
|
||||
`);
|
||||
res.json(windows);
|
||||
} catch (error) {
|
||||
console.error('Get maintenance error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
async function createMaintenance(req, res) {
|
||||
try {
|
||||
const { title, description, endpoint_id, start_time, end_time } = req.body;
|
||||
|
||||
if (!title || !start_time || !end_time) {
|
||||
return res.status(400).json({ error: 'Title, start_time, and end_time are required' });
|
||||
}
|
||||
|
||||
const db = getDatabase();
|
||||
const result = await db.run(
|
||||
`INSERT INTO maintenance_windows (title, description, endpoint_id, start_time, end_time)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[title, description || '', endpoint_id || null, start_time, end_time]
|
||||
);
|
||||
|
||||
const window = await db.get('SELECT * FROM maintenance_windows WHERE id = ?', [result.lastID]);
|
||||
res.status(201).json(window);
|
||||
} catch (error) {
|
||||
console.error('Create maintenance error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
async function updateMaintenance(req, res) {
|
||||
try {
|
||||
const { title, description, endpoint_id, start_time, end_time } = req.body;
|
||||
const db = getDatabase();
|
||||
|
||||
await db.run(
|
||||
`UPDATE maintenance_windows
|
||||
SET title = ?, description = ?, endpoint_id = ?, start_time = ?, end_time = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
[title, description || '', endpoint_id || null, start_time, end_time, req.params.id]
|
||||
);
|
||||
|
||||
const window = await db.get('SELECT * FROM maintenance_windows WHERE id = ?', [req.params.id]);
|
||||
res.json(window);
|
||||
} catch (error) {
|
||||
console.error('Update maintenance error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMaintenance(req, res) {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
await db.run('DELETE FROM maintenance_windows WHERE id = ?', [req.params.id]);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete maintenance error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
// Reopen
|
||||
|
||||
async function reopenIncident(req, res) {
|
||||
try {
|
||||
const incident = await getIncidentRecordById(req.params.id);
|
||||
if (!incident) return res.status(404).json({ error: 'Incident not found' });
|
||||
|
||||
if (!incident.resolved_at) {
|
||||
return res.status(400).json({ error: 'Incident is not resolved' });
|
||||
}
|
||||
|
||||
// Admins may re-open within 7 days of resolution
|
||||
const REOPEN_WINDOW_DAYS = 7;
|
||||
const resolvedAt = new Date(incident.resolved_at.replace(' ', 'T') + 'Z');
|
||||
const ageMs = Date.now() - resolvedAt.getTime();
|
||||
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (ageDays > REOPEN_WINDOW_DAYS) {
|
||||
return res.status(403).json({
|
||||
error: `This incident can no longer be re-opened. The ${REOPEN_WINDOW_DAYS}-day re-open window has expired.`,
|
||||
expired: true,
|
||||
});
|
||||
}
|
||||
|
||||
const { message = '', created_by = 'admin' } = req.body;
|
||||
await runInTransaction(async () => {
|
||||
await reopenIncidentRecord(req.params.id);
|
||||
await createIncidentUpdate(
|
||||
req.params.id,
|
||||
message || 'This incident has been re-opened by an administrator.',
|
||||
'investigating',
|
||||
created_by
|
||||
);
|
||||
});
|
||||
|
||||
const updated = await getIncidentRecordById(req.params.id);
|
||||
await enrichIncident(updated, { includeUpdates: true });
|
||||
await queueIncidentNotification('incident_updated', updated.id, message || 'Incident re-opened by administrator.');
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
console.error('Reopen incident error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
// Post-mortem
|
||||
|
||||
async function setPostMortem(req, res) {
|
||||
try {
|
||||
const { post_mortem } = req.body;
|
||||
const incident = await getIncidentRecordById(req.params.id);
|
||||
if (!incident) return res.status(404).json({ error: 'Incident not found' });
|
||||
|
||||
await setIncidentPostMortem(req.params.id, post_mortem || null);
|
||||
|
||||
const updated = await getIncidentRecordById(req.params.id);
|
||||
await enrichIncident(updated, { includeUpdates: true });
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
console.error('Set post-mortem error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAllIncidents,
|
||||
getIncidentById,
|
||||
createIncident,
|
||||
updateIncident,
|
||||
resolveIncident,
|
||||
deleteIncident,
|
||||
addIncidentUpdate,
|
||||
reopenIncident,
|
||||
setPostMortem,
|
||||
getAllMaintenance,
|
||||
createMaintenance,
|
||||
updateMaintenance,
|
||||
deleteMaintenance,
|
||||
};
|
||||
Reference in New Issue
Block a user