diff --git a/CHANGELOG.md b/CHANGELOG.md index 5368c00..82530d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.08.24] - 2026-02-20 + +### Added +- **Sidebar: Konfigurationen-Gruppe** (closes #4 Aufgabe 1–2): Neue Gruppenüberschrift + mit eingerückten Untereinträgen für Scheduler, NINA und Konfiguration. + CSS-Klassen `.sidebar-group-label` und `.sidebar-link-sub` ergänzt. +- **Neue Seite `/config`** (closes #4 Aufgabe 3–4): Bearbeitbare Bot-Konfiguration + (Bot, Meshtastic, Web, Links). `GET/PUT /api/config` (Admin). + `config.py`: `save()`-Funktion für persistentes Schreiben in `config.yaml`. + +### Changed +- **Umbenennung MeshDD-Bot → MeshDD-Dashboard** (closes #4 Aufgabe 5): + Alle HTML-Seiten (`` + Navbar-Text) umbenannt. + ## [0.08.23] - 2026-02-20 ### Fixed diff --git a/config/config.yaml b/config/config.yaml index 09d7ff7..507b0e8 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,4 +1,4 @@ -version: "0.08.23" +version: "0.08.24" bot: name: "MeshDD-Bot" diff --git a/meshbot/config.py b/meshbot/config.py index c176fd9..d9eed49 100644 --- a/meshbot/config.py +++ b/meshbot/config.py @@ -48,6 +48,24 @@ async def watch(interval: float = 2.0): _reload_if_changed() +def _deep_merge(base: dict, updates: dict): + for k, v in updates.items(): + if isinstance(v, dict) and isinstance(base.get(k), dict): + _deep_merge(base[k], v) + else: + base[k] = v + + +def save(updates: dict): + """Deep-merge updates into the live config and persist to config.yaml.""" + global _config, _mtime + _deep_merge(_config, updates) + with open(CONFIG_PATH, "w") as f: + yaml.dump(_config, f, default_flow_style=False, allow_unicode=True, sort_keys=False) + _mtime = os.path.getmtime(CONFIG_PATH) + logger.info("Config saved") + + def get(key: str, default=None): keys = key.split(".") val = _config diff --git a/meshbot/webserver.py b/meshbot/webserver.py index 11e5729..db0b6bf 100644 --- a/meshbot/webserver.py +++ b/meshbot/webserver.py @@ -81,6 +81,8 @@ class WebServer: self.app.router.add_put("/api/nina/config", self._api_nina_update) self.app.router.add_get("/api/nina/alerts", self._api_nina_alerts) self.app.router.add_get("/api/links", self._api_links) + self.app.router.add_get("/api/config", self._api_config_get) + self.app.router.add_put("/api/config", self._api_config_update) self.app.router.add_get("/login", self._serve_login) self.app.router.add_get("/register", self._serve_login) self.app.router.add_get("/admin", self._serve_admin) @@ -90,6 +92,7 @@ class WebServer: self.app.router.add_get("/map", self._serve_map) self.app.router.add_get("/packets", self._serve_packets) self.app.router.add_get("/messages", self._serve_messages) + self.app.router.add_get("/config", self._serve_config) self.app.router.add_get("/", self._serve_index) self.app.router.add_static("/static", STATIC_DIR) @@ -220,6 +223,29 @@ class WebServer: async def _serve_nina(self, request: web.Request) -> web.Response: return web.FileResponse(os.path.join(STATIC_DIR, "nina.html")) + async def _serve_config(self, request: web.Request) -> web.Response: + return web.FileResponse(os.path.join(STATIC_DIR, "config.html")) + + async def _api_config_get(self, request: web.Request) -> web.Response: + require_admin_api(request) + return web.json_response({ + "bot": config.get("bot", {}), + "meshtastic": config.get("meshtastic", {}), + "web": { + "port": config.get("web.port", 8081), + "online_threshold": config.get("web.online_threshold", 900), + }, + "links": config.get("links", []) or [], + }) + + async def _api_config_update(self, request: web.Request) -> web.Response: + require_admin_api(request) + data = await request.json() + allowed_keys = {"bot", "meshtastic", "web", "links"} + updates = {k: v for k, v in data.items() if k in allowed_keys} + config.save(updates) + return web.json_response({"ok": True}) + async def _api_nina_get(self, request: web.Request) -> web.Response: require_admin_api(request) if not self.nina: diff --git a/static/admin.html b/static/admin.html index 35756a7..6f969d0 100644 --- a/static/admin.html +++ b/static/admin.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>MeshDD-Bot Admin + MeshDD-Dashboard Admin @@ -15,7 +15,7 @@ - MeshDD-Bot + MeshDD-Dashboard
diff --git a/static/config.html b/static/config.html new file mode 100644 index 0000000..ecf2e5b --- /dev/null +++ b/static/config.html @@ -0,0 +1,147 @@ + + + + + + MeshDD-Dashboard Konfiguration + + + + + + + + + + + + +
+
+
Konfiguration
+
+ + +
+
+ +
+ +
+
+
Bot
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
Meshtastic
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
Webserver
+
+
+ + +
+
+ + +
Node gilt als online wenn zuletzt innerhalb dieser Zeit gesehen.
+
+
+
+
+ + +
+
+
Links
+
+
+ + + + + + + + + +
LabelURL
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ + + + + + + + diff --git a/static/css/style.css b/static/css/style.css index cd9573e..74fa40c 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -80,6 +80,22 @@ color: var(--bs-secondary-color); } +.sidebar-group-label { + display: block; + padding: .75rem .75rem .2rem; + font-size: .65rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .08em; + color: var(--bs-secondary-color); + border-top: 1px solid var(--tblr-border-color, var(--bs-border-color)); + margin-top: .25rem; +} + +.sidebar-link-sub { + padding-left: 1.5rem; +} + /* ── Mobile sidebar ──────────────────────────────────────────── */ .sidebar-backdrop { diff --git a/static/index.html b/static/index.html index becb37a..3dddfcb 100644 --- a/static/index.html +++ b/static/index.html @@ -3,7 +3,7 @@ - MeshDD-Bot Dashboard + MeshDD-Dashboard Dashboard @@ -16,7 +16,7 @@ - MeshDD-Bot + MeshDD-Dashboard
diff --git a/static/js/app.js b/static/js/app.js index 597a054..6ecbb1f 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,17 +1,18 @@ -// MeshDD-Bot – Shared page module +// MeshDD-Dashboard – Shared page module // Provides: initPage(), escapeHtml(), applyTheme() // ── Sidebar definition ──────────────────────────────────────── const _SIDEBAR_LINKS = [ - { href: '/', icon: 'bi-speedometer2', label: 'Dashboard', admin: false, user: false }, - { href: '/scheduler', icon: 'bi-clock-history', label: 'Scheduler', admin: true, user: false }, - { href: '/nina', icon: 'bi-shield-exclamation', label: 'NINA', admin: true, user: false }, - { href: '/map', icon: 'bi-map', label: 'Karte', admin: false, user: false }, - { href: '/packets', icon: 'bi-reception-4', label: 'Pakete', admin: false, user: false }, - { href: '/messages', icon: 'bi-chat-dots', label: 'Nachrichten', admin: false, user: true }, - { href: '/settings', icon: 'bi-gear', label: 'Einstellungen',admin: true, user: false }, - { href: '/admin', icon: 'bi-people', label: 'Benutzer', admin: true, user: false }, + { href: '/', icon: 'bi-speedometer2', label: 'Dashboard', admin: false, user: false }, + { href: '/map', icon: 'bi-map', label: 'Karte', admin: false, user: false }, + { href: '/packets', icon: 'bi-reception-4', label: 'Pakete', admin: false, user: false }, + { href: '/messages', icon: 'bi-chat-dots', label: 'Nachrichten', admin: false, user: true }, + { type: 'group', label: 'Konfigurationen',admin: true }, + { href: '/scheduler', icon: 'bi-clock-history', label: 'Scheduler', admin: true, user: false, sub: true }, + { href: '/nina', icon: 'bi-shield-exclamation', label: 'NINA', admin: true, user: false, sub: true }, + { href: '/config', icon: 'bi-sliders', label: 'Einstellungen', admin: true, user: false, sub: true }, + { href: '/admin', icon: 'bi-people', label: 'Benutzer', admin: true, user: false }, ]; function _injectSidebar() { @@ -20,10 +21,15 @@ function _injectSidebar() { const currentPath = window.location.pathname; sidebar.innerHTML = ''; diff --git a/static/js/config.js b/static/js/config.js new file mode 100644 index 0000000..ef13d83 --- /dev/null +++ b/static/js/config.js @@ -0,0 +1,112 @@ +let links = []; + +initPage({ onAuth: (user) => { + if (!user || user.role !== 'admin') { + window.location.href = '/'; + } +}}); + +// ── Links table ─────────────────────────────────────────────── + +function renderLinks() { + const tbody = document.getElementById('linksList'); + if (links.length === 0) { + tbody.innerHTML = 'Keine Links.'; + return; + } + tbody.innerHTML = links.map((l, i) => + ` + ${escapeHtml(l.label)} + ${escapeHtml(l.url)} + + + + ` + ).join(''); +} + +function removeLink(idx) { + links.splice(idx, 1); + renderLinks(); +} + +document.getElementById('btnAddLink').addEventListener('click', () => { + const label = document.getElementById('newLinkLabel').value.trim(); + const url = document.getElementById('newLinkUrl').value.trim(); + if (!label || !url) return; + links.push({ label, url }); + renderLinks(); + document.getElementById('newLinkLabel').value = ''; + document.getElementById('newLinkUrl').value = ''; +}); + +// ── Load ───────────────────────────────────────────────────── + +async function loadConfig() { + try { + const resp = await fetch('/api/config'); + if (!resp.ok) return; + const cfg = await resp.json(); + + document.getElementById('botName').value = cfg.bot?.name ?? ''; + document.getElementById('botPrefix').value = cfg.bot?.command_prefix ?? ''; + document.getElementById('meshHost').value = cfg.meshtastic?.host ?? ''; + document.getElementById('meshPort').value = cfg.meshtastic?.port ?? ''; + document.getElementById('webPort').value = cfg.web?.port ?? ''; + document.getElementById('onlineThreshold').value = cfg.web?.online_threshold ?? ''; + + links = Array.isArray(cfg.links) ? [...cfg.links] : []; + renderLinks(); + } catch (e) { + console.error('Config load failed:', e); + } +} + +// ── Save ────────────────────────────────────────────────────── + +document.getElementById('btnSave').addEventListener('click', async () => { + const payload = { + bot: { + name: document.getElementById('botName').value.trim(), + command_prefix: document.getElementById('botPrefix').value.trim(), + }, + meshtastic: { + host: document.getElementById('meshHost').value.trim(), + port: parseInt(document.getElementById('meshPort').value) || 4403, + }, + web: { + port: parseInt(document.getElementById('webPort').value) || 8081, + online_threshold: parseInt(document.getElementById('onlineThreshold').value) || 900, + }, + links: [...links], + }; + + const statusEl = document.getElementById('saveStatus'); + try { + const resp = await fetch('/api/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + if (resp.ok) { + statusEl.textContent = 'Gespeichert ✓'; + statusEl.className = 'small text-success'; + statusEl.classList.remove('d-none'); + setTimeout(() => statusEl.classList.add('d-none'), 3000); + } else { + statusEl.textContent = 'Fehler beim Speichern'; + statusEl.className = 'small text-danger'; + statusEl.classList.remove('d-none'); + } + } catch (e) { + console.error('Save failed:', e); + statusEl.textContent = 'Netzwerkfehler'; + statusEl.className = 'small text-danger'; + statusEl.classList.remove('d-none'); + } +}); + +loadConfig(); diff --git a/static/login.html b/static/login.html index 1df0d50..ec5d3cc 100644 --- a/static/login.html +++ b/static/login.html @@ -3,7 +3,7 @@ - MeshDD-Bot Login + MeshDD-Dashboard Login @@ -12,7 +12,7 @@