From 487c8aa3c143032e733cfd3b41277c5dd2d314ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Sat, 20 Jun 2026 23:11:57 +0200 Subject: [PATCH] =?UTF-8?q?feat(admin):=20Redesign=20=E2=80=94=205=20Berei?= =?UTF-8?q?che=20+=20=C3=9Cbersicht-Dashboard=20+=20Sub-Navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Von 8 gemischten Tabs auf 5 logische Bereiche verdichtet: - 📊 Übersicht (neu, Start): Kennzahlen (Nutzer, Anfragen, Notfälle) + LLM-Backend/GPU + Schnell-Sprünge. - 👥 Nutzer: Sub-Tabs Verwalten + Gespräche. - 🚨 Notfälle. - 🖥 System: Sub-Tabs Status (inkl. LLM-Steuerung) + Metriken + Log. - ⚙ Konfiguration: Sub-Tabs Einstellungen + Wörterbuch. - Sub-Navigation (Segmented-Buttons) für Bereiche mit mehreren Ansichten. - Bestehende Content-Blöcke + Loader unverändert wiederverwendet (nur neu gebündelt) -> geringes Risiko. loadOverview neu (vorhandene Endpunkte). - Doku §7.5 auf 5-Bereiche-Struktur aktualisiert; Wörterbuch-Hinweis (alle 8 Sprachen, Zeile-anklicken-bearbeiten) korrigiert. 167 grün, JS-Syntax ok. Co-Authored-By: Claude Opus 4.8 --- BEDIENUNGSANLEITUNG.md | 43 ++++++++----- app/web/app.js | 136 ++++++++++++++++++++++++++++++++++------- app/web/index.html | 32 +++++++--- 3 files changed, 165 insertions(+), 46 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 4c7630b..1c5be23 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -1761,15 +1761,29 @@ Vollständige Anleitung: [deploy/README.md](deploy/README.md). > 🔧 Admin — erreichbar über den **⚙️-Button** im Web-Interface (nur für Admin-Nutzer sichtbar) -Das Admin-Panel öffnet sich als Vollbild-Overlay über dem Chat. Es enthält sieben Tabs: +Das Admin-Panel öffnet sich als Vollbild-Overlay über dem Chat und gliedert sich in +**fünf Bereiche**. Bereiche mit mehreren Ansichten zeigen darunter eine Sub-Navigation: -#### Nutzer +| Bereich | Inhalt | +|---------|--------| +| **📊 Übersicht** | Start-Dashboard: Kennzahlen (Nutzer, Anfragen gesamt, Notfälle) + LLM-Backend/GPU; Direktsprünge | +| **👥 Nutzer** | Sub-Tabs *Verwalten* (anlegen/umbenennen/Token/löschen, Erinnerungen) und *Gespräche* (Transkripte) | +| **🚨 Notfälle** | protokollierte Notfall-Ereignisse | +| **🖥 System** | Sub-Tabs *Status* (inkl. LLM-Steuerung), *Metriken*, *Log* | +| **⚙ Konfiguration** | Sub-Tabs *Einstellungen* (Laufzeit-Config) und *Wörterbuch* (Aussprache) | + +#### Übersicht + +Beim Öffnen sichtbar: Kacheln mit Nutzerzahl, Anfragen gesamt, Notfall-Anzahl und +aktivem LLM-Backend/Modell, dazu die GPU-Auslastung und Schnell-Sprünge in die Bereiche. + +#### Nutzer › Verwalten Nutzer anlegen (Name eingeben → „Anlegen" → Token erscheint **einmalig** — sofort kopieren!), umbenennen, Token zurücksetzen und löschen. Erinnerungen je Nutzer auf- und zuklappen, neue Erinnerungen hinzufügen oder vorhandene löschen. -#### Gespräche +#### Nutzer › Gespräche Nutzerliste links → Session auswählen → Gesprächs-Transkript als Chat-Bubbles ansehen. @@ -1778,7 +1792,7 @@ Nutzerliste links → Session auswählen → Gesprächs-Transkript als Chat-Bubb Tabellarische Übersicht aller protokollierten Notfall-Ereignisse (Zeitpunkt, Nutzer, Kategorie, Textausschnitt). -#### Status +#### System › Status Zeigt aktives Profil, Provider-Konfiguration, Laufzeit-Metriken und verfügbare Provider. Zusätzlich eine **LLM-Backend-Karte**: aktives Backend (Ollama/llama.cpp), Modell, ob @@ -1795,27 +1809,24 @@ wechseln"** und **„Gateway neu starten"**. Am Ende: **⬇ voice-assistant.db herunterladen** — lädt die SQLite-Datenbank als Backup. -#### Metriken +#### System › Metriken Nutzungsstatistik je Nutzer (Anfragen, Einheiten, letzte Aktivität) als Tabelle und CSS-Balkendiagramm. -#### Wörterbuch +#### Konfiguration › Wörterbuch Aussprache-Lexikon direkt im Browser bearbeiten — kein Kommandozeilen-Skript nötig: -1. Sprache wählen (Dropdown: **Deutsch** / **Englisch**). +1. Sprache wählen (alle 8 Sprachen: de, en, fr, es, it, nl, ru, zh). 2. Sektion wählen: **Abkürzungen**, **Einheiten**, **Begriffe / Aussprache**. -3. Vorhandene Einträge: Maus drüber → **✕** erscheint → löschen. -4. Neuer Eintrag: Schlüssel + Ersetzung eingeben → **+ Hinzufügen**. - Die Änderung greift sofort (Server-Cache wird automatisch geleert). +3. Eintrag bearbeiten: Zeile anklicken → lädt in die Felder unten (Button wird zu „Speichern"). +4. Eintrag löschen: Maus drüber → **✕**. +5. Neuer Eintrag: Schlüssel + Ersetzung → **+ Hinzufügen**. + Die Liste wird nach jedem Speichern alphabetisch sortiert; Änderungen greifen sofort + (Server-Cache wird automatisch geleert). Vollständige Anleitung → § 6.5.4. -> **Andere Sprachen (fr, es, it, nl, ru, zh):** Das Wörterbuch-Tab zeigt aktuell nur -> Deutsch und Englisch. Für andere Sprachen die YAML-Datei direkt editieren -> (`config/pronunciation..yaml`) oder die REST-API nutzen — beides ohne Neustart -> möglich (API) bzw. mit Neustart (YAML direkt). Vollständige Anleitung → § 6.5.4. - -#### Log +#### System › Log Zeigt den systemd-Journal-Log des `voice-assistant.service` live im Browser: diff --git a/app/web/app.js b/app/web/app.js index 999fa3d..483d548 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -673,43 +673,137 @@ async function adminFetch(url, method = "GET", body = null) { // ---------- Panel öffnen / schließen ---------- function openAdminPanel() { $("#admin-panel").classList.remove("hidden"); - switchAdminTab("users"); + switchAdminTab("overview"); } $("#admin-close").addEventListener("click", () => $("#admin-panel").classList.add("hidden")); -// ---------- Tab-Umschalter ---------- -function switchAdminTab(name) { +// ---------- Tab-Struktur: 5 Bereiche, je mit 1+ Abschnitten ---------- +// section = vorhandener Content-Block (#admin-tab-
); Bereiche mit mehreren +// Abschnitten bekommen eine Sub-Navigation. +const ADMIN_TABS = { + overview: [["overview", "Übersicht"]], + users: [["users", "Verwalten"], ["chat", "Gespräche"]], + emergency: [["emergency", "Notfälle"]], + system: [["status", "Status"], ["metrics", "Metriken"], ["log", "Log"]], + config: [["settings", "Einstellungen"], ["words", "Wörterbuch"]], +}; +const SECTION_LOADER = { + overview: () => loadOverview(), + users: () => loadAdminUsers(), + chat: () => loadChatUsers(), + emergency: () => loadEmergencyEvents(), + status: () => loadStatus(), + metrics: () => loadMetrics(), + words: () => loadWoerterbuch(), + settings: () => loadSettings(), + log: () => {}, // Log: Nutzer verbindet manuell +}; + +function _setTabActive(btn, active) { + btn.classList.toggle("border-blue-600", active); + btn.classList.toggle("text-blue-600", active); + btn.classList.toggle("dark:border-blue-400", active); + btn.classList.toggle("dark:text-blue-400", active); + btn.classList.toggle("border-transparent", !active); + btn.classList.toggle("text-slate-500", !active); +} + +function showAdminSection(section, top) { document.querySelectorAll('[id^="admin-tab-"]').forEach((el) => el.classList.add("hidden")); - $("#admin-tab-" + name).classList.remove("hidden"); - - document.querySelectorAll(".admin-tab").forEach((btn) => { - const active = btn.dataset.tab === name; - btn.classList.toggle("border-blue-600", active); - btn.classList.toggle("text-blue-600", active); - btn.classList.toggle("dark:border-blue-400", active); - btn.classList.toggle("dark:text-blue-400", active); - btn.classList.toggle("border-transparent", !active); - btn.classList.toggle("text-slate-500", !active); + const panel = $("#admin-tab-" + section); + if (panel) panel.classList.remove("hidden"); + // Sub-Tab markieren + document.querySelectorAll(".admin-subtab").forEach((b) => { + const active = b.dataset.sec === section; + b.classList.toggle("bg-blue-600", active); + b.classList.toggle("text-white", active); + b.classList.toggle("border-blue-600", active); + b.classList.toggle("text-slate-600", !active); + b.classList.toggle("dark:text-slate-300", !active); + b.classList.toggle("border-slate-300", !active); + b.classList.toggle("dark:border-slate-600", !active); }); + const loader = SECTION_LOADER[section]; + if (loader) loader(); +} - if (name === "users") loadAdminUsers(); - if (name === "chat") loadChatUsers(); - if (name === "emergency") loadEmergencyEvents(); - if (name === "status") loadStatus(); - if (name === "metrics") loadMetrics(); - if (name === "words") loadWoerterbuch(); - if (name === "log") { /* User muss manuell verbinden */ } - if (name === "settings") loadSettings(); +function switchAdminTab(top) { + const sections = ADMIN_TABS[top] || [[top, top]]; + document.querySelectorAll(".admin-tab").forEach((btn) => _setTabActive(btn, btn.dataset.tab === top)); + + const subnav = $("#admin-subnav"); + if (sections.length <= 1) { + subnav.classList.add("hidden"); + subnav.innerHTML = ""; + showAdminSection(sections[0][0], top); + } else { + subnav.classList.remove("hidden"); + subnav.innerHTML = sections.map(([sec, label]) => + `` + ).join(""); + subnav.querySelectorAll(".admin-subtab").forEach((b) => + b.addEventListener("click", () => showAdminSection(b.dataset.sec, top)) + ); + showAdminSection(sections[0][0], top); + } } document.querySelectorAll(".admin-tab").forEach((btn) => btn.addEventListener("click", () => switchAdminTab(btn.dataset.tab)) ); +$("#load-overview").addEventListener("click", loadOverview); $("#load-emergency").addEventListener("click", loadEmergencyEvents); $("#load-status").addEventListener("click", loadStatus); $("#load-metrics").addEventListener("click", loadMetrics); +// ════════════════════════════════════════ +// TAB: ÜBERSICHT (Dashboard) +// ════════════════════════════════════════ +async function loadOverview() { + const c = $("#overview-content"); + c.innerHTML = '

lade …

'; + const [users, usage, emergencies, llm] = await Promise.all([ + adminFetch("/api/admin/users"), + adminFetch("/api/admin/usage"), + adminFetch("/api/admin/emergency-events"), + adminFetch("/api/admin/llm/status"), + ]); + const userCount = users ? users.length : "—"; + const reqTotal = usage ? usage.reduce((s, u) => s + (u.total_requests || 0), 0) : "—"; + const emCount = emergencies ? emergencies.length : "—"; + const backend = llm ? ({ ollama: "Ollama", llamacpp: "llama.cpp" }[llm.backend] || llm.backend) : "—"; + const gpu = (llm && llm.gpus && llm.gpus.length) + ? llm.gpus.map((g) => `GPU ${g.index}: ${g.percent}%`).join(" · ") : "—"; + + const stat = (label, value, sub) => ` +
+

${escHtml(label)}

+

${escHtml(value)}

+ ${sub ? `

${escHtml(sub)}

` : ""} +
`; + + const jump = (to, label) => + ``; + + c.innerHTML = ` +
+ ${stat("Nutzer", userCount)} + ${stat("Anfragen gesamt", reqTotal)} + ${stat("Notfälle", emCount, (typeof emCount === "number" && emCount > 0) ? "protokolliert" : "")} + ${stat("LLM-Backend", backend, llm ? llm.model : "")} +
+
+

GPU-Auslastung

+

${escHtml(gpu)}

+
+
+ ${jump("users", "→ Nutzer")} ${jump("system", "→ System")} ${jump("emergency", "→ Notfälle")} +
`; + c.querySelectorAll(".ov-jump").forEach((b) => + b.addEventListener("click", () => switchAdminTab(b.dataset.jump))); +} + // ════════════════════════════════════════ // TAB: NUTZER // ════════════════════════════════════════ diff --git a/app/web/index.html b/app/web/index.html index da699ae..2916416 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -38,21 +38,35 @@ class="h-9 w-9 grid place-items-center rounded-lg text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 text-xl leading-none transition-colors">✕ - + + + +
+ + +
@@ -304,6 +318,6 @@
- +