2026-10-13 10:33:31 +02:00
/* Incident actions operate only on metadata. Server roles and revision CAS remain authoritative. */
2026-10-13 16:58:15 +02:00
let incidentSelection = null , incidentCursor = null , incidentGeneration = 0 ;
function applyIncidentPermissions ( principal ){ incidentEvidenceViewer . permissions (); if ( ! principal ) return ; const permissions = new Set ( principal . permissions || []), read = permissions . has ( 'incidents.read' ), write = permissions . has ( 'incidents.triage' ); $ ( 'incidents' ). hidden =! read ; document . querySelector ( 'nav a[href="#incidents"]' ). hidden =! read ; for ( const id of [ 'incident-state' , 'incident-severity' , 'incident-assignee' , 'incident-comment' , 'incident-save' ]) $ ( id ). disabled =! write ; for ( const button of document . querySelectorAll ( '[data-incident-create]' )) button . disabled =! write ;}
async function loadIncidents ( append = false ){ incidentGeneration ++ ; incidentEvidenceViewer . reset (); const query = new URLSearchParams ({ limit : '50' }); if ( $ ( 'incident-status-filter' ). value ) query . set ( 'status' , $ ( 'incident-status-filter' ). value ); if ( $ ( 'incident-severity-filter' ). value ) query . set ( 'severity' , $ ( 'incident-severity-filter' ). value ); if ( append && incidentCursor !== null ) query . set ( 'before' , String ( incidentCursor )); const result = await api ( `/api/admin/incidents? ${ query } ` ); if ( ! append ) $ ( 'incident-rows' ). replaceChildren (); for ( const incident of result . incidents ){ const row = document . createElement ( 'tr' ); row . dataset . incidentId = incident . id ; cell ( row , new Date ( incident . createdAt ). toLocaleString ()); cell ( row , incident . event . kind ); cell ( row , incident . status ); cell ( row , incident . severity ); cell ( row , incident . assignee || 'Unassigned' ); const button = document . createElement ( 'button' ); button . textContent = 'Review' ; button . className = 'secondary' ; button . onclick = task (()=> openIncident ( incident . id )); cell ( row , '' ). append ( button ); $ ( 'incident-rows' ). append ( row );} incidentCursor = result . nextBefore ?? null ; $ ( 'incidents-more' ). hidden = incidentCursor === null ; $ ( 'incidents-status' ). textContent = ` ${ $ ( 'incident-rows' ). children . length } incidents shown ${ incidentCursor === null ? '' : ' · older incidents available' } ` ; applyIncidentPermissions ( currentPrincipal );}
async function openIncident ( id ){ incidentEvidenceViewer . reset (); const generation =++ incidentGeneration , principal = currentPrincipal ; const result = await api ( `/api/admin/incidents/ ${ encodeURIComponent ( id ) } ` ), incident = result . incident || result ; if ( generation !== incidentGeneration || principal !== currentPrincipal || document . hidden ) return ; incidentSelection = incident ; $ ( 'incident-detail' ). hidden = false ; $ ( 'incident-title' ). textContent = `Incident · ${ incident . event . kind } ` ; $ ( 'incident-revision' ). textContent = ` ${ incident . id } · revision ${ incident . revision } · updated ${ new Date ( incident . updatedAt ). toLocaleString () } ` ; $ ( 'incident-event' ). textContent = JSON . stringify ( incident . event , null , 2 ); $ ( 'incident-state' ). value = incident . status ; $ ( 'incident-severity' ). value = incident . severity ; $ ( 'incident-assignee' ). value = incident . assignee || '' ; $ ( 'incident-comment' ). value = '' ; $ ( 'incident-detail-status' ). textContent = '' ; $ ( 'incident-history' ). replaceChildren (); for ( const entry of incident . history || []){ const item = document . createElement ( 'article' ); item . className = 'change-item' ; const title = document . createElement ( 'strong' ), note = document . createElement ( 'p' ); title . textContent = `Revision ${ entry . revision } · ${ entry . status } · ${ entry . severity } · ${ entry . actor } · ${ new Date ( entry . time ). toLocaleString () } ` ; note . textContent = `Assignee: ${ entry . assignee || 'Unassigned' }${ entry . comment ? ' · ' + entry . comment : '' } ` ; item . append ( title , note ); $ ( 'incident-history' ). append ( item );} applyIncidentPermissions ( currentPrincipal ); await incidentEvidenceViewer . open ( id );}
2026-10-13 10:33:31 +02:00
$ ( 'incidents-refresh' ). onclick = task (()=> loadIncidents ()); for ( const id of [ 'incident-status-filter' , 'incident-severity-filter' ]) $ ( id ). onchange = task (()=> loadIncidents ()); $ ( 'incidents-more' ). onclick = task (()=> loadIncidents ( true )); $ ( 'incident-detail-refresh' ). onclick = task ( async ()=>{ if ( incidentSelection ) await openIncident ( incidentSelection . id );});
$ ( 'incident-save' ). onclick = task ( async ()=>{ if ( ! incidentSelection ) throw Error ( 'Select an incident first.' ); try { await api ( `/api/admin/incidents/ ${ encodeURIComponent ( incidentSelection . id ) } ` ,{ expectedRevision : incidentSelection . revision , status : $ ( 'incident-state' ). value , severity : $ ( 'incident-severity' ). value , assignee : $ ( 'incident-assignee' ). value . trim () || null ,...( $ ( 'incident-comment' ). value . trim () ? { comment : $ ( 'incident-comment' ). value . trim ()} : {})}, 'PATCH' ); await openIncident ( incidentSelection . id ); await loadIncidents (); $ ( 'incident-detail-status' ). textContent = 'Investigation update saved.' ;} catch ( error ){ $ ( 'incident-detail-status' ). textContent = `Update was not saved: ${ error . message } . Your edits are preserved. Refresh the incident before trying again.` ; throw error ;}});