feat(ui): Sidebar-Gruppe Konfigurationen, /config-Seite, MeshDD-Dashboard (closes #4)
- app.js: Sidebar um Gruppen-Support erweitert; Konfigurationen-Gruppe mit Scheduler, NINA, Einstellungen (/config) als Untereinträge - style.css: .sidebar-group-label + .sidebar-link-sub - config.py: save()-Funktion für persistentes Schreiben in config.yaml - webserver.py: GET/PUT /api/config + GET /config Route (Admin) - static/config.html + static/js/config.js: neue Konfigurationsseite (Bot, Meshtastic, Web, Links editierbar) - Alle HTML-Dateien: MeshDD-Bot → MeshDD-Dashboard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cbe934ef6e
commit
511ff20842
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -1,5 +1,19 @@
|
||||||
# Changelog
|
# 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 (`<title>` + Navbar-Text) umbenannt.
|
||||||
|
|
||||||
## [0.08.23] - 2026-02-20
|
## [0.08.23] - 2026-02-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
version: "0.08.23"
|
version: "0.08.24"
|
||||||
|
|
||||||
bot:
|
bot:
|
||||||
name: "MeshDD-Bot"
|
name: "MeshDD-Bot"
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,24 @@ async def watch(interval: float = 2.0):
|
||||||
_reload_if_changed()
|
_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):
|
def get(key: str, default=None):
|
||||||
keys = key.split(".")
|
keys = key.split(".")
|
||||||
val = _config
|
val = _config
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,8 @@ class WebServer:
|
||||||
self.app.router.add_put("/api/nina/config", self._api_nina_update)
|
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/nina/alerts", self._api_nina_alerts)
|
||||||
self.app.router.add_get("/api/links", self._api_links)
|
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("/login", self._serve_login)
|
||||||
self.app.router.add_get("/register", self._serve_login)
|
self.app.router.add_get("/register", self._serve_login)
|
||||||
self.app.router.add_get("/admin", self._serve_admin)
|
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("/map", self._serve_map)
|
||||||
self.app.router.add_get("/packets", self._serve_packets)
|
self.app.router.add_get("/packets", self._serve_packets)
|
||||||
self.app.router.add_get("/messages", self._serve_messages)
|
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_get("/", self._serve_index)
|
||||||
self.app.router.add_static("/static", STATIC_DIR)
|
self.app.router.add_static("/static", STATIC_DIR)
|
||||||
|
|
||||||
|
|
@ -220,6 +223,29 @@ class WebServer:
|
||||||
async def _serve_nina(self, request: web.Request) -> web.Response:
|
async def _serve_nina(self, request: web.Request) -> web.Response:
|
||||||
return web.FileResponse(os.path.join(STATIC_DIR, "nina.html"))
|
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:
|
async def _api_nina_get(self, request: web.Request) -> web.Response:
|
||||||
require_admin_api(request)
|
require_admin_api(request)
|
||||||
if not self.nina:
|
if not self.nina:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>MeshDD-Bot Admin</title>
|
<title>MeshDD-Dashboard Admin</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<i class="bi bi-list fs-5"></i>
|
<i class="bi bi-list fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="fw-bold me-auto">
|
<span class="fw-bold me-auto">
|
||||||
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
|
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Dashboard
|
||||||
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
||||||
</span>
|
</span>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
|
|
||||||
147
static/config.html
Normal file
147
static/config.html
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de" data-bs-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>MeshDD-Dashboard Konfiguration</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body class="antialiased">
|
||||||
|
<!-- Top Navbar -->
|
||||||
|
<nav class="top-navbar d-flex align-items-center px-3">
|
||||||
|
<button class="btn btn-link text-body p-0 me-2 d-lg-none" id="sidebarToggle">
|
||||||
|
<i class="bi bi-list fs-5"></i>
|
||||||
|
</button>
|
||||||
|
<span class="fw-bold me-auto">
|
||||||
|
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Dashboard
|
||||||
|
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
||||||
|
</span>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span id="userMenu" class="d-none">
|
||||||
|
<small class="text-body-secondary me-1"><i class="bi bi-person-fill me-1"></i><span id="userName"></span></small>
|
||||||
|
<a href="/auth/logout" class="btn btn-sm btn-outline-secondary py-0 px-1" title="Abmelden">
|
||||||
|
<i class="bi bi-box-arrow-right" style="font-size:.75rem"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<a href="/login" id="loginBtn" class="btn btn-sm btn-outline-info py-0 px-1 d-none">
|
||||||
|
<i class="bi bi-person" style="font-size:.75rem"></i> Login
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary py-0 px-1" id="themeToggle" title="Theme wechseln">
|
||||||
|
<i class="bi bi-sun-fill" id="themeIcon" style="font-size:.75rem"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<aside class="sidebar" id="sidebar"></aside>
|
||||||
|
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<main class="content-wrapper">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h6 class="mb-0"><i class="bi bi-sliders me-1 text-info"></i>Konfiguration</h6>
|
||||||
|
<div class="d-flex gap-2 align-items-center">
|
||||||
|
<span id="saveStatus" class="d-none small"></span>
|
||||||
|
<button class="btn btn-sm btn-primary" id="btnSave">
|
||||||
|
<i class="bi bi-floppy me-1"></i>Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<!-- Bot -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-outline card-info">
|
||||||
|
<div class="card-header"><i class="bi bi-robot me-1"></i>Bot</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="botName" class="form-label">Name</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="botName" placeholder="MeshDD-Bot">
|
||||||
|
</div>
|
||||||
|
<div class="mb-0">
|
||||||
|
<label for="botPrefix" class="form-label">Befehlspräfix</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="botPrefix" maxlength="3" placeholder="?">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Meshtastic -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-outline card-warning">
|
||||||
|
<div class="card-header"><i class="bi bi-broadcast me-1"></i>Meshtastic</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="meshHost" class="form-label">Host / IP</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="meshHost" placeholder="192.168.1.1">
|
||||||
|
</div>
|
||||||
|
<div class="mb-0">
|
||||||
|
<label for="meshPort" class="form-label">Port</label>
|
||||||
|
<input type="number" class="form-control form-control-sm" id="meshPort" min="1" max="65535" placeholder="4403">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Web -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-outline card-success">
|
||||||
|
<div class="card-header"><i class="bi bi-globe me-1"></i>Webserver</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="webPort" class="form-label">Port</label>
|
||||||
|
<input type="number" class="form-control form-control-sm" id="webPort" min="1" max="65535" placeholder="8081">
|
||||||
|
</div>
|
||||||
|
<div class="mb-0">
|
||||||
|
<label for="onlineThreshold" class="form-label">Online-Schwellwert (Sek.)</label>
|
||||||
|
<input type="number" class="form-control form-control-sm" id="onlineThreshold" min="60" step="60" placeholder="900">
|
||||||
|
<div class="form-text">Node gilt als online wenn zuletzt innerhalb dieser Zeit gesehen.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Links -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-outline card-secondary">
|
||||||
|
<div class="card-header"><i class="bi bi-link-45deg me-1"></i>Links</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive mb-2" style="max-height:160px;overflow-y:auto">
|
||||||
|
<table class="table table-sm table-hover table-striped mb-0 align-middle">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Label</th>
|
||||||
|
<th>URL</th>
|
||||||
|
<th style="width:42px"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="linksList"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-4">
|
||||||
|
<input type="text" class="form-control form-control-sm" id="newLinkLabel" placeholder="Label">
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<input type="text" class="form-control form-control-sm" id="newLinkUrl" placeholder="https://...">
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm w-100" id="btnAddLink" type="button">
|
||||||
|
<i class="bi bi-plus-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer id="pageFooter" class="page-footer"></footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="/static/js/app.js"></script>
|
||||||
|
<script src="/static/js/config.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -80,6 +80,22 @@
|
||||||
color: var(--bs-secondary-color);
|
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 ──────────────────────────────────────────── */
|
/* ── Mobile sidebar ──────────────────────────────────────────── */
|
||||||
|
|
||||||
.sidebar-backdrop {
|
.sidebar-backdrop {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>MeshDD-Bot Dashboard</title>
|
<title>MeshDD-Dashboard Dashboard</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<i class="bi bi-list fs-5"></i>
|
<i class="bi bi-list fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="fw-bold me-auto">
|
<span class="fw-bold me-auto">
|
||||||
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
|
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Dashboard
|
||||||
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
||||||
</span>
|
</span>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
// MeshDD-Bot – Shared page module
|
// MeshDD-Dashboard – Shared page module
|
||||||
// Provides: initPage(), escapeHtml(), applyTheme()
|
// Provides: initPage(), escapeHtml(), applyTheme()
|
||||||
|
|
||||||
// ── Sidebar definition ────────────────────────────────────────
|
// ── Sidebar definition ────────────────────────────────────────
|
||||||
|
|
||||||
const _SIDEBAR_LINKS = [
|
const _SIDEBAR_LINKS = [
|
||||||
{ href: '/', icon: 'bi-speedometer2', label: 'Dashboard', admin: false, user: false },
|
{ href: '/', icon: 'bi-speedometer2', label: 'Dashboard', admin: false, user: false },
|
||||||
{ href: '/scheduler', icon: 'bi-clock-history', label: 'Scheduler', admin: true, user: false },
|
{ href: '/map', icon: 'bi-map', label: 'Karte', admin: false, user: false },
|
||||||
{ href: '/nina', icon: 'bi-shield-exclamation', label: 'NINA', admin: true, user: false },
|
{ href: '/packets', icon: 'bi-reception-4', label: 'Pakete', admin: false, user: false },
|
||||||
{ href: '/map', icon: 'bi-map', label: 'Karte', admin: false, user: false },
|
{ href: '/messages', icon: 'bi-chat-dots', label: 'Nachrichten', admin: false, user: true },
|
||||||
{ href: '/packets', icon: 'bi-reception-4', label: 'Pakete', admin: false, user: false },
|
{ type: 'group', label: 'Konfigurationen',admin: true },
|
||||||
{ href: '/messages', icon: 'bi-chat-dots', label: 'Nachrichten', admin: false, user: true },
|
{ href: '/scheduler', icon: 'bi-clock-history', label: 'Scheduler', admin: true, user: false, sub: true },
|
||||||
{ href: '/settings', icon: 'bi-gear', label: 'Einstellungen',admin: true, user: false },
|
{ href: '/nina', icon: 'bi-shield-exclamation', label: 'NINA', admin: true, user: false, sub: true },
|
||||||
{ href: '/admin', icon: 'bi-people', label: 'Benutzer', admin: true, user: false },
|
{ 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() {
|
function _injectSidebar() {
|
||||||
|
|
@ -20,10 +21,15 @@ function _injectSidebar() {
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
sidebar.innerHTML = '<nav class="sidebar-nav">' +
|
sidebar.innerHTML = '<nav class="sidebar-nav">' +
|
||||||
_SIDEBAR_LINKS.map(link => {
|
_SIDEBAR_LINKS.map(link => {
|
||||||
const active = currentPath === link.href ? ' active' : '';
|
if (link.type === 'group') {
|
||||||
const adm = link.admin ? ' sidebar-admin' : '';
|
const adm = link.admin ? ' sidebar-admin' : '';
|
||||||
const usr = link.user ? ' sidebar-user' : '';
|
return `<span class="sidebar-group-label${adm}">${link.label}</span>`;
|
||||||
return `<a href="${link.href}" class="sidebar-link${active}${adm}${usr}">` +
|
}
|
||||||
|
const active = currentPath === link.href ? ' active' : '';
|
||||||
|
const adm = link.admin ? ' sidebar-admin' : '';
|
||||||
|
const usr = link.user ? ' sidebar-user' : '';
|
||||||
|
const sub = link.sub ? ' sidebar-link-sub' : '';
|
||||||
|
return `<a href="${link.href}" class="sidebar-link${sub}${active}${adm}${usr}">` +
|
||||||
`<i class="bi ${link.icon}"></i><span>${link.label}</span></a>`;
|
`<i class="bi ${link.icon}"></i><span>${link.label}</span></a>`;
|
||||||
}).join('') +
|
}).join('') +
|
||||||
'</nav>';
|
'</nav>';
|
||||||
|
|
|
||||||
112
static/js/config.js
Normal file
112
static/js/config.js
Normal file
|
|
@ -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 = '<tr><td colspan="3" class="text-center text-body-secondary py-2"><small>Keine Links.</small></td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = links.map((l, i) =>
|
||||||
|
`<tr>
|
||||||
|
<td><small>${escapeHtml(l.label)}</small></td>
|
||||||
|
<td><small class="text-body-secondary">${escapeHtml(l.url)}</small></td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm py-0 px-1"
|
||||||
|
onclick="removeLink(${i})" title="Entfernen">
|
||||||
|
<i class="bi bi-trash" style="font-size:.75rem"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>`
|
||||||
|
).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();
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>MeshDD-Bot Login</title>
|
<title>MeshDD-Dashboard Login</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<!-- Top Navbar -->
|
<!-- Top Navbar -->
|
||||||
<nav class="top-navbar d-flex align-items-center px-3">
|
<nav class="top-navbar d-flex align-items-center px-3">
|
||||||
<span class="fw-bold me-auto">
|
<span class="fw-bold me-auto">
|
||||||
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
|
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Dashboard
|
||||||
</span>
|
</span>
|
||||||
<button class="btn btn-sm btn-outline-secondary py-0 px-1" id="themeToggle" title="Theme wechseln">
|
<button class="btn btn-sm btn-outline-secondary py-0 px-1" id="themeToggle" title="Theme wechseln">
|
||||||
<i class="bi bi-sun-fill" id="themeIcon" style="font-size:.75rem"></i>
|
<i class="bi bi-sun-fill" id="themeIcon" style="font-size:.75rem"></i>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>MeshDD-Bot Karte</title>
|
<title>MeshDD-Dashboard Karte</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<i class="bi bi-list fs-5"></i>
|
<i class="bi bi-list fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="fw-bold me-auto">
|
<span class="fw-bold me-auto">
|
||||||
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
|
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Dashboard
|
||||||
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
||||||
</span>
|
</span>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>MeshDD-Bot Nachrichten</title>
|
<title>MeshDD-Dashboard Nachrichten</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<i class="bi bi-list fs-5"></i>
|
<i class="bi bi-list fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="fw-bold me-auto">
|
<span class="fw-bold me-auto">
|
||||||
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
|
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Dashboard
|
||||||
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
||||||
</span>
|
</span>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>MeshDD-Bot NINA</title>
|
<title>MeshDD-Dashboard NINA</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<i class="bi bi-list fs-5"></i>
|
<i class="bi bi-list fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="fw-bold me-auto">
|
<span class="fw-bold me-auto">
|
||||||
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
|
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Dashboard
|
||||||
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
||||||
</span>
|
</span>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>MeshDD-Bot Paket-Log</title>
|
<title>MeshDD-Dashboard Paket-Log</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<i class="bi bi-list fs-5"></i>
|
<i class="bi bi-list fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="fw-bold me-auto">
|
<span class="fw-bold me-auto">
|
||||||
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
|
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Dashboard
|
||||||
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
||||||
</span>
|
</span>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>MeshDD-Bot Scheduler</title>
|
<title>MeshDD-Dashboard Scheduler</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<i class="bi bi-list fs-5"></i>
|
<i class="bi bi-list fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="fw-bold me-auto">
|
<span class="fw-bold me-auto">
|
||||||
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
|
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Dashboard
|
||||||
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
||||||
</span>
|
</span>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>MeshDD-Bot Einstellungen</title>
|
<title>MeshDD-Dashboard Einstellungen</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<i class="bi bi-list fs-5"></i>
|
<i class="bi bi-list fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="fw-bold me-auto">
|
<span class="fw-bold me-auto">
|
||||||
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
|
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Dashboard
|
||||||
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
||||||
</span>
|
</span>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue