Version 0.4

This commit is contained in:
2026-04-16 23:08:23 +01:00
parent e011afbff1
commit 39eb818e7e
110 changed files with 18905 additions and 14 deletions

View 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,
};