Merge pull request 'v0.8.3: NINA Gebietsanzeige, AGS-Ortsname, Sachsen-Combobox' (#8) from nina_test into main
This commit is contained in:
commit
f2ffe0fd9d
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -1,5 +1,19 @@
|
||||||
# Changelog
|
# 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
|
## [0.8.2] - 2026-02-19
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
version: "0.8.2"
|
version: "0.8.3"
|
||||||
|
|
||||||
bot:
|
bot:
|
||||||
name: "MeshDD-Bot"
|
name: "MeshDD-Bot"
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,23 @@ ID_NORMALIZATIONS = [
|
||||||
("mow.", "mowas."),
|
("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 = {
|
DEFAULT_CONFIG = {
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
"send_to_mesh": True,
|
"send_to_mesh": True,
|
||||||
|
|
@ -283,12 +300,12 @@ class NinaBot:
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
try:
|
try:
|
||||||
await self._process_dashboard_item(item, min_level, channel, sources)
|
await self._process_dashboard_item(item, min_level, channel, sources, ags)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("NINA dashboard: error processing %s", item.get("id"))
|
logger.exception("NINA dashboard: error processing %s", item.get("id"))
|
||||||
|
|
||||||
async def _process_dashboard_item(
|
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", "")
|
identifier = item.get("id", "")
|
||||||
if not identifier:
|
if not identifier:
|
||||||
|
|
@ -316,10 +333,11 @@ class NinaBot:
|
||||||
|
|
||||||
headline = data.get("headline", "Warnung")
|
headline = data.get("headline", "Warnung")
|
||||||
description = data.get("description", "")
|
description = data.get("description", "")
|
||||||
|
area = AGS_LABELS.get(ags.ljust(12, "0"), ags)
|
||||||
|
|
||||||
text = self._format_alert(msg_type, severity, headline, description)
|
text = self._format_alert(msg_type, severity, headline, description, area)
|
||||||
logger.info("NINA dashboard alert: %s (id=%s)", headline, identifier)
|
logger.info("NINA dashboard alert: %s (id=%s, area=%s)", headline, identifier, area)
|
||||||
await self._send(identifier, severity, msg_type, headline, sent, text, channel)
|
await self._send(identifier, severity, msg_type, headline, sent, text, channel, area)
|
||||||
|
|
||||||
# ── mapData endpoint ─────────────────────────────────────────────────────
|
# ── mapData endpoint ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -373,16 +391,17 @@ class NinaBot:
|
||||||
|
|
||||||
text = self._format_alert(msg_type, severity, headline, "")
|
text = self._format_alert(msg_type, severity, headline, "")
|
||||||
logger.info("NINA mapData alert: %s (id=%s)", headline, identifier)
|
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 ───────────────────────────────────────────────────────
|
# ── Shared helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@staticmethod
|
@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":
|
if msg_type == "Cancel":
|
||||||
return f"[NINA] Aufgehoben: {headline}"
|
return f"[NINA] Aufgehoben: {headline}{area_suffix}"
|
||||||
sev_text = SEVERITY_LABELS.get(severity, severity)
|
sev_text = SEVERITY_LABELS.get(severity, severity)
|
||||||
text = f"[NINA] {sev_text}: {headline}"
|
text = f"[NINA] {sev_text}: {headline}{area_suffix}"
|
||||||
if description:
|
if description:
|
||||||
short = description.strip()[:120]
|
short = description.strip()[:120]
|
||||||
if len(description.strip()) > 120:
|
if len(description.strip()) > 120:
|
||||||
|
|
@ -399,6 +418,7 @@ class NinaBot:
|
||||||
sent: str,
|
sent: str,
|
||||||
text: str,
|
text: str,
|
||||||
channel: int,
|
channel: int,
|
||||||
|
area: str = "",
|
||||||
):
|
):
|
||||||
dedup_key = self._normalise_id(identifier)
|
dedup_key = self._normalise_id(identifier)
|
||||||
|
|
||||||
|
|
@ -427,5 +447,6 @@ class NinaBot:
|
||||||
"msgType": msg_type,
|
"msgType": msg_type,
|
||||||
"headline": headline,
|
"headline": headline,
|
||||||
"sent": sent,
|
"sent": sent,
|
||||||
|
"area": area,
|
||||||
"monitor_only": not self.config.get("send_to_mesh", True),
|
"monitor_only": not self.config.get("send_to_mesh", True),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,50 @@ const alerts = [];
|
||||||
|
|
||||||
initPage({ onAuth: (user) => { currentUser = user; } });
|
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 ────────────────────────────────────────────────────────────
|
// ── AGS code list ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function renderAgsList() {
|
function renderAgsList() {
|
||||||
const tbody = document.getElementById('agsList');
|
const tbody = document.getElementById('agsList');
|
||||||
if (agsCodes.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = agsCodes.map((code, idx) =>
|
tbody.innerHTML = agsCodes.map((code, idx) =>
|
||||||
`<tr>
|
`<tr>
|
||||||
<td><code>${escapeHtml(code)}</code></td>
|
<td><code>${escapeHtml(code)}</code></td>
|
||||||
|
<td class="text-body-secondary">${escapeHtml(agsName(code))}</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<button type="button" class="btn btn-outline-danger btn-sm py-0 px-1"
|
<button type="button" class="btn btn-outline-danger btn-sm py-0 px-1"
|
||||||
onclick="removeAgs(${idx})" title="Entfernen">
|
onclick="removeAgs(${idx})" title="Entfernen">
|
||||||
|
|
@ -170,7 +203,7 @@ const SEV_LABEL = {
|
||||||
function renderAlerts() {
|
function renderAlerts() {
|
||||||
const tbody = document.getElementById('alertsTable');
|
const tbody = document.getElementById('alertsTable');
|
||||||
if (alerts.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = alerts.map(a => {
|
tbody.innerHTML = alerts.map(a => {
|
||||||
|
|
@ -182,9 +215,11 @@ function renderAlerts() {
|
||||||
const meshIcon = a.monitor_only
|
const meshIcon = a.monitor_only
|
||||||
? '<i class="bi bi-eye text-warning" title="Nur Weboberfläche"></i>'
|
? '<i class="bi bi-eye text-warning" title="Nur Weboberfläche"></i>'
|
||||||
: '<i class="bi bi-broadcast text-success" title="Ins Mesh gesendet"></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>
|
return `<tr>
|
||||||
<td><span class="badge bg-${bgCls} ${txCls}">${escapeHtml(sevLabel)}</span></td>
|
<td><span class="badge bg-${bgCls} ${txCls}">${escapeHtml(sevLabel)}</span></td>
|
||||||
<td>${escapeHtml(a.headline)}</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><small class="text-body-secondary">${escapeHtml(a.id?.split('.')[0] ?? '–')}</small></td>
|
||||||
<td class="text-center">${meshIcon}</td>
|
<td class="text-center">${meshIcon}</td>
|
||||||
<td><small class="text-body-secondary">${ts}</small></td>
|
<td><small class="text-body-secondary">${ts}</small></td>
|
||||||
|
|
@ -217,5 +252,6 @@ function connectWebSocket() {
|
||||||
|
|
||||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fillDatalist();
|
||||||
loadConfig();
|
loadConfig();
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,8 @@
|
||||||
<table class="table table-sm table-hover table-striped mb-0 align-middle">
|
<table class="table table-sm table-hover table-striped mb-0 align-middle">
|
||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>AGS-Code</th>
|
<th style="width:130px">AGS-Code</th>
|
||||||
|
<th>Ort / Region</th>
|
||||||
<th style="width:42px"></th>
|
<th style="width:42px"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -116,11 +117,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="text" class="form-control" id="agsInput"
|
<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">
|
<button class="btn btn-outline-secondary" type="button" id="btnAddAgs">
|
||||||
<i class="bi bi-plus-lg"></i> Hinzufügen
|
<i class="bi bi-plus-lg"></i> Hinzufügen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<datalist id="agsSachsenList"></datalist>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
8- oder 12-stelliger AGS-Code des Landkreises/der kreisfreien Stadt.
|
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"
|
<a href="https://www.destatis.de/DE/Themen/Laender-Regionen/Regionales/Gemeindeverzeichnis/_inhalt.html"
|
||||||
|
|
@ -187,13 +190,14 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:90px">Schweregrad</th>
|
<th style="width:90px">Schweregrad</th>
|
||||||
<th>Meldung</th>
|
<th>Meldung</th>
|
||||||
|
<th style="width:140px">Gebiet</th>
|
||||||
<th style="width:110px">Typ</th>
|
<th style="width:110px">Typ</th>
|
||||||
<th style="width:60px" class="text-center">Mesh</th>
|
<th style="width:60px" class="text-center">Mesh</th>
|
||||||
<th style="width:130px">Zeitstempel</th>
|
<th style="width:130px">Zeitstempel</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="alertsTable">
|
<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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue