/* ros-app.js — Scenario A interactivity */ (function () { const data = window.ROS_DATA; /* -------------------------------------------------- THEME TOGGLE */ const themeToggle = document.getElementById('themeToggle'); const themeLabel = document.getElementById('themeLabel'); const stored = localStorage.getItem('ros-theme'); if (stored) document.documentElement.setAttribute('data-theme', stored); function syncThemeLabel() { const t = document.documentElement.getAttribute('data-theme') || 'light'; themeLabel.textContent = t === 'dark' ? 'Lyst' : 'Mørkt'; } syncThemeLabel(); themeToggle.addEventListener('click', () => { const cur = document.documentElement.getAttribute('data-theme') || 'light'; const next = cur === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', next); localStorage.setItem('ros-theme', next); syncThemeLabel(); drawRadar(); // redraw since some colors are computed }); /* -------------------------------------------------- SCREEN ROUTING */ const tabs = document.querySelectorAll('.screen-tab'); const screens = document.querySelectorAll('.screen'); function showScreen(name) { tabs.forEach(t => t.setAttribute('aria-current', t.dataset.screen === name ? 'true' : 'false')); screens.forEach(s => s.dataset.active = s.dataset.screen === name ? 'true' : 'false'); history.replaceState(null, '', '#' + name); } tabs.forEach(t => t.addEventListener('click', () => showScreen(t.dataset.screen))); document.querySelectorAll('[data-goto]').forEach(b => b.addEventListener('click', () => showScreen(b.dataset.goto))); const initial = (location.hash || '#matrix').slice(1); if (['intake','matrix','findings','summary'].includes(initial)) showScreen(initial); else showScreen('matrix'); /* -------------------------------------------------- MATRIX */ // 5x5 grid + axis ticks. Bottom-left origin: row 5 = konsekvens 5 (highest at top) const matrix = document.getElementById('rosMatrix'); let showResidual = false; function buildMatrix() { matrix.innerHTML = ''; // For each row from konsekvens=5 down to 1 for (let k = 5; k >= 1; k--) { // Y-tick const tick = document.createElement('div'); tick.className = 'matrix__y-tick'; tick.textContent = k; matrix.appendChild(tick); // 5 cells for (let s = 1; s <= 5; s++) { const cell = document.createElement('button'); cell.type = 'button'; const score = s * k; cell.className = 'matrix__cell'; cell.dataset.score = score; cell.dataset.s = s; cell.dataset.k = k; cell.setAttribute('aria-label', `Sannsynlighet ${s}, konsekvens ${k}, score ${score}`); const scoreLabel = document.createElement('span'); scoreLabel.className = 'matrix__cell-score'; scoreLabel.textContent = score; cell.appendChild(scoreLabel); const bubbles = document.createElement('span'); bubbles.className = 'matrix__cell-bubbles'; // Find threats in this cell const threats = data.threats.filter(t => { const sa = showResidual ? t.restrisiko.sannsynlighet : t.sannsynlighet; const ko = showResidual ? t.restrisiko.konsekvens : t.konsekvens; return sa === s && ko === k; }); threats.slice(0, 3).forEach(t => { const b = document.createElement('span'); b.className = 'matrix__bubble'; b.textContent = t.id; b.title = t.tittel; bubbles.appendChild(b); }); // Aggregate count from cellCounts (only when not showing residual) const extra = !showResidual ? (data.cellCounts[`${s},${k}`] || 0) : 0; const overflow = (threats.length > 3) ? (threats.length - 3) : 0; const totalExtra = extra + overflow; if (totalExtra > 0) { const c = document.createElement('span'); c.className = 'matrix__bubble matrix__bubble--count'; c.textContent = '+' + totalExtra; bubbles.appendChild(c); } cell.appendChild(bubbles); cell.addEventListener('click', () => { // Pick first named threat in this cell, else show count info if (threats.length) openThreatPanel(threats[0].id); }); matrix.appendChild(cell); } } // Bottom row: corner + 5 x-ticks const corner = document.createElement('div'); corner.className = 'matrix__corner'; matrix.appendChild(corner); for (let s = 1; s <= 5; s++) { const xt = document.createElement('div'); xt.className = 'matrix__x-tick'; xt.textContent = s; matrix.appendChild(xt); } } buildMatrix(); document.getElementById('toggleResidual').addEventListener('click', (e) => { showResidual = !showResidual; e.target.textContent = showResidual ? 'Vis nåværende risiko' : 'Vis restrisiko etter tiltak'; buildMatrix(); }); /* -------------------------------------------------- RADAR */ function drawRadar() { const svg = document.querySelector('.radar__svg #radarGrid'); if (!svg) return; svg.innerHTML = ''; const axes = data.radarAxes; const N = axes.length; const R = 100; // Grid rings for (let r = 1; r <= 5; r++) { const radius = (R / 5) * r; const points = []; for (let i = 0; i < N; i++) { const a = (-Math.PI / 2) + (i / N) * Math.PI * 2; points.push((Math.cos(a) * radius).toFixed(2) + ',' + (Math.sin(a) * radius).toFixed(2)); } const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); poly.setAttribute('points', points.join(' ')); poly.setAttribute('class', 'radar__grid-line'); svg.appendChild(poly); } // Axes for (let i = 0; i < N; i++) { const a = (-Math.PI / 2) + (i / N) * Math.PI * 2; const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); line.setAttribute('x1', 0); line.setAttribute('y1', 0); line.setAttribute('x2', (Math.cos(a) * R).toFixed(2)); line.setAttribute('y2', (Math.sin(a) * R).toFixed(2)); line.setAttribute('class', 'radar__axis'); svg.appendChild(line); // Label const lx = Math.cos(a) * (R + 22); const ly = Math.sin(a) * (R + 22); const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text'); txt.setAttribute('x', lx.toFixed(2)); txt.setAttribute('y', (ly + 4).toFixed(2)); txt.setAttribute('class', 'radar__label'); txt.textContent = axes[i].label; svg.appendChild(txt); } // Series helper function series(values, klass) { const points = []; for (let i = 0; i < N; i++) { const a = (-Math.PI / 2) + (i / N) * Math.PI * 2; const r = (values[i] / 5) * R; points.push((Math.cos(a) * r).toFixed(2) + ',' + (Math.sin(a) * r).toFixed(2)); } const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); poly.setAttribute('points', points.join(' ')); poly.setAttribute('class', klass); svg.appendChild(poly); } series(axes.map(a => a.target), 'radar__series radar__series--target'); series(axes.map(a => a.current), 'radar__series'); // Scores list const dl = document.getElementById('radarScores'); if (dl) { dl.innerHTML = ''; axes.forEach(a => { const row = document.createElement('div'); row.className = 'radar__score-row'; row.innerHTML = `
${a.label}
${a.current.toFixed(1)} → ${a.target.toFixed(1)}
`; dl.appendChild(row); }); } } drawRadar(); /* -------------------------------------------------- FINDINGS BROWSER */ const findingsGroups = document.getElementById('findingsGroups'); const findingDetail = document.getElementById('findingDetail'); function severityFromScore(score) { if (score >= 20) return 'critical'; if (score >= 15) return 'high'; if (score >= 9) return 'medium'; return 'low'; } function zoneFromScore(score) { if (score >= 20) return 'critical'; if (score >= 15) return 'high'; if (score >= 9) return 'medium'; return 'low'; } function buildFindings() { findingsGroups.innerHTML = ''; const grouped = { critical: [], high: [], medium: [], low: [] }; data.threats.forEach(t => { const sev = severityFromScore(t.sannsynlighet * t.konsekvens); grouped[sev].push(t); }); const labels = { critical: 'Kritisk', high: 'Høy', medium: 'Middels', low: 'Lav' }; Object.keys(grouped).forEach(sev => { if (!grouped[sev].length) return; const grp = document.createElement('div'); grp.className = 'findings__group'; const hdr = document.createElement('div'); hdr.className = 'findings__group-header'; hdr.innerHTML = `${labels[sev]}${grouped[sev].length}`; grp.appendChild(hdr); const ul = document.createElement('ul'); ul.className = 'findings__items'; grouped[sev].forEach(t => { const li = document.createElement('li'); li.className = 'findings__item'; li.tabIndex = 0; li.dataset.id = t.id; li.innerHTML = ` ${t.id} · ${t.kategori} ${t.tittel} ${t.sannsynlighet}×${t.konsekvens} = ${t.sannsynlighet*t.konsekvens} ${t.mitigeringer.length} mitig. `; li.addEventListener('click', () => selectFinding(t.id)); li.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectFinding(t.id); } }); ul.appendChild(li); }); grp.appendChild(ul); findingsGroups.appendChild(grp); }); } function selectFinding(id) { document.querySelectorAll('.findings__item').forEach(el => { el.setAttribute('aria-selected', el.dataset.id === id ? 'true' : 'false'); }); renderFindingDetail(id); } function renderFindingDetail(id) { const t = data.threats.find(x => x.id === id); if (!t) return; const cur = t.sannsynlighet * t.konsekvens; const res = t.restrisiko.sannsynlighet * t.restrisiko.konsekvens; findingDetail.innerHTML = `
${t.id} · ${t.kategori}

${t.tittel}

Før tiltak
${cur}
${t.sannsynlighet} × ${t.konsekvens}
Etter tiltak
${res}
${t.restrisiko.sannsynlighet} × ${t.restrisiko.konsekvens}

Beskrivelse

${t.kilde}

Begrunnelse — sannsynlighet ${t.sannsynlighet}/5

${t.sannsynlighetBegrunnelse}

Begrunnelse — konsekvens ${t.konsekvens}/5

${t.konsekvensBegrunnelse}

Mitigeringer (${t.mitigeringer.length})

`; } buildFindings(); selectFinding('T-001'); /* -------------------------------------------------- SIDEPANEL (matrix click) */ const sidepanel = document.getElementById('sidepanel'); const scrim = document.getElementById('scrim'); function openThreatPanel(id) { const t = data.threats.find(x => x.id === id); if (!t) return; document.getElementById('sidepanelId').textContent = `${t.id} · ${t.kategori}`; document.getElementById('sidepanelTitle').textContent = t.tittel; const cur = t.sannsynlighet * t.konsekvens; const res = t.restrisiko.sannsynlighet * t.restrisiko.konsekvens; document.getElementById('sidepanelBody').innerHTML = `
Før tiltak
${cur}
Etter tiltak
${res}

Beskrivelse

${t.kilde}

Mitigeringer

`; sidepanel.dataset.open = 'true'; sidepanel.setAttribute('aria-hidden', 'false'); scrim.dataset.open = 'true'; } function closePanel() { sidepanel.dataset.open = 'false'; sidepanel.setAttribute('aria-hidden', 'true'); scrim.dataset.open = 'false'; } document.getElementById('sidepanelClose').addEventListener('click', closePanel); scrim.addEventListener('click', closePanel); document.addEventListener('keydown', e => { if (e.key === 'Escape') closePanel(); }); /* -------------------------------------------------- TOP RISKS */ const topRisksEl = document.getElementById('topRisks'); if (topRisksEl) { const sorted = [...data.threats] .map(t => ({...t, score: t.sannsynlighet*t.konsekvens, residualScore: t.restrisiko.sannsynlighet*t.restrisiko.konsekvens})) .sort((a,b) => b.score - a.score) .slice(0,5); sorted.forEach((t, i) => { const li = document.createElement('li'); li.className = 'top-risk'; li.innerHTML = ` ${String(i+1).padStart(2,'0')} ${t.score}
${t.id}
${t.tittel}
${t.score} → ${t.residualScore} `; li.addEventListener('click', () => openThreatPanel(t.id)); topRisksEl.appendChild(li); }); } })();