diff --git a/CHANGELOG.md b/CHANGELOG.md index 694c11f..88694a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.08.22] - 2026-02-20 + +### Changed +- **NINA Quellenkennung** (closes #2 Aufgabe 1): Präfix `[NINA]` → `[DWD@NINA]`, + `[KATWARN@NINA]` usw. je nach Warnquelle. +- **NINA Schalter nebeneinander** (closes #2 Aufgabe 2): "Aktiviert" und + "Ins Mesh senden" liegen jetzt in einer Zeile nebeneinander. +- **NINA Intervalle in Minuten** (closes #2 Aufgabe 3): UI zeigt und speichert + Abfrage- und Wiederholungsintervall in Minuten (intern weiterhin Sekunden). + +### Added +- **NINA Zuletzt gesendet** (closes #2 Aufgabe 4): Neues Feld unter dem + Abfrageintervall zeigt, wann zuletzt eine Meldung ins Mesh gesendet wurde. + ## [0.08.21] - 2026-02-20 ### Changed diff --git a/config/config.yaml b/config/config.yaml index 0885466..64ac69d 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,4 +1,4 @@ -version: "0.08.21" +version: "0.08.22" bot: name: "MeshDD-Bot" diff --git a/meshbot/nina.py b/meshbot/nina.py index 800aad3..c2fe257 100644 --- a/meshbot/nina.py +++ b/meshbot/nina.py @@ -113,6 +113,7 @@ class NinaBot: self._task: asyncio.Task | None = None self._resend_task: asyncio.Task | None = None self._last_poll: str = "" + self._last_sent: str = "" self._load() # ── Config ────────────────────────────────────────────────────────────── @@ -154,7 +155,7 @@ class NinaBot: logger.exception("Error saving nina.yaml") def get_config(self) -> dict: - return {**self.config, "last_poll": self._last_poll} + return {**self.config, "last_poll": self._last_poll, "last_sent": self._last_sent} def get_active_alerts(self) -> list[dict]: return sorted(self._active.values(), key=lambda x: x.get("sent", ""), reverse=True) @@ -351,7 +352,7 @@ class NinaBot: description = data.get("description", "") area = AGS_LABELS.get(ags.ljust(12, "0"), ags) - text = self._format_alert(msg_type, severity, headline, description, area) + text = self._format_alert(msg_type, severity, headline, description, area, src_key or "") logger.info("NINA dashboard alert: %s (id=%s, area=%s)", headline, identifier, area) await self._send(identifier, severity, msg_type, headline, sent, text, channel, area) @@ -379,11 +380,11 @@ class NinaBot: for item in items: try: - await self._process_map_item(item, min_level, channel) + await self._process_map_item(item, min_level, channel, source_key) except Exception: logger.exception("NINA mapData: error processing %s", item.get("id")) - async def _process_map_item(self, item: dict, min_level: int, channel: int): + async def _process_map_item(self, item: dict, min_level: int, channel: int, source_key: str = ""): identifier = item.get("id", "") if not identifier: return @@ -405,19 +406,20 @@ class NinaBot: i18n = item.get("i18nTitle", {}) headline = i18n.get("de") or i18n.get("en") or identifier - text = self._format_alert(msg_type, severity, headline, "") + text = self._format_alert(msg_type, severity, headline, "", "", source_key) logger.info("NINA mapData alert: %s (id=%s)", headline, identifier) await self._send(identifier, severity, msg_type, headline, sent, text, channel, "") # ── Shared helpers ─────────────────────────────────────────────────────── @staticmethod - def _format_alert(msg_type: str, severity: str, headline: str, description: str, area: str = "") -> str: + def _format_alert(msg_type: str, severity: str, headline: str, description: str, area: str = "", source: str = "") -> str: + prefix = f"[{source.upper()}@NINA]" if source else "[NINA]" area_suffix = f" ({area})" if area else "" if msg_type == "Cancel": - return f"[NINA] Aufgehoben: {headline}{area_suffix}" + return f"{prefix} Aufgehoben: {headline}{area_suffix}" sev_text = SEVERITY_LABELS.get(severity, severity) - text = f"[NINA] {sev_text}: {headline}{area_suffix}" + text = f"{prefix} {sev_text}: {headline}{area_suffix}" if description: short = description.strip()[:120] if len(description.strip()) > 120: @@ -456,6 +458,7 @@ class NinaBot: if self.config.get("send_to_mesh", True): await self.send_callback(text, channel) + self._last_sent = datetime.now(timezone.utc).isoformat() else: logger.info("NINA monitor-only: Mesh-Versand deaktiviert, nur WebSocket-Broadcast") diff --git a/static/js/nina.js b/static/js/nina.js index 80a7eae..a8da7bf 100644 --- a/static/js/nina.js +++ b/static/js/nina.js @@ -105,8 +105,8 @@ async function loadConfig() { function applyConfig(cfg) { 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('resendInterval').value = cfg.resend_interval ?? 3600; + 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'; @@ -123,12 +123,15 @@ function applyConfig(cfg) { const lpEl = document.getElementById('lastPoll'); if (lpEl) { - if (cfg.last_poll) { - const d = new Date(cfg.last_poll); - lpEl.textContent = 'Letzte Abfrage: ' + d.toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'medium' }); - } else { - lpEl.textContent = ''; - } + 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' }) + : ''; } } @@ -151,8 +154,8 @@ 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, - resend_interval: parseInt(document.getElementById('resendInterval').value) || 3600, + 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], diff --git a/static/nina.html b/static/nina.html index 15a5c66..7fbac80 100644 --- a/static/nina.html +++ b/static/nina.html @@ -56,8 +56,8 @@
-
-
+
+
@@ -72,15 +72,18 @@
- + -
Neue Warnmeldungen
+ min="1" max="60" step="1" value="5"> +
Neue Warnmeldungen + + +
- + + min="1" step="1" value="60">
Aktive Warnungen