fix(web): Session-ID pro Nutzer statt globalem web-main

Die feste Session "web-main" gehoerte dem ersten Nutzer (lokal: anonymous) ->
SSO-Nutzer bekam "Session gehoert einem anderen Nutzer" (403). Die Web-UI leitet
die Session jetzt aus /api/me ab (web-<user_id>); ensureSession() wartet vor dem
ersten Turn auf /api/me. Asset-Version v4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-18 08:50:30 +02:00
commit 6073ba1133
2 changed files with 41 additions and 22 deletions

View file

@ -1,6 +1,5 @@
"use strict";
const SESSION_ID = "web-main";
const $ = (sel) => document.querySelector(sel);
const messagesEl = $("#messages");
@ -10,16 +9,22 @@ const formEl = $("#prompt-form");
const micBtn = $("#mic");
let busy = false;
// 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;
// ---------- Identitaet / Menue ----------
async function loadMe() {
function loadMe() {
mePromise = (async () => {
try {
const res = await fetch("/api/me", { headers: { Accept: "application/json" } });
if (!res.ok) {
$("#identity").textContent = "Nicht angemeldet";
return;
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");
@ -29,9 +34,21 @@ async function loadMe() {
if (me.is_admin) {
$("#admin").classList.remove("hidden");
}
return me;
} catch (e) {
$("#identity").textContent = "Verbindung fehlgeschlagen";
return null;
}
})();
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 */ }
}
if (!sessionId) sessionId = "web-anon";
}
async function loadUsers() {
@ -91,7 +108,7 @@ function playPcm(chunks, sampleRate) {
// ---------- WS-Turn (Text und Sprache teilen die Event-Logik) ----------
function wsUrl(path) {
const proto = location.protocol === "https:" ? "wss" : "ws";
return `${proto}://${location.host}${path}?session_id=${encodeURIComponent(SESSION_ID)}`;
return `${proto}://${location.host}${path}?session_id=${encodeURIComponent(sessionId)}`;
}
// onopen: (ws) => sendet die Eingabe. Liefert ein Promise, das beim done-Event endet.
@ -148,6 +165,7 @@ function runTurn(path, onopen) {
async function sendText(text) {
if (busy || !text.trim()) return;
busy = true;
await ensureSession();
addMessage("user", text);
statusEl.textContent = "denkt …";
await runTurn("/ws/chat", (ws) => {
@ -198,6 +216,7 @@ function stopRecording() {
async function sendVoice(bytes) {
if (busy) return;
busy = true;
await ensureSession();
statusEl.textContent = "verarbeite Sprache …";
await runTurn("/ws/voice", (ws) => {
ws.send(JSON.stringify({ type: "start", format: "webm", stream: true, audio_stream: true }));

View file

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Voice Assistant</title>
<link rel="stylesheet" href="/style.css?v=3" />
<link rel="stylesheet" href="/style.css?v=4" />
</head>
<body>
<div id="layout">
@ -31,6 +31,6 @@
<div id="status" class="muted"></div>
</main>
</div>
<script src="/app.js?v=3"></script>
<script src="/app.js?v=4"></script>
</body>
</html>