// ClawdDotNet — WebUI Haupteinstiegspunkt // // Initialisiert Authentifizierung, Router, Ereignisstrom, Farbschema, Kopfzeile und Instanzenverwaltung. import { api, hatToken, loescheToken, beiUnberechtigt, holeAktiveInstanz, setzeAktiveInstanz, beiInstanzWechsel } from "./modules/api.js"; import { verbindeStrom, trenneStrom, beiVerbindungsStatus, abonnieren } from "./modules/events.js"; import { initialisiereTheme, wechsleTheme } from "./modules/theme.js"; import { registriereRoute, initialisiereRouter, navigiere } from "./modules/router.js"; import { initialisiereLogin, zeigeAnmeldung, versteckeAnmeldung } from "./views/login.js"; import { erstelleElement, erstellePlakette, instanzZustandInfo, zeigeMeldung } from "./modules/dom.js"; // Ansichten import { rendereChat } from "./views/chat.js"; import { rendereAgenten } from "./views/agents.js"; import { rendereAufgaben } from "./views/tasks.js"; import { rendereDienste } from "./views/services.js"; import { rendereStaging, holeWartendeAnzahl } from "./views/staging.js"; import { rendereVerbrauch } from "./views/usage.js"; import { rendereProtokoll, trenneLogStream } from "./views/logs.js"; import { rendereInfo } from "./views/info.js"; import { rendereInstanzen, stoppePolling as stoppeInstanzenPolling } from "./views/instances.js"; import { rendereEinstellungen } from "./views/settings.js"; import { rendereSicherung } from "./views/backup.js"; import { rendereAgentenChats, trenneAgentenChatsStream } from "./views/agent-chats.js"; import { loadLanguage, translateDocument, setLanguage, currentLanguage, t } from "./modules/i18n.js"; import { renderUsageRuns } from "./views/usage-runs.js"; import { renderUsageTimeline } from "./views/usage-timeline.js"; import { renderRunDetail } from "./views/run-detail.js"; import { renderPromptHistory } from "./views/prompt-history.js"; import { renderTeam, leaveTeam } from "./views/team.js"; import { renderOrganisation } from "./views/organisation.js"; // ─── 1. Routen registrieren ─── function verlasseAnsicht() { leaveTeam(); trenneLogStream(); trenneAgentenChatsStream(); stoppeInstanzenPolling(); } registriereRoute("/instanzen", () => { verlasseAnsicht(); return rendereInstanzen(); }); registriereRoute("/chat", () => { verlasseAnsicht(); return rendereChat(); }); registriereRoute("/chat/:agentId", (p) => { verlasseAnsicht(); return rendereChat(p); }); registriereRoute("/agenten", () => { verlasseAnsicht(); return rendereAgenten(); }); registriereRoute("/agenten/:agentId", (p) => { verlasseAnsicht(); return rendereAgenten(p); }); registriereRoute("/aufgaben", () => { verlasseAnsicht(); return rendereAufgaben(); }); registriereRoute("/team", () => { verlasseAnsicht(); return renderTeam(); }); registriereRoute("/organisation", () => { verlasseAnsicht(); return renderOrganisation(); }); registriereRoute("/dienste", () => { verlasseAnsicht(); return rendereDienste(); }); registriereRoute("/staging", () => { verlasseAnsicht(); return rendereStaging(); }); registriereRoute("/verbrauch", () => { verlasseAnsicht(); return rendereVerbrauch(); }); // #660: token usage sub-views and the prompt history registriereRoute("/verbrauch/runs", () => { verlasseAnsicht(); return renderUsageRuns(); }); registriereRoute("/verbrauch/runs/:runId", (p) => { verlasseAnsicht(); return renderRunDetail(p.runId, "/verbrauch/runs"); }); registriereRoute("/verbrauch/timeline", () => { verlasseAnsicht(); return renderUsageTimeline(); }); registriereRoute("/prompt-history", () => { verlasseAnsicht(); return renderPromptHistory(); }); registriereRoute("/prompt-history/:runId", (p) => { verlasseAnsicht(); return renderRunDetail(p.runId, "/prompt-history"); }); registriereRoute("/agenten-chats", () => { verlasseAnsicht(); return rendereAgentenChats(); }); registriereRoute("/protokoll", () => { verlasseAnsicht(); return rendereProtokoll(); }); registriereRoute("/sicherung", () => { verlasseAnsicht(); return rendereSicherung(); }); registriereRoute("/einstellungen", () => { verlasseAnsicht(); return rendereEinstellungen(); }); registriereRoute("/info", () => { verlasseAnsicht(); return rendereInfo(); }); // ─── 2. DOM-Verdrahtung ─── const seitenleiste = document.getElementById("seitenleiste"); const sidebarToggle = document.getElementById("sidebar-toggle"); const mobilMenueBtn = document.getElementById("mobil-menue-btn"); const seitenleisteBackdrop = document.getElementById("seitenleiste-backdrop"); const themeSchalter = document.getElementById("theme-schalter"); const abmeldenBtn = document.getElementById("abmelden-btn"); const verbindungPunkt = document.getElementById("verbindung-punkt"); let kopfInstanzSelect = document.getElementById("kopf-instanz-select"); let kopfInstanzStatus = document.getElementById("kopf-instanz-status"); const kopfOpenRouter = document.getElementById("kopf-openrouter-status"); let bekannteInstanzen = []; let instanzenPoller = null; // Seitenleiste ein-/ausklappen (Desktop & Mobil) function toggleSeitenleiste() { if (window.innerWidth <= 768) { const istOffen = seitenleiste?.classList.contains("mobil-offen"); if (istOffen) { schliesseMobilMenue(); } else { seitenleiste?.classList.add("mobil-offen"); if (seitenleisteBackdrop) seitenleisteBackdrop.hidden = false; } } else { seitenleiste?.classList.toggle("kollabiert"); } } sidebarToggle?.addEventListener("click", toggleSeitenleiste); mobilMenueBtn?.addEventListener("click", toggleSeitenleiste); // Mobil-Menü schließen bei Klick auf Backdrop oder Link function schliesseMobilMenue() { seitenleiste?.classList.remove("mobil-offen"); if (seitenleisteBackdrop) seitenleisteBackdrop.hidden = true; } seitenleisteBackdrop?.addEventListener("click", schliesseMobilMenue); document.querySelectorAll(".nav-link").forEach((link) => { link.addEventListener("click", () => { if (window.innerWidth <= 768) schliesseMobilMenue(); }); }); // Sprache umschalten const spracheSchalter = document.getElementById("sprache-schalter"); spracheSchalter?.addEventListener("change", async (e) => { await setLanguage(e.target.value); }); window.addEventListener("clawd:language-changed", () => { if (spracheSchalter) spracheSchalter.value = currentLanguage(); const hash = window.location.hash || "#/chat"; navigiere(hash); }); // Farbschema umschalten themeSchalter?.addEventListener("click", () => { wechsleTheme(); }); // Abmelden abmeldenBtn?.addEventListener("click", () => { beendeHauptAnwendung(); loescheToken(); zeigeAnmeldung(); }); // Bei unberechtigtem API-Aufruf (401) sofort zur Anmeldung springen beiUnberechtigt(() => { beendeHauptAnwendung(); zeigeAnmeldung(); }); function beendeHauptAnwendung() { if (instanzenPoller) { clearInterval(instanzenPoller); instanzenPoller = null; } trenneStrom(); trenneLogStream(); stoppeInstanzenPolling(); } // Verbindungsstatus des Ereignisstroms beiVerbindungsStatus((istOffen) => { if (!verbindungPunkt) return; if (istOffen) { verbindungPunkt.classList.add("offen"); verbindungPunkt.title = t("header.eventStreamConnected"); } else { verbindungPunkt.classList.remove("offen"); verbindungPunkt.title = t("header.eventStreamDisconnected"); } }); // Instanzwechsel über Dropdown kopfInstanzSelect?.addEventListener("change", (e) => { const neueId = e.target.value; setzeAktiveInstanz(neueId); }); // Bei Instanzwechsel Zustand synchronisieren und Ansicht aktualisieren beiInstanzWechsel(async (neueId) => { if (kopfInstanzSelect && kopfInstanzSelect.value !== neueId) { kopfInstanzSelect.value = neueId; } aktualisiereInstanzPlakette(); await aktualisiereKopfStatus(); await aktualisiereStagingZaehler(); // Aktuelle Ansicht neu laden, damit die Daten der neuen Instanz geladen werden if (window.location.hash) { window.dispatchEvent(new HashChangeEvent("hashchange")); } }); function aktualisiereInstanzPlakette() { kopfInstanzStatus = document.getElementById("kopf-instanz-status"); const startBtn = document.getElementById("kopf-instanz-start-btn"); const gewaehlteId = holeAktiveInstanz(); const aktive = bekannteInstanzen.find((i) => i.id === gewaehlteId); const laufende = bekannteInstanzen.filter((i) => i.zustand === "Running"); if (kopfInstanzStatus) { if (aktive) { const zInfo = instanzZustandInfo(aktive.zustand); const neuePlakette = erstellePlakette(zInfo.text, zInfo.art); neuePlakette.id = "kopf-instanz-status"; kopfInstanzStatus.replaceWith(neuePlakette); kopfInstanzStatus = neuePlakette; } else { const text = laufende.length > 1 ? t("header.multipleRunning", { count: laufende.length }) : t("header.noneActive"); const art = laufende.length > 1 ? "warnung" : "neutral"; const neuePlakette = erstellePlakette(text, art); neuePlakette.id = "kopf-instanz-status"; kopfInstanzStatus.replaceWith(neuePlakette); kopfInstanzStatus = neuePlakette; } } if (startBtn) { if (aktive && aktive.zustand !== "Running") { startBtn.hidden = false; startBtn.disabled = false; startBtn.textContent = t("header.startInstance"); startBtn.onclick = async () => { startBtn.disabled = true; startBtn.textContent = t("header.starting"); try { const res = await api(`/verwaltung/instanzen/${encodeURIComponent(aktive.id)}/start`, { method: "POST" }); zeigeMeldung(res?.meldung || `Instanz '${aktive.name}' gestartet.`, "erfolg"); await aktualisiereInstanzenListe(); if (window.location.hash) { window.dispatchEvent(new HashChangeEvent("hashchange")); } } catch (err) { if (err.lizenzFehlt) { zeigeMeldung("Start abgelehnt: Ohne gültige Lizenz startet keine Instanz (HTTP 402).", "gefahr", 6000); } else { zeigeMeldung(`Start fehlgeschlagen: ${err.message}`, "gefahr"); } startBtn.disabled = false; startBtn.textContent = t("header.startInstance"); } }; } else { startBtn.hidden = true; } } } async function aktualisiereInstanzenListe(vorhandene = null) { try { bekannteInstanzen = vorhandene || (await api("/verwaltung/instanzen")); } catch (err) { if (err.status !== 401) { console.warn("Konnte Instanzenliste nicht abrufen:", err.message); } return; } kopfInstanzSelect = document.getElementById("kopf-instanz-select"); if (!kopfInstanzSelect) return; let gewaehlteId = holeAktiveInstanz(); const laufende = bekannteInstanzen.filter((i) => i.zustand === "Running"); // Automatische Wahl, wenn genau eine Instanz läuft und keine explizit gewählt ist if (!gewaehlteId && laufende.length === 1) { gewaehlteId = laufende[0].id; setzeAktiveInstanz(gewaehlteId); } else if (gewaehlteId && !bekannteInstanzen.some((i) => i.id === gewaehlteId)) { if (laufende.length === 1) { gewaehlteId = laufende[0].id; setzeAktiveInstanz(gewaehlteId); } else { gewaehlteId = ""; setzeAktiveInstanz(""); } } // Dropdown neu befüllen kopfInstanzSelect.replaceChildren(); const placeholderOpt = erstelleElement("option", { value: "", text: bekannteInstanzen.length === 0 ? t("header.noInstances") : t("header.selectInstancePlaceholder") }); kopfInstanzSelect.append(placeholderOpt); for (const instanz of bekannteInstanzen) { const zInfo = instanzZustandInfo(instanz.zustand); const opt = erstelleElement("option", { value: instanz.id, text: `${instanz.name} (${zInfo.text})` }); kopfInstanzSelect.append(opt); } kopfInstanzSelect.value = gewaehlteId || ""; aktualisiereInstanzPlakette(); } // ─── 3. Start & Initialisierung ─── initialisiereTheme(); initialisiereLogin(starteHauptAnwendung); async function aktualisiereKopfStatus() { if (!holeAktiveInstanz()) { if (kopfOpenRouter) kopfOpenRouter.textContent = ""; return; } try { const status = await api("/status"); if (kopfOpenRouter && status) { kopfOpenRouter.textContent = status.creditsText ? `OpenRouter: ${status.creditsText}` : (status.isApiReachable ? "OpenRouter: OK" : "OpenRouter: offline"); } } catch (err) { if (kopfOpenRouter) { if (err.instanzUnerreichbar) { kopfOpenRouter.textContent = t("header.instanceOffline"); } else if (err.instanzKonflikt) { kopfOpenRouter.textContent = t("header.selectInstancePrompt"); } else { kopfOpenRouter.textContent = ""; } } } } async function aktualisiereStagingZaehler() { if (!holeAktiveInstanz()) { const badge = document.getElementById("staging-badge"); if (badge) badge.hidden = true; return; } const anzahl = await holeWartendeAnzahl(); const badge = document.getElementById("staging-badge"); if (badge) { if (anzahl > 0) { badge.textContent = String(anzahl); badge.hidden = false; } else { badge.hidden = true; } } } async function starteHauptAnwendung(instanzen) { versteckeAnmeldung(); // Instanzenliste initialisieren await aktualisiereInstanzenListe(Array.isArray(instanzen) ? instanzen : null); // Hintergrundabgleich der Instanzen alle 5s if (instanzenPoller) clearInterval(instanzenPoller); instanzenPoller = setInterval(() => { aktualisiereInstanzenListe(); }, 5000); // Ereignisstrom verbinden verbindeStrom(); // Status & Zähler laden await aktualisiereKopfStatus(); await aktualisiereStagingZaehler(); // Ereignisse für Kopf-Updates abonnieren("status", () => { aktualisiereKopfStatus(); }); abonnieren("instance", (daten) => { if (daten.kind === "Settings") { aktualisiereKopfStatus(); } }); // #661: language packs before the first render await loadLanguage(); translateDocument(); if (spracheSchalter) spracheSchalter.value = currentLanguage(); // Router initialisieren initialisiereRouter(); } // Prüfe bestehende Session if (hatToken()) { api("/verwaltung/instanzen") .then((instanzen) => { starteHauptAnwendung(instanzen); }) .catch(() => { loescheToken(); zeigeAnmeldung(); }); } else { zeigeAnmeldung(); }