v0.8.4: NINA Sofortabfrage nach Speichern + Zeitstempel letzte Abfrage

- Konfiguration speichern löst sofort eine NINA-Abfrage aus (trigger_poll)
- Frontend lädt Config 5s nach Save neu um last_poll-Zeitstempel zu zeigen
- Unterhalb Abfrageintervall-Feld: Datum/Uhrzeit der letzten Abfrage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ppfeiffer 2026-02-19 16:18:15 +01:00
parent f2ffe0fd9d
commit 2db03510c8
6 changed files with 40 additions and 3 deletions

View file

@ -1,5 +1,15 @@
# Changelog # Changelog
## [0.8.4] - 2026-02-19
### Added
- **NINA Sofortabfrage nach Speichern**: Nach dem Speichern der Konfiguration wird
unmittelbar eine Warnmeldungsabfrage gestartet (kein Warten auf den nächsten
Intervall-Zyklus). Das Frontend lädt die Konfiguration 5 Sekunden nach dem Speichern
automatisch neu, um den Zeitstempel der letzten Abfrage zu aktualisieren.
- **Letzte Abfrage anzeigen**: Unterhalb des Abfrageintervall-Felds wird Datum und Uhrzeit
der letzten erfolgreichen NINA-Abfrage angezeigt (`last_poll`-Feld im Config-Response).
## [0.8.3] - 2026-02-19 ## [0.8.3] - 2026-02-19
### Added ### Added

View file

@ -1,4 +1,4 @@
version: "0.8.3" version: "0.8.4"
bot: bot:
name: "MeshDD-Bot" name: "MeshDD-Bot"

View file

@ -1,6 +1,7 @@
import asyncio import asyncio
import logging import logging
import os import os
from datetime import datetime, timezone
from typing import Callable, Awaitable from typing import Callable, Awaitable
import aiohttp import aiohttp
@ -111,6 +112,7 @@ class NinaBot:
self._running = False self._running = False
self._task: asyncio.Task | None = None self._task: asyncio.Task | None = None
self._resend_task: asyncio.Task | None = None self._resend_task: asyncio.Task | None = None
self._last_poll: str = ""
self._load() self._load()
# ── Config ────────────────────────────────────────────────────────────── # ── Config ──────────────────────────────────────────────────────────────
@ -152,7 +154,7 @@ class NinaBot:
logger.exception("Error saving nina.yaml") logger.exception("Error saving nina.yaml")
def get_config(self) -> dict: def get_config(self) -> dict:
return self.config return {**self.config, "last_poll": self._last_poll}
def update_config(self, updates: dict) -> dict: def update_config(self, updates: dict) -> dict:
if "sources" in updates: if "sources" in updates:
@ -199,11 +201,22 @@ class NinaBot:
# ── Polling ────────────────────────────────────────────────────────────── # ── Polling ──────────────────────────────────────────────────────────────
async def trigger_poll(self) -> None:
"""Run _check_alerts immediately (e.g. triggered after config save)."""
if not self.config.get("enabled"):
return
try:
await self._check_alerts()
self._last_poll = datetime.now(timezone.utc).isoformat()
except Exception:
logger.exception("NINA triggered poll error")
async def _poll_loop(self): async def _poll_loop(self):
while self._running: while self._running:
try: try:
if self.config.get("enabled"): if self.config.get("enabled"):
await self._check_alerts() await self._check_alerts()
self._last_poll = datetime.now(timezone.utc).isoformat()
except Exception: except Exception:
logger.exception("NINA polling error") logger.exception("NINA polling error")
interval = max(60, int(self.config.get("poll_interval", 300))) interval = max(60, int(self.config.get("poll_interval", 300)))

View file

@ -219,6 +219,7 @@ class WebServer:
return web.json_response({"error": "NINA not available"}, status=503) return web.json_response({"error": "NINA not available"}, status=503)
updates = await request.json() updates = await request.json()
cfg = self.nina.update_config(updates) cfg = self.nina.update_config(updates)
asyncio.create_task(self.nina.trigger_poll())
return web.json_response(cfg) return web.json_response(cfg)
async def _api_scheduler_get(self, request: web.Request) -> web.Response: async def _api_scheduler_get(self, request: web.Request) -> web.Response:

View file

@ -120,6 +120,16 @@ function applyConfig(cfg) {
document.getElementById('srcDwd').checked = src.dwd !== false; document.getElementById('srcDwd').checked = src.dwd !== false;
document.getElementById('srcLhp').checked = src.lhp !== false; document.getElementById('srcLhp').checked = src.lhp !== false;
document.getElementById('srcPolice').checked = !!src.police; document.getElementById('srcPolice').checked = !!src.police;
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 = '';
}
}
} }
function updateStatusBadge(cfg) { function updateStatusBadge(cfg) {
@ -166,10 +176,13 @@ document.getElementById('btnSaveNina').addEventListener('click', async () => {
if (resp.ok) { if (resp.ok) {
const cfg = await resp.json(); const cfg = await resp.json();
updateStatusBadge(cfg); updateStatusBadge(cfg);
applyConfig(cfg);
statusEl.textContent = 'Gespeichert ✓'; statusEl.textContent = 'Gespeichert ✓';
statusEl.className = 'align-self-center small text-success'; statusEl.className = 'align-self-center small text-success';
statusEl.classList.remove('d-none'); statusEl.classList.remove('d-none');
setTimeout(() => statusEl.classList.add('d-none'), 3000); setTimeout(() => statusEl.classList.add('d-none'), 3000);
// Config nach kurzer Pause nachladen, damit last_poll den abgeschlossenen Poll zeigt
setTimeout(loadConfig, 5000);
} else { } else {
statusEl.textContent = 'Fehler beim Speichern'; statusEl.textContent = 'Fehler beim Speichern';
statusEl.className = 'align-self-center small text-danger'; statusEl.className = 'align-self-center small text-danger';

View file

@ -75,7 +75,7 @@
<label for="pollInterval" class="form-label">Abfrage&shy;intervall (Sek.)</label> <label for="pollInterval" class="form-label">Abfrage&shy;intervall (Sek.)</label>
<input type="number" class="form-control form-control-sm" id="pollInterval" <input type="number" class="form-control form-control-sm" id="pollInterval"
min="60" max="3600" step="60" value="300"> min="60" max="3600" step="60" value="300">
<div class="form-text">Neue Warnmeldungen</div> <div class="form-text">Neue Warnmeldungen<span id="lastPoll" class="d-block text-body-secondary"></span></div>
</div> </div>
<div class="col-5"> <div class="col-5">
<label for="resendInterval" class="form-label">Wieder&shy;holungsintervall (Sek.)</label> <label for="resendInterval" class="form-label">Wieder&shy;holungsintervall (Sek.)</label>