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