Compare commits

..

2 commits

Author SHA1 Message Date
Dieter Schlüter
487c8aa3c1 feat(admin): Redesign — 5 Bereiche + Übersicht-Dashboard + Sub-Navigation
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 <noreply@anthropic.com>
2026-06-20 23:11:57 +02:00
Dieter Schlüter
35fd3a5d15 fix(ui): Senden als runder Icon-Button (Fußzeile überlaufsicher)
- Senden ➤ als runder Icon-Button (gleich groß wie Mikrofon) statt Text -> passt
  auch im Querformat/schmalen Geräten, kein Abschneiden mehr.
- Eingabefeld min-w-0, Buttons shrink-0 -> Reihe kann nicht überlaufen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:04:35 +02:00
3 changed files with 173 additions and 50 deletions

View file

@ -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) > 🔧 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!), Nutzer anlegen (Name eingeben → „Anlegen" → Token erscheint **einmalig** — sofort kopieren!),
umbenennen, Token zurücksetzen und löschen. Erinnerungen je Nutzer auf- und zuklappen, umbenennen, Token zurücksetzen und löschen. Erinnerungen je Nutzer auf- und zuklappen,
neue Erinnerungen hinzufügen oder vorhandene löschen. 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. 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, Tabellarische Übersicht aller protokollierten Notfall-Ereignisse (Zeitpunkt, Nutzer,
Kategorie, Textausschnitt). Kategorie, Textausschnitt).
#### Status #### System Status
Zeigt aktives Profil, Provider-Konfiguration, Laufzeit-Metriken und verfügbare Provider. Zeigt aktives Profil, Provider-Konfiguration, Laufzeit-Metriken und verfügbare Provider.
Zusätzlich eine **LLM-Backend-Karte**: aktives Backend (Ollama/llama.cpp), Modell, ob 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. 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 Nutzungsstatistik je Nutzer (Anfragen, Einheiten, letzte Aktivität) als Tabelle
und CSS-Balkendiagramm. und CSS-Balkendiagramm.
#### Wörterbuch #### Konfiguration Wörterbuch
Aussprache-Lexikon direkt im Browser bearbeiten — kein Kommandozeilen-Skript nötig: 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**. 2. Sektion wählen: **Abkürzungen**, **Einheiten**, **Begriffe / Aussprache**.
3. Vorhandene Einträge: Maus drüber → **✕** erscheint → löschen. 3. Eintrag bearbeiten: Zeile anklicken → lädt in die Felder unten (Button wird zu „Speichern").
4. Neuer Eintrag: Schlüssel + Ersetzung eingeben → **+ Hinzufügen**. 4. Eintrag löschen: Maus drüber → **✕**.
Die Änderung greift sofort (Server-Cache wird automatisch geleert). 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 #### System Log
> Deutsch und Englisch. Für andere Sprachen die YAML-Datei direkt editieren
> (`config/pronunciation.<lang>.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: Zeigt den systemd-Journal-Log des `voice-assistant.service` live im Browser:

View file

@ -673,43 +673,137 @@ async function adminFetch(url, method = "GET", body = null) {
// ---------- Panel öffnen / schließen ---------- // ---------- Panel öffnen / schließen ----------
function openAdminPanel() { function openAdminPanel() {
$("#admin-panel").classList.remove("hidden"); $("#admin-panel").classList.remove("hidden");
switchAdminTab("users"); switchAdminTab("overview");
} }
$("#admin-close").addEventListener("click", () => $("#admin-panel").classList.add("hidden")); $("#admin-close").addEventListener("click", () => $("#admin-panel").classList.add("hidden"));
// ---------- Tab-Umschalter ---------- // ---------- Tab-Struktur: 5 Bereiche, je mit 1+ Abschnitten ----------
function switchAdminTab(name) { // section = vorhandener Content-Block (#admin-tab-<section>); Bereiche mit mehreren
document.querySelectorAll('[id^="admin-tab-"]').forEach((el) => el.classList.add("hidden")); // Abschnitten bekommen eine Sub-Navigation.
$("#admin-tab-" + name).classList.remove("hidden"); 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
};
document.querySelectorAll(".admin-tab").forEach((btn) => { function _setTabActive(btn, active) {
const active = btn.dataset.tab === name;
btn.classList.toggle("border-blue-600", active); btn.classList.toggle("border-blue-600", active);
btn.classList.toggle("text-blue-600", active); btn.classList.toggle("text-blue-600", active);
btn.classList.toggle("dark:border-blue-400", active); btn.classList.toggle("dark:border-blue-400", active);
btn.classList.toggle("dark:text-blue-400", active); btn.classList.toggle("dark:text-blue-400", active);
btn.classList.toggle("border-transparent", !active); btn.classList.toggle("border-transparent", !active);
btn.classList.toggle("text-slate-500", !active); btn.classList.toggle("text-slate-500", !active);
}); }
if (name === "users") loadAdminUsers(); function showAdminSection(section, top) {
if (name === "chat") loadChatUsers(); document.querySelectorAll('[id^="admin-tab-"]').forEach((el) => el.classList.add("hidden"));
if (name === "emergency") loadEmergencyEvents(); const panel = $("#admin-tab-" + section);
if (name === "status") loadStatus(); if (panel) panel.classList.remove("hidden");
if (name === "metrics") loadMetrics(); // Sub-Tab markieren
if (name === "words") loadWoerterbuch(); document.querySelectorAll(".admin-subtab").forEach((b) => {
if (name === "log") { /* User muss manuell verbinden */ } const active = b.dataset.sec === section;
if (name === "settings") loadSettings(); 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();
}
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]) =>
`<button class="admin-subtab text-xs rounded-lg border px-3 py-1.5 whitespace-nowrap transition-colors border-slate-300 dark:border-slate-600 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700" data-sec="${sec}">${escHtml(label)}</button>`
).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) => document.querySelectorAll(".admin-tab").forEach((btn) =>
btn.addEventListener("click", () => switchAdminTab(btn.dataset.tab)) btn.addEventListener("click", () => switchAdminTab(btn.dataset.tab))
); );
$("#load-overview").addEventListener("click", loadOverview);
$("#load-emergency").addEventListener("click", loadEmergencyEvents); $("#load-emergency").addEventListener("click", loadEmergencyEvents);
$("#load-status").addEventListener("click", loadStatus); $("#load-status").addEventListener("click", loadStatus);
$("#load-metrics").addEventListener("click", loadMetrics); $("#load-metrics").addEventListener("click", loadMetrics);
// ════════════════════════════════════════
// TAB: ÜBERSICHT (Dashboard)
// ════════════════════════════════════════
async function loadOverview() {
const c = $("#overview-content");
c.innerHTML = '<p class="text-sm text-slate-400">lade …</p>';
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) => `
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
<p class="text-xs text-slate-500 dark:text-slate-400">${escHtml(label)}</p>
<p class="text-2xl font-semibold mt-1">${escHtml(value)}</p>
${sub ? `<p class="text-xs text-slate-400 mt-0.5 truncate">${escHtml(sub)}</p>` : ""}
</div>`;
const jump = (to, label) =>
`<button class="ov-jump text-sm rounded-lg border border-slate-300 dark:border-slate-600 px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors" data-jump="${to}">${escHtml(label)}</button>`;
c.innerHTML = `
<div class="grid grid-cols-2 gap-3">
${stat("Nutzer", userCount)}
${stat("Anfragen gesamt", reqTotal)}
${stat("Notfälle", emCount, (typeof emCount === "number" && emCount > 0) ? "protokolliert" : "")}
${stat("LLM-Backend", backend, llm ? llm.model : "")}
</div>
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
<h4 class="font-semibold text-sm mb-2">GPU-Auslastung</h4>
<p class="text-xs font-mono text-slate-600 dark:text-slate-300">${escHtml(gpu)}</p>
</div>
<div class="flex flex-wrap gap-2">
${jump("users", "→ Nutzer")} ${jump("system", "→ System")} ${jump("emergency", "→ Notfälle")}
</div>`;
c.querySelectorAll(".ov-jump").forEach((b) =>
b.addEventListener("click", () => switchAdminTab(b.dataset.jump)));
}
// ════════════════════════════════════════ // ════════════════════════════════════════
// TAB: NUTZER // TAB: NUTZER
// ════════════════════════════════════════ // ════════════════════════════════════════

View file

@ -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">✕</button> 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">✕</button>
</header> </header>
<!-- Tab Navigation --> <!-- Tab Navigation (5 logische Bereiche) -->
<nav class="shrink-0 flex border-b border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 overflow-x-auto"> <nav class="shrink-0 flex border-b border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 overflow-x-auto">
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400" data-tab="users">👥 Nutzer</button> <button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="overview">📊 Übersicht</button>
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="chat">💬 Gespräche</button> <button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="users">👥 Nutzer</button>
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="emergency">🚨 Notfälle</button> <button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="emergency">🚨 Notfälle</button>
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="status">📡 Status</button> <button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="system">🖥 System</button>
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="metrics">📊 Metriken</button> <button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="config">⚙ Konfiguration</button>
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="words">🔤 Wörterbuch</button>
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="log">📋 Log</button>
<button class="admin-tab px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors" data-tab="settings">⚙ Einstellungen</button>
</nav> </nav>
<!-- Sub-Navigation (dynamisch, nur bei mehreren Abschnitten) -->
<div id="admin-subnav" class="hidden shrink-0 flex gap-1.5 px-3 py-2 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/40 overflow-x-auto"></div>
<!-- Tab Content Container --> <!-- Tab Content Container -->
<div class="flex-1 overflow-hidden"> <div class="flex-1 overflow-hidden">
<!-- ── Übersicht (Dashboard) ── -->
<div id="admin-tab-overview" class="hidden h-full overflow-y-auto p-4">
<div class="max-w-3xl mx-auto">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-base">Übersicht</h3>
<button id="load-overview"
class="rounded-lg border border-slate-300 dark:border-slate-600 text-sm px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Aktualisieren</button>
</div>
<div id="overview-content" class="space-y-4">
<p class="text-sm text-slate-400">lade …</p>
</div>
</div>
</div>
<!-- ── Tab: Nutzer ── --> <!-- ── Tab: Nutzer ── -->
<div id="admin-tab-users" class="h-full overflow-y-auto p-4"> <div id="admin-tab-users" class="h-full overflow-y-auto p-4">
<div class="max-w-3xl mx-auto space-y-4"> <div class="max-w-3xl mx-auto space-y-4">
@ -290,16 +304,20 @@
<footer class="shrink-0 border-t border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-3 pt-3 pb-[calc(env(safe-area-inset-bottom)+0.6rem)]"> <footer class="shrink-0 border-t border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-3 pt-3 pb-[calc(env(safe-area-inset-bottom)+0.6rem)]">
<form id="prompt-form" class="flex items-center gap-2 w-full max-w-3xl mx-auto"> <form id="prompt-form" class="flex items-center gap-2 w-full max-w-3xl mx-auto">
<button type="button" id="mic" title="Mikrofon" <button type="button" id="mic" title="Mikrofon" aria-label="Mikrofon"
class="shrink-0 h-11 w-11 grid place-items-center rounded-full bg-emerald-600 text-white text-lg">🎤</button> class="shrink-0 h-11 w-11 grid place-items-center rounded-full bg-emerald-600 text-white text-lg">🎤</button>
<input id="prompt" type="text" autocomplete="off" placeholder="Nachricht …" <input id="prompt" type="text" autocomplete="off" placeholder="Nachricht …"
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" /> class="flex-1 min-w-0 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" />
<button type="submit" id="send" <button type="submit" id="send" title="Senden" aria-label="Senden"
class="shrink-0 rounded-xl bg-blue-600 hover:bg-blue-700 text-white px-4 py-2.5 font-medium transition-colors">Senden</button> class="shrink-0 h-11 w-11 grid place-items-center rounded-full bg-blue-600 hover:bg-blue-700 text-white transition-colors">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.27 4.36a.6.6 0 0 1 .82-.74l16.2 7.83a.6.6 0 0 1 0 1.08l-16.2 7.83a.6.6 0 0 1-.82-.74L6 12Zm0 0h7" />
</svg>
</button>
</form> </form>
<div id="status" class="w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400"></div> <div id="status" class="w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400"></div>
</footer> </footer>
<script src="/app.js?v=35"></script> <script src="/app.js?v=37"></script>
</body> </body>
</html> </html>