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:
@@ -326,6 +326,33 @@ a:hover {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.kopf-instanz-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.kopf-instanz-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--gedaempft);
|
||||
}
|
||||
|
||||
.kopf-instanz-select {
|
||||
max-width: 220px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#kopf-instanz-start-btn {
|
||||
padding: 3px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-punkt {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
@@ -871,6 +898,46 @@ button.klein {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-instanz {
|
||||
font-family: var(--schrift-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 1px 5px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--divider);
|
||||
color: var(--accent);
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* ─── Ansicht: Instanzen ─── */
|
||||
|
||||
.instanzen-raster {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.instanz-karte {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.instanz-karte.aktiv {
|
||||
border-left: 4px solid var(--accent);
|
||||
}
|
||||
|
||||
.instanz-aktionen {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--divider);
|
||||
}
|
||||
|
||||
/* ─── Modal Dialoge ─── */
|
||||
|
||||
.modal-overlay {
|
||||
@@ -987,6 +1054,166 @@ button.klein {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* ─── Modale Dialoge ─── */
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1500;
|
||||
padding: 16px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.modal-overlay.sichtbar {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.modal-overlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-box {
|
||||
max-width: 520px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--divider);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.modal-kopf {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--divider);
|
||||
background: var(--surface-subtil);
|
||||
}
|
||||
|
||||
.modal-kopf h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-family: var(--schrift-titel);
|
||||
}
|
||||
|
||||
.modal-koerper {
|
||||
padding: 18px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.modal-fuss {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 14px 18px;
|
||||
border-top: 1px solid var(--divider);
|
||||
background: var(--surface-subtil);
|
||||
}
|
||||
|
||||
/* Sicherheits- und Countdown-Elemente */
|
||||
|
||||
.sicherheits-box {
|
||||
background: rgba(192, 57, 43, 0.08);
|
||||
border: 1px solid var(--gefahr);
|
||||
border-left: 4px solid var(--gefahr);
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sicherheits-box h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
color: var(--gefahr);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sicherheits-box p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.sicherheits-countdown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
background: var(--bg);
|
||||
border: 1px dashed var(--divider);
|
||||
font-family: var(--schrift-mono);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--warnung);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sicherheits-countdown.abgelaufen {
|
||||
color: var(--gefahr);
|
||||
border-color: var(--gefahr);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
/* ─── Einstellungen & Systemarchitektur ─── */
|
||||
|
||||
.einstellungen-ansicht {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.einstellungen-sektion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 20px;
|
||||
background: var(--surface-subtil);
|
||||
border: 1px solid var(--divider);
|
||||
}
|
||||
|
||||
.einstellungen-sektion.sektion-manager {
|
||||
border-left: 4px solid var(--warnung);
|
||||
}
|
||||
|
||||
.einstellungen-sektion.sektion-instanz {
|
||||
border-left: 4px solid var(--erfolg);
|
||||
}
|
||||
|
||||
.sektion-titel-zeile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--divider);
|
||||
}
|
||||
|
||||
.sektion-titel-zeile h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* ─── Responsive Optimierung (Handy / 375px) ─── */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -1036,4 +1263,115 @@ button.klein {
|
||||
.chat-aktionen {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.kopf-instanz-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kopf-instanz-select {
|
||||
max-width: 140px;
|
||||
font-size: 13px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.instanzen-raster {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Datensicherung & Wiederherstellung ─── */
|
||||
|
||||
.sicherung-ansicht {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.ordner-pfad-anzeige {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--surface-subtil);
|
||||
border: 1px solid var(--divider);
|
||||
padding: 8px 12px;
|
||||
margin-top: 8px;
|
||||
font-family: var(--schrift-mono);
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.dateiname-zelle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.spalte-aktionen {
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ─── Interne Agenten-Kommunikation (A2A) ─── */
|
||||
|
||||
.agenten-chats-ansicht {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.a2a-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.a2a-eintrag {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--divider);
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.a2a-eintrag.a2a-anfrage {
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
|
||||
.a2a-eintrag.a2a-antwort {
|
||||
border-left: 3px solid var(--erfolg);
|
||||
background: var(--surface-subtil);
|
||||
}
|
||||
|
||||
.a2a-eintrag-kopf {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.a2a-pfeil {
|
||||
color: var(--gedaempft);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.a2a-zeit {
|
||||
font-size: 12px;
|
||||
color: var(--gedaempft);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.a2a-inhalt {
|
||||
font-family: var(--schrift-mono);
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: var(--bg);
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--divider);
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<h2>ClawdDotNet</h2>
|
||||
</div>
|
||||
<p class="hinweis">
|
||||
Bitte gib das Zugangstoken der Instanz ein (<code>webUiToken</code> in der Instanzdatei).
|
||||
Bitte gib das Zugangstoken des Instanzmanagers ein (<code>webUiToken</code> in <code>manager.json</code>).
|
||||
</p>
|
||||
<form id="anmeldeformular" class="formular-raster">
|
||||
<input type="password" id="token-eingabe" placeholder="Zugangstoken (mind. 20 Zeichen)" autocomplete="current-password" required>
|
||||
@@ -87,6 +87,12 @@
|
||||
|
||||
<div class="nav-gruppe">
|
||||
<div class="nav-gruppe-titel">System</div>
|
||||
<a href="#/instanzen" class="nav-link" title="Instanzen">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M2 20h20 M2 14h20 M2 8h20 M2 4h20 M6 6h.01 M6 12h.01 M6 18h.01"/>
|
||||
</svg>
|
||||
<span class="nav-label">Instanzen</span>
|
||||
</a>
|
||||
<a href="#/staging" class="nav-link" title="Freigaben">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z M9 12l2 2 4-4"/>
|
||||
@@ -129,14 +135,21 @@
|
||||
<div class="hauptbereich">
|
||||
<header class="kopfzeile">
|
||||
<div class="kopfzeile-links">
|
||||
<button id="mobil-menue-btn" class="schaltflaeche-icon flach" title="Menü öffnen" type="button">
|
||||
<button id="mobil-menue-btn" class="schaltflaeche-icon flach" title="Seitenleiste ein-/ausklappen" type="button">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="12" x2="21" y2="12"/>
|
||||
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||
<line x1="3" y1="18" x2="21" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span id="kopf-instanz" class="gedaempft"></span>
|
||||
<div class="kopf-instanz-container" id="kopf-instanz-container">
|
||||
<label for="kopf-instanz-select" class="kopf-instanz-label">Instanz:</label>
|
||||
<select id="kopf-instanz-select" class="eingabe-select klein kopf-instanz-select" title="Aktive Instanz wechseln">
|
||||
<option value="">(Keine Instanz)</option>
|
||||
</select>
|
||||
<span id="kopf-instanz-status" class="plakette plakette-neutral">Lädt…</span>
|
||||
<button id="kopf-instanz-start-btn" class="schaltflaeche klein primaer" title="Ausgewählte Instanz jetzt starten" type="button" hidden>Starten</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kopfzeile-rechts">
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
// Verwaltet das Zugangstoken im sessionStorage und kapselt fetch-Aufrufe.
|
||||
|
||||
export const TOKEN_SCHLUESSEL = "clawd.token";
|
||||
export const INSTANZ_SCHLUESSEL = "clawd.instanz";
|
||||
|
||||
let token = sessionStorage.getItem(TOKEN_SCHLUESSEL) ?? "";
|
||||
let aktiveInstanz = sessionStorage.getItem(INSTANZ_SCHLUESSEL) ?? "";
|
||||
const unberechtigtCallbacks = new Set();
|
||||
const instanzWechselCallbacks = new Set();
|
||||
|
||||
/**
|
||||
* Gibt das aktuell gespeicherte Token zurück.
|
||||
@@ -29,11 +32,58 @@ export function setzeToken(neuesToken) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht das Token (Abmeldung).
|
||||
* Löscht das Token und die aktive Instanz (Abmeldung).
|
||||
*/
|
||||
export function loescheToken() {
|
||||
token = "";
|
||||
sessionStorage.removeItem(TOKEN_SCHLUESSEL);
|
||||
loescheAktiveInstanz();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuell ausgewählte Instanz-ID zurück.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function holeAktiveInstanz() {
|
||||
return aktiveInstanz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert die aktive Instanz im Speicher und sessionStorage.
|
||||
* @param {string} neueInstanzId
|
||||
*/
|
||||
export function setzeAktiveInstanz(neueInstanzId) {
|
||||
const id = (neueInstanzId || "").trim();
|
||||
if (aktiveInstanz === id) return;
|
||||
aktiveInstanz = id;
|
||||
if (aktiveInstanz) {
|
||||
sessionStorage.setItem(INSTANZ_SCHLUESSEL, aktiveInstanz);
|
||||
} else {
|
||||
sessionStorage.removeItem(INSTANZ_SCHLUESSEL);
|
||||
}
|
||||
for (const cb of instanzWechselCallbacks) {
|
||||
try {
|
||||
cb(aktiveInstanz);
|
||||
} catch (err) {
|
||||
console.error("Fehler im InstanzWechsel-Callback:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt die aktive Instanz.
|
||||
*/
|
||||
export function loescheAktiveInstanz() {
|
||||
setzeAktiveInstanz("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Registriert einen Rückruf für Instanzwechsel.
|
||||
* @param {(instanzId: string) => void} callback
|
||||
*/
|
||||
export function beiInstanzWechsel(callback) {
|
||||
instanzWechselCallbacks.add(callback);
|
||||
return () => instanzWechselCallbacks.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,6 +116,7 @@ function meldeUnberechtigt() {
|
||||
|
||||
/**
|
||||
* Führt einen Aufruf gegen die API aus.
|
||||
* Sendet bei gesetzter aktiver Instanz automatisch die X-Instanz-Kopfzeile mit.
|
||||
* @param {string} pfad Pfad relativ zu /api, z.B. "/info" oder "/agents"
|
||||
* @param {RequestInit} [optionen={}]
|
||||
* @returns {Promise<any>}
|
||||
@@ -76,6 +127,10 @@ export async function api(pfad, optionen = {}) {
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
|
||||
if (aktiveInstanz && !kopfzeilen["X-Instanz"] && !pfad.startsWith("/verwaltung")) {
|
||||
kopfzeilen["X-Instanz"] = aktiveInstanz;
|
||||
}
|
||||
|
||||
if (optionen.body && typeof optionen.body === "object" && !(optionen.body instanceof FormData)) {
|
||||
kopfzeilen["Content-Type"] = "application/json";
|
||||
optionen.body = JSON.stringify(optionen.body);
|
||||
@@ -98,12 +153,15 @@ export async function api(pfad, optionen = {}) {
|
||||
let meldung = `HTTP ${antwort.status}`;
|
||||
try {
|
||||
const rumpf = await antwort.json();
|
||||
meldung = rumpf?.message ?? rumpf?.fehler ?? rumpf?.detail ?? meldung;
|
||||
meldung = rumpf?.message ?? rumpf?.meldung ?? rumpf?.fehler ?? rumpf?.detail ?? meldung;
|
||||
} catch {
|
||||
// Wenn kein JSON geliefert wird, bleibt es beim HTTP-Code
|
||||
}
|
||||
const fehler = new Error(meldung);
|
||||
fehler.status = antwort.status;
|
||||
if (antwort.status === 402) fehler.lizenzFehlt = true;
|
||||
if (antwort.status === 409) fehler.instanzKonflikt = true;
|
||||
if (antwort.status === 502) fehler.instanzUnerreichbar = true;
|
||||
throw fehler;
|
||||
}
|
||||
|
||||
|
||||
@@ -109,9 +109,33 @@ export const PFADE = {
|
||||
pfeilUnten: "M6 9l6 6 6-6",
|
||||
haken: "M20 6L9 17l-5-5",
|
||||
ausrufezeichen: "M12 8v4 M12 16h.01 M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z",
|
||||
ausloggen: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4 M16 17l5-5-5-5 M21 12H9"
|
||||
ausloggen: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4 M16 17l5-5-5-5 M21 12H9",
|
||||
server: "M2 20h20 M2 14h20 M2 8h20 M2 4h20 M6 6h.01 M6 12h.01 M6 18h.01",
|
||||
fenster: "M3 3h18v18H3z M3 9h18 M9 21V9",
|
||||
stop: "M6 6h12v12H6z"
|
||||
};
|
||||
|
||||
/**
|
||||
* Übersetzt den technischen Instanzzustand in deutschen Text und Plakettenstil.
|
||||
* @param {string} zustand "Running"|"Stopped"|"Silent"|"Crashed"|"Unknown"
|
||||
* @returns {{ text: string, art: "erfolg"|"warnung"|"gefahr"|"neutral" }}
|
||||
*/
|
||||
export function instanzZustandInfo(zustand) {
|
||||
const z = (zustand || "").toLowerCase();
|
||||
switch (z) {
|
||||
case "running":
|
||||
return { text: "Läuft", art: "erfolg" };
|
||||
case "stopped":
|
||||
return { text: "Beendet", art: "neutral" };
|
||||
case "silent":
|
||||
return { text: "Verstummt", art: "warnung" };
|
||||
case "crashed":
|
||||
return { text: "Abgestürzt", art: "gefahr" };
|
||||
default:
|
||||
return { text: zustand || "Unbekannt", art: "neutral" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt eine Statusplakette (Badge).
|
||||
* @param {string} text
|
||||
@@ -154,11 +178,12 @@ export function erstelleKarte(titel, elemente = [], optionen = {}) {
|
||||
* @param {string} titel
|
||||
* @param {HTMLElement} inhalt
|
||||
* @param {HTMLElement[]} [aktionen]
|
||||
* @param {object} [optionen]
|
||||
* @returns {{ dialog: HTMLElement, oeffnen: () => void, schliessen: () => void }}
|
||||
*/
|
||||
export function erstelleModal(titel, inhalt, aktionen = []) {
|
||||
export function erstelleModal(titel, inhalt, aktionen = [], optionen = {}) {
|
||||
const overlay = erstelleElement("div", { klasse: "modal-overlay", hidden: true });
|
||||
const box = erstelleElement("div", { klasse: "modal-box karte" });
|
||||
const box = erstelleElement("div", { klasse: `modal-box karte ${optionen.klasse ?? ""}`.trim() });
|
||||
|
||||
const kopf = erstelleElement("div", { klasse: "modal-kopf" });
|
||||
const h = erstelleElement("h2", { text: titel });
|
||||
@@ -172,16 +197,33 @@ export function erstelleModal(titel, inhalt, aktionen = []) {
|
||||
const koerper = erstelleElement("div", { klasse: "modal-koerper" }, inhalt);
|
||||
|
||||
const fuss = erstelleElement("div", { klasse: "modal-fuss" });
|
||||
for (const aktion of aktionen) fuss.append(aktion);
|
||||
|
||||
// Wenn der Inhalt ein Formular ist oder enthält, verknüpfe Submit-Buttons im Fuß
|
||||
const formElement = (inhalt && inhalt.tagName === "FORM")
|
||||
? inhalt
|
||||
: (inhalt?.querySelector ? inhalt.querySelector("form") : null);
|
||||
|
||||
for (const aktion of aktionen) {
|
||||
if (formElement && (aktion.type === "submit" || aktion.getAttribute?.("type") === "submit")) {
|
||||
aktion.addEventListener("click", () => {
|
||||
formElement.requestSubmit();
|
||||
});
|
||||
}
|
||||
fuss.append(aktion);
|
||||
}
|
||||
|
||||
box.append(kopf, koerper, fuss);
|
||||
overlay.append(box);
|
||||
document.body.append(overlay);
|
||||
|
||||
const schliessen = () => {
|
||||
if (overlay.hidden) return;
|
||||
overlay.hidden = true;
|
||||
overlay.classList.remove("sichtbar");
|
||||
document.removeEventListener("keydown", tasteHandler);
|
||||
if (typeof optionen.onSchliessen === "function") {
|
||||
optionen.onSchliessen();
|
||||
}
|
||||
};
|
||||
|
||||
const oeffnen = () => {
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
// ClawdDotNet — Interne Agenten-Kommunikation A2A (Vorhaben #208)
|
||||
//
|
||||
// Analyse und Protokollierung des Nachrichtenaustauschs zwischen Agenten.
|
||||
// - Filterbar nach beteiligten Agenten
|
||||
// - Dialog-Timeline zwischen Quelle und Ziel
|
||||
// - Live-Aktualisierung über Server-Sent Events (SSE "chat" mit source === "agentcomm")
|
||||
// Strikt DOM API, kein innerHTML.
|
||||
|
||||
import { api, holeAktiveInstanz } from "../modules/api.js";
|
||||
import { abonnieren } from "../modules/events.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleKarte,
|
||||
erstellePlakette,
|
||||
erstelleIcon,
|
||||
PFADE,
|
||||
formatiereDatumZeit
|
||||
} from "../modules/dom.js";
|
||||
|
||||
let sseAbmeldung = null;
|
||||
|
||||
export async function rendereAgentenChats() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
if (sseAbmeldung) {
|
||||
sseAbmeldung();
|
||||
sseAbmeldung = null;
|
||||
}
|
||||
|
||||
const instanzId = holeAktiveInstanz();
|
||||
const layout = erstelleElement("div", { klasse: "agenten-chats-ansicht" });
|
||||
|
||||
// Kopfzeile
|
||||
const kopf = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
const titelGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
titelGruppe.append(
|
||||
erstelleElement("h1", { text: "Agenten-Kommunikation (A2A)" })
|
||||
);
|
||||
|
||||
const filterGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
|
||||
const agentSelect = erstelleElement("select", {
|
||||
klasse: "eingabe",
|
||||
stil: { minWidth: "180px" },
|
||||
title: "Nach Agent filtern"
|
||||
});
|
||||
const optAlle = erstelleElement("option", { value: "", text: "Alle Agenten" });
|
||||
agentSelect.append(optAlle);
|
||||
|
||||
const aktualisierenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Kommunikation neu laden",
|
||||
onclick: () => ladeVerlauf()
|
||||
},
|
||||
erstelleIcon(PFADE.aktualisieren, 16),
|
||||
erstelleElement("span", { text: " Aktualisieren" })
|
||||
);
|
||||
|
||||
filterGruppe.append(agentSelect, aktualisierenBtn);
|
||||
kopf.append(titelGruppe, filterGruppe);
|
||||
layout.append(kopf);
|
||||
|
||||
if (!instanzId) {
|
||||
layout.append(
|
||||
erstelleKarte("Keine Instanz ausgewählt", [
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Bitte wähle oben im Kopfbereich eine Instanz aus, um deren Agenten-Kommunikation anzuzeigen."
|
||||
})
|
||||
])
|
||||
);
|
||||
container.append(layout);
|
||||
return;
|
||||
}
|
||||
|
||||
// Infokarte
|
||||
const infoKarte = erstelleKarte("Interne Kommunikation", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Hier wird der direkte Nachrichtenaustausch zwischen Agenten (A2A) protokolliert, wenn Agenten über das Werkzeug 'SendMessage' miteinander interagieren."
|
||||
})
|
||||
]);
|
||||
layout.append(infoKarte);
|
||||
|
||||
// Verlauf-Container
|
||||
const verlaufContainer = erstelleElement("div", { klasse: "a2a-timeline" });
|
||||
layout.append(verlaufContainer);
|
||||
container.append(layout);
|
||||
|
||||
let alleNachrichten = [];
|
||||
let agentenListe = [];
|
||||
|
||||
// Agenten laden für Filter
|
||||
try {
|
||||
const agenten = await api("/agents");
|
||||
agentenListe = Array.isArray(agenten) ? agenten : [];
|
||||
for (const a of agentenListe) {
|
||||
const opt = erstelleElement("option", {
|
||||
value: a.agentId,
|
||||
text: a.displayName || a.agentId
|
||||
});
|
||||
agentSelect.append(opt);
|
||||
}
|
||||
} catch {
|
||||
// Wenn Agenten nicht ladbar sind, Filter leer lassen
|
||||
}
|
||||
|
||||
agentSelect.addEventListener("change", () => {
|
||||
rendereNachrichten();
|
||||
});
|
||||
|
||||
async function ladeVerlauf() {
|
||||
verlaufContainer.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Kommunikationsverlauf…" })
|
||||
);
|
||||
|
||||
try {
|
||||
const daten = await api("/agents/communications");
|
||||
alleNachrichten = Array.isArray(daten) ? daten : [];
|
||||
rendereNachrichten();
|
||||
} catch (err) {
|
||||
verlaufContainer.replaceChildren(
|
||||
erstelleKarte("Fehler", [
|
||||
erstelleElement("p", {
|
||||
klasse: "fehler-meldung",
|
||||
text: `Kommunikation konnte nicht geladen werden: ${err.message}`
|
||||
})
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function rendereNachrichten() {
|
||||
const gewaehlterAgent = agentSelect.value;
|
||||
const gefiltert = gewaehlterAgent
|
||||
? alleNachrichten.filter(
|
||||
(m) =>
|
||||
m.targetAgentId === gewaehlterAgent ||
|
||||
m.sourceAgentId === gewaehlterAgent
|
||||
)
|
||||
: alleNachrichten;
|
||||
|
||||
if (gefiltert.length === 0) {
|
||||
verlaufContainer.replaceChildren(
|
||||
erstelleKarte("Keine Nachrichten", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: gewaehlterAgent
|
||||
? "Für den ausgewählten Agenten liegt noch keine interne Kommunikation vor."
|
||||
: "In dieser Instanz wurde bisher keine interne Agent-zu-Agent-Kommunikation protokolliert."
|
||||
})
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
verlaufContainer.replaceChildren();
|
||||
|
||||
for (const m of gefiltert) {
|
||||
const eintrag = erstelleNachrichtElement(m);
|
||||
verlaufContainer.append(eintrag);
|
||||
}
|
||||
}
|
||||
|
||||
function erstelleNachrichtElement(m) {
|
||||
const istAntwort = m.role === "assistant";
|
||||
const kasten = erstelleElement("div", {
|
||||
klasse: `a2a-eintrag ${istAntwort ? "a2a-antwort" : "a2a-anfrage"}`
|
||||
});
|
||||
|
||||
const kopf = erstelleElement("div", { klasse: "a2a-eintrag-kopf" });
|
||||
const absenderGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
|
||||
if (istAntwort) {
|
||||
// Antwort vom Zielagenten
|
||||
absenderGruppe.append(
|
||||
erstellePlakette(m.targetAgentName || m.targetAgentId, "erfolg"),
|
||||
erstelleElement("span", { klasse: "a2a-pfeil", text: "➔ Antwort" })
|
||||
);
|
||||
} else {
|
||||
// Anfrage/Nachricht von Quelle an Ziel
|
||||
const quelleName = m.sourceAgentName || m.sourceAgentId || "Agent";
|
||||
const zielName = m.targetAgentName || m.targetAgentId;
|
||||
absenderGruppe.append(
|
||||
erstellePlakette(quelleName, "neutral"),
|
||||
erstelleElement("span", { klasse: "a2a-pfeil", text: "➔" }),
|
||||
erstellePlakette(zielName, "primaer")
|
||||
);
|
||||
}
|
||||
|
||||
const zeit = erstelleElement("span", {
|
||||
klasse: "a2a-zeit",
|
||||
text: formatiereDatumZeit(m.timestamp)
|
||||
});
|
||||
|
||||
kopf.append(absenderGruppe, zeit);
|
||||
|
||||
const inhalt = erstelleElement("pre", {
|
||||
klasse: "a2a-inhalt",
|
||||
text: m.content
|
||||
});
|
||||
|
||||
kasten.append(kopf, inhalt);
|
||||
return kasten;
|
||||
}
|
||||
|
||||
// SSE Live-Aktualisierung
|
||||
sseAbmeldung = abonnieren("chat", (daten) => {
|
||||
if (daten?.source === "agentcomm") {
|
||||
// Neue Nachricht kam live herein: an die Liste anhängen
|
||||
const zielAgent = agentenListe.find((a) => a.agentId === daten.agentId);
|
||||
const zielName = zielAgent ? zielAgent.displayName : daten.agentId;
|
||||
|
||||
let senderId = null;
|
||||
let senderName = null;
|
||||
let content = daten.content || "";
|
||||
|
||||
if (daten.role === "user" && content.startsWith("[Nachricht von Agent \"")) {
|
||||
const endQuote = content.indexOf('"', 22);
|
||||
if (endQuote > 22) {
|
||||
senderName = content.slice(22, endQuote);
|
||||
const openParen = content.indexOf("(", endQuote);
|
||||
const closeParen = content.indexOf(")", openParen);
|
||||
if (openParen > 0 && closeParen > openParen) {
|
||||
senderId = content.slice(openParen + 1, closeParen);
|
||||
}
|
||||
}
|
||||
const doubleNl = content.indexOf("\n\n");
|
||||
if (doubleNl > 0) {
|
||||
content = content.slice(doubleNl + 2);
|
||||
}
|
||||
}
|
||||
|
||||
const neueNachricht = {
|
||||
targetAgentId: daten.agentId,
|
||||
targetAgentName: zielName,
|
||||
sourceAgentId: senderId,
|
||||
sourceAgentName: senderName,
|
||||
role: daten.role,
|
||||
content: content,
|
||||
timestamp: daten.timestamp || new Date().toISOString()
|
||||
};
|
||||
|
||||
alleNachrichten.push(neueNachricht);
|
||||
|
||||
const gewaehlterAgent = agentSelect.value;
|
||||
if (
|
||||
!gewaehlterAgent ||
|
||||
neueNachricht.targetAgentId === gewaehlterAgent ||
|
||||
neueNachricht.sourceAgentId === gewaehlterAgent
|
||||
) {
|
||||
// Falls vorher "Keine Nachrichten" da stand:
|
||||
const leerKarte = verlaufContainer.querySelector(".karte");
|
||||
if (leerKarte) verlaufContainer.replaceChildren();
|
||||
|
||||
const el = erstelleNachrichtElement(neueNachricht);
|
||||
verlaufContainer.append(el);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ladeVerlauf();
|
||||
}
|
||||
|
||||
export function trenneAgentenChatsStream() {
|
||||
if (sseAbmeldung) {
|
||||
sseAbmeldung();
|
||||
sseAbmeldung = null;
|
||||
}
|
||||
}
|
||||
@@ -74,12 +74,16 @@ async function ladeUndRendereListe(zielId = null) {
|
||||
listeContainer.replaceChildren();
|
||||
|
||||
if (!agenten || agenten.length === 0) {
|
||||
listeContainer.append(
|
||||
erstelleElement("div", {
|
||||
klasse: "hinweis-zentriert",
|
||||
text: "Keine Agenten eingerichtet."
|
||||
})
|
||||
const leerKasten = erstelleElement("div", { klasse: "hinweis-zentriert" });
|
||||
leerKasten.append(
|
||||
erstelleElement("p", { klasse: "gedaempft", text: "In dieser Instanz sind noch keine Agenten eingerichtet." }),
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche primaer klein",
|
||||
onclick: oeffneNeuerAgentDialog,
|
||||
style: "margin-top: 10px;"
|
||||
}, erstelleIcon(PFADE.plus, 14), erstelleElement("span", { text: " Ersten Agenten anlegen" }))
|
||||
);
|
||||
listeContainer.append(leerKasten);
|
||||
rendereDetails(null);
|
||||
return;
|
||||
}
|
||||
@@ -123,8 +127,13 @@ async function ladeUndRendereListe(zielId = null) {
|
||||
|
||||
rendereDetails(auswahlId);
|
||||
} catch (err) {
|
||||
const text = err.instanzUnerreichbar
|
||||
? "Die ausgewählte Instanz läuft derzeit nicht. Bitte starte sie zuerst im Kopfbereich oder unter 'Instanzen'."
|
||||
: (err.instanzKonflikt
|
||||
? "Keine aktive Instanz gewählt. Bitte wähle oben eine Instanz aus."
|
||||
: err.message);
|
||||
listeContainer.replaceChildren(
|
||||
erstelleElement("div", { klasse: "fehler-zentriert", text: err.message })
|
||||
erstelleElement("div", { klasse: "fehler-zentriert", text })
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -260,7 +269,10 @@ async function rendereDetails(agentId) {
|
||||
}
|
||||
|
||||
function oeffneNeuerAgentDialog() {
|
||||
const formular = erstelleElement("form", { klasse: "formular-raster" });
|
||||
const formular = erstelleElement("form", {
|
||||
klasse: "formular-raster",
|
||||
id: "neuer-agent-formular"
|
||||
});
|
||||
|
||||
const idFeld = erstelleElement("input", {
|
||||
type: "text",
|
||||
@@ -315,7 +327,11 @@ function oeffneNeuerAgentDialog() {
|
||||
"button",
|
||||
{
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche primaer"
|
||||
klasse: "schaltflaeche primaer",
|
||||
form: "neuer-agent-formular",
|
||||
onclick: () => {
|
||||
formular.requestSubmit();
|
||||
}
|
||||
},
|
||||
erstelleElement("span", { text: "Agent anlegen" })
|
||||
);
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
// ClawdDotNet — Datensicherung & Wiederherstellung (Vorhaben #530, Stufe 6)
|
||||
//
|
||||
// Verwaltet Sicherungen (.zip) der aktiven Instanz:
|
||||
// - Auflistung aller Archive im Sicherungsverzeichnis
|
||||
// - Neue Sicherung anlegen (Passphrase-Schutz, Logs, Chatverlauf)
|
||||
// - Wiederherstellung (mit geordnetem Stop/Start der Instanz)
|
||||
// - Löschen von Archiven
|
||||
// Strikt DOM API, kein innerHTML.
|
||||
|
||||
import { api, holeAktiveInstanz } from "../modules/api.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleKarte,
|
||||
erstellePlakette,
|
||||
erstelleIcon,
|
||||
erstelleModal,
|
||||
PFADE,
|
||||
formatiereDatumZeit,
|
||||
zeigeMeldung
|
||||
} from "../modules/dom.js";
|
||||
|
||||
function formatiereBytes(bytes) {
|
||||
if (bytes === 0) return "0 B";
|
||||
if (!bytes) return "—";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
export async function rendereSicherung() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
const instanzId = holeAktiveInstanz();
|
||||
const layout = erstelleElement("div", { klasse: "sicherung-ansicht" });
|
||||
|
||||
// Kopfzeile
|
||||
const kopf = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
const titelGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
titelGruppe.append(erstelleElement("h1", { text: "Datensicherung & Wiederherstellung" }));
|
||||
|
||||
const aktionenGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
|
||||
const aktualisierenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Sicherungen neu laden",
|
||||
onclick: () => ladeSicherungen()
|
||||
},
|
||||
erstelleIcon(PFADE.aktualisieren, 16),
|
||||
erstelleElement("span", { text: " Aktualisieren" })
|
||||
);
|
||||
|
||||
const neuBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche primaer",
|
||||
title: "Neue Sicherung erstellen",
|
||||
disabled: !instanzId,
|
||||
onclick: () => oeffneSicherungsDialog()
|
||||
},
|
||||
erstelleIcon(PFADE.plus, 16),
|
||||
erstelleElement("span", { text: " Neue Sicherung" })
|
||||
);
|
||||
|
||||
aktionenGruppe.append(aktualisierenBtn, neuBtn);
|
||||
kopf.append(titelGruppe, aktionenGruppe);
|
||||
layout.append(kopf);
|
||||
|
||||
if (!instanzId) {
|
||||
layout.append(
|
||||
erstelleKarte("Keine Instanz ausgewählt", [
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Bitte wähle oben im Kopfbereich eine Instanz aus, um deren Sicherungen zu verwalten."
|
||||
})
|
||||
])
|
||||
);
|
||||
container.append(layout);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verzeichnis-Info & Inhalt
|
||||
const infoBereich = erstelleElement("div", { klasse: "sicherung-info-bereich" });
|
||||
const inhaltBereich = erstelleElement("div", { klasse: "sicherung-inhalt-bereich" });
|
||||
|
||||
layout.append(infoBereich, inhaltBereich);
|
||||
container.append(layout);
|
||||
|
||||
async function ladeSicherungen() {
|
||||
inhaltBereich.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Sicherungen…" })
|
||||
);
|
||||
|
||||
try {
|
||||
const daten = await api(`/verwaltung/instanzen/${instanzId}/backups`);
|
||||
const verzeichnis = daten?.directory ?? "—";
|
||||
const backups = daten?.backups ?? [];
|
||||
|
||||
infoBereich.replaceChildren(
|
||||
erstelleKarte("Sicherungsverzeichnis", [
|
||||
erstelleElement("p", { klasse: "gedaempft", text: "Sicherungen werden komprimiert als ZIP-Archiv abgelegt. Die Datenbank (state.db) wird konsistent per VACUUM INTO kopiert." }),
|
||||
erstelleElement("div", { klasse: "ordner-pfad-anzeige" },
|
||||
erstelleIcon(PFADE.server, 14),
|
||||
erstelleElement("code", { text: verzeichnis })
|
||||
)
|
||||
])
|
||||
);
|
||||
|
||||
if (backups.length === 0) {
|
||||
inhaltBereich.replaceChildren(
|
||||
erstelleKarte("Vorhandene Sicherungen", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Für diese Instanz sind noch keine Sicherungsarchive vorhanden. Erstelle über die Schaltfläche oben eine neue Sicherung."
|
||||
})
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const tabelle = erstelleElement("table", { klasse: "tabelle sicherungen-tabelle" });
|
||||
const thead = erstelleElement("thead", {},
|
||||
erstelleElement("tr", {},
|
||||
erstelleElement("th", { text: "Archivdatei" }),
|
||||
erstelleElement("th", { text: "Erstellt am" }),
|
||||
erstelleElement("th", { text: "Größe" }),
|
||||
erstelleElement("th", { text: "Zugangsdaten" }),
|
||||
erstelleElement("th", { text: "Dateien" }),
|
||||
erstelleElement("th", { klasse: "spalte-aktionen", text: "Aktionen" })
|
||||
)
|
||||
);
|
||||
|
||||
const tbody = erstelleElement("tbody");
|
||||
|
||||
for (const b of backups) {
|
||||
const tr = erstelleElement("tr");
|
||||
|
||||
// Dateiname & Icon
|
||||
const dateiTd = erstelleElement("td", { klasse: "dateiname-zelle" },
|
||||
erstelleIcon(PFADE.sicherung, 16),
|
||||
erstelleElement("strong", { text: b.fileName })
|
||||
);
|
||||
|
||||
// Datum
|
||||
const datumTd = erstelleElement("td", { text: formatiereDatumZeit(b.createdAt) });
|
||||
|
||||
// Größe
|
||||
const groesseTd = erstelleElement("td", { text: formatiereBytes(b.sizeBytes) });
|
||||
|
||||
// Zugangsdaten Modus
|
||||
const hatPassphrase = b.secrets === "Passphrase";
|
||||
const secretsPlakette = erstellePlakette(
|
||||
hatPassphrase ? `Passphrase (${b.secretCount})` : "Ausgeschlossen",
|
||||
hatPassphrase ? "erfolg" : "neutral"
|
||||
);
|
||||
const secretsTd = erstelleElement("td", {}, secretsPlakette);
|
||||
|
||||
// Dateienanzahl
|
||||
const dateienTd = erstelleElement("td", { text: b.fileCount > 0 ? `${b.fileCount} Dateien` : "—" });
|
||||
|
||||
// Aktionen
|
||||
const aktionenTd = erstelleElement("td", { klasse: "spalte-aktionen" });
|
||||
|
||||
const restoreBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche klein sekundär",
|
||||
title: "Diese Sicherung wiederherstellen",
|
||||
onclick: () => oeffneRestoreDialog(b)
|
||||
},
|
||||
erstelleIcon(PFADE.abspielen, 14),
|
||||
erstelleElement("span", { text: " Wiederherstellen" })
|
||||
);
|
||||
|
||||
const deleteBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche klein gefahr flach",
|
||||
title: "Sicherungsdatei unwiderruflich löschen",
|
||||
onclick: () => oeffneLoeschenDialog(b.fileName)
|
||||
},
|
||||
erstelleIcon(PFADE.muelleimer, 14)
|
||||
);
|
||||
|
||||
aktionenTd.append(restoreBtn, deleteBtn);
|
||||
tr.append(dateiTd, datumTd, groesseTd, secretsTd, dateienTd, aktionenTd);
|
||||
tbody.append(tr);
|
||||
}
|
||||
|
||||
tabelle.append(thead, tbody);
|
||||
inhaltBereich.replaceChildren(
|
||||
erstelleKarte(`Vorhandene Sicherungen (${backups.length})`, [tabelle])
|
||||
);
|
||||
} catch (err) {
|
||||
inhaltBereich.replaceChildren(
|
||||
erstelleKarte("Fehler", [
|
||||
erstelleElement("p", { klasse: "fehler-meldung", text: `Sicherungen konnten nicht geladen werden: ${err.message}` })
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Modal: Neue Sicherung erstellen ───
|
||||
function oeffneSicherungsDialog() {
|
||||
const form = erstelleElement("form", { klasse: "formular" });
|
||||
|
||||
// Modus Zugangsdaten
|
||||
const secretsGruppe = erstelleElement("div", { klasse: "form-gruppe" });
|
||||
secretsGruppe.append(
|
||||
erstelleElement("label", { text: "Zugangsdaten & Schlüssel" }),
|
||||
erstelleElement("p", { klasse: "form-hilfe", text: "DPAPI-geschützte Daten können mit einer Passphrase geschützt oder sicherheitshalber entfernt werden." })
|
||||
);
|
||||
|
||||
const secretsSelect = erstelleElement("select", { klasse: "eingabe" });
|
||||
const optExclude = erstelleElement("option", { value: "Exclude", text: "Entfernen (gefahrlos ablegbar, Schlüssel müssen neu eingetragen werden)" });
|
||||
const optPassphrase = erstelleElement("option", { value: "Passphrase", text: "Mit Passphrase schützen (vollständig übertragbar)" });
|
||||
secretsSelect.append(optExclude, optPassphrase);
|
||||
secretsGruppe.append(secretsSelect);
|
||||
|
||||
// Passphrase Feld
|
||||
const passGruppe = erstelleElement("div", { klasse: "form-gruppe", stil: { display: "none" } });
|
||||
const passInput = erstelleElement("input", {
|
||||
type: "password",
|
||||
klasse: "eingabe",
|
||||
placeholder: "Passphrase für Verschlüsselung eingeben…"
|
||||
});
|
||||
passGruppe.append(
|
||||
erstelleElement("label", { text: "Passphrase" }),
|
||||
passInput
|
||||
);
|
||||
|
||||
secretsSelect.addEventListener("change", () => {
|
||||
passGruppe.style.display = secretsSelect.value === "Passphrase" ? "block" : "none";
|
||||
if (secretsSelect.value === "Passphrase") passInput.focus();
|
||||
});
|
||||
|
||||
// Checkboxen
|
||||
const chatsGruppe = erstelleElement("div", { klasse: "form-gruppe-checkbox" });
|
||||
const chatsCheckbox = erstelleElement("input", { type: "checkbox", id: "backup-chk-chats", checked: true });
|
||||
const chatsLabel = erstelleElement("label", { for: "backup-chk-chats", text: "Chatverläufe sichern" });
|
||||
chatsGruppe.append(chatsCheckbox, chatsLabel);
|
||||
|
||||
const logsGruppe = erstelleElement("div", { klasse: "form-gruppe-checkbox" });
|
||||
const logsCheckbox = erstelleElement("input", { type: "checkbox", id: "backup-chk-logs", checked: false });
|
||||
const logsLabel = erstelleElement("label", { for: "backup-chk-logs", text: "Protokolldateien (Logs) einschließen" });
|
||||
logsGruppe.append(logsCheckbox, logsLabel);
|
||||
|
||||
form.append(secretsGruppe, passGruppe, chatsGruppe, logsGruppe);
|
||||
|
||||
let modal;
|
||||
const abbrechenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche",
|
||||
text: "Abbrechen",
|
||||
onclick: () => modal.schliessen()
|
||||
});
|
||||
|
||||
const speichernBtn = erstelleElement("button", {
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche primaer",
|
||||
text: "Sicherung erstellen"
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const secrets = secretsSelect.value;
|
||||
const passphrase = passInput.value.trim();
|
||||
|
||||
if (secrets === "Passphrase" && !passphrase) {
|
||||
zeigeMeldung("Bitte gib eine Passphrase ein.", "warnung");
|
||||
passInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
speichernBtn.disabled = true;
|
||||
speichernBtn.textContent = "Erstelle Sicherung…";
|
||||
|
||||
try {
|
||||
const res = await api(`/verwaltung/instanzen/${instanzId}/backups`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
secrets,
|
||||
passphrase: passphrase || null,
|
||||
includeLogs: logsCheckbox.checked,
|
||||
includeChatHistory: chatsCheckbox.checked
|
||||
}
|
||||
});
|
||||
zeigeMeldung(res.meldung || "Sicherung erfolgreich erstellt.", "erfolg");
|
||||
modal.schliessen();
|
||||
ladeSicherungen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler beim Sichern: ${err.message}`, "gefahr");
|
||||
speichernBtn.disabled = false;
|
||||
speichernBtn.textContent = "Sicherung erstellen";
|
||||
}
|
||||
});
|
||||
|
||||
modal = erstelleModal("Neue Sicherung erstellen", form, [abbrechenBtn, speichernBtn]);
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
// ─── Modal: Wiederherstellen ───
|
||||
function oeffneRestoreDialog(backup) {
|
||||
const form = erstelleElement("form", { klasse: "formular" });
|
||||
|
||||
const warnung = erstelleElement("div", { klasse: "hinweis-box warnung" },
|
||||
erstelleIcon(PFADE.ausrufezeichen, 18),
|
||||
erstelleElement("div", {},
|
||||
erstelleElement("strong", { text: "Achtung: Überschreiben bestehender Daten" }),
|
||||
erstelleElement("p", {
|
||||
text: `Die Wiederherstellung überschreibt die aktuellen Konfigurationen und Daten der Instanz mit dem Stand vom ${formatiereDatumZeit(backup.createdAt)}.`
|
||||
}),
|
||||
erstelleElement("p", {
|
||||
text: "Die Instanz wird dafür kurz geordnet gestoppt und nach erfolgreicher Rücksicherung wieder hochgefahren."
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
form.append(warnung);
|
||||
|
||||
let passInput = null;
|
||||
if (backup.secrets === "Passphrase") {
|
||||
const passGruppe = erstelleElement("div", { klasse: "form-gruppe", stil: { marginTop: "16px" } });
|
||||
passInput = erstelleElement("input", {
|
||||
type: "password",
|
||||
klasse: "eingabe",
|
||||
placeholder: "Passphrase der Sicherung eingeben…",
|
||||
required: true
|
||||
});
|
||||
passGruppe.append(
|
||||
erstelleElement("label", { text: "Passphrase für Zugangsdaten" }),
|
||||
passInput
|
||||
);
|
||||
form.append(passGruppe);
|
||||
}
|
||||
|
||||
let modal;
|
||||
const abbrechenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche",
|
||||
text: "Abbrechen",
|
||||
onclick: () => modal.schliessen()
|
||||
});
|
||||
|
||||
const restoreBtn = erstelleElement("button", {
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche warnung",
|
||||
text: "Jetzt Wiederherstellen"
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const passphrase = passInput?.value?.trim() || null;
|
||||
|
||||
if (backup.secrets === "Passphrase" && !passphrase) {
|
||||
zeigeMeldung("Bitte gib die Passphrase an.", "warnung");
|
||||
passInput?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
restoreBtn.disabled = true;
|
||||
restoreBtn.textContent = "Stelle wieder her…";
|
||||
|
||||
try {
|
||||
const res = await api(`/verwaltung/instanzen/${instanzId}/backups/${encodeURIComponent(backup.fileName)}/restore`, {
|
||||
method: "POST",
|
||||
body: { passphrase }
|
||||
});
|
||||
zeigeMeldung(res.meldung || "Wiederherstellung erfolgreich abgeschlossen.", "erfolg");
|
||||
modal.schliessen();
|
||||
ladeSicherungen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Wiederherstellung fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
restoreBtn.disabled = false;
|
||||
restoreBtn.textContent = "Jetzt Wiederherstellen";
|
||||
}
|
||||
});
|
||||
|
||||
modal = erstelleModal(`Sicherung wiederherstellen: ${backup.fileName}`, form, [abbrechenBtn, restoreBtn]);
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
// ─── Modal: Löschen ───
|
||||
function oeffneLoeschenDialog(fileName) {
|
||||
const text = erstelleElement("p", {
|
||||
text: `Soll die Sicherungsdatei '${fileName}' wirklich unwiderruflich gelöscht werden?`
|
||||
});
|
||||
|
||||
let modal;
|
||||
const abbrechenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche",
|
||||
text: "Abbrechen",
|
||||
onclick: () => modal.schliessen()
|
||||
});
|
||||
|
||||
const loeschenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche gefahr",
|
||||
text: "Löschen",
|
||||
onclick: async () => {
|
||||
loeschenBtn.disabled = true;
|
||||
try {
|
||||
const res = await api(`/verwaltung/instanzen/${instanzId}/backups/${encodeURIComponent(fileName)}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
zeigeMeldung(res.meldung || "Sicherung gelöscht.", "erfolg");
|
||||
modal.schliessen();
|
||||
ladeSicherungen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Löschen fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
loeschenBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
modal = erstelleModal("Sicherung löschen", text, [abbrechenBtn, loeschenBtn]);
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
ladeSicherungen();
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// Leitfaden: Sicherer Chatverlauf (strikt textContent), Echtzeit-Ereignisse über SSE.
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { api, holeAktiveInstanz } from "../modules/api.js";
|
||||
import { abonnieren } from "../modules/events.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
@@ -131,7 +131,8 @@ export async function rendereChat(parameter = {}) {
|
||||
// SSE-Abonnements
|
||||
entladeEreignisse.push(
|
||||
abonnieren("chat", (daten) => {
|
||||
if (daten.agentId === aktiverAgentId) {
|
||||
const aktiveId = holeAktiveInstanz();
|
||||
if (daten.agentId === aktiverAgentId && (!daten.instanz || !aktiveId || daten.instanz === aktiveId)) {
|
||||
fuegeNachrichtEin(daten);
|
||||
}
|
||||
})
|
||||
@@ -139,7 +140,8 @@ export async function rendereChat(parameter = {}) {
|
||||
|
||||
entladeEreignisse.push(
|
||||
abonnieren("step", (daten) => {
|
||||
if (daten.agentId === aktiverAgentId) {
|
||||
const aktiveId = holeAktiveInstanz();
|
||||
if (daten.agentId === aktiverAgentId && (!daten.instanz || !aktiveId || daten.instanz === aktiveId)) {
|
||||
zeigeSchritt(daten.text);
|
||||
}
|
||||
})
|
||||
@@ -147,14 +149,18 @@ export async function rendereChat(parameter = {}) {
|
||||
|
||||
entladeEreignisse.push(
|
||||
abonnieren("run", (daten) => {
|
||||
versteckeSchritt();
|
||||
aktualisiereLaufStatus(false);
|
||||
const aktiveId = holeAktiveInstanz();
|
||||
if (!daten.instanz || !aktiveId || daten.instanz === aktiveId) {
|
||||
versteckeSchritt();
|
||||
aktualisiereLaufStatus(false);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
entladeEreignisse.push(
|
||||
abonnieren("instance", (daten) => {
|
||||
if (daten.kind === "Agents") {
|
||||
const aktiveId = holeAktiveInstanz();
|
||||
if (daten.kind === "Agents" && (!daten.instanz || !aktiveId || daten.instanz === aktiveId)) {
|
||||
ladeAgenten(aktiverAgentId);
|
||||
}
|
||||
})
|
||||
@@ -286,6 +292,10 @@ function fuegeNachrichtEin(eintrag) {
|
||||
});
|
||||
|
||||
kopf.append(autor);
|
||||
if (eintrag.instanzName) {
|
||||
const instanzBadge = erstelleElement("span", { klasse: "quelle-tag", text: eintrag.instanzName });
|
||||
kopf.append(instanzBadge);
|
||||
}
|
||||
if (eintrag.source) {
|
||||
const quelleBadge = erstelleElement("span", { klasse: "quelle-tag", text: eintrag.source });
|
||||
kopf.append(quelleBadge);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// Zeigt Anwendungsversion, Instanzkennung, Teilsysteme und OpenRouter-Guthaben an.
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { api, holeAktiveInstanz } from "../modules/api.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleKarte,
|
||||
@@ -21,6 +21,18 @@ export async function rendereInfo() {
|
||||
layout.append(gitter);
|
||||
container.append(layout);
|
||||
|
||||
if (!holeAktiveInstanz()) {
|
||||
gitter.append(
|
||||
erstelleKarte("Keine Instanz ausgewählt", [
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Wähle oben im Kopfbereich eine aktive Instanz aus oder wechsle in den Bereich 'Instanzen', um eine Instanz zu starten."
|
||||
})
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
gitter.append(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Systemdaten…" })
|
||||
);
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
// ClawdDotNet — Instanzverwaltung
|
||||
//
|
||||
// Leitfaden: Übersicht, Anlegen, Starten, Anhalten, Desktop-Fenster nach vorn holen,
|
||||
// Auswahl der aktiven Instanz für das WebUI. Kein innerHTML, strikt textContent.
|
||||
|
||||
import { api, holeAktiveInstanz, setzeAktiveInstanz } from "../modules/api.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
PFADE,
|
||||
erstellePlakette,
|
||||
erstelleKarte,
|
||||
erstelleModal,
|
||||
instanzZustandInfo,
|
||||
zeigeMeldung
|
||||
} from "../modules/dom.js";
|
||||
|
||||
let aktualisierungsIntervall = null;
|
||||
|
||||
export async function rendereInstanzen() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
stoppePolling();
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "instanzen-ansicht" });
|
||||
|
||||
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
const titelGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
titelGruppe.append(erstelleElement("h1", { text: "Instanzverwaltung" }));
|
||||
|
||||
const aktionenGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
|
||||
const aktualisierenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Instanzliste manuell aktualisieren",
|
||||
onclick: () => ladeInstanzen()
|
||||
},
|
||||
erstelleIcon(PFADE.aktualisieren, 16),
|
||||
erstelleElement("span", { text: " Aktualisieren" })
|
||||
);
|
||||
|
||||
const neuBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche primaer",
|
||||
title: "Neue Instanz anlegen",
|
||||
onclick: oeffneNeueInstanzDialog
|
||||
},
|
||||
erstelleIcon(PFADE.plus, 16),
|
||||
erstelleElement("span", { text: " Neue Instanz" })
|
||||
);
|
||||
|
||||
aktionenGruppe.append(aktualisierenBtn, neuBtn);
|
||||
kopfZeile.append(titelGruppe, aktionenGruppe);
|
||||
|
||||
const raster = erstelleElement("div", { klasse: "instanzen-raster", id: "instanzen-raster" });
|
||||
|
||||
layout.append(kopfZeile, raster);
|
||||
container.append(layout);
|
||||
|
||||
await ladeInstanzen();
|
||||
|
||||
// Hintergrund-Polling alle 4 Sekunden, da sich Zustände ohne Klick ändern können
|
||||
aktualisierungsIntervall = setInterval(() => {
|
||||
ladeInstanzen(true);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
export function stoppePolling() {
|
||||
if (aktualisierungsIntervall) {
|
||||
clearInterval(aktualisierungsIntervall);
|
||||
aktualisierungsIntervall = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function ladeInstanzen(istHintergrund = false) {
|
||||
const raster = document.getElementById("instanzen-raster");
|
||||
if (!raster) {
|
||||
stoppePolling();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!istHintergrund && raster.children.length === 0) {
|
||||
raster.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Instanzen…" })
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const instanzen = await api("/verwaltung/instanzen");
|
||||
if (!raster) return;
|
||||
raster.replaceChildren();
|
||||
|
||||
if (!Array.isArray(instanzen) || instanzen.length === 0) {
|
||||
raster.append(
|
||||
erstelleKarte("Keine Instanzen gefunden", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Es sind noch keine Instanzen eingerichtet. Lege mit 'Neue Instanz' eine an."
|
||||
})
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const aktiveId = holeAktiveInstanz();
|
||||
|
||||
for (const instanz of instanzen) {
|
||||
const istAktiv = instanz.id === aktiveId;
|
||||
const zustandInfo = instanzZustandInfo(instanz.zustand);
|
||||
const istLaufend = instanz.zustand === "Running";
|
||||
const istStumm = instanz.zustand === "Silent";
|
||||
const istAbgestuerzt = instanz.zustand === "Crashed";
|
||||
|
||||
// Instanzkarte
|
||||
const karteKopfAktionen = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
if (istAktiv) {
|
||||
karteKopfAktionen.append(erstellePlakette("Aktiv im WebUI", "erfolg"));
|
||||
}
|
||||
karteKopfAktionen.append(erstellePlakette(zustandInfo.text, zustandInfo.art));
|
||||
|
||||
// Details
|
||||
const dl = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dl.append(
|
||||
erstelleElement("dt", { text: "Instanz-Kennung" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: instanz.id })),
|
||||
erstelleElement("dt", { text: "Zustand" }),
|
||||
erstelleElement("dd", { text: zustandInfo.text }),
|
||||
erstelleElement("dt", { text: "Prozess (PID)" }),
|
||||
erstelleElement("dd", { text: instanz.prozess ? String(instanz.prozess) : "—" }),
|
||||
erstelleElement("dt", { text: "Autostart durch Manager" }),
|
||||
erstelleElement("dd", {
|
||||
text: instanz.autostart ? "Aktiviert" : "Aus",
|
||||
klasse: instanz.autostart ? "" : "gedaempft"
|
||||
})
|
||||
);
|
||||
|
||||
// Aktionsleiste
|
||||
const fussAktionen = erstelleElement("div", { klasse: "instanz-aktionen" });
|
||||
|
||||
// Auswählen-Knopf
|
||||
const waehlenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: istAktiv ? "schaltflaeche klein" : "schaltflaeche primaer klein",
|
||||
disabled: istAktiv,
|
||||
title: istAktiv ? "Diese Instanz wird aktuell im WebUI bedient" : "Diese Instanz für das WebUI auswählen",
|
||||
onclick: () => {
|
||||
setzeAktiveInstanz(instanz.id);
|
||||
zeigeMeldung(`Instanz '${instanz.name}' im WebUI ausgewählt.`, "info");
|
||||
ladeInstanzen();
|
||||
}
|
||||
},
|
||||
erstelleElement("span", { text: istAktiv ? "Ausgewählt" : "Auswählen" })
|
||||
);
|
||||
|
||||
// Starten-Knopf
|
||||
const startenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach klein",
|
||||
disabled: istLaufend,
|
||||
title: istLaufend ? "Instanz läuft bereits" : "Instanz starten",
|
||||
onclick: () => starteInstanz(instanz)
|
||||
},
|
||||
erstelleIcon(PFADE.abspielen, 14),
|
||||
erstelleElement("span", { text: " Starten" })
|
||||
);
|
||||
|
||||
// Anhalten-Knopf
|
||||
const anhaltenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche gefahr flach klein",
|
||||
disabled: !istLaufend && !istStumm,
|
||||
title: (!istLaufend && !istStumm) ? "Instanz läuft nicht" : "Instanz geordnet anhalten",
|
||||
onclick: () => stoppeInstanz(instanz)
|
||||
},
|
||||
erstelleIcon(PFADE.stop, 14),
|
||||
erstelleElement("span", { text: " Anhalten" })
|
||||
);
|
||||
|
||||
// Fenster nach vorn
|
||||
const fensterBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach klein",
|
||||
disabled: !istLaufend,
|
||||
title: !istLaufend ? "Instanz läuft nicht" : "Desktop-Fenster dieser Instanz in den Vordergrund holen",
|
||||
onclick: () => holeFensterNachVorn(instanz)
|
||||
},
|
||||
erstelleIcon(PFADE.fenster, 14),
|
||||
erstelleElement("span", { text: " Fenster" })
|
||||
);
|
||||
|
||||
fussAktionen.append(waehlenBtn, startenBtn, anhaltenBtn, fensterBtn);
|
||||
|
||||
const karte = erstelleKarte(instanz.name, [dl, fussAktionen], {
|
||||
aktionen: karteKopfAktionen,
|
||||
klasse: istAktiv ? "instanz-karte aktiv" : "instanz-karte"
|
||||
});
|
||||
|
||||
raster.append(karte);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!istHintergrund) {
|
||||
raster.replaceChildren(
|
||||
erstelleElement("div", { klasse: "fehler-zentriert", text: `Fehler beim Laden: ${err.message}` })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function starteInstanz(instanz) {
|
||||
try {
|
||||
const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/start`, {
|
||||
method: "POST"
|
||||
});
|
||||
zeigeMeldung(antwort?.meldung || `Instanz '${instanz.name}' wird gestartet…`, "erfolg");
|
||||
// Wenn bisher keine Instanz im WebUI gewählt war, diese direkt wählen
|
||||
if (!holeAktiveInstanz()) {
|
||||
setzeAktiveInstanz(instanz.id);
|
||||
}
|
||||
await ladeInstanzen();
|
||||
} 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stoppeInstanz(instanz) {
|
||||
oeffneSicherheitsStoppDialog(instanz);
|
||||
}
|
||||
|
||||
function oeffneSicherheitsStoppDialog(instanz) {
|
||||
let verbleibend = 10;
|
||||
let timerId = null;
|
||||
|
||||
const inhalt = erstelleElement("div", { klasse: "formular-raster" });
|
||||
|
||||
// Sicherheits-Warnbox
|
||||
const sicherheitsBox = erstelleElement("div", { klasse: "sicherheits-box" });
|
||||
const boxTitel = erstelleElement(
|
||||
"h3",
|
||||
{},
|
||||
erstelleIcon(PFADE.ausrufezeichen, 18),
|
||||
erstelleElement("span", { text: "Achtung: Produktivinstanz beenden" })
|
||||
);
|
||||
const boxText1 = erstelleElement("p", {
|
||||
text: "ClawdDotNet-Instanzen sind auf Dauerbetrieb ausgelegt. Das Beenden stoppt alle aktiven Agenten, Automationen, Web-Zugriffe und Hintergrundaufgaben dieser Instanz geordnet."
|
||||
});
|
||||
const boxText2 = erstelleElement("p", {
|
||||
text: "Um ein versehentliches Beenden durch unbedachtes Klicken zu verhindern, ist die Bestätigung für 10 Sekunden gesperrt."
|
||||
});
|
||||
sicherheitsBox.append(boxTitel, boxText1, boxText2);
|
||||
|
||||
// Instanzdetails
|
||||
const dl = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dl.append(
|
||||
erstelleElement("dt", { text: "Instanz" }),
|
||||
erstelleElement("dd", {}, erstelleElement("strong", { text: instanz.name })),
|
||||
erstelleElement("dt", { text: "Kennung" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: instanz.id })),
|
||||
erstelleElement("dt", { text: "Prozess (PID)" }),
|
||||
erstelleElement("dd", { text: instanz.prozess ? String(instanz.prozess) : "—" })
|
||||
);
|
||||
|
||||
// Countdown-Anzeige
|
||||
const countdownBox = erstelleElement("div", {
|
||||
klasse: "sicherheits-countdown",
|
||||
text: `Bestätigung gesperrt: Noch ${verbleibend} Sekunden…`
|
||||
});
|
||||
|
||||
inhalt.append(sicherheitsBox, dl, countdownBox);
|
||||
|
||||
// Aktionen
|
||||
const abbrechenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen(),
|
||||
text: "Abbrechen (Instanz läuft weiter)"
|
||||
});
|
||||
|
||||
const anhaltenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
klasse: "schaltflaeche gefahr",
|
||||
disabled: true
|
||||
},
|
||||
erstelleIcon(PFADE.stop, 14),
|
||||
erstelleElement("span", { text: ` Instanz anhalten (${verbleibend}s)` })
|
||||
);
|
||||
|
||||
const modal = erstelleModal(
|
||||
`Instanz anhalten — ${instanz.name}`,
|
||||
inhalt,
|
||||
[abbrechenBtn, anhaltenBtn],
|
||||
{
|
||||
onSchliessen: () => {
|
||||
if (timerId) {
|
||||
clearInterval(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
timerId = setInterval(() => {
|
||||
verbleibend--;
|
||||
if (verbleibend > 0) {
|
||||
countdownBox.textContent = `Bestätigung gesperrt: Noch ${verbleibend} Sekunden…`;
|
||||
const textSpan = anhaltenBtn.querySelector("span");
|
||||
if (textSpan) textSpan.textContent = ` Instanz anhalten (${verbleibend}s)`;
|
||||
} else {
|
||||
clearInterval(timerId);
|
||||
timerId = null;
|
||||
anhaltenBtn.disabled = false;
|
||||
const textSpan = anhaltenBtn.querySelector("span");
|
||||
if (textSpan) textSpan.textContent = " Instanz jetzt endgültig anhalten";
|
||||
countdownBox.className = "sicherheits-countdown abgelaufen";
|
||||
countdownBox.textContent = "Sperre aufgehoben — Bestätigung jetzt möglich.";
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
anhaltenBtn.addEventListener("click", async () => {
|
||||
if (verbleibend > 0) return;
|
||||
anhaltenBtn.disabled = true;
|
||||
const textSpan = anhaltenBtn.querySelector("span");
|
||||
if (textSpan) textSpan.textContent = " Wird angehalten…";
|
||||
|
||||
try {
|
||||
const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/stop`, {
|
||||
method: "POST"
|
||||
});
|
||||
modal.schliessen();
|
||||
zeigeMeldung(antwort?.meldung || `Instanz '${instanz.name}' beendet.`, "info");
|
||||
await ladeInstanzen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Anhalten fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
anhaltenBtn.disabled = false;
|
||||
if (textSpan) textSpan.textContent = " Instanz jetzt endgültig anhalten";
|
||||
}
|
||||
});
|
||||
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
async function holeFensterNachVorn(instanz) {
|
||||
try {
|
||||
const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/fenster`, {
|
||||
method: "POST"
|
||||
});
|
||||
zeigeMeldung(antwort?.meldung || `Fenster von '${instanz.name}' nach vorn geholt.`, "info");
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fensteraufruf fehlgeschlagen: ${err.message}`, "warnung");
|
||||
}
|
||||
}
|
||||
|
||||
function oeffneNeueInstanzDialog() {
|
||||
const formular = erstelleElement("form", {
|
||||
klasse: "formular-raster",
|
||||
id: "neue-instanz-formular"
|
||||
});
|
||||
|
||||
const nameInput = erstelleElement("input", {
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "z.B. Produktion oder Agenten-Labor",
|
||||
autocomplete: "off"
|
||||
});
|
||||
|
||||
const hinweis = erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Der Instanzmanager legt dafür ein eigenes Verzeichnis mit getrennten Protokollen und Einstellungen an."
|
||||
});
|
||||
|
||||
formular.append(
|
||||
erstelleElement("label", { text: "Name der neuen Instanz:" }),
|
||||
nameInput,
|
||||
hinweis
|
||||
);
|
||||
|
||||
const abbrechenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen(),
|
||||
text: "Abbrechen"
|
||||
});
|
||||
|
||||
// Kein eigenes onclick und kein form-Attribut: erstelleModal verknüpft Submit-Knöpfe
|
||||
// im Fuß selbst mit dem Formular. Beides zusammen löste requestSubmit() zweimal aus —
|
||||
// ein Klick, zwei POST, zwei Instanzen. `disabled` hilft dagegen nicht, denn
|
||||
// form.requestSubmit() schert sich nicht um den Zustand eines Knopfes.
|
||||
const erstellenBtn = erstelleElement("button", {
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche primaer",
|
||||
text: "Instanz anlegen"
|
||||
});
|
||||
|
||||
const modal = erstelleModal("Neue Instanz anlegen", formular, [abbrechenBtn, erstellenBtn]);
|
||||
|
||||
formular.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const name = nameInput.value.trim();
|
||||
if (!name) return;
|
||||
|
||||
erstellenBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const antwort = await api("/verwaltung/instanzen", {
|
||||
method: "POST",
|
||||
body: { name }
|
||||
});
|
||||
modal.schliessen();
|
||||
zeigeMeldung(antwort?.meldung || `Instanz '${name}' erfolgreich angelegt.`, "erfolg");
|
||||
await ladeInstanzen();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler beim Anlegen: ${err.message}`, "gefahr");
|
||||
} finally {
|
||||
erstellenBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
modal.oeffnen();
|
||||
}
|
||||
@@ -29,10 +29,10 @@ export function initialisiereLogin(onErfolg) {
|
||||
setzeToken(token);
|
||||
|
||||
try {
|
||||
// Teste das Token gegen /api/info
|
||||
const info = await api("/info");
|
||||
// Teste das Token gegen /api/verwaltung/instanzen (gehört dem Manager direkt und scheitert nie an Instanzanzahl)
|
||||
const instanzen = await api("/verwaltung/instanzen");
|
||||
versteckeAnmeldung();
|
||||
if (loginCallback) loginCallback(info);
|
||||
if (loginCallback) loginCallback(instanzen);
|
||||
} catch (err) {
|
||||
loescheToken();
|
||||
if (fehlerBox) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "../modules/dom.js";
|
||||
|
||||
let logStream = null;
|
||||
let instanzFilter = "";
|
||||
let modulFilter = "";
|
||||
let levelFilter = "All";
|
||||
let autoScroll = true;
|
||||
@@ -38,6 +39,16 @@ export async function rendereProtokoll() {
|
||||
|
||||
const steuerungGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
|
||||
// Instanz-Auswahl
|
||||
const instanzSelect = erstelleElement("select", {
|
||||
klasse: "eingabe-select",
|
||||
title: "Nach Instanz filtern",
|
||||
onchange: (e) => {
|
||||
instanzFilter = e.target.value;
|
||||
}
|
||||
});
|
||||
instanzSelect.append(erstelleElement("option", { value: "", text: "Alle Instanzen" }));
|
||||
|
||||
// Modul-Auswahl
|
||||
const modulSelect = erstelleElement("select", {
|
||||
klasse: "eingabe-select",
|
||||
@@ -102,13 +113,25 @@ export async function rendereProtokoll() {
|
||||
erstelleElement("span", { text: " Leeren" })
|
||||
);
|
||||
|
||||
steuerungGruppe.append(modulSelect, levelSelect, autoScrollLabel, pauseButton, leerenBtn);
|
||||
steuerungGruppe.append(instanzSelect, modulSelect, levelSelect, autoScrollLabel, pauseButton, leerenBtn);
|
||||
kopfZeile.append(titelGruppe, steuerungGruppe);
|
||||
|
||||
logContainer = erstelleElement("div", { klasse: "protokoll-strom code-block" });
|
||||
layout.append(kopfZeile, logContainer);
|
||||
container.append(layout);
|
||||
|
||||
// Lade verfügbare Instanzen für Filter
|
||||
try {
|
||||
const instanzen = await api("/verwaltung/instanzen");
|
||||
if (Array.isArray(instanzen)) {
|
||||
for (const inst of instanzen) {
|
||||
instanzSelect.append(erstelleElement("option", { value: inst.id, text: inst.name }));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Nicht kritisch
|
||||
}
|
||||
|
||||
// Lade verfügbare Module
|
||||
try {
|
||||
const module = await api("/logs/modules");
|
||||
@@ -159,8 +182,22 @@ function starteLogStream() {
|
||||
function fuegeLogZeileEin(eintrag) {
|
||||
if (!logContainer) return;
|
||||
|
||||
// Filter nach Instanz falls gewählt
|
||||
if (instanzFilter && eintrag.instanz && eintrag.instanz !== instanzFilter && eintrag.instanzName !== instanzFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zeile = erstelleElement("div", { klasse: "log-zeile" });
|
||||
|
||||
if (eintrag.instanzName || eintrag.instanz) {
|
||||
zeile.append(
|
||||
erstelleElement("span", {
|
||||
klasse: "log-instanz",
|
||||
text: `[${eintrag.instanzName || eintrag.instanz}]`
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (eintrag.level) {
|
||||
const lvl = String(eintrag.level).toLowerCase();
|
||||
const badgeArt =
|
||||
|
||||
@@ -27,28 +27,6 @@ export function rendereSicherung() {
|
||||
container.append(layout);
|
||||
}
|
||||
|
||||
export function rendereEinstellungen() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "platzhalter-ansicht" });
|
||||
layout.append(erstelleElement("h1", { text: "Instanz-Einstellungen" }));
|
||||
|
||||
const karte = erstelleKarte("In Vorbereitung", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Die Konfiguration der Instanz (Port, Token, Logging, Budget) erfolgt über die Datei 'InstanceSettings.json' bzw. über den Einstellungsdialog im Desktop-Fenster."
|
||||
}),
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Ein dedizierter Web-Endpunkt zum Schreiben sensibler Zugangsdaten ist für einen späteren Meilenstein vorgesehen."
|
||||
})
|
||||
]);
|
||||
|
||||
layout.append(karte);
|
||||
container.append(layout);
|
||||
}
|
||||
|
||||
export function rendereAgentenChats() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
// ClawdDotNet — Einstellungen & Systemarchitektur
|
||||
//
|
||||
// Strikte Trennung zwischen globaler Manager-Ebene (Installation) und isolierter Instanz-Ebene.
|
||||
|
||||
import { api, holeAktiveInstanz } from "../modules/api.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleKarte,
|
||||
erstellePlakette,
|
||||
instanzZustandInfo,
|
||||
erstelleIcon,
|
||||
PFADE
|
||||
} from "../modules/dom.js";
|
||||
|
||||
export async function rendereEinstellungen() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "einstellungen-ansicht" });
|
||||
|
||||
// Kopfbereich
|
||||
const kopf = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
const kopfText = erstelleElement("div");
|
||||
kopfText.append(
|
||||
erstelleElement("h1", { text: "Einstellungen & Systemarchitektur" }),
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Übersicht der Konfiguration nach dem Instanzmanager-Konzept: Strikte Trennung zwischen globaler Manager-Ebene (Installation) und isolierter Instanz-Ebene."
|
||||
})
|
||||
);
|
||||
kopf.append(kopfText);
|
||||
layout.append(kopf);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// SEKTION 1: Instanzmanager (Globale Ebene / Installation)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
const managerSektion = erstelleElement("section", { klasse: "einstellungen-sektion sektion-manager" });
|
||||
|
||||
const managerKopf = erstelleElement("div", { klasse: "sektion-titel-zeile" });
|
||||
const managerTitelLinks = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
managerTitelLinks.append(
|
||||
erstelleIcon(PFADE.server, 20),
|
||||
erstelleElement("h2", { text: "Instanzmanager" }),
|
||||
erstellePlakette("Manager-Ebene · Global", "warnung")
|
||||
);
|
||||
managerKopf.append(managerTitelLinks);
|
||||
|
||||
const managerIntro = erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: "Gilt für alle Instanzen und die Weboberfläche. Gespeichert in manager.json im Anwendungs-Hauptverzeichnis."
|
||||
});
|
||||
|
||||
const managerGitter = erstelleElement("div", { klasse: "info-gitter" });
|
||||
|
||||
// Karte 1: Weboberfläche & Netzwerk
|
||||
const dlNetz = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlNetz.append(
|
||||
erstelleElement("dt", { text: "Konfigurationsdatei" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: "manager.json" })),
|
||||
erstelleElement("dt", { text: "WebUI-Port" }),
|
||||
erstelleElement("dd", { text: "7000 (Vorgabe, webUiPort)" }),
|
||||
erstelleElement("dt", { text: "Netzwerk-Bindung" }),
|
||||
erstelleElement("dd", { text: "loopback (127.0.0.1, webUiBinding)" }),
|
||||
erstelleElement("dt", { text: "Zugangsschutz" }),
|
||||
erstelleElement("dd", {}, erstellePlakette("Token aktiv (≥ 20 Zeichen)", "erfolg"))
|
||||
);
|
||||
managerGitter.append(erstelleKarte("Weboberfläche & Netzwerk", [dlNetz]));
|
||||
|
||||
// Karte 2: Lizenz & Programm-Updates
|
||||
const dlLizenz = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlLizenz.append(
|
||||
erstelleElement("dt", { text: "Lizenz-Geltung" }),
|
||||
erstelleElement("dd", { text: "Installationsebene (E-M3: Beliebig viele Instanzen)" }),
|
||||
erstelleElement("dt", { text: "Lizenzschlüssel" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: "●●●●-●●●●-●●●●-●●●● (verschlüsselt)" })),
|
||||
erstelleElement("dt", { text: "Update-Kanal" }),
|
||||
erstelleElement("dd", { text: "prod (Offizielle Release-Versionen)" }),
|
||||
erstelleElement("dt", { text: "Update-Prüfung" }),
|
||||
erstelleElement("dd", { text: "Zentral auf Manager-Ebene" })
|
||||
);
|
||||
managerGitter.append(erstelleKarte("Lizenz & Updates", [dlLizenz]));
|
||||
|
||||
// Karte 3: Deploymentcenter & Aufsicht
|
||||
const dlAufsicht = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlAufsicht.append(
|
||||
erstelleElement("dt", { text: "Deploymentcenter" }),
|
||||
erstelleElement("dd", { text: "https://dc.mhdf.de" }),
|
||||
erstelleElement("dt", { text: "Master-Token" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: "●●●●●●●● (erzeugt Sub-Token je Instanz)" })),
|
||||
erstelleElement("dt", { text: "Desktop-Fenster" }),
|
||||
erstelleElement("dd", { text: "Aktiv (jede Instanz hat ein eigenes Fenster)" }),
|
||||
erstelleElement("dt", { text: "Absturz-Schutz" }),
|
||||
erstelleElement("dd", { text: "Max. 5 Neustarts / 10 Minuten" })
|
||||
);
|
||||
managerGitter.append(erstelleKarte("Deploymentcenter & Aufsicht", [dlAufsicht]));
|
||||
|
||||
managerSektion.append(managerKopf, managerIntro, managerGitter);
|
||||
layout.append(managerSektion);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// SEKTION 2: Aktive Instanz (Instanz-Ebene)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
const instanzSektion = erstelleElement("section", { klasse: "einstellungen-sektion sektion-instanz" });
|
||||
|
||||
const aktiveId = holeAktiveInstanz();
|
||||
let instanzenListe = [];
|
||||
try {
|
||||
instanzenListe = await api("/verwaltung/instanzen");
|
||||
} catch {
|
||||
// Wenn Liste nicht abrufbar, weiter
|
||||
}
|
||||
|
||||
const aktiveInstanz = instanzenListe.find((i) => i.id === aktiveId);
|
||||
const zInfo = aktiveInstanz ? instanzZustandInfo(aktiveInstanz.zustand) : { text: "Keine gewählt", art: "neutral" };
|
||||
|
||||
const instanzKopf = erstelleElement("div", { klasse: "sektion-titel-zeile" });
|
||||
const instanzTitelLinks = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
instanzTitelLinks.append(
|
||||
erstelleIcon(PFADE.fenster, 20),
|
||||
erstelleElement("h2", { text: aktiveInstanz ? `Aktive Instanz: ${aktiveInstanz.name}` : "Instanzbezogene Einstellungen" }),
|
||||
erstellePlakette("Instanz-Ebene · Isoliert", "erfolg"),
|
||||
aktiveInstanz ? erstellePlakette(zInfo.text, zInfo.art) : erstellePlakette("Keine Instanz aktiv", "neutral")
|
||||
);
|
||||
instanzKopf.append(instanzTitelLinks);
|
||||
|
||||
const instanzIntro = erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: "Gilt ausschließlich für die aktuell ausgewählte Instanz. Alle Pfade, Protokolle, Agenten und Budgets sind strikt von anderen Instanzen isoliert."
|
||||
});
|
||||
|
||||
instanzSektion.append(instanzKopf, instanzIntro);
|
||||
|
||||
if (!aktiveInstanz) {
|
||||
const leerKarte = erstelleKarte("Keine Instanz ausgewählt", [
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Bitte wähle oben im Kopfbereich eine aktive Instanz aus oder wechsle in den Bereich 'Instanzen', um eine Instanz zu starten oder auszuwählen."
|
||||
}),
|
||||
erstelleElement("a", {
|
||||
href: "#/instanzen",
|
||||
klasse: "schaltflaeche primaer klein",
|
||||
text: "Zur Instanzen-Übersicht"
|
||||
})
|
||||
]);
|
||||
instanzSektion.append(leerKarte);
|
||||
} else {
|
||||
// Daten von der laufenden Instanz abfragen (sofern sie läuft)
|
||||
const [instanzInfo, instanzStatus] = await Promise.all([
|
||||
api("/info").catch(() => null),
|
||||
api("/status").catch(() => null)
|
||||
]);
|
||||
|
||||
const instanzGitter = erstelleElement("div", { klasse: "info-gitter" });
|
||||
|
||||
// Karte 1: Instanz-Identität & Autostart
|
||||
const dlIdent = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlIdent.append(
|
||||
erstelleElement("dt", { text: "Instanzname" }),
|
||||
erstelleElement("dd", {}, erstelleElement("strong", { text: aktiveInstanz.name })),
|
||||
erstelleElement("dt", { text: "Instanz-ID" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: aktiveInstanz.id })),
|
||||
erstelleElement("dt", { text: "Konfigurationsdatei" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/InstanceSettings.json` })),
|
||||
erstelleElement("dt", { text: "Autostart durch Manager" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(aktiveInstanz.autostart ? "Aktiv (Am Laufen halten)" : "Inaktiv (Manueller Start)", aktiveInstanz.autostart ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Kommunikation" }),
|
||||
erstelleElement("dd", { text: "HTTP über Named Pipe (Windows) / Socket (Linux)" })
|
||||
);
|
||||
instanzGitter.append(erstelleKarte("Identität & Prozess", [dlIdent]));
|
||||
|
||||
// Karte 2: Isolierte Verzeichnisse & Speicher
|
||||
const dlPfade = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlPfade.append(
|
||||
erstelleElement("dt", { text: "Protokolle (Logs)" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/Logs/` })),
|
||||
erstelleElement("dt", { text: "Datensicherungen" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/Backups/` })),
|
||||
erstelleElement("dt", { text: "Zustandsdatenbank" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/state.db (exklusiv)` })),
|
||||
erstelleElement("dt", { text: "Arbeitsbereich (Shared)" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: `Instances/${aktiveInstanz.name}/Agents/SharedWorkspace/` }))
|
||||
);
|
||||
instanzGitter.append(erstelleKarte("Isolierte Verzeichnisse", [dlPfade]));
|
||||
|
||||
// Karte 3: Modellanbindung & Budget (OpenRouter)
|
||||
const dlModell = erstelleElement("dl", { klasse: "datenliste" });
|
||||
if (instanzStatus) {
|
||||
dlModell.append(
|
||||
erstelleElement("dt", { text: "OpenRouter-Status" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzStatus.isApiReachable ? "Verbunden" : "Offline", instanzStatus.isApiReachable ? "erfolg" : "gefahr")),
|
||||
erstelleElement("dt", { text: "Statusmeldung" }),
|
||||
erstelleElement("dd", { text: instanzStatus.statusText || "—" }),
|
||||
erstelleElement("dt", { text: "Guthaben / Credits" }),
|
||||
erstelleElement("dd", {}, erstelleElement("strong", { text: instanzStatus.creditsText || "—" })),
|
||||
erstelleElement("dt", { text: "API-Schlüssel" }),
|
||||
erstelleElement("dd", { text: "In InstanceSettings.json dieser Instanz maskiert hinterlegt" })
|
||||
);
|
||||
} else {
|
||||
dlModell.append(
|
||||
erstelleElement("dt", { text: "OpenRouter" }),
|
||||
erstelleElement("dd", {
|
||||
klasse: "gedaempft",
|
||||
text: aktiveInstanz.zustand === "Running"
|
||||
? "Kein Schlüssel hinterlegt oder Dienst nicht erreichbar."
|
||||
: "Instanz läuft nicht. Daten werden nach dem Start geladen."
|
||||
})
|
||||
);
|
||||
}
|
||||
instanzGitter.append(erstelleKarte("Modellanbindung (OpenRouter)", [dlModell]));
|
||||
|
||||
// Karte 4: Teilsysteme der Instanz
|
||||
const dlSysteme = erstelleElement("dl", { klasse: "datenliste" });
|
||||
if (instanzInfo) {
|
||||
dlSysteme.append(
|
||||
erstelleElement("dt", { text: "Agent Engine" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzInfo.engineActive ? "Aktiv" : "Aus", instanzInfo.engineActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Scanner / Scheduler" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzInfo.schedulerActive ? "Aktiv" : "Aus", instanzInfo.schedulerActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Freigabestelle (Staging)" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzInfo.stagingActive ? "Aktiv" : "Aus", instanzInfo.stagingActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Deploymentcenter Sub-Token" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(instanzInfo.deploymentcenterActive ? "Aktiv" : "Aus", instanzInfo.deploymentcenterActive ? "erfolg" : "neutral"))
|
||||
);
|
||||
} else {
|
||||
dlSysteme.append(
|
||||
erstelleElement("dt", { text: "Subsysteme" }),
|
||||
erstelleElement("dd", {
|
||||
klasse: "gedaempft",
|
||||
text: aktiveInstanz.zustand === "Running"
|
||||
? "Laufzeitdaten werden abgefragt…"
|
||||
: "Instanz ist beendet — Teilsysteme ruhen."
|
||||
})
|
||||
);
|
||||
}
|
||||
instanzGitter.append(erstelleKarte("Laufzeit & Teilsysteme", [dlSysteme]));
|
||||
|
||||
instanzSektion.append(instanzGitter);
|
||||
}
|
||||
|
||||
layout.append(instanzSektion);
|
||||
container.append(layout);
|
||||
}
|
||||
Reference in New Issue
Block a user