Files
ClawdDotNet/src/ClawdDotNet.Manager.Web/wwwroot/app.js
T
RichardandClaude Opus 5.5 d4b7f34200 #795: Updates aus der WebUI ueber den Instanzmanager
Das Update-Angebot gab es nur im Desktop-Fenster. Der Manager prueft jetzt
selbst und spielt ein: erst alle Instanzen geordnet anhalten, dann
- ohne systemd: update-agent mit --wait-for-pid und --restart,
- unter systemd: clawddotnet-update.service anstossen (Vorlage unter
  deploy/systemd), damit der agent keinen zweiten Manager neben dem
  Dienst startet (DC SETUP_INTEGRATION_GUIDE 4A).
Fehlt die Unit, wird nichts angehalten; scheitert die Uebergabe, kommen
die Instanzen wieder hoch. Anleitung in InstanceSetupGuide Abschnitt 8.

Nebenbei: Sprachpaket wird vor dem Kopf geladen (rohe Schluessel im Kopf).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:15:18 +02:00

421 lines
15 KiB
JavaScript

// 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 { renderFileHistory } from "./views/file-history.js";
import { renderTeam, leaveTeam } from "./views/team.js";
import { renderOrganisation } from "./views/organisation.js";
import { renderCases, holeMenschenfaelle } from "./views/cases.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("/cases", () => { verlasseAnsicht(); return renderCases(); });
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("/file-history", () => { verlasseAnsicht(); return renderFileHistory(); });
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;
const faelle = document.getElementById("faelle-badge");
if (faelle) faelle.hidden = true;
return;
}
// #689: Cases that wait for a human are counted beside the approvals.
const faelleAnzahl = await holeMenschenfaelle();
const faelleBadge = document.getElementById("faelle-badge");
if (faelleBadge) {
faelleBadge.textContent = String(faelleAnzahl);
faelleBadge.hidden = faelleAnzahl === 0;
}
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();
// #661: language packs before the first render. #764: Before the header as well - it stood
// below, so the instance list and the header status were drawn first and showed raw keys
// ("header.instanceOffline", "status-running") until something drew them again.
await loadLanguage();
translateDocument();
if (spracheSchalter) spracheSchalter.value = currentLanguage();
// 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();
}
});
// Router initialisieren
initialisiereRouter();
}
// Prüfe bestehende Session
if (hatToken()) {
api("/verwaltung/instanzen")
.then((instanzen) => {
starteHauptAnwendung(instanzen);
})
.catch(() => {
loescheToken();
zeigeAnmeldung();
});
} else {
zeigeAnmeldung();
}