Files
ClawdDotNet/src/ClawdDotNet.Manager.Web/wwwroot/app.js
T
RichardandClaude Opus 5.5 e4ecf2d5ca Einheiten, Stufe 4: API und WebUI-Seite "Organisation" (#683)
- /api/organisation: Baum (Einheiten mit Pfad, Tiefe, eigener und
  wirksamer Obergrenze, Vorgaben; Agenten mit Einheit, Leitung, effektiver
  Leitung, Werkzeugen, Budget), Probleme der Struktur, bekannte Werkzeuge.
  Anlegen, Aendern, Loeschen, Leitung, Umzug von Agent und Einheit,
  Obergrenze, Sammelwerte mit Vorschau. Umzug und Obergrenze kennen
  preview=true; die Entscheidung trifft allein OrgAdministration.
- Neue Agenten koennen ueber die API gleich in eine Einheit gelegt werden.
- WebUI #/organisation: links der Baum (oberste Ebene, Einheiten, Leitungen
  mit Stern, Mitglieder), rechts die Bearbeitung. Eine Begruendung fuer das
  Audit gilt fuer jede Aenderung. Wo etwas verloren gehen kann, zuerst die
  Vorschau: laufende Agenten und Probleme halten an, wegfallende Werkzeuge
  muessen in einem Dialog bestaetigt werden. Sammelwerte als Tabelle mit
  Ausreissern markiert und abgewaehlt. Bei aktivierter Begrenzung ist
  vorausgewaehlt, was von oben ohnehin erlaubt ist. Sprachpakete en/de.

Geprueft im Browser mit einem Pruefgestell (API durch Attrappe ersetzt):
Baum, Auswahl, Obergrenze mit Bestaetigungsdialog, Sammelwert-Vorschau
mit Ausreisser und das Schreiben nur der gewaehlten Agenten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 12:54:16 +02:00

405 lines
14 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 { 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();
}