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>
421 lines
15 KiB
JavaScript
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();
|
|
}
|