feat(nina): Quellenkennung, Schalter nebeneinander, Min.-Intervalle, last_sent (closes #2)
- _format_alert: Präfix [SOURCE@NINA] je nach Quelle - _send(): _last_sent bei Mesh-Versand setzen - get_config(): last_sent zurückgeben - nina.html: Toggles nebeneinander (d-flex gap-4), Intervall-Labels in Min., lastSent-Element ergänzt - nina.js: applyConfig ÷60 / Speichern ×60, lastSent befüllen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
76f04105b7
commit
07676a8c96
14
CHANGELOG.md
14
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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
version: "0.08.21"
|
||||
version: "0.08.22"
|
||||
|
||||
bot:
|
||||
name: "MeshDD-Bot"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@
|
|||
<div class="card-body">
|
||||
<form id="ninaForm">
|
||||
<!-- Enable + send toggles -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<div class="mb-3 d-flex gap-4 flex-wrap">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="ninaEnabled" role="switch">
|
||||
<label class="form-check-label fw-semibold" for="ninaEnabled">Aktiviert</label>
|
||||
</div>
|
||||
|
|
@ -72,15 +72,18 @@
|
|||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-5">
|
||||
<label for="pollInterval" class="form-label">Abfrage­intervall (Sek.)</label>
|
||||
<label for="pollInterval" class="form-label">Abfrage­intervall (Min.)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="pollInterval"
|
||||
min="60" max="3600" step="60" value="300">
|
||||
<div class="form-text">Neue Warnmeldungen<span id="lastPoll" class="d-block text-body-secondary"></span></div>
|
||||
min="1" max="60" step="1" value="5">
|
||||
<div class="form-text">Neue Warnmeldungen
|
||||
<span id="lastPoll" class="d-block text-body-secondary"></span>
|
||||
<span id="lastSent" class="d-block text-body-secondary"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<label for="resendInterval" class="form-label">Wieder­holungsintervall (Sek.)</label>
|
||||
<label for="resendInterval" class="form-label">Wieder­holungsintervall (Min.)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="resendInterval"
|
||||
min="60" step="60" value="3600">
|
||||
min="1" step="1" value="60">
|
||||
<div class="form-text">Aktive Warnungen</div>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
|
|
|
|||
Loading…
Reference in a new issue