feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
const $ = (sel) => document.querySelector(sel);
|
|
|
|
|
|
|
|
|
|
const messagesEl = $("#messages");
|
|
|
|
|
const statusEl = $("#status");
|
|
|
|
|
const promptEl = $("#prompt");
|
|
|
|
|
const formEl = $("#prompt-form");
|
|
|
|
|
const micBtn = $("#mic");
|
2026-06-18 09:49:25 +02:00
|
|
|
const ttsSel = $("#tts");
|
|
|
|
|
|
|
|
|
|
// Gewählter TTS-Provider (leer = Default/piper) als Request-Override.
|
|
|
|
|
function overrides() {
|
|
|
|
|
const t = ttsSel && ttsSel.value;
|
|
|
|
|
return t ? { tts_provider: t } : {};
|
|
|
|
|
}
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
|
|
|
|
|
let busy = false;
|
2026-06-18 08:50:30 +02:00
|
|
|
// Session-ID pro Nutzer (sonst "gehoert einem anderen Nutzer"-Konflikt). Wird aus
|
|
|
|
|
// /api/me abgeleitet; bis dahin null -> ensureSession() wartet auf loadMe().
|
|
|
|
|
let sessionId = null;
|
|
|
|
|
let mePromise = null;
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
|
|
|
|
|
// ---------- Identitaet / Menue ----------
|
2026-06-18 08:50:30 +02:00
|
|
|
function loadMe() {
|
|
|
|
|
mePromise = (async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch("/api/me", { headers: { Accept: "application/json" } });
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
$("#identity").textContent = "Nicht angemeldet";
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const me = await res.json();
|
|
|
|
|
sessionId = "web-" + me.user_id;
|
|
|
|
|
$("#identity").textContent = "Angemeldet als " + (me.display_name || me.external_id || "Gast");
|
|
|
|
|
if (me.sso_logout_url) {
|
|
|
|
|
const logout = $("#logout");
|
|
|
|
|
logout.href = me.sso_logout_url;
|
|
|
|
|
logout.classList.remove("hidden");
|
|
|
|
|
}
|
|
|
|
|
if (me.is_admin) {
|
2026-06-18 10:20:24 +02:00
|
|
|
$("#admin-toggle").classList.remove("hidden");
|
2026-06-18 08:50:30 +02:00
|
|
|
}
|
|
|
|
|
return me;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
$("#identity").textContent = "Verbindung fehlgeschlagen";
|
|
|
|
|
return null;
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
}
|
2026-06-18 08:50:30 +02:00
|
|
|
})();
|
|
|
|
|
return mePromise;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stellt sicher, dass eine nutzerspezifische Session-ID feststeht, bevor ein Turn startet.
|
|
|
|
|
async function ensureSession() {
|
|
|
|
|
if (!sessionId && mePromise) {
|
|
|
|
|
try { await mePromise; } catch (e) { /* ignore */ }
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
}
|
2026-06-18 08:50:30 +02:00
|
|
|
if (!sessionId) sessionId = "web-anon";
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadUsers() {
|
|
|
|
|
const ul = $("#user-list");
|
|
|
|
|
ul.innerHTML = "<li class='muted'>lade …</li>";
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch("/api/admin/users");
|
|
|
|
|
if (!res.ok) { ul.innerHTML = "<li class='muted'>kein Zugriff</li>"; return; }
|
|
|
|
|
const users = await res.json();
|
|
|
|
|
ul.innerHTML = "";
|
|
|
|
|
for (const u of users) {
|
|
|
|
|
const li = document.createElement("li");
|
|
|
|
|
li.textContent = (u.external_id || u.display_name) + " (" + u.user_id.slice(0, 8) + ")";
|
|
|
|
|
ul.appendChild(li);
|
|
|
|
|
}
|
|
|
|
|
if (!users.length) ul.innerHTML = "<li class='muted'>keine Nutzer</li>";
|
|
|
|
|
} catch (e) {
|
|
|
|
|
ul.innerHTML = "<li class='muted'>Fehler</li>";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- Nachrichten-UI ----------
|
2026-06-18 07:39:40 +02:00
|
|
|
// Nach dem naechsten Layout ans Ende scrollen -> die neueste Antwort ist sichtbar.
|
|
|
|
|
function scrollToBottom() {
|
|
|
|
|
requestAnimationFrame(() => { messagesEl.scrollTop = messagesEl.scrollHeight; });
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 10:20:24 +02:00
|
|
|
const BUBBLE = {
|
|
|
|
|
user: "ml-auto max-w-[80%] rounded-2xl rounded-br-sm bg-blue-600 px-4 py-2 text-white whitespace-pre-wrap",
|
|
|
|
|
assistant: "mr-auto max-w-[80%] rounded-2xl rounded-bl-sm border border-slate-200 bg-white px-4 py-2 whitespace-pre-wrap dark:border-slate-700 dark:bg-slate-800",
|
|
|
|
|
system: "mx-auto max-w-[90%] rounded-lg bg-amber-100 px-3 py-1.5 text-xs text-amber-800 dark:bg-amber-900/40 dark:text-amber-200",
|
|
|
|
|
emergency: "mx-auto max-w-[90%] rounded-lg bg-red-100 px-3 py-1.5 text-sm font-semibold text-red-800 dark:bg-red-900/40 dark:text-red-200",
|
|
|
|
|
};
|
|
|
|
|
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
function addMessage(role, text) {
|
|
|
|
|
const div = document.createElement("div");
|
2026-06-18 10:20:24 +02:00
|
|
|
div.className = BUBBLE[role] || BUBBLE.assistant;
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
div.textContent = text;
|
|
|
|
|
messagesEl.appendChild(div);
|
2026-06-18 07:39:40 +02:00
|
|
|
scrollToBottom();
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
return div;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- Audio-Wiedergabe (PCM s16le) ----------
|
|
|
|
|
let audioCtx = null;
|
|
|
|
|
function playPcm(chunks, sampleRate) {
|
|
|
|
|
if (!chunks.length) return;
|
|
|
|
|
const total = chunks.reduce((n, c) => n + c.byteLength, 0);
|
|
|
|
|
const merged = new Uint8Array(total);
|
|
|
|
|
let off = 0;
|
|
|
|
|
for (const c of chunks) { merged.set(new Uint8Array(c), off); off += c.byteLength; }
|
|
|
|
|
const view = new DataView(merged.buffer);
|
|
|
|
|
const n = Math.floor(merged.byteLength / 2);
|
|
|
|
|
audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)();
|
|
|
|
|
const buf = audioCtx.createBuffer(1, n, sampleRate || 24000);
|
|
|
|
|
const ch = buf.getChannelData(0);
|
|
|
|
|
for (let i = 0; i < n; i++) ch[i] = view.getInt16(i * 2, true) / 32768;
|
|
|
|
|
const src = audioCtx.createBufferSource();
|
|
|
|
|
src.buffer = buf;
|
|
|
|
|
src.connect(audioCtx.destination);
|
|
|
|
|
src.start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- WS-Turn (Text und Sprache teilen die Event-Logik) ----------
|
|
|
|
|
function wsUrl(path) {
|
|
|
|
|
const proto = location.protocol === "https:" ? "wss" : "ws";
|
2026-06-18 08:50:30 +02:00
|
|
|
return `${proto}://${location.host}${path}?session_id=${encodeURIComponent(sessionId)}`;
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// onopen: (ws) => sendet die Eingabe. Liefert ein Promise, das beim done-Event endet.
|
|
|
|
|
function runTurn(path, onopen) {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const ws = new WebSocket(wsUrl(path));
|
|
|
|
|
ws.binaryType = "arraybuffer";
|
|
|
|
|
const pcm = [];
|
|
|
|
|
let answerEl = null;
|
|
|
|
|
let sampleRate = 24000;
|
|
|
|
|
|
|
|
|
|
ws.onopen = () => onopen(ws);
|
|
|
|
|
ws.onerror = () => { statusEl.textContent = "Verbindungsfehler"; resolve(); };
|
|
|
|
|
ws.onclose = () => resolve();
|
|
|
|
|
|
|
|
|
|
ws.onmessage = (event) => {
|
|
|
|
|
if (typeof event.data !== "string") { pcm.push(event.data); return; }
|
|
|
|
|
let msg;
|
|
|
|
|
try { msg = JSON.parse(event.data); } catch { return; }
|
|
|
|
|
switch (msg.type) {
|
|
|
|
|
case "transcript":
|
|
|
|
|
if (msg.text) addMessage("user", msg.text);
|
|
|
|
|
break;
|
|
|
|
|
case "token":
|
|
|
|
|
if (!answerEl) answerEl = addMessage("assistant", "");
|
|
|
|
|
answerEl.textContent += msg.text || "";
|
2026-06-18 07:39:40 +02:00
|
|
|
scrollToBottom();
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
break;
|
|
|
|
|
case "semantic":
|
|
|
|
|
if (!answerEl) answerEl = addMessage("assistant", "");
|
|
|
|
|
if (msg.text) answerEl.textContent = msg.text;
|
2026-06-18 07:39:40 +02:00
|
|
|
scrollToBottom();
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
break;
|
|
|
|
|
case "emergency":
|
|
|
|
|
addMessage("emergency", "⚠ Notfall erkannt (" + (msg.category || "?") + ")");
|
|
|
|
|
break;
|
|
|
|
|
case "done":
|
|
|
|
|
sampleRate = msg.sample_rate || 24000;
|
|
|
|
|
playPcm(pcm, sampleRate);
|
|
|
|
|
statusEl.textContent = "";
|
2026-06-18 07:39:40 +02:00
|
|
|
scrollToBottom();
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
ws.close();
|
|
|
|
|
break;
|
|
|
|
|
case "error":
|
|
|
|
|
addMessage("system", "Fehler: " + (msg.detail || msg.status || "unbekannt"));
|
|
|
|
|
ws.close();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- Text senden ----------
|
|
|
|
|
async function sendText(text) {
|
|
|
|
|
if (busy || !text.trim()) return;
|
|
|
|
|
busy = true;
|
2026-06-18 08:50:30 +02:00
|
|
|
await ensureSession();
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
addMessage("user", text);
|
|
|
|
|
statusEl.textContent = "denkt …";
|
|
|
|
|
await runTurn("/ws/chat", (ws) => {
|
2026-06-18 09:49:25 +02:00
|
|
|
ws.send(JSON.stringify({ text, stream: true, audio_stream: true, ...overrides() }));
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
});
|
|
|
|
|
busy = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
formEl.addEventListener("submit", (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const text = promptEl.value;
|
|
|
|
|
promptEl.value = "";
|
|
|
|
|
sendText(text);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---------- Mikrofon (Push-to-Talk) ----------
|
|
|
|
|
let mediaRecorder = null;
|
|
|
|
|
let recChunks = [];
|
|
|
|
|
|
|
|
|
|
async function startRecording() {
|
|
|
|
|
if (busy) return;
|
|
|
|
|
let stream;
|
|
|
|
|
try {
|
|
|
|
|
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
addMessage("system", "Mikrofon nicht verfuegbar (HTTPS noetig?). " + e.message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
recChunks = [];
|
|
|
|
|
mediaRecorder = new MediaRecorder(stream);
|
|
|
|
|
mediaRecorder.ondataavailable = (e) => { if (e.data.size) recChunks.push(e.data); };
|
|
|
|
|
mediaRecorder.onstop = async () => {
|
|
|
|
|
stream.getTracks().forEach((t) => t.stop());
|
|
|
|
|
const blob = new Blob(recChunks, { type: mediaRecorder.mimeType || "audio/webm" });
|
|
|
|
|
const bytes = await blob.arrayBuffer();
|
|
|
|
|
sendVoice(bytes);
|
|
|
|
|
};
|
|
|
|
|
mediaRecorder.start();
|
2026-06-18 10:20:24 +02:00
|
|
|
micBtn.classList.remove("bg-emerald-600");
|
|
|
|
|
micBtn.classList.add("bg-red-600", "animate-pulse");
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
statusEl.textContent = "Aufnahme … (zum Stoppen erneut tippen)";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stopRecording() {
|
|
|
|
|
if (mediaRecorder && mediaRecorder.state !== "inactive") mediaRecorder.stop();
|
2026-06-18 10:20:24 +02:00
|
|
|
micBtn.classList.remove("bg-red-600", "animate-pulse");
|
|
|
|
|
micBtn.classList.add("bg-emerald-600");
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function sendVoice(bytes) {
|
|
|
|
|
if (busy) return;
|
|
|
|
|
busy = true;
|
2026-06-18 08:50:30 +02:00
|
|
|
await ensureSession();
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
statusEl.textContent = "verarbeite Sprache …";
|
|
|
|
|
await runTurn("/ws/voice", (ws) => {
|
2026-06-18 09:49:25 +02:00
|
|
|
ws.send(JSON.stringify({ type: "start", format: "webm", stream: true, audio_stream: true, ...overrides() }));
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
ws.send(bytes);
|
|
|
|
|
ws.send(JSON.stringify({ type: "end" }));
|
|
|
|
|
});
|
|
|
|
|
busy = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
micBtn.addEventListener("click", () => {
|
|
|
|
|
if (mediaRecorder && mediaRecorder.state === "recording") stopRecording();
|
|
|
|
|
else startRecording();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$("#reload-users").addEventListener("click", loadUsers);
|
2026-06-18 10:20:24 +02:00
|
|
|
$("#admin-toggle").addEventListener("click", () => $("#admin").classList.toggle("hidden"));
|
|
|
|
|
|
|
|
|
|
// Tag/Nacht: Umschalter (manuell) + automatisch dem System folgen, solange nichts gewählt.
|
|
|
|
|
const themeBtn = $("#theme-toggle");
|
|
|
|
|
const setThemeIcon = () =>
|
|
|
|
|
(themeBtn.textContent = document.documentElement.classList.contains("dark") ? "☀️" : "🌙");
|
|
|
|
|
themeBtn.addEventListener("click", () => {
|
|
|
|
|
const dark = document.documentElement.classList.toggle("dark");
|
|
|
|
|
localStorage.setItem("theme", dark ? "dark" : "light");
|
|
|
|
|
setThemeIcon();
|
|
|
|
|
});
|
|
|
|
|
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
|
|
|
|
if (!localStorage.getItem("theme")) {
|
|
|
|
|
document.documentElement.classList.toggle("dark", e.matches);
|
|
|
|
|
setThemeIcon();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
setThemeIcon();
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
|
2026-06-18 07:39:40 +02:00
|
|
|
// Wenn die Bildschirmtastatur die sichtbare Hoehe aendert (iOS), unten bleiben.
|
|
|
|
|
if (window.visualViewport) {
|
|
|
|
|
window.visualViewport.addEventListener("resize", scrollToBottom);
|
|
|
|
|
}
|
|
|
|
|
window.addEventListener("resize", scrollToBottom);
|
|
|
|
|
promptEl.addEventListener("focus", () => setTimeout(scrollToBottom, 300));
|
|
|
|
|
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
loadMe();
|