Merge pull request 'v0.8.3: NINA Gebietsanzeige, AGS-Ortsname, Sachsen-Combobox' (#8) from nina_test into main

This commit is contained in:
ppfeiffer 2026-02-19 16:10:32 +01:00
commit f2ffe0fd9d
5 changed files with 90 additions and 15 deletions

View file

@ -1,5 +1,19 @@
# Changelog
## [0.8.3] - 2026-02-19
### Added
- **NINA Gebietsanzeige**: Warnmeldungen zeigen jetzt das Herkunftsgebiet (AGS-Regionsname)
sowohl in der Weboberfläche (neue Spalte "Gebiet" in der Alerts-Tabelle) als auch im
Mesh-Nachrichtentext (z.B. `[NINA] Schwerwiegend: Sturmböen (Dresden, Stadt)`).
- **AGS-Ortsname in der Konfigurationstabelle**: Die AGS-Code-Tabelle zeigt jetzt den
lesbaren Ortsnamen je Code als zweite Spalte an.
- **Sächsische AGS-Combobox**: Bei der Eingabe neuer AGS-Codes schlägt eine Datalist
alle sächsischen Landkreise und kreisfreien Städte vor (Name + Code).
### Fixed
- **colspan**: Leere Zeile in der Alerts-Tabelle auf 6 Spalten aktualisiert.
## [0.8.2] - 2026-02-19
### Fixed

View file

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

View file

@ -54,6 +54,23 @@ ID_NORMALIZATIONS = [
("mow.", "mowas."),
]
# Lesbarer Name je sächsischem AGS-Code (12-stellig)
AGS_LABELS: dict[str, str] = {
"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",
}
DEFAULT_CONFIG = {
"enabled": False,
"send_to_mesh": True,
@ -283,12 +300,12 @@ class NinaBot:
for item in items:
try:
await self._process_dashboard_item(item, min_level, channel, sources)
await self._process_dashboard_item(item, min_level, channel, sources, ags)
except Exception:
logger.exception("NINA dashboard: error processing %s", item.get("id"))
async def _process_dashboard_item(
self, item: dict, min_level: int, channel: int, sources: dict
self, item: dict, min_level: int, channel: int, sources: dict, ags: str = ""
):
identifier = item.get("id", "")
if not identifier:
@ -316,10 +333,11 @@ class NinaBot:
headline = data.get("headline", "Warnung")
description = data.get("description", "")
area = AGS_LABELS.get(ags.ljust(12, "0"), ags)
text = self._format_alert(msg_type, severity, headline, description)
logger.info("NINA dashboard alert: %s (id=%s)", headline, identifier)
await self._send(identifier, severity, msg_type, headline, sent, text, channel)
text = self._format_alert(msg_type, severity, headline, description, area)
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)
# ── mapData endpoint ─────────────────────────────────────────────────────
@ -373,16 +391,17 @@ class NinaBot:
text = self._format_alert(msg_type, severity, headline, "")
logger.info("NINA mapData alert: %s (id=%s)", headline, identifier)
await self._send(identifier, severity, msg_type, headline, sent, text, channel)
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) -> str:
def _format_alert(msg_type: str, severity: str, headline: str, description: str, area: str = "") -> str:
area_suffix = f" ({area})" if area else ""
if msg_type == "Cancel":
return f"[NINA] Aufgehoben: {headline}"
return f"[NINA] Aufgehoben: {headline}{area_suffix}"
sev_text = SEVERITY_LABELS.get(severity, severity)
text = f"[NINA] {sev_text}: {headline}"
text = f"[NINA] {sev_text}: {headline}{area_suffix}"
if description:
short = description.strip()[:120]
if len(description.strip()) > 120:
@ -399,6 +418,7 @@ class NinaBot:
sent: str,
text: str,
channel: int,
area: str = "",
):
dedup_key = self._normalise_id(identifier)
@ -427,5 +447,6 @@ class NinaBot:
"msgType": msg_type,
"headline": headline,
"sent": sent,
"area": area,
"monitor_only": not self.config.get("send_to_mesh", True),
})

View file

@ -5,17 +5,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]) => `<option value="${escapeHtml(code)}">${escapeHtml(name)}</option>`)
.join('');
}
// ── AGS code list ────────────────────────────────────────────────────────────
function renderAgsList() {
const tbody = document.getElementById('agsList');
if (agsCodes.length === 0) {
tbody.innerHTML = '<tr><td colspan="2" class="text-center text-body-secondary py-2"><small>Keine AGS-Codes konfiguriert.</small></td></tr>';
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-body-secondary py-2"><small>Keine AGS-Codes konfiguriert.</small></td></tr>';
return;
}
tbody.innerHTML = agsCodes.map((code, idx) =>
`<tr>
<td><code>${escapeHtml(code)}</code></td>
<td class="text-body-secondary">${escapeHtml(agsName(code))}</td>
<td class="text-end">
<button type="button" class="btn btn-outline-danger btn-sm py-0 px-1"
onclick="removeAgs(${idx})" title="Entfernen">
@ -170,7 +203,7 @@ const SEV_LABEL = {
function renderAlerts() {
const tbody = document.getElementById('alertsTable');
if (alerts.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-body-secondary py-3">Keine Meldungen empfangen.</td></tr>';
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-body-secondary py-3">Keine Meldungen empfangen.</td></tr>';
return;
}
tbody.innerHTML = alerts.map(a => {
@ -182,9 +215,11 @@ function renderAlerts() {
const meshIcon = a.monitor_only
? '<i class="bi bi-eye text-warning" title="Nur Weboberfläche"></i>'
: '<i class="bi bi-broadcast text-success" title="Ins Mesh gesendet"></i>';
const area = a.area ? escapeHtml(a.area) : '<span class="text-body-secondary"></span>';
return `<tr>
<td><span class="badge bg-${bgCls} ${txCls}">${escapeHtml(sevLabel)}</span></td>
<td>${escapeHtml(a.headline)}</td>
<td><small class="text-body-secondary">${area}</small></td>
<td><small class="text-body-secondary">${escapeHtml(a.id?.split('.')[0] ?? '')}</small></td>
<td class="text-center">${meshIcon}</td>
<td><small class="text-body-secondary">${ts}</small></td>
@ -217,5 +252,6 @@ function connectWebSocket() {
// ── Init ──────────────────────────────────────────────────────────────────────
fillDatalist();
loadConfig();
connectWebSocket();

View file

@ -107,7 +107,8 @@
<table class="table table-sm table-hover table-striped mb-0 align-middle">
<thead class="table-dark">
<tr>
<th>AGS-Code</th>
<th style="width:130px">AGS-Code</th>
<th>Ort / Region</th>
<th style="width:42px"></th>
</tr>
</thead>
@ -116,11 +117,13 @@
</div>
<div class="input-group input-group-sm">
<input type="text" class="form-control" id="agsInput"
placeholder="z.B. 091620000000 (München)" maxlength="12">
list="agsSachsenList"
placeholder="Code oder Ort eingeben…" maxlength="50">
<button class="btn btn-outline-secondary" type="button" id="btnAddAgs">
<i class="bi bi-plus-lg"></i> Hinzufügen
</button>
</div>
<datalist id="agsSachsenList"></datalist>
<div class="form-text">
8- oder 12-stelliger AGS-Code des Landkreises/der kreisfreien Stadt.
<a href="https://www.destatis.de/DE/Themen/Laender-Regionen/Regionales/Gemeindeverzeichnis/_inhalt.html"
@ -187,13 +190,14 @@
<tr>
<th style="width:90px">Schweregrad</th>
<th>Meldung</th>
<th style="width:140px">Gebiet</th>
<th style="width:110px">Typ</th>
<th style="width:60px" class="text-center">Mesh</th>
<th style="width:130px">Zeitstempel</th>
</tr>
</thead>
<tbody id="alertsTable">
<tr><td colspan="5" class="text-center text-body-secondary py-3">Keine Meldungen NINA aktivieren und AGS-Codes konfigurieren.</td></tr>
<tr><td colspan="6" class="text-center text-body-secondary py-3">Keine Meldungen NINA aktivieren und AGS-Codes konfigurieren.</td></tr>
</tbody>
</table>
</div>