Weboberflaeche (#531): X-Instanz an der einen fetch-Stelle, Instanzauswahl in der Kopfzeile, Verwaltungsansicht mit Anlegen/Starten/Anhalten/Fenster, Herkunft der Ereignisse, Sicherungs- und Einstellungsansicht. 402/409/502 werden eigen behandelt. Sicherungen im Manager (#530): Auflisten, Anlegen, Wiederherstellen, Loeschen. Das Zurueckspielen haelt die Instanz vorher an und startet sie danach wieder. Geordnetes Ende (Konzept 9.8). Zwei Befunde waren der Anlass: Der Manager konnte eine Instanz MIT FENSTER nie geordnet beenden. Control.ShutdownRequested war nur in HeadlessRun verdrahtet, nicht in der Avalonia-Anwendung - und ein Fenster ist die Vorgabe. Die Bitte wurde mit 202 beantwortet, niemand hoerte zu, und der Manager schoss den Prozess nach Ablauf seiner Frist jedes Mal ab. InstanceSupervisor.StopAsync hielt Socket-Schweigen fuer einen Todesnachweis und meldete "beendet", ohne den Prozess anzufassen. Bei einer verstummten Instanz ist der Socket von vornherein still - der Neustart haette einen zweiten Prozess auf dieselbe state.db gesetzt. Genau der Schaden, gegen den #528 gebaut ist. - ShutdownCoordinator: erst die Tuer schliessen, dann auf offene Arbeiten warten, Frist zwei Minuten. Vorher brach TaskScanner.DisposeAsync mitten in ProcessAsync ab, sodass die Aufgabe bis zum Lease-Ablauf auf "running" stehenblieb. - GET /api/instanz/puls hinter dem Socket-Gate: Phase und offene Arbeiten. Weiter gefragt statt gemeldet - ein Puls, den die Instanz von sich aus schickt, fehlt genau dann, wenn sie haengt. - Eskalationsleiter: drei Durchgaenge ohne Antwort, einmal um ein Ende bitten, nach 30s beenden und den Vollzug abwarten, gemeldet als Absturz. - EnsureDeadAsync vor jedem Start. Weiteres aus dem Review: - .gitignore verankert: "Instances/" passte auch auf src/ClawdDotNet.Core/Instances/. InstanceEndpoint.cs und InstanceLayout.cs waren dadurch nie eingecheckt. - Das WebUI-Token stand im Protokoll des Managers. Konsole ja, Datei nein. - Ein Klick auf "Instanz anlegen" loeste requestSubmit() zweimal aus. - [Obsolete] auf WebServerPort wieder entfernt: sechs Warnungen, die niemand abstellen konnte, solange E-M5 nicht fertig ist. - Die Wiederherstellung bricht nicht mehr ab, wenn der Browser weggeht. - Entwicklungspfade zum Instanzmanager aus Segmenten statt mit Rueckstrichen, und der Port kommt aus manager.json statt fest aus der Zeile. Offen und gemeldet: --managed hebelt die Lizenzpruefung aus (#535). 806 Tests gruen, 0 Warnungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,21 @@
|
||||
// ClawdDotNet — WebUI Haupteinstiegspunkt
|
||||
//
|
||||
// Initialisiert Authentifizierung, Router, Ereignisstrom, Farbschema und Kopfzeile.
|
||||
// Initialisiert Authentifizierung, Router, Ereignisstrom, Farbschema, Kopfzeile und Instanzenverwaltung.
|
||||
|
||||
import { api, hatToken, loescheToken, beiUnberechtigt } from "./modules/api.js";
|
||||
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";
|
||||
@@ -17,23 +26,33 @@ 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 { rendereSicherung, rendereEinstellungen, rendereAgentenChats } from "./views/placeholders.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";
|
||||
|
||||
// ─── 1. Routen registrieren ───
|
||||
|
||||
registriereRoute("/chat", () => { trenneLogStream(); return rendereChat(); });
|
||||
registriereRoute("/chat/:agentId", (p) => { trenneLogStream(); return rendereChat(p); });
|
||||
registriereRoute("/agenten", () => { trenneLogStream(); return rendereAgenten(); });
|
||||
registriereRoute("/agenten/:agentId", (p) => { trenneLogStream(); return rendereAgenten(p); });
|
||||
registriereRoute("/aufgaben", () => { trenneLogStream(); return rendereAufgaben(); });
|
||||
registriereRoute("/dienste", () => { trenneLogStream(); return rendereDienste(); });
|
||||
registriereRoute("/staging", () => { trenneLogStream(); return rendereStaging(); });
|
||||
registriereRoute("/verbrauch", () => { trenneLogStream(); return rendereVerbrauch(); });
|
||||
registriereRoute("/agenten-chats", () => { trenneLogStream(); return rendereAgentenChats(); });
|
||||
registriereRoute("/protokoll", () => rendereProtokoll());
|
||||
registriereRoute("/sicherung", () => { trenneLogStream(); return rendereSicherung(); });
|
||||
registriereRoute("/einstellungen", () => { trenneLogStream(); return rendereEinstellungen(); });
|
||||
registriereRoute("/info", () => { trenneLogStream(); return rendereInfo(); });
|
||||
function verlasseAnsicht() {
|
||||
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("/dienste", () => { verlasseAnsicht(); return rendereDienste(); });
|
||||
registriereRoute("/staging", () => { verlasseAnsicht(); return rendereStaging(); });
|
||||
registriereRoute("/verbrauch", () => { verlasseAnsicht(); return rendereVerbrauch(); });
|
||||
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 ───
|
||||
|
||||
@@ -44,19 +63,30 @@ const seitenleisteBackdrop = document.getElementById("seitenleiste-backdrop");
|
||||
const themeSchalter = document.getElementById("theme-schalter");
|
||||
const abmeldenBtn = document.getElementById("abmelden-btn");
|
||||
const verbindungPunkt = document.getElementById("verbindung-punkt");
|
||||
const kopfInstanz = document.getElementById("kopf-instanz");
|
||||
let kopfInstanzSelect = document.getElementById("kopf-instanz-select");
|
||||
let kopfInstanzStatus = document.getElementById("kopf-instanz-status");
|
||||
const kopfOpenRouter = document.getElementById("kopf-openrouter-status");
|
||||
|
||||
// Seitenleiste kollabieren (Desktop)
|
||||
sidebarToggle?.addEventListener("click", () => {
|
||||
seitenleiste?.classList.toggle("kollabiert");
|
||||
});
|
||||
let bekannteInstanzen = [];
|
||||
let instanzenPoller = null;
|
||||
|
||||
// Mobil-Menü öffnen
|
||||
mobilMenueBtn?.addEventListener("click", () => {
|
||||
seitenleiste?.classList.add("mobil-offen");
|
||||
if (seitenleisteBackdrop) seitenleisteBackdrop.hidden = false;
|
||||
});
|
||||
// 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() {
|
||||
@@ -78,19 +108,27 @@ themeSchalter?.addEventListener("click", () => {
|
||||
|
||||
// Abmelden
|
||||
abmeldenBtn?.addEventListener("click", () => {
|
||||
beendeHauptAnwendung();
|
||||
loescheToken();
|
||||
trenneStrom();
|
||||
trenneLogStream();
|
||||
zeigeAnmeldung();
|
||||
});
|
||||
|
||||
// Bei unberechtigtem API-Aufruf (401) sofort zur Anmeldung springen
|
||||
beiUnberechtigt(() => {
|
||||
trenneStrom();
|
||||
trenneLogStream();
|
||||
beendeHauptAnwendung();
|
||||
zeigeAnmeldung();
|
||||
});
|
||||
|
||||
function beendeHauptAnwendung() {
|
||||
if (instanzenPoller) {
|
||||
clearInterval(instanzenPoller);
|
||||
instanzenPoller = null;
|
||||
}
|
||||
trenneStrom();
|
||||
trenneLogStream();
|
||||
stoppeInstanzenPolling();
|
||||
}
|
||||
|
||||
// Verbindungsstatus des Ereignisstroms
|
||||
beiVerbindungsStatus((istOffen) => {
|
||||
if (!verbindungPunkt) return;
|
||||
@@ -103,12 +141,148 @@ beiVerbindungsStatus((istOffen) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 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 ? `${laufende.length} laufen` : "Keine aktiv";
|
||||
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 = "Starten";
|
||||
startBtn.onclick = async () => {
|
||||
startBtn.disabled = true;
|
||||
startBtn.textContent = "Startet…";
|
||||
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 = "Starten";
|
||||
}
|
||||
};
|
||||
} 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 ? "Keine Instanzen" : "— Instanz wählen —"
|
||||
});
|
||||
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) {
|
||||
@@ -116,12 +290,26 @@ async function aktualisiereKopfStatus() {
|
||||
? `OpenRouter: ${status.creditsText}`
|
||||
: (status.isApiReachable ? "OpenRouter: OK" : "OpenRouter: offline");
|
||||
}
|
||||
} catch {
|
||||
if (kopfOpenRouter) kopfOpenRouter.textContent = "";
|
||||
} catch (err) {
|
||||
if (kopfOpenRouter) {
|
||||
if (err.instanzUnerreichbar) {
|
||||
kopfOpenRouter.textContent = "Instanz offline (502)";
|
||||
} else if (err.instanzKonflikt) {
|
||||
kopfOpenRouter.textContent = "Instanz wählen (409)";
|
||||
} 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) {
|
||||
@@ -134,12 +322,17 @@ async function aktualisiereStagingZaehler() {
|
||||
}
|
||||
}
|
||||
|
||||
async function starteHauptAnwendung(info) {
|
||||
async function starteHauptAnwendung(instanzen) {
|
||||
versteckeAnmeldung();
|
||||
|
||||
if (kopfInstanz && info) {
|
||||
kopfInstanz.textContent = `${info.instanceName} (${info.instanceId})`;
|
||||
}
|
||||
// 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();
|
||||
@@ -165,9 +358,9 @@ async function starteHauptAnwendung(info) {
|
||||
|
||||
// Prüfe bestehende Session
|
||||
if (hatToken()) {
|
||||
api("/info")
|
||||
.then((info) => {
|
||||
starteHauptAnwendung(info);
|
||||
api("/verwaltung/instanzen")
|
||||
.then((instanzen) => {
|
||||
starteHauptAnwendung(instanzen);
|
||||
})
|
||||
.catch(() => {
|
||||
loescheToken();
|
||||
|
||||
Reference in New Issue
Block a user