diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b1fc2..b062d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [0.8.1] - 2026-02-19 + +### Added +- **NINA Wiederholungsintervall** (`resend_interval`): Zweiter konfigurierbarer Intervall, + der aktive Warnmeldungen in regelmäßigen Abständen erneut ins Mesh sendet + (nur wenn `send_to_mesh=true`). Standard: 3600 Sekunden (1 Stunde). +- **NINA AGS-Code-Tabelle**: AGS-Codes werden jetzt in einer Tabelle mit Lösch-Button + je Zeile angezeigt – übersichtlicher als die bisherigen Badge-Einträge. + +### Fixed +- **Badge-Lesbarkeit**: Severity-Badges in der Alerts-Tabelle haben jetzt explizite + Textfarben (`text-white` / `text-dark`) ohne `bg-opacity`, damit der Text auf allen + Hintergründen und in beiden Themes lesbar bleibt. +- **colspan**: Leere Zeile in der Alerts-Tabelle korrekt auf 5 Spalten gesetzt. + ## [0.8.0] - 2026-02-19 ### Added diff --git a/conf/nina.yaml b/conf/nina.yaml index c66f842..1f66473 100644 --- a/conf/nina.yaml +++ b/conf/nina.yaml @@ -1,6 +1,7 @@ enabled: false send_to_mesh: false # true = ins Mesh senden | false = nur Weboberfläche (Monitor-Modus) poll_interval: 300 +resend_interval: 3600 # Aktive Warnungen alle N Sekunden erneut ins Mesh senden channel: 0 min_severity: Severe ags_codes: diff --git a/config.yaml b/config.yaml index 6295f5f..b7dfb6b 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,4 @@ -version: "0.8.0" +version: "0.8.1" bot: name: "MeshDD-Bot" diff --git a/meshbot/nina.py b/meshbot/nina.py index e872dfa..9fa98b4 100644 --- a/meshbot/nina.py +++ b/meshbot/nina.py @@ -58,6 +58,7 @@ DEFAULT_CONFIG = { "enabled": False, "send_to_mesh": True, "poll_interval": 300, + "resend_interval": 3600, "channel": 0, "min_severity": "Severe", "ags_codes": [], @@ -88,9 +89,11 @@ class NinaBot: self.ws_manager = ws_manager self.config: dict = {} self._mtime: float = 0.0 - self._known: dict[str, str] = {} # normalised_id -> sent (de-dup) + self._known: dict[str, str] = {} # normalised_id -> sent (de-dup) + self._active: dict[str, dict] = {} # normalised_id -> {text, channel, headline, severity, id, sent} self._running = False self._task: asyncio.Task | None = None + self._resend_task: asyncio.Task | None = None self._load() # ── Config ────────────────────────────────────────────────────────────── @@ -149,16 +152,18 @@ class NinaBot: async def start(self): self._running = True self._task = asyncio.create_task(self._poll_loop()) + self._resend_task = asyncio.create_task(self._resend_loop()) logger.info("NinaBot started") async def stop(self): self._running = False - if self._task: - self._task.cancel() - try: - await self._task - except asyncio.CancelledError: - pass + for task in (self._task, self._resend_task): + if task: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass # ── Hot-reload ─────────────────────────────────────────────────────────── @@ -187,6 +192,20 @@ class NinaBot: interval = max(60, int(self.config.get("poll_interval", 300))) await asyncio.sleep(interval) + async def _resend_loop(self): + """Re-broadcast all active warnings at resend_interval when send_to_mesh is enabled.""" + while self._running: + interval = max(60, int(self.config.get("resend_interval", 3600))) + await asyncio.sleep(interval) + try: + if self.config.get("enabled") and self.config.get("send_to_mesh", True): + if self._active: + logger.info("NINA resend: %d aktive Warnmeldungen", len(self._active)) + for entry in list(self._active.values()): + await self.send_callback(entry["text"], entry["channel"]) + except Exception: + logger.exception("NINA resend error") + async def _check_alerts(self): min_level = SEVERITY_ORDER.get(self.config.get("min_severity", "Severe"), 2) channel = int(self.config.get("channel", 0)) @@ -377,6 +396,21 @@ class NinaBot: text: str, channel: int, ): + dedup_key = self._normalise_id(identifier) + + # Keep _active up to date for re-broadcast + if msg_type == "Cancel": + self._active.pop(dedup_key, None) + else: + self._active[dedup_key] = { + "text": text, + "channel": channel, + "headline": headline, + "severity": severity, + "id": identifier, + "sent": sent, + } + if self.config.get("send_to_mesh", True): await self.send_callback(text, channel) else: diff --git a/nina.yaml b/nina.yaml index c66f842..1f66473 100644 --- a/nina.yaml +++ b/nina.yaml @@ -1,6 +1,7 @@ enabled: false send_to_mesh: false # true = ins Mesh senden | false = nur Weboberfläche (Monitor-Modus) poll_interval: 300 +resend_interval: 3600 # Aktive Warnungen alle N Sekunden erneut ins Mesh senden channel: 0 min_severity: Severe ags_codes: diff --git a/static/js/nina.js b/static/js/nina.js index 3d28b65..856646f 100644 --- a/static/js/nina.js +++ b/static/js/nina.js @@ -8,17 +8,21 @@ initPage({ onAuth: (user) => { currentUser = user; } }); // ── AGS code list ──────────────────────────────────────────────────────────── function renderAgsList() { - const container = document.getElementById('agsList'); + const tbody = document.getElementById('agsList'); if (agsCodes.length === 0) { - container.innerHTML = 'Keine AGS-Codes konfiguriert.'; + tbody.innerHTML = 'Keine AGS-Codes konfiguriert.'; return; } - container.innerHTML = agsCodes.map((code, idx) => - ` - ${escapeHtml(code)} - - ` + tbody.innerHTML = agsCodes.map((code, idx) => + ` + ${escapeHtml(code)} + + + + ` ).join(''); } @@ -66,11 +70,12 @@ async function loadConfig() { } function applyConfig(cfg) { - document.getElementById('ninaEnabled').checked = !!cfg.enabled; + document.getElementById('ninaEnabled').checked = !!cfg.enabled; document.getElementById('ninaSendToMesh').checked = cfg.send_to_mesh !== false; - document.getElementById('pollInterval').value = cfg.poll_interval ?? 300; - document.getElementById('ninaChannel').value = cfg.channel ?? 0; - document.getElementById('minSeverity').value = cfg.min_severity ?? 'Severe'; + document.getElementById('pollInterval').value = cfg.poll_interval ?? 300; + document.getElementById('resendInterval').value = cfg.resend_interval ?? 3600; + 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(); @@ -92,7 +97,7 @@ function updateStatusBadge(cfg) { 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'; + badge.className = 'badge bg-secondary text-white'; badge.textContent = 'Deaktiviert'; } } @@ -101,11 +106,12 @@ function updateStatusBadge(cfg) { 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) || 300, - channel: parseInt(document.getElementById('ninaChannel').value) || 0, - min_severity: document.getElementById('minSeverity').value, + enabled: document.getElementById('ninaEnabled').checked, + send_to_mesh: document.getElementById('ninaSendToMesh').checked, + poll_interval: parseInt(document.getElementById('pollInterval').value) || 300, + resend_interval: parseInt(document.getElementById('resendInterval').value) || 3600, + channel: parseInt(document.getElementById('ninaChannel').value) || 0, + min_severity: document.getElementById('minSeverity').value, ags_codes: [...agsCodes], sources: { katwarn: document.getElementById('srcKatwarn').checked, @@ -147,10 +153,10 @@ document.getElementById('btnSaveNina').addEventListener('click', async () => { // ── Alerts table ───────────────────────────────────────────────────────────── const SEV_CLASS = { - Extreme: 'danger', - Severe: 'warning', - Moderate: 'info', - Minor: 'secondary', + 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 = { @@ -164,18 +170,20 @@ const SEV_LABEL = { function renderAlerts() { const tbody = document.getElementById('alertsTable'); if (alerts.length === 0) { - tbody.innerHTML = 'Keine Meldungen empfangen.'; + tbody.innerHTML = 'Keine Meldungen empfangen.'; return; } tbody.innerHTML = alerts.map(a => { - const cls = a.msgType === 'Cancel' ? 'secondary' : (SEV_CLASS[a.severity] ?? 'secondary'); + 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 ? '' : ''; return ` - ${escapeHtml(sevLabel)} + ${escapeHtml(sevLabel)} ${escapeHtml(a.headline)} ${escapeHtml(a.id?.split('.')[0] ?? '–')} ${meshIcon} diff --git a/static/nina.html b/static/nina.html index 51632bc..bdbfdde 100644 --- a/static/nina.html +++ b/static/nina.html @@ -71,13 +71,19 @@
-
- +
+ -
Min. 60 Sekunden
+
Neue Warnmeldungen
+ + +
Aktive Warnungen
+
+
@@ -97,12 +103,22 @@
-
+
+ + + + + + + + +
AGS-Code
+
@@ -177,7 +193,7 @@ - Keine Meldungen – NINA aktivieren und AGS-Codes konfigurieren. + Keine Meldungen – NINA aktivieren und AGS-Codes konfigurieren.