const API = '/api/'; let teams = []; let events = []; let nodes = []; let links = []; let selectedNodeId = null; // Network canvas state let canvas, ctx; let canvasNodes = []; let canvasLinks = []; let isDragging = false; let dragNode = null; let offsetX, offsetY; let panX = 0, panY = 0; let isPanning = false; let panStartX, panStartY; document.addEventListener('DOMContentLoaded', () => { canvas = document.getElementById('networkCanvas'); ctx = canvas.getContext('2d'); resizeCanvas(); loadTeams().then(() => { loadEvents(); }); loadNetworkData(); document.getElementById('saveEvent').addEventListener('click', saveEvent); document.getElementById('saveNode').addEventListener('click', saveNode); document.getElementById('saveLink').addEventListener('click', saveLink); document.getElementById('teamFilter').addEventListener('change', renderTimeline); document.getElementById('searchEvents').addEventListener('input', renderTimeline); canvas.addEventListener('mousedown', onCanvasMouseDown); canvas.addEventListener('mousemove', onCanvasMouseMove); canvas.addEventListener('mouseup', onCanvasMouseUp); canvas.addEventListener('dblclick', onCanvasDblClick); window.addEventListener('resize', () => { resizeCanvas(); renderNetwork(); }); document.querySelectorAll('[data-bs-toggle="tab"]').forEach(tab => { tab.addEventListener('shown.bs.tab', () => { if (tab.id === 'network-tab') { resizeCanvas(); renderNetwork(); } }); }); // Bootstrap dark mode default document.documentElement.setAttribute('data-bs-theme', 'dark'); }); function resizeCanvas() { const wrapper = document.getElementById('networkCanvasWrapper'); canvas.width = wrapper.clientWidth; canvas.height = wrapper.clientHeight; } // ==================== API HELPERS ==================== async function apiFetch(path, options = {}) { const res = await fetch(API + path, { headers: { 'Content-Type': 'application/json' }, ...options }); return res.json(); } // ==================== TEAMS ==================== async function loadTeams() { teams = await apiFetch('teams'); const selTeam = document.getElementById('eventTeam'); const filter = document.getElementById('teamFilter'); selTeam.innerHTML = ''; filter.innerHTML = ''; teams.forEach(t => { selTeam.innerHTML += ``; filter.innerHTML += ``; }); } // ==================== EVENTS / TIMELINE ==================== async function loadEvents() { events = await apiFetch('events'); renderTimeline(); } function renderTimeline() { const container = document.getElementById('timelineContainer'); const teamFilter = document.getElementById('teamFilter').value; const search = document.getElementById('searchEvents').value.toLowerCase(); let filtered = events; if (teamFilter) filtered = filtered.filter(e => e.team_id == teamFilter); if (search) filtered = filtered.filter(e => e.title.toLowerCase().includes(search) || (e.description && e.description.toLowerCase().includes(search)) ); if (!filtered.length) { container.innerHTML = `
No events yet. Create your first incident entry!
${esc(e.description)}
` : ''}