381 lines
12 KiB
JavaScript
381 lines
12 KiB
JavaScript
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,
|
|
};
|