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();