- /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>
405 lines
14 KiB
JavaScript
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();
|
|
}
|