let currentUser = null; let agsCodes = []; const MAX_ALERTS = 50; const alerts = []; initPage({ onAuth: (user) => { currentUser = user; } }); // ── Sachsen AGS-Lookup ──────────────────────────────────────────────────────── const AGS_NAMES = { '145110000000': 'Chemnitz, Stadt', '145210000000': 'Erzgebirgskreis', '145220000000': 'Mittelsachsen', '145230000000': 'Vogtlandkreis', '145240000000': 'Zwickau', '146120000000': 'Dresden, Stadt', '146250000000': 'Bautzen', '146260000000': 'Görlitz', '146270000000': 'Meißen', '146280000000': 'Sächsische Schweiz-Osterzgebirge', '147130000000': 'Leipzig, Stadt', '147290000000': 'Landkreis Leipzig', '147300000000': 'Nordsachsen', }; function agsName(code) { // Normalisiere auf 12 Stellen für den Lookup const padded = code.padEnd(12, '0'); return AGS_NAMES[padded] || AGS_NAMES[code] || '–'; } function fillDatalist() { const dl = document.getElementById('agsSachsenList'); if (!dl) return; dl.innerHTML = Object.entries(AGS_NAMES) .map(([code, name]) => ``) .join(''); } // ── AGS code list ──────────────────────────────────────────────────────────── function renderAgsList() { const tbody = document.getElementById('agsList'); if (agsCodes.length === 0) { tbody.innerHTML = 'Keine AGS-Codes konfiguriert.'; return; } tbody.innerHTML = agsCodes.map((code, idx) => ` ${escapeHtml(code)} ${escapeHtml(agsName(code))} ` ).join(''); } function removeAgs(idx) { agsCodes.splice(idx, 1); renderAgsList(); } document.getElementById('btnAddAgs').addEventListener('click', () => { const input = document.getElementById('agsInput'); const code = input.value.trim().replace(/\D/g, ''); if (!code) return; if (code.length < 5 || code.length > 12) { input.setCustomValidity('AGS-Code muss 5–12 Stellen haben.'); input.reportValidity(); return; } input.setCustomValidity(''); if (!agsCodes.includes(code)) { agsCodes.push(code); renderAgsList(); } input.value = ''; }); document.getElementById('agsInput').addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); document.getElementById('btnAddAgs').click(); } }); // ── Load config ────────────────────────────────────────────────────────────── async function loadConfig() { try { const resp = await fetch('/api/nina/config'); if (!resp.ok) return; const cfg = await resp.json(); applyConfig(cfg); updateStatusBadge(cfg); } catch (e) { console.error('NINA config load failed:', e); } } function applyConfig(cfg) { document.getElementById('ninaEnabled').checked = !!cfg.enabled; document.getElementById('ninaSendToMesh').checked = cfg.send_to_mesh !== false; document.getElementById('pollInterval').value = Math.round((cfg.poll_interval ?? 300) / 60); document.getElementById('resendInterval').value = Math.round((cfg.resend_interval ?? 3600) / 60); document.getElementById('ninaChannel').value = cfg.channel ?? 0; document.getElementById('minSeverity').value = cfg.min_severity ?? 'Severe'; agsCodes = Array.isArray(cfg.ags_codes) ? [...cfg.ags_codes] : []; renderAgsList(); const src = cfg.sources ?? {}; document.getElementById('srcKatwarn').checked = src.katwarn !== false; document.getElementById('srcBiwapp').checked = src.biwapp !== false; document.getElementById('srcMowas').checked = src.mowas !== false; document.getElementById('srcDwd').checked = src.dwd !== false; document.getElementById('srcLhp').checked = src.lhp !== false; document.getElementById('srcPolice').checked = !!src.police; const lpEl = document.getElementById('lastPoll'); if (lpEl) { lpEl.textContent = cfg.last_poll ? 'Letzte Abfrage: ' + new Date(cfg.last_poll).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'medium' }) : ''; } const lsEl = document.getElementById('lastSent'); if (lsEl) { lsEl.textContent = cfg.last_sent ? 'Zuletzt gesendet: ' + new Date(cfg.last_sent).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'medium' }) : ''; } } function updateStatusBadge(cfg) { const badge = document.getElementById('statusBadge'); if (cfg.enabled) { const codes = Array.isArray(cfg.ags_codes) ? cfg.ags_codes.length : 0; const mode = cfg.send_to_mesh !== false ? 'Mesh+Web' : 'Nur Web'; badge.className = cfg.send_to_mesh !== false ? 'badge bg-success' : 'badge bg-warning text-dark'; badge.textContent = `Aktiv · ${codes} Region${codes !== 1 ? 'en' : ''} · ${mode}`; } else { badge.className = 'badge bg-secondary text-white'; badge.textContent = 'Deaktiviert'; } } // ── Save config ────────────────────────────────────────────────────────────── document.getElementById('btnSaveNina').addEventListener('click', async () => { const payload = { enabled: document.getElementById('ninaEnabled').checked, send_to_mesh: document.getElementById('ninaSendToMesh').checked, poll_interval: (parseInt(document.getElementById('pollInterval').value) || 5) * 60, resend_interval: (parseInt(document.getElementById('resendInterval').value) || 60) * 60, channel: parseInt(document.getElementById('ninaChannel').value) || 0, min_severity: document.getElementById('minSeverity').value, ags_codes: [...agsCodes], sources: { katwarn: document.getElementById('srcKatwarn').checked, biwapp: document.getElementById('srcBiwapp').checked, mowas: document.getElementById('srcMowas').checked, dwd: document.getElementById('srcDwd').checked, lhp: document.getElementById('srcLhp').checked, police: document.getElementById('srcPolice').checked, }, }; const statusEl = document.getElementById('saveStatus'); try { const resp = await fetch('/api/nina/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (resp.ok) { const cfg = await resp.json(); updateStatusBadge(cfg); applyConfig(cfg); statusEl.textContent = 'Gespeichert ✓'; statusEl.className = 'align-self-center small text-success'; statusEl.classList.remove('d-none'); setTimeout(() => statusEl.classList.add('d-none'), 3000); // Config nach kurzer Pause nachladen, damit last_poll den abgeschlossenen Poll zeigt setTimeout(loadConfig, 5000); } else { statusEl.textContent = 'Fehler beim Speichern'; statusEl.className = 'align-self-center small text-danger'; statusEl.classList.remove('d-none'); } } catch (e) { console.error('Save failed:', e); statusEl.textContent = 'Netzwerkfehler'; statusEl.className = 'align-self-center small text-danger'; statusEl.classList.remove('d-none'); } }); // ── Alerts table ───────────────────────────────────────────────────────────── const SEV_CLASS = { Extreme: { bg: 'danger', text: 'text-white' }, Severe: { bg: 'warning', text: 'text-dark' }, Moderate: { bg: 'info', text: 'text-white' }, Minor: { bg: 'secondary', text: 'text-white' }, }; const SEV_LABEL = { Extreme: 'EXTREM', Severe: 'Schwerwiegend', Moderate: 'Mäßig', Minor: 'Gering', Unknown: 'Unbekannt', }; function renderAlerts() { const tbody = document.getElementById('alertsTable'); if (alerts.length === 0) { tbody.innerHTML = 'Keine Meldungen empfangen.'; return; } tbody.innerHTML = alerts.map(a => { const sev = a.msgType === 'Cancel' ? null : (SEV_CLASS[a.severity] ?? { bg: 'secondary', text: 'text-white' }); const bgCls = a.msgType === 'Cancel' ? 'secondary' : sev.bg; const txCls = a.msgType === 'Cancel' ? 'text-white' : sev.text; const sevLabel = a.msgType === 'Cancel' ? 'Aufgehoben' : (SEV_LABEL[a.severity] ?? a.severity); const ts = a.sent ? new Date(a.sent).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' }) : '–'; const meshIcon = a.monitor_only ? '' : ''; const area = a.area ? escapeHtml(a.area) : ''; return ` ${escapeHtml(sevLabel)} ${escapeHtml(a.headline)} ${area} ${escapeHtml(a.id?.split('.')[0] ?? '–')} ${meshIcon} ${ts} `; }).join(''); } function addAlert(alert) { // Replace existing entry with same id (dedup) const idx = alerts.findIndex(a => a.id === alert.id); if (idx !== -1) alerts.splice(idx, 1); alerts.unshift(alert); if (alerts.length > MAX_ALERTS) alerts.pop(); renderAlerts(); } async function loadAlerts() { try { const resp = await fetch('/api/nina/alerts'); if (!resp.ok) return; const data = await resp.json(); if (!Array.isArray(data) || data.length === 0) return; data.forEach(a => { alerts.push(a); }); if (alerts.length > MAX_ALERTS) alerts.length = MAX_ALERTS; renderAlerts(); } catch (e) { console.error('NINA alerts load failed:', e); } } // ── WebSocket ───────────────────────────────────────────────────────────────── function connectWebSocket() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const ws = new WebSocket(`${proto}//${location.host}/ws`); ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'nina_alert') { addAlert(msg.data); } }; ws.onclose = () => { setTimeout(connectWebSocket, 3000); }; ws.onerror = () => { ws.close(); }; } // ── Init ────────────────────────────────────────────────────────────────────── fillDatalist(); loadConfig(); loadAlerts(); connectWebSocket();