diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 1c5be23..4c7630b 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -1761,29 +1761,15 @@ 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 und gliedert sich in -**fünf Bereiche**. Bereiche mit mehreren Ansichten zeigen darunter eine Sub-Navigation: +Das Admin-Panel öffnet sich als Vollbild-Overlay über dem Chat. Es enthält sieben Tabs: -| 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 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. -#### Nutzer › Gespräche +#### Gespräche Nutzerliste links → Session auswählen → Gesprächs-Transkript als Chat-Bubbles ansehen. @@ -1792,7 +1778,7 @@ Nutzerliste links → Session auswählen → Gesprächs-Transkript als Chat-Bubb Tabellarische Übersicht aller protokollierten Notfall-Ereignisse (Zeitpunkt, Nutzer, Kategorie, Textausschnitt). -#### System › Status +#### 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 @@ -1809,24 +1795,27 @@ wechseln"** und **„Gateway neu starten"**. Am Ende: **⬇ voice-assistant.db herunterladen** — lädt die SQLite-Datenbank als Backup. -#### System › Metriken +#### Metriken Nutzungsstatistik je Nutzer (Anfragen, Einheiten, letzte Aktivität) als Tabelle und CSS-Balkendiagramm. -#### Konfiguration › Wörterbuch +#### Wörterbuch Aussprache-Lexikon direkt im Browser bearbeiten — kein Kommandozeilen-Skript nötig: -1. Sprache wählen (alle 8 Sprachen: de, en, fr, es, it, nl, ru, zh). +1. Sprache wählen (Dropdown: **Deutsch** / **Englisch**). 2. Sektion wählen: **Abkürzungen**, **Einheiten**, **Begriffe / Aussprache**. -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. +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). -#### System › Log +> **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 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 483d548..999fa3d 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -673,137 +673,43 @@ async function adminFetch(url, method = "GET", body = null) { // ---------- Panel öffnen / schließen ---------- function openAdminPanel() { $("#admin-panel").classList.remove("hidden"); - switchAdminTab("overview"); + switchAdminTab("users"); } $("#admin-close").addEventListener("click", () => $("#admin-panel").classList.add("hidden")); -// ---------- 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) { +// ---------- Tab-Umschalter ---------- +function switchAdminTab(name) { document.querySelectorAll('[id^="admin-tab-"]').forEach((el) => el.classList.add("hidden")); - 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); + $("#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 loader = SECTION_LOADER[section]; - if (loader) loader(); -} -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); - } + 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(); } 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 2916416..44634b0 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -38,35 +38,21 @@ 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,20 +290,16 @@
- - + class="flex-1 rounded-xl border border-slate-300 dark:border-slate-600 bg-transparent px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500" /> +
- +