MeshDD-Bot/static/js/app.js
ppfeiffer 0ca0ffb0d1 feat: NINA BBK Warn-App Integration (v0.8.0)
Neue NINA-Integration: Automatisches Polling der BBK-Warn-API
(warnung.bund.de/api31) und Weiterleitung von Warnmeldungen ins
Meshtastic-Netz. Separate Admin-Konfigurationsseite (/nina) analog
zum Scheduler.

- meshbot/nina.py: NinaBot – Polling, De-Duplikation, Schweregrad-
  und Quellen-Filterung, WebSocket-Broadcast (nina_alert)
- nina.yaml + conf/nina.yaml: Hot-reload-faehige Konfiguration
- static/nina.html + static/js/nina.js: Konfigurationsseite mit
  AGS-Code-Verwaltung, Quellen-Auswahl und Live-Alerts-Tabelle
- webserver.py: GET/PUT /api/nina/config + GET /nina (Admin-only)
- main.py: NinaBot initialisieren, watch/start/stop im Lifecycle
- app.js: NINA-Sidebar-Eintrag (Admin-only, shield-exclamation)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 11:21:01 +01:00

124 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// MeshDD-Bot Shared page module
// Provides: initPage(), escapeHtml(), applyTheme()
// ── Sidebar definition ────────────────────────────────────────
const _SIDEBAR_LINKS = [
{ href: '/', icon: 'bi-speedometer2', label: 'Dashboard', admin: false },
{ href: '/scheduler', icon: 'bi-clock-history', label: 'Scheduler', admin: true },
{ href: '/nina', icon: 'bi-shield-exclamation', label: 'NINA', admin: true },
{ href: '/map', icon: 'bi-map', label: 'Karte', admin: false },
{ href: '/packets', icon: 'bi-reception-4', label: 'Pakete', admin: false },
{ href: '/settings', icon: 'bi-gear', label: 'Einstellungen',admin: true },
{ href: '/admin', icon: 'bi-people', label: 'Benutzer', admin: true },
];
function _injectSidebar() {
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
const currentPath = window.location.pathname;
sidebar.innerHTML = '<nav class="sidebar-nav">' +
_SIDEBAR_LINKS.map(link => {
const active = currentPath === link.href ? ' active' : '';
const adm = link.admin ? ' sidebar-admin' : '';
return `<a href="${link.href}" class="sidebar-link${active}${adm}">` +
`<i class="bi ${link.icon}"></i><span>${link.label}</span></a>`;
}).join('') +
'</nav>';
}
// ── Navbar ────────────────────────────────────────────────────
function _updateNavbar(user) {
const userMenu = document.getElementById('userMenu');
const loginBtn = document.getElementById('loginBtn');
const userName = document.getElementById('userName');
if (user) {
if (userName) userName.textContent = user.name;
if (userMenu) userMenu.classList.remove('d-none');
if (loginBtn) loginBtn.classList.add('d-none');
} else {
if (userMenu) userMenu.classList.add('d-none');
if (loginBtn) loginBtn.classList.remove('d-none');
}
}
function _updateSidebar(user) {
const isAdmin = user && user.role === 'admin';
document.querySelectorAll('.sidebar-admin').forEach(el => {
el.style.display = isAdmin ? '' : 'none';
});
}
// ── Theme ─────────────────────────────────────────────────────
function applyTheme(theme) {
document.documentElement.setAttribute('data-bs-theme', theme);
const icon = document.getElementById('themeIcon');
if (icon) icon.className = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill';
localStorage.setItem('theme', theme);
document.dispatchEvent(new CustomEvent('themechange', { detail: { theme } }));
}
function _setupTheme() {
applyTheme(localStorage.getItem('theme') || 'light');
const btn = document.getElementById('themeToggle');
if (btn) {
btn.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-bs-theme');
applyTheme(current === 'dark' ? 'light' : 'dark');
});
}
}
// ── Sidebar toggle (mobile) ───────────────────────────────────
function _setupSidebarToggle() {
const toggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
const backdrop = document.getElementById('sidebarBackdrop');
if (toggle && sidebar) toggle.addEventListener('click', () => sidebar.classList.toggle('open'));
if (backdrop && sidebar) backdrop.addEventListener('click', () => sidebar.classList.remove('open'));
}
// ── Utilities ─────────────────────────────────────────────────
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// ── Public init ───────────────────────────────────────────────
function _injectFooter(version) {
const footer = document.getElementById('pageFooter');
if (!footer) return;
const now = new Date();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const yyyy = now.getFullYear();
const ver = version ? ` · v${version}` : '';
footer.textContent = `© MeshDD / PPfeiffer${ver} · ${mm}/${yyyy}`;
}
function initPage({ onAuth = null } = {}) {
_injectSidebar();
_setupTheme();
fetch('/api/auth/me')
.then(r => r.ok ? r.json() : null)
.then(user => {
_updateNavbar(user);
_updateSidebar(user);
_setupSidebarToggle();
if (onAuth) onAuth(user);
});
const vl = document.getElementById('versionLabel');
fetch('/api/stats')
.then(r => r.ok ? r.json() : null)
.then(d => {
if (d?.version && vl) vl.textContent = `v${d.version}`;
_injectFooter(d?.version);
});
}