Instanzmanager Stufe 4: eine Adresse, ein Zugang, alle Instanzen (#528, E-M5)
Der Managerprozess. ClawdDotNet.Instanzmanager ist eine Konsolenanwendung: Lizenz pruefen, uebernehmen was laeuft, Weboberflaeche aufhaengen, warten. Beim Herunterfahren beendet er keine Instanz. Zwei neue Projekte. ClawdDotNet.Manager.Web traegt die Weboberflaeche - ohne jeden Verweis auf ClawdDotNet.Web, denn dort haengt App und daran sechzehn Werkzeugprojekte. ClawdDotNet.ManagerApp ist der Einstiegspunkt. wwwroot ist aus der Instanz in den Manager gezogen. DER SCHNITT DER HTTP-FLAECHE: Unter /api/verwaltung steht, was den Manager angeht - auflisten, anlegen, starten, anhalten. Alles andere unter /api geht unveraendert an eine Instanz. Er leitet weiter, er implementiert nicht. Welche Instanz gemeint ist, sagt die Kopfzeile X-Instanz; laeuft genau eine, ist sie gemeint, und laufen mehrere, wird nachgefragt statt geraten - eine Anfrage an die falsche Instanz ist schlimmer als eine abgelehnte. Der zusammengefuehrte Ereignisstrom haelt je laufender Instanz eine Zuleitung und schreibt an jedes Ereignis, von wem es kommt. Ergaenzt statt verpackt: Ein Umschlag waere einfacher gewesen, haette aber jede Ansicht der Weboberflaeche zum Umschreiben gezwungen. ABNAHME gegen laufende Prozesse: 401 ohne Token, Oberflaeche ohne Token 200, Instanz ueber den Manager gestartet und angehalten, /api/info und /api/status unverfaelscht durchgereicht, /events/logs mit "instanz":"Instance-Test" an jeder Zeile. Manager abgeschossen: Instanz laeuft weiter. Manager neu gestartet: "1 Instanzen bekannt, 1 laufen bereits" - uebernommen. E-M6 damit in beide Richtungen belegt. DREI DEFEKTE, die erst der Lauf gezeigt hat: 1. Der Manager hing an einer Konsolenabfrage nach dem Lizenzschluessel. Ein Test auf Console.IsInputRedirected reicht dafuer NICHT: Er beantwortet "ist die Eingabe umgeleitet", nicht "kann jemand antworten" - ein Prozess ganz ohne Konsole, der gefaehrlichste Fall, ist nicht umgeleitet. Der Manager fragt jetzt gar nicht. 2. Eine unlesbare manager.json wurde stillschweigend durch Vorgaben ersetzt: falsches Instanzverzeichnis, anderer Port, Lizenzfrage - und nichts sagte, woran es lag. LoadProblem meldet den Grund jetzt. 3. Der Instanzprozess erbte CLAWD_CONFIG_DIR vom Manager und legte sich in dessen Konfigurationsverzeichnis eine eigene Settings.json an. Eine Umgebungsvariable vererbt sich, ein Argument nicht - der Manager nimmt sein Verzeichnis jetzt ueber --config. SharedLoggerProvider ist nach Core gezogen: Es gibt jetzt zwei Webhosts, die dasselbe brauchen, und sie duerfen einander nicht kennen. NOCH OFFEN in dieser Stufe: Die Weboberflaeche kennt noch keine Instanzen - Instanzauswahl, X-Instanz und die Verwaltungsansicht sind Arbeit an wwwroot. Und webServerPort steht noch in der Instanz. Build ohne Warnungen, 775 Tests gruen, 8 uebersprungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
// ClawdDotNet — Agenten-Verwaltung
|
||||
//
|
||||
// Übersicht, Anlegen, Löschen von Agenten, Werkzeug-Schalter und maskierte Einstellungen.
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { abonnieren } from "../modules/events.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
PFADE,
|
||||
erstellePlakette,
|
||||
erstelleKarte,
|
||||
erstelleModal,
|
||||
zeigeMeldung
|
||||
} from "../modules/dom.js";
|
||||
|
||||
let gewaehlterAgentId = null;
|
||||
let agenten = [];
|
||||
let entladeEreignisse = [];
|
||||
|
||||
export async function rendereAgenten(parameter = {}) {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
entladeEreignisse.forEach((c) => c());
|
||||
entladeEreignisse = [];
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "agenten-ansicht" });
|
||||
|
||||
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
kopfZeile.append(
|
||||
erstelleElement("h1", { text: "Agenten" }),
|
||||
erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche primaer",
|
||||
onclick: oeffneNeuerAgentDialog
|
||||
},
|
||||
erstelleIcon(PFADE.plus, 16),
|
||||
erstelleElement("span", { text: " Neuer Agent" })
|
||||
)
|
||||
);
|
||||
|
||||
const raster = erstelleElement("div", { klasse: "agenten-raster" });
|
||||
const listeContainer = erstelleElement("div", { klasse: "agenten-liste-bereich" });
|
||||
const detailContainer = erstelleElement("div", { klasse: "agenten-detail-bereich" });
|
||||
|
||||
raster.append(listeContainer, detailContainer);
|
||||
layout.append(kopfZeile, raster);
|
||||
container.append(layout);
|
||||
|
||||
await ladeUndRendereListe(parameter.agentId);
|
||||
|
||||
entladeEreignisse.push(
|
||||
abonnieren("instance", (daten) => {
|
||||
if (daten.kind === "Agents") {
|
||||
ladeUndRendereListe(gewaehlterAgentId);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function ladeUndRendereListe(zielId = null) {
|
||||
const listeContainer = document.querySelector(".agenten-liste-bereich");
|
||||
if (!listeContainer) return;
|
||||
|
||||
listeContainer.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Agenten…" })
|
||||
);
|
||||
|
||||
try {
|
||||
agenten = await api("/agents");
|
||||
listeContainer.replaceChildren();
|
||||
|
||||
if (!agenten || agenten.length === 0) {
|
||||
listeContainer.append(
|
||||
erstelleElement("div", {
|
||||
klasse: "hinweis-zentriert",
|
||||
text: "Keine Agenten eingerichtet."
|
||||
})
|
||||
);
|
||||
rendereDetails(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const auswahlId = zielId && agenten.some((a) => a.agentId === zielId)
|
||||
? zielId
|
||||
: agenten[0].agentId;
|
||||
|
||||
for (const a of agenten) {
|
||||
const eintrag = erstelleElement("div", {
|
||||
klasse: `agenten-karte ${a.agentId === auswahlId ? "aktiv" : ""}`,
|
||||
onclick: () => {
|
||||
document.querySelectorAll(".agenten-karte").forEach((k) => k.classList.remove("aktiv"));
|
||||
eintrag.classList.add("aktiv");
|
||||
rendereDetails(a.agentId);
|
||||
}
|
||||
});
|
||||
|
||||
const top = erstelleElement("div", { klasse: "agent-karte-top" });
|
||||
const name = erstelleElement("strong", { text: a.displayName });
|
||||
const status = a.isRunning
|
||||
? erstellePlakette("Beschäftigt", "warnung")
|
||||
: erstellePlakette("Bereit", "neutral");
|
||||
top.append(name, status);
|
||||
|
||||
const meta = erstelleElement("div", { klasse: "agent-karte-meta" });
|
||||
meta.append(
|
||||
erstelleElement("span", { klasse: "gedaempft", text: `ID: ${a.agentId}` }),
|
||||
erstelleElement("span", { klasse: "gedaempft", text: ` · Modell: ${a.model}` })
|
||||
);
|
||||
|
||||
const werkzeugeZaehler = a.tools ? a.tools.filter((t) => t.enabled).length : 0;
|
||||
const werkzeugeText = erstelleElement("div", {
|
||||
klasse: "agent-karte-werkzeuge gedaempft",
|
||||
text: `${werkzeugeZaehler} Werkzeuge aktiv`
|
||||
});
|
||||
|
||||
eintrag.append(top, meta, werkzeugeText);
|
||||
listeContainer.append(eintrag);
|
||||
}
|
||||
|
||||
rendereDetails(auswahlId);
|
||||
} catch (err) {
|
||||
listeContainer.replaceChildren(
|
||||
erstelleElement("div", { klasse: "fehler-zentriert", text: err.message })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function rendereDetails(agentId) {
|
||||
gewaehlterAgentId = agentId;
|
||||
const detailContainer = document.querySelector(".agenten-detail-bereich");
|
||||
if (!detailContainer) return;
|
||||
detailContainer.replaceChildren();
|
||||
|
||||
if (!agentId) {
|
||||
detailContainer.append(
|
||||
erstelleKarte("Agent-Details", [
|
||||
erstelleElement("p", { klasse: "gedaempft", text: "Wähle links einen Agenten aus." })
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const agent = agenten.find((a) => a.agentId === agentId);
|
||||
if (!agent) return;
|
||||
|
||||
// Kopf-Aktionen (Chatten, Löschen)
|
||||
const aktionen = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
|
||||
const zumChatBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Mit diesem Agenten chatten",
|
||||
onclick: () => {
|
||||
window.location.hash = `#/chat/${encodeURIComponent(agent.agentId)}`;
|
||||
}
|
||||
},
|
||||
erstelleIcon(PFADE.chat, 14),
|
||||
erstelleElement("span", { text: " Chatten" })
|
||||
);
|
||||
|
||||
const loeschenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche gefahr flach",
|
||||
title: "Diesen Agenten löschen",
|
||||
onclick: () => loescheAgent(agent.agentId, agent.displayName)
|
||||
},
|
||||
erstelleIcon(PFADE.muelleimer, 14),
|
||||
erstelleElement("span", { text: " Löschen" })
|
||||
);
|
||||
|
||||
aktionen.append(zumChatBtn, loeschenBtn);
|
||||
|
||||
// Stammdaten-Karte
|
||||
const stammdatenListe = erstelleElement("dl", { klasse: "datenliste" });
|
||||
const felder = [
|
||||
["Name", agent.displayName],
|
||||
["Kennung (ID)", agent.agentId],
|
||||
["Modell", agent.model],
|
||||
["Beschreibung", agent.description || "—"]
|
||||
];
|
||||
|
||||
for (const [bez, wert] of felder) {
|
||||
stammdatenListe.append(
|
||||
erstelleElement("dt", { text: bez }),
|
||||
erstelleElement("dd", { text: wert })
|
||||
);
|
||||
}
|
||||
|
||||
const basisKarte = erstelleKarte(agent.displayName, [stammdatenListe], { aktionen });
|
||||
|
||||
// Werkzeuge-Karte
|
||||
const werkzeugeListe = erstelleElement("div", { klasse: "werkzeuge-liste" });
|
||||
|
||||
if (!agent.tools || agent.tools.length === 0) {
|
||||
werkzeugeListe.append(
|
||||
erstelleElement("p", { klasse: "gedaempft", text: "Keine Werkzeuge für diesen Agenten konfiguriert." })
|
||||
);
|
||||
} else {
|
||||
for (const tool of agent.tools) {
|
||||
const zeile = erstelleElement("div", { klasse: "werkzeug-zeile" });
|
||||
|
||||
const links = erstelleElement("div", { klasse: "werkzeug-info" });
|
||||
const toolTitel = erstelleElement("strong", { text: tool.toolName });
|
||||
const toolDesc = erstelleElement("div", {
|
||||
klasse: "werkzeug-beschreibung gedaempft",
|
||||
text: tool.description || ""
|
||||
});
|
||||
links.append(toolTitel, toolDesc);
|
||||
|
||||
const rechts = erstelleElement("div", { klasse: "werkzeug-aktionen" });
|
||||
|
||||
const einstellungenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach klein",
|
||||
title: "Werkzeug-Einstellungen einsehen",
|
||||
onclick: () => zeigeWerkzeugEinstellungen(agent.agentId, tool.toolName)
|
||||
},
|
||||
erstelleIcon(PFADE.einstellungen, 14),
|
||||
erstelleElement("span", { text: " Konfiguration" })
|
||||
);
|
||||
|
||||
const toggleLabel = erstelleElement("label", { klasse: "schalter" });
|
||||
const toggleInput = erstelleElement("input", {
|
||||
type: "checkbox",
|
||||
checked: Boolean(tool.enabled),
|
||||
onchange: async (e) => {
|
||||
const aktiv = e.target.checked;
|
||||
try {
|
||||
await api(
|
||||
`/agents/${encodeURIComponent(agent.agentId)}/tools/${encodeURIComponent(tool.toolName)}?enabled=${aktiv}`,
|
||||
{ method: "PUT" }
|
||||
);
|
||||
tool.enabled = aktiv;
|
||||
zeigeMeldung(`Werkzeug '${tool.toolName}' ${aktiv ? "aktiviert" : "deaktiviert"}.`, "info");
|
||||
} catch (err) {
|
||||
e.target.checked = !aktiv;
|
||||
zeigeMeldung(`Änderung fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
});
|
||||
const toggleSpan = erstelleElement("span", { klasse: "schalter-regler" });
|
||||
toggleLabel.append(toggleInput, toggleSpan);
|
||||
|
||||
rechts.append(einstellungenBtn, toggleLabel);
|
||||
zeile.append(links, rechts);
|
||||
werkzeugeListe.append(zeile);
|
||||
}
|
||||
}
|
||||
|
||||
const werkzeugeKarte = erstelleKarte("Verfügbare Werkzeuge", [werkzeugeListe]);
|
||||
|
||||
detailContainer.append(basisKarte, werkzeugeKarte);
|
||||
}
|
||||
|
||||
function oeffneNeuerAgentDialog() {
|
||||
const formular = erstelleElement("form", { klasse: "formular-raster" });
|
||||
|
||||
const idFeld = erstelleElement("input", {
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "z.B. scout",
|
||||
id: "neu-agent-id"
|
||||
});
|
||||
|
||||
const nameFeld = erstelleElement("input", {
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "z.B. Scout Agent",
|
||||
id: "neu-agent-name"
|
||||
});
|
||||
|
||||
const modellFeld = erstelleElement("input", {
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "z.B. anthropic/claude-3.5-sonnet",
|
||||
value: "anthropic/claude-3.5-sonnet",
|
||||
id: "neu-agent-modell"
|
||||
});
|
||||
|
||||
const descFeld = erstelleElement("textarea", {
|
||||
rows: "3",
|
||||
placeholder: "Optionale Beschreibung des Agenten…",
|
||||
id: "neu-agent-desc"
|
||||
});
|
||||
|
||||
formular.append(
|
||||
erstelleElement("label", { text: "Agent-ID (eindeutiger Schlüssel):" }),
|
||||
idFeld,
|
||||
erstelleElement("label", { text: "Anzeigename:" }),
|
||||
nameFeld,
|
||||
erstelleElement("label", { text: "Modell (OpenRouter):" }),
|
||||
modellFeld,
|
||||
erstelleElement("label", { text: "Beschreibung:" }),
|
||||
descFeld
|
||||
);
|
||||
|
||||
const abbrechenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen()
|
||||
},
|
||||
erstelleElement("span", { text: "Abbrechen" })
|
||||
);
|
||||
|
||||
const speichernBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche primaer"
|
||||
},
|
||||
erstelleElement("span", { text: "Agent anlegen" })
|
||||
);
|
||||
|
||||
const modal = erstelleModal("Neuen Agenten anlegen", formular, [abbrechenBtn, speichernBtn]);
|
||||
|
||||
formular.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
speichernBtn.disabled = true;
|
||||
|
||||
try {
|
||||
await api("/agents", {
|
||||
method: "POST",
|
||||
body: {
|
||||
agentId: idFeld.value.trim(),
|
||||
displayName: nameFeld.value.trim(),
|
||||
model: modellFeld.value.trim(),
|
||||
description: descFeld.value.trim()
|
||||
}
|
||||
});
|
||||
modal.schliessen();
|
||||
zeigeMeldung(`Agent '${nameFeld.value.trim()}' wurde erfolgreich angelegt.`, "erfolg");
|
||||
await ladeUndRendereListe(idFeld.value.trim());
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler beim Anlegen: ${err.message}`, "gefahr");
|
||||
} finally {
|
||||
speichernBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
async function loescheAgent(agentId, name) {
|
||||
if (!confirm(`Soll der Agent '${name}' (${agentId}) wirklich gelöscht werden?`)) return;
|
||||
|
||||
try {
|
||||
await api(`/agents/${encodeURIComponent(agentId)}`, { method: "DELETE" });
|
||||
zeigeMeldung(`Agent '${name}' wurde gelöscht.`, "info");
|
||||
await ladeUndRendereListe();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Löschen fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
async function zeigeWerkzeugEinstellungen(agentId, toolName) {
|
||||
const box = erstelleElement("div", { klasse: "einstellungen-box" });
|
||||
box.append(erstelleElement("p", { klasse: "gedaempft", text: "Lade Konfiguration…" }));
|
||||
|
||||
const schliessenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen(),
|
||||
text: "Schließen"
|
||||
});
|
||||
|
||||
const modal = erstelleModal(
|
||||
`Einstellungen: ${toolName} (${agentId})`,
|
||||
box,
|
||||
[schliessenBtn]
|
||||
);
|
||||
modal.oeffnen();
|
||||
|
||||
try {
|
||||
const daten = await api(
|
||||
`/agents/${encodeURIComponent(agentId)}/tools/${encodeURIComponent(toolName)}/settings`
|
||||
);
|
||||
box.replaceChildren();
|
||||
|
||||
const hinweis = erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Geheime Werte (Passwörter, Schlüssel) werden vom Server maskiert ausgegeben."
|
||||
});
|
||||
box.append(hinweis);
|
||||
|
||||
if (!daten || Object.keys(daten).length === 0) {
|
||||
box.append(
|
||||
erstelleElement("p", { klasse: "gedaempft", text: "Keine individuellen Einstellungen hinterlegt." })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const dl = erstelleElement("dl", { klasse: "datenliste" });
|
||||
for (const [schluessel, wert] of Object.entries(daten)) {
|
||||
const dt = erstelleElement("dt", { text: schluessel });
|
||||
const dd = erstelleElement("dd", { text: typeof wert === "object" ? JSON.stringify(wert) : String(wert) });
|
||||
dl.append(dt, dd);
|
||||
}
|
||||
box.append(dl);
|
||||
} catch (err) {
|
||||
box.replaceChildren(
|
||||
erstelleElement("p", { klasse: "fehler", text: `Laden fehlgeschlagen: ${err.message}` })
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
// ClawdDotNet — Chat-Ansicht
|
||||
//
|
||||
// Leitfaden: Sicherer Chatverlauf (strikt textContent), Echtzeit-Ereignisse über SSE.
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { abonnieren } from "../modules/events.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
PFADE,
|
||||
formatiereZeit,
|
||||
erstellePlakette,
|
||||
zeigeMeldung
|
||||
} from "../modules/dom.js";
|
||||
|
||||
let aktiverAgentId = null;
|
||||
let agentenListe = [];
|
||||
let nachrichtenContainer = null;
|
||||
let schrittBox = null;
|
||||
let eingabeTextarea = null;
|
||||
let sendenButton = null;
|
||||
let abbrechenButton = null;
|
||||
let agentenSelector = null;
|
||||
let statusPlakette = null;
|
||||
let entladeEreignisse = [];
|
||||
|
||||
export async function rendereChat(parameter = {}) {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
// Alte Ereignisse aufräumen
|
||||
entladeEreignisse.forEach((cleanup) => cleanup());
|
||||
entladeEreignisse = [];
|
||||
|
||||
const chatShell = erstelleElement("div", { klasse: "chat-ansicht" });
|
||||
|
||||
// 1. Kopfleiste der Chat-Ansicht
|
||||
const kopfleiste = erstelleElement("div", { klasse: "chat-kopf" });
|
||||
|
||||
const agentenWahl = erstelleElement("div", { klasse: "chat-agenten-wahl" });
|
||||
agentenSelector = erstelleElement("select", {
|
||||
klasse: "eingabe-select",
|
||||
onchange: (e) => waehleAgent(e.target.value)
|
||||
});
|
||||
|
||||
statusPlakette = erstellePlakette("Lädt…", "neutral");
|
||||
agentenWahl.append(erstelleElement("label", { text: "Agent: " }), agentenSelector, statusPlakette);
|
||||
|
||||
const aktionen = erstelleElement("div", { klasse: "chat-aktionen" });
|
||||
|
||||
const weckenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Agenten manuell ausführen / wecken",
|
||||
onclick: weckeAktivenAgent
|
||||
},
|
||||
erstelleIcon(PFADE.abspielen, 14),
|
||||
erstelleElement("span", { text: " Wecken" })
|
||||
);
|
||||
|
||||
const leerenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Chatverlauf dieses Agenten leeren",
|
||||
onclick: leereChatverlauf
|
||||
},
|
||||
erstelleIcon(PFADE.leeren, 14),
|
||||
erstelleElement("span", { text: " Verlauf leeren" })
|
||||
);
|
||||
|
||||
abbrechenButton = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche gefahr flach",
|
||||
title: "Laufenden Lauf abbrechen",
|
||||
hidden: true,
|
||||
onclick: brecheChatAb
|
||||
},
|
||||
erstelleIcon(PFADE.abbrechen, 14),
|
||||
erstelleElement("span", { text: " Abbrechen" })
|
||||
);
|
||||
|
||||
aktionen.append(weckenBtn, leerenBtn, abbrechenButton);
|
||||
kopfleiste.append(agentenWahl, aktionen);
|
||||
|
||||
// 2. Nachrichtenverlauf & Schrittanzeige
|
||||
nachrichtenContainer = erstelleElement("div", { klasse: "chat-verlauf" });
|
||||
|
||||
schrittBox = erstelleElement("div", { klasse: "chat-schritt-box", hidden: true });
|
||||
const schrittIcon = erstelleIcon(PFADE.aktualisieren, 14, "rotierend");
|
||||
const schrittText = erstelleElement("span", { klasse: "schritt-text", text: "" });
|
||||
schrittBox.append(schrittIcon, schrittText);
|
||||
|
||||
// 3. Eingabebereich
|
||||
const eingabeBereich = erstelleElement("div", { klasse: "chat-eingabe-bereich" });
|
||||
|
||||
eingabeTextarea = erstelleElement("textarea", {
|
||||
klasse: "chat-eingabe",
|
||||
placeholder: "Nachricht an den Agenten eingeben (Enter zum Senden, Umschalt+Enter für neue Zeile)…",
|
||||
rows: "3",
|
||||
onkeydown: (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendeNachricht();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sendenButton = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche primaer",
|
||||
title: "Nachricht senden",
|
||||
onclick: sendeNachricht
|
||||
},
|
||||
erstelleIcon(PFADE.senden, 16),
|
||||
erstelleElement("span", { text: "Senden" })
|
||||
);
|
||||
|
||||
eingabeBereich.append(eingabeTextarea, sendenButton);
|
||||
|
||||
chatShell.append(kopfleiste, nachrichtenContainer, schrittBox, eingabeBereich);
|
||||
container.append(chatShell);
|
||||
|
||||
// Lade Agenten und initialisiere
|
||||
await ladeAgenten(parameter.agentId);
|
||||
|
||||
// SSE-Abonnements
|
||||
entladeEreignisse.push(
|
||||
abonnieren("chat", (daten) => {
|
||||
if (daten.agentId === aktiverAgentId) {
|
||||
fuegeNachrichtEin(daten);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
entladeEreignisse.push(
|
||||
abonnieren("step", (daten) => {
|
||||
if (daten.agentId === aktiverAgentId) {
|
||||
zeigeSchritt(daten.text);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
entladeEreignisse.push(
|
||||
abonnieren("run", (daten) => {
|
||||
versteckeSchritt();
|
||||
aktualisiereLaufStatus(false);
|
||||
})
|
||||
);
|
||||
|
||||
entladeEreignisse.push(
|
||||
abonnieren("instance", (daten) => {
|
||||
if (daten.kind === "Agents") {
|
||||
ladeAgenten(aktiverAgentId);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function ladeAgenten(gewuenschteId = null) {
|
||||
try {
|
||||
agentenListe = await api("/agents");
|
||||
agentenSelector.replaceChildren();
|
||||
|
||||
if (!agentenListe || agentenListe.length === 0) {
|
||||
const opt = erstelleElement("option", { text: "Keine Agenten vorhanden", value: "" });
|
||||
agentenSelector.append(opt);
|
||||
statusPlakette.replaceWith(erstellePlakette("Keine Agenten", "warnung"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const a of agentenListe) {
|
||||
const opt = erstelleElement("option", {
|
||||
value: a.agentId,
|
||||
text: `${a.displayName} (${a.agentId})`
|
||||
});
|
||||
agentenSelector.append(opt);
|
||||
}
|
||||
|
||||
const zielId = gewuenschteId && agentenListe.some((a) => a.agentId === gewuenschteId)
|
||||
? gewuenschteId
|
||||
: agentenListe[0].agentId;
|
||||
|
||||
agentenSelector.value = zielId;
|
||||
await waehleAgent(zielId);
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler beim Laden der Agenten: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
async function waehleAgent(agentId) {
|
||||
aktiverAgentId = agentId;
|
||||
const agent = agentenListe.find((a) => a.agentId === agentId);
|
||||
aktualisiereLaufStatus(agent?.isRunning ?? false);
|
||||
|
||||
window.location.hash = `#/chat/${encodeURIComponent(agentId)}`;
|
||||
|
||||
await ladeChatVerlauf(agentId);
|
||||
}
|
||||
|
||||
function aktualisiereLaufStatus(laeuft) {
|
||||
if (statusPlakette) {
|
||||
const neuePlakette = laeuft
|
||||
? erstellePlakette("Beschäftigt", "warnung")
|
||||
: erstellePlakette("Bereit", "erfolg");
|
||||
statusPlakette.replaceWith(neuePlakette);
|
||||
statusPlakette = neuePlakette;
|
||||
}
|
||||
|
||||
if (abbrechenButton) {
|
||||
abbrechenButton.hidden = !laeuft;
|
||||
}
|
||||
}
|
||||
|
||||
async function ladeChatVerlauf(agentId) {
|
||||
if (!nachrichtenContainer) return;
|
||||
nachrichtenContainer.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Verlauf…" })
|
||||
);
|
||||
|
||||
try {
|
||||
const verlauf = await api(`/agents/${encodeURIComponent(agentId)}/chat`);
|
||||
nachrichtenContainer.replaceChildren();
|
||||
|
||||
if (!verlauf || verlauf.length === 0) {
|
||||
nachrichtenContainer.append(
|
||||
erstelleElement("div", {
|
||||
klasse: "hinweis-zentriert",
|
||||
text: "Noch keine Nachrichten im Verlauf vorhanden."
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const eintrag of verlauf) {
|
||||
fuegeNachrichtEin(eintrag);
|
||||
}
|
||||
|
||||
scrolleNachUnten();
|
||||
} catch (err) {
|
||||
nachrichtenContainer.replaceChildren(
|
||||
erstelleElement("div", {
|
||||
klasse: "fehler-zentriert",
|
||||
text: `Verlauf konnte nicht geladen werden: ${err.message}`
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function fuegeNachrichtEin(eintrag) {
|
||||
// Falls aktuell der Hinweis angezeigt wird, entfernen
|
||||
const platzhalter = nachrichtenContainer.querySelector(".hinweis-zentriert");
|
||||
if (platzhalter) platzhalter.remove();
|
||||
|
||||
const rolle = (eintrag.role || "user").toLowerCase();
|
||||
const istUser = rolle === "user";
|
||||
const istSystem = rolle === "system";
|
||||
const istTool = rolle === "tool";
|
||||
|
||||
let rollenKlasse = "nachricht-benutzer";
|
||||
let rollenTitel = "Benutzer";
|
||||
|
||||
if (istSystem) {
|
||||
rollenKlasse = "nachricht-system";
|
||||
rollenTitel = "System";
|
||||
} else if (istTool) {
|
||||
rollenKlasse = "nachricht-werkzeug";
|
||||
rollenTitel = "Werkzeug";
|
||||
} else if (!istUser) {
|
||||
rollenKlasse = "nachricht-agent";
|
||||
rollenTitel = agentenListe.find((a) => a.agentId === aktiverAgentId)?.displayName || "Agent";
|
||||
}
|
||||
|
||||
const zeile = erstelleElement("div", { klasse: `chat-zeile ${istUser ? "zeile-benutzer" : "zeile-fremd"}` });
|
||||
const nachrichtCard = erstelleElement("div", { klasse: `nachricht-blase ${rollenKlasse}` });
|
||||
|
||||
const kopf = erstelleElement("div", { klasse: "nachricht-meta" });
|
||||
const autor = erstelleElement("strong", { text: rollenTitel });
|
||||
const zeit = erstelleElement("span", {
|
||||
klasse: "nachricht-zeit",
|
||||
text: formatiereZeit(eintrag.timestamp)
|
||||
});
|
||||
|
||||
kopf.append(autor);
|
||||
if (eintrag.source) {
|
||||
const quelleBadge = erstelleElement("span", { klasse: "quelle-tag", text: eintrag.source });
|
||||
kopf.append(quelleBadge);
|
||||
}
|
||||
kopf.append(zeit);
|
||||
|
||||
// WICHTIG (Regel 3.1): Fremdtext ist NIE HTML! Strikt textContent!
|
||||
const textKörper = erstelleElement("div", {
|
||||
klasse: "nachricht-text",
|
||||
text: eintrag.content || ""
|
||||
});
|
||||
|
||||
nachrichtCard.append(kopf, textKörper);
|
||||
zeile.append(nachrichtCard);
|
||||
nachrichtenContainer.append(zeile);
|
||||
|
||||
scrolleNachUnten();
|
||||
}
|
||||
|
||||
function zeigeSchritt(text) {
|
||||
if (!schrittBox) return;
|
||||
const span = schrittBox.querySelector(".schritt-text");
|
||||
if (span) span.textContent = text;
|
||||
schrittBox.hidden = false;
|
||||
aktualisiereLaufStatus(true);
|
||||
}
|
||||
|
||||
function versteckeSchritt() {
|
||||
if (schrittBox) schrittBox.hidden = true;
|
||||
}
|
||||
|
||||
function scrolleNachUnten() {
|
||||
if (nachrichtenContainer) {
|
||||
nachrichtenContainer.scrollTop = nachrichtenContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendeNachricht() {
|
||||
if (!aktiverAgentId || !eingabeTextarea) return;
|
||||
const nachricht = eingabeTextarea.value.trim();
|
||||
if (!nachricht) return;
|
||||
|
||||
eingabeTextarea.value = "";
|
||||
sendenButton.disabled = true;
|
||||
aktualisiereLaufStatus(true);
|
||||
|
||||
try {
|
||||
await api(`/agents/${encodeURIComponent(aktiverAgentId)}/chat`, {
|
||||
method: "POST",
|
||||
body: { message: nachricht }
|
||||
});
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Nachricht konnte nicht gesendet werden: ${err.message}`, "gefahr");
|
||||
aktualisiereLaufStatus(false);
|
||||
} finally {
|
||||
sendenButton.disabled = false;
|
||||
eingabeTextarea.focus();
|
||||
}
|
||||
}
|
||||
|
||||
async function brecheChatAb() {
|
||||
if (!aktiverAgentId) return;
|
||||
try {
|
||||
await api(`/agents/${encodeURIComponent(aktiverAgentId)}/chat/abort`, {
|
||||
method: "POST"
|
||||
});
|
||||
versteckeSchritt();
|
||||
aktualisiereLaufStatus(false);
|
||||
zeigeMeldung("Lauf wurde abgebrochen.", "warnung");
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Abbruch fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
async function weckeAktivenAgent() {
|
||||
if (!aktiverAgentId) return;
|
||||
try {
|
||||
await api(`/agents/${encodeURIComponent(aktiverAgentId)}/run`, {
|
||||
method: "POST"
|
||||
});
|
||||
aktualisiereLaufStatus(true);
|
||||
zeigeMeldung("Agent wurde manuell gestartet.", "info");
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Starten fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
async function leereChatverlauf() {
|
||||
if (!aktiverAgentId) return;
|
||||
if (!confirm("Soll der gesamte Chatverlauf dieses Agenten unwiderruflich gelöscht werden?")) return;
|
||||
|
||||
try {
|
||||
await api(`/agents/${encodeURIComponent(aktiverAgentId)}/chat`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
nachrichtenContainer.replaceChildren(
|
||||
erstelleElement("div", {
|
||||
klasse: "hinweis-zentriert",
|
||||
text: "Verlauf wurde geleert."
|
||||
})
|
||||
);
|
||||
zeigeMeldung("Chatverlauf geleert.", "info");
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Leeren fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// ClawdDotNet — System-Info & Status
|
||||
//
|
||||
// Zeigt Anwendungsversion, Instanzkennung, Teilsysteme und OpenRouter-Guthaben an.
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleKarte,
|
||||
erstellePlakette
|
||||
} from "../modules/dom.js";
|
||||
|
||||
export async function rendereInfo() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "info-ansicht" });
|
||||
layout.append(erstelleElement("h1", { text: "System & Status" }));
|
||||
|
||||
const gitter = erstelleElement("div", { klasse: "info-gitter" });
|
||||
layout.append(gitter);
|
||||
container.append(layout);
|
||||
|
||||
gitter.append(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Systemdaten…" })
|
||||
);
|
||||
|
||||
try {
|
||||
const [info, status] = await Promise.all([
|
||||
api("/info").catch(() => null),
|
||||
api("/status").catch(() => null)
|
||||
]);
|
||||
|
||||
gitter.replaceChildren();
|
||||
|
||||
// 1. Instanz & Version
|
||||
if (info) {
|
||||
const dlInstanz = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlInstanz.append(
|
||||
erstelleElement("dt", { text: "Instanzname" }),
|
||||
erstelleElement("dd", {}, erstelleElement("strong", { text: info.instanceName })),
|
||||
erstelleElement("dt", { text: "Instanz-ID" }),
|
||||
erstelleElement("dd", {}, erstelleElement("code", { text: info.instanceId })),
|
||||
erstelleElement("dt", { text: "Version" }),
|
||||
erstelleElement("dd", { text: info.appVersion }),
|
||||
erstelleElement("dt", { text: "Build" }),
|
||||
erstelleElement("dd", { text: info.buildSummary })
|
||||
);
|
||||
gitter.append(erstelleKarte("Instanz & Programm", [dlInstanz]));
|
||||
|
||||
// 2. Teilsysteme
|
||||
const dlSysteme = erstelleElement("dl", { klasse: "datenliste" });
|
||||
dlSysteme.append(
|
||||
erstelleElement("dt", { text: "Agent Engine" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(info.engineActive ? "Aktiv" : "Aus", info.engineActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Scanner / Scheduler" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(info.schedulerActive ? "Aktiv" : "Aus", info.schedulerActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Freigabestelle (Staging)" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(info.stagingActive ? "Aktiv" : "Aus", info.stagingActive ? "erfolg" : "neutral")),
|
||||
erstelleElement("dt", { text: "Deploymentcenter" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(info.deploymentcenterActive ? "Verbunden" : "Nicht konfiguriert", info.deploymentcenterActive ? "erfolg" : "neutral"))
|
||||
);
|
||||
gitter.append(erstelleKarte("Dienste & Teilsysteme", [dlSysteme]));
|
||||
}
|
||||
|
||||
// 3. OpenRouter / API-Status
|
||||
const dlStatus = erstelleElement("dl", { klasse: "datenliste" });
|
||||
if (status) {
|
||||
dlStatus.append(
|
||||
erstelleElement("dt", { text: "API-Erreichbarkeit" }),
|
||||
erstelleElement("dd", {}, erstellePlakette(status.isApiReachable ? "Verbunden" : "Getrennt", status.isApiReachable ? "erfolg" : "gefahr")),
|
||||
erstelleElement("dt", { text: "Statusmeldung" }),
|
||||
erstelleElement("dd", { text: status.statusText || "—" }),
|
||||
erstelleElement("dt", { text: "Guthaben / Credits" }),
|
||||
erstelleElement("dd", {}, erstelleElement("strong", { text: status.creditsText || "—" }))
|
||||
);
|
||||
} else {
|
||||
dlStatus.append(
|
||||
erstelleElement("dt", { text: "OpenRouter" }),
|
||||
erstelleElement("dd", { klasse: "gedaempft", text: "Kein API-Schlüssel oder Dienst nicht erreichbar." })
|
||||
);
|
||||
}
|
||||
gitter.append(erstelleKarte("Modellanbindung (OpenRouter)", [dlStatus]));
|
||||
|
||||
} catch (err) {
|
||||
gitter.replaceChildren(
|
||||
erstelleElement("div", { klasse: "fehler-zentriert", text: err.message })
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// ClawdDotNet — Anmeldeseite
|
||||
//
|
||||
// Leitfaden: Ohne Token soll das UI sofort auf die Login- bzw. Tokeneingabe springen.
|
||||
|
||||
import { setzeToken, loescheToken, api } from "../modules/api.js";
|
||||
import { erstelleElement, PFADE, erstelleIcon } from "../modules/dom.js";
|
||||
|
||||
let loginCallback = null;
|
||||
|
||||
export function initialisiereLogin(onErfolg) {
|
||||
loginCallback = onErfolg;
|
||||
const formular = document.getElementById("anmeldeformular");
|
||||
const fehlerBox = document.getElementById("anmeldefehler");
|
||||
|
||||
if (!formular) return;
|
||||
|
||||
formular.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const tokenInput = document.getElementById("token-eingabe");
|
||||
const token = tokenInput?.value.trim() || "";
|
||||
|
||||
if (!token) return;
|
||||
|
||||
if (fehlerBox) {
|
||||
fehlerBox.hidden = true;
|
||||
fehlerBox.textContent = "";
|
||||
}
|
||||
|
||||
setzeToken(token);
|
||||
|
||||
try {
|
||||
// Teste das Token gegen /api/info
|
||||
const info = await api("/info");
|
||||
versteckeAnmeldung();
|
||||
if (loginCallback) loginCallback(info);
|
||||
} catch (err) {
|
||||
loescheToken();
|
||||
if (fehlerBox) {
|
||||
fehlerBox.textContent = err.abgelehnt
|
||||
? "Token ungültig oder kürzer als 20 Zeichen."
|
||||
: `Anmeldefehler: ${err.message}`;
|
||||
fehlerBox.hidden = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function zeigeAnmeldung() {
|
||||
const anmeldung = document.getElementById("anmeldung");
|
||||
const anwendung = document.getElementById("anwendung");
|
||||
const fehlerBox = document.getElementById("anmeldefehler");
|
||||
const tokenInput = document.getElementById("token-eingabe");
|
||||
|
||||
if (anmeldung) anmeldung.hidden = false;
|
||||
if (anwendung) anwendung.hidden = true;
|
||||
if (fehlerBox) {
|
||||
fehlerBox.hidden = true;
|
||||
fehlerBox.textContent = "";
|
||||
}
|
||||
if (tokenInput) {
|
||||
tokenInput.value = "";
|
||||
tokenInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
export function versteckeAnmeldung() {
|
||||
const anmeldung = document.getElementById("anmeldung");
|
||||
const anwendung = document.getElementById("anwendung");
|
||||
if (anmeldung) anmeldung.hidden = true;
|
||||
if (anwendung) anwendung.hidden = false;
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// ClawdDotNet — Live-Protokollansicht (Logs)
|
||||
//
|
||||
// Fortlaufender Stream über /events/logs, Modul- und Level-Filterung, Autoscroll, strikt textContent.
|
||||
|
||||
import { api, holeToken } from "../modules/api.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
PFADE,
|
||||
erstelleKarte,
|
||||
erstellePlakette
|
||||
} from "../modules/dom.js";
|
||||
|
||||
let logStream = null;
|
||||
let modulFilter = "";
|
||||
let levelFilter = "All";
|
||||
let autoScroll = true;
|
||||
let logContainer = null;
|
||||
let streamStatusPlakette = null;
|
||||
let pauseButton = null;
|
||||
let istPausiert = false;
|
||||
|
||||
export async function rendereProtokoll() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
trenneLogStream();
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "protokoll-ansicht" });
|
||||
|
||||
// Kopfleiste mit Filtern und Steuerungen
|
||||
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
const titelGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
titelGruppe.append(erstelleElement("h1", { text: "Protokoll" }));
|
||||
streamStatusPlakette = erstellePlakette("Verbinde…", "neutral");
|
||||
titelGruppe.append(streamStatusPlakette);
|
||||
|
||||
const steuerungGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
|
||||
// Modul-Auswahl
|
||||
const modulSelect = erstelleElement("select", {
|
||||
klasse: "eingabe-select",
|
||||
onchange: (e) => {
|
||||
modulFilter = e.target.value;
|
||||
starteLogStream();
|
||||
}
|
||||
});
|
||||
modulSelect.append(erstelleElement("option", { value: "", text: "Alle Module" }));
|
||||
|
||||
// Level-Auswahl
|
||||
const levelSelect = erstelleElement("select", {
|
||||
klasse: "eingabe-select",
|
||||
onchange: (e) => {
|
||||
levelFilter = e.target.value;
|
||||
starteLogStream();
|
||||
}
|
||||
});
|
||||
const levelOptionen = [
|
||||
["All", "Alle Stufen"],
|
||||
["Debug", "Debug"],
|
||||
["Information", "Info"],
|
||||
["Warning", "Warnung"],
|
||||
["Error", "Fehler"],
|
||||
["Critical", "Kritisch"]
|
||||
];
|
||||
for (const [val, label] of levelOptionen) {
|
||||
levelSelect.append(erstelleElement("option", { value: val, text: label }));
|
||||
}
|
||||
|
||||
// Autoscroll-Checkbox
|
||||
const autoScrollLabel = erstelleElement("label", { klasse: "checkbox-label gedaempft" });
|
||||
const autoScrollInput = erstelleElement("input", {
|
||||
type: "checkbox",
|
||||
checked: true,
|
||||
onchange: (e) => {
|
||||
autoScroll = e.target.checked;
|
||||
}
|
||||
});
|
||||
autoScrollLabel.append(autoScrollInput, erstelleElement("span", { text: " Auto-Scroll" }));
|
||||
|
||||
// Pause / Fortsetzen
|
||||
pauseButton = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: togglePause
|
||||
},
|
||||
erstelleIcon(PFADE.abbrechen, 14),
|
||||
erstelleElement("span", { text: " Anhalten" })
|
||||
);
|
||||
|
||||
// Leeren
|
||||
const leerenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
title: "Anzeige leeren",
|
||||
onclick: () => logContainer?.replaceChildren()
|
||||
},
|
||||
erstelleIcon(PFADE.leeren, 14),
|
||||
erstelleElement("span", { text: " Leeren" })
|
||||
);
|
||||
|
||||
steuerungGruppe.append(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 Module
|
||||
try {
|
||||
const module = await api("/logs/modules");
|
||||
if (Array.isArray(module)) {
|
||||
for (const m of module) {
|
||||
modulSelect.append(erstelleElement("option", { value: m, text: m }));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Nicht kritisch, Filter bleibt auf "Alle Module"
|
||||
}
|
||||
|
||||
starteLogStream();
|
||||
}
|
||||
|
||||
function starteLogStream() {
|
||||
trenneLogStream();
|
||||
if (istPausiert) return;
|
||||
|
||||
const token = holeToken();
|
||||
if (!token) return;
|
||||
|
||||
let url = `/events/logs?token=${encodeURIComponent(token)}`;
|
||||
if (modulFilter) url += `&module=${encodeURIComponent(modulFilter)}`;
|
||||
if (levelFilter) url += `&level=${encodeURIComponent(levelFilter)}`;
|
||||
|
||||
logStream = new EventSource(url);
|
||||
|
||||
logStream.onopen = () => {
|
||||
aktualisiereStatus(true);
|
||||
};
|
||||
|
||||
logStream.onerror = () => {
|
||||
aktualisiereStatus(false);
|
||||
};
|
||||
|
||||
logStream.addEventListener("log", (e) => {
|
||||
if (istPausiert) return;
|
||||
try {
|
||||
const zeile = JSON.parse(e.data);
|
||||
fuegeLogZeileEin(zeile);
|
||||
} catch {
|
||||
fuegeLogZeileEin({ text: e.data });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fuegeLogZeileEin(eintrag) {
|
||||
if (!logContainer) return;
|
||||
|
||||
const zeile = erstelleElement("div", { klasse: "log-zeile" });
|
||||
|
||||
if (eintrag.level) {
|
||||
const lvl = String(eintrag.level).toLowerCase();
|
||||
const badgeArt =
|
||||
lvl.includes("err") || lvl.includes("crit")
|
||||
? "gefahr"
|
||||
: lvl.includes("warn")
|
||||
? "warnung"
|
||||
: lvl.includes("info")
|
||||
? "erfolg"
|
||||
: "neutral";
|
||||
|
||||
zeile.append(erstelleElement("span", { klasse: `log-tag tag-${badgeArt}`, text: eintrag.level }));
|
||||
}
|
||||
|
||||
if (eintrag.module) {
|
||||
zeile.append(erstelleElement("span", { klasse: "log-modul", text: `[${eintrag.module}] ` }));
|
||||
}
|
||||
|
||||
// Wichtig: textContent, kein innerHTML!
|
||||
const textSpan = erstelleElement("span", { klasse: "log-text", text: eintrag.text || "" });
|
||||
zeile.append(textSpan);
|
||||
|
||||
logContainer.append(zeile);
|
||||
|
||||
// Begrenze auf 300 Zeilen im DOM
|
||||
while (logContainer.children.length > 300) {
|
||||
logContainer.firstElementChild?.remove();
|
||||
}
|
||||
|
||||
if (autoScroll) {
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function togglePause() {
|
||||
istPausiert = !istPausiert;
|
||||
if (istPausiert) {
|
||||
trenneLogStream();
|
||||
pauseButton.replaceChildren(erstelleIcon(PFADE.abspielen, 14), erstelleElement("span", { text: " Fortsetzen" }));
|
||||
aktualisiereStatus(false, "Pausiert");
|
||||
} else {
|
||||
pauseButton.replaceChildren(erstelleIcon(PFADE.abbrechen, 14), erstelleElement("span", { text: " Anhalten" }));
|
||||
starteLogStream();
|
||||
}
|
||||
}
|
||||
|
||||
function aktualisiereStatus(offen, text = null) {
|
||||
if (!streamStatusPlakette) return;
|
||||
const t = text || (offen ? "Live" : "Getrennt");
|
||||
const art = offen ? "erfolg" : "neutral";
|
||||
const neuePlakette = erstellePlakette(t, art);
|
||||
streamStatusPlakette.replaceWith(neuePlakette);
|
||||
streamStatusPlakette = neuePlakette;
|
||||
}
|
||||
|
||||
export function trenneLogStream() {
|
||||
if (logStream) {
|
||||
logStream.close();
|
||||
logStream = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// ClawdDotNet — Platzhalter-Ansichten für noch nicht freigegebene Kern-Endpunkte
|
||||
//
|
||||
// Leitfaden Abschnitt 7: Sicherung, Einstellungen und Agenten-Chats (Analyse).
|
||||
|
||||
import { erstelleElement, erstelleKarte, erstelleIcon, PFADE } from "../modules/dom.js";
|
||||
|
||||
export function rendereSicherung() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "platzhalter-ansicht" });
|
||||
layout.append(erstelleElement("h1", { text: "Datensicherung & Wiederherstellung" }));
|
||||
|
||||
const karte = erstelleKarte("In Vorbereitung", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Die API-Endpunkte für die automatisierte Sicherung und Rücksicherung über das WebUI befinden sich in Arbeit."
|
||||
}),
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Bitte verwende für die Sicherung von Instanzen aktuell die Desktop-Oberfläche von ClawdDotNet."
|
||||
})
|
||||
]);
|
||||
|
||||
layout.append(karte);
|
||||
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");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "platzhalter-ansicht" });
|
||||
layout.append(erstelleElement("h1", { text: "Agenten-Kommunikation" }));
|
||||
|
||||
const karte = erstelleKarte("Analyse: Interne Agenten-Chats", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Die Protokollierung und Auswertung der direkten Kommunikation zwischen zwei Agenten (A2A) wird mit Vorhaben #208 bereitgestellt."
|
||||
}),
|
||||
erstelleElement("p", {
|
||||
klasse: "hinweis",
|
||||
text: "Direkte Chats mit Agenten können im Bereich 'Chat' geführt werden."
|
||||
})
|
||||
]);
|
||||
|
||||
layout.append(karte);
|
||||
container.append(layout);
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// ClawdDotNet — Hintergrunddienste
|
||||
//
|
||||
// Übersicht und Verwaltung der Instanz-Dienste (Port, Protokoll, Status, Ein-/Ausschalten).
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { abonnieren } from "../modules/events.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
PFADE,
|
||||
erstelleKarte,
|
||||
erstelleModal,
|
||||
zeigeMeldung
|
||||
} from "../modules/dom.js";
|
||||
|
||||
let services = [];
|
||||
let entladeEreignisse = [];
|
||||
|
||||
export async function rendereDienste() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
entladeEreignisse.forEach((c) => c());
|
||||
entladeEreignisse = [];
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "dienste-ansicht" });
|
||||
|
||||
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
kopfZeile.append(
|
||||
erstelleElement("h1", { text: "Dienste" }),
|
||||
erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche primaer",
|
||||
onclick: oeffneNeuerDienstDialog
|
||||
},
|
||||
erstelleIcon(PFADE.plus, 16),
|
||||
erstelleElement("span", { text: " Neuer Dienst" })
|
||||
)
|
||||
);
|
||||
|
||||
const tabelleBereich = erstelleElement("div", { id: "dienste-liste-bereich" });
|
||||
layout.append(kopfZeile, tabelleBereich);
|
||||
container.append(layout);
|
||||
|
||||
await ladeDienste();
|
||||
|
||||
entladeEreignisse.push(
|
||||
abonnieren("instance", (daten) => {
|
||||
if (daten.kind === "Services") {
|
||||
ladeDienste();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function ladeDienste() {
|
||||
const container = document.getElementById("dienste-liste-bereich");
|
||||
if (!container) return;
|
||||
|
||||
container.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Dienste…" })
|
||||
);
|
||||
|
||||
try {
|
||||
services = await api("/services");
|
||||
container.replaceChildren();
|
||||
|
||||
if (!services || services.length === 0) {
|
||||
container.append(
|
||||
erstelleKarte("Keine Dienste", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Für diese Instanz sind noch keine Hintergrunddienste eingerichtet."
|
||||
})
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const tabelle = erstelleElement("table", { klasse: "daten-tabelle" });
|
||||
const thead = erstelleElement("thead");
|
||||
thead.append(
|
||||
erstelleElement("tr", {},
|
||||
erstelleElement("th", { text: "Aktiv" }),
|
||||
erstelleElement("th", { text: "Dienstname" }),
|
||||
erstelleElement("th", { text: "Typ" }),
|
||||
erstelleElement("th", { text: "Port" }),
|
||||
erstelleElement("th", { text: "Beschreibung" }),
|
||||
erstelleElement("th", { text: "Aktionen", stil: "text-align: right;" })
|
||||
)
|
||||
);
|
||||
|
||||
const tbody = erstelleElement("tbody");
|
||||
|
||||
for (const s of services) {
|
||||
const tr = erstelleElement("tr");
|
||||
|
||||
// Schalter
|
||||
const tdStatus = erstelleElement("td");
|
||||
const schalterLabel = erstelleElement("label", { klasse: "schalter" });
|
||||
const schalterInput = erstelleElement("input", {
|
||||
type: "checkbox",
|
||||
checked: Boolean(s.enabled),
|
||||
onchange: async (e) => {
|
||||
const aktiv = e.target.checked;
|
||||
try {
|
||||
await api(`/services/${encodeURIComponent(s.id)}/enabled?enabled=${aktiv}`, {
|
||||
method: "PUT"
|
||||
});
|
||||
s.enabled = aktiv;
|
||||
zeigeMeldung(`Dienst '${s.name}' ${aktiv ? "aktiviert" : "deaktiviert"}.`, "info");
|
||||
} catch (err) {
|
||||
e.target.checked = !aktiv;
|
||||
zeigeMeldung(`Fehler: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
});
|
||||
schalterLabel.append(schalterInput, erstelleElement("span", { klasse: "schalter-regler" }));
|
||||
tdStatus.append(schalterLabel);
|
||||
|
||||
const tdName = erstelleElement("td", {}, erstelleElement("strong", { text: s.name }));
|
||||
const tdTyp = erstelleElement("td", { text: s.type || "—" });
|
||||
const tdPort = erstelleElement("td", {}, s.port ? erstelleElement("code", { text: String(s.port) }) : erstelleElement("span", { text: "—" }));
|
||||
const tdDesc = erstelleElement("td", { klasse: "gedaempft", text: s.description || "—" });
|
||||
|
||||
const tdAktionen = erstelleElement("td", { stil: "text-align: right;" });
|
||||
const loeschenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche gefahr flach klein",
|
||||
title: "Dienst löschen",
|
||||
onclick: () => loescheDienst(s)
|
||||
},
|
||||
erstelleIcon(PFADE.muelleimer, 14)
|
||||
);
|
||||
tdAktionen.append(loeschenBtn);
|
||||
|
||||
tr.append(tdStatus, tdName, tdTyp, tdPort, tdDesc, tdAktionen);
|
||||
tbody.append(tr);
|
||||
}
|
||||
|
||||
tabelle.append(thead, tbody);
|
||||
container.append(tabelle);
|
||||
} catch (err) {
|
||||
container.replaceChildren(
|
||||
erstelleElement("div", { klasse: "fehler-zentriert", text: err.message })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function loescheDienst(s) {
|
||||
if (!confirm(`Soll der Dienst '${s.name}' (${s.id}) gelöscht werden?`)) return;
|
||||
|
||||
try {
|
||||
await api(`/services/${encodeURIComponent(s.id)}`, { method: "DELETE" });
|
||||
zeigeMeldung(`Dienst '${s.name}' wurde gelöscht.`, "info");
|
||||
await ladeDienste();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Löschen fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
function oeffneNeuerDienstDialog() {
|
||||
const formular = erstelleElement("form", { klasse: "formular-raster" });
|
||||
|
||||
const nameFeld = erstelleElement("input", {
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "z.B. Webhook-Empfänger"
|
||||
});
|
||||
|
||||
const typFeld = erstelleElement("input", {
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "z.B. HTTP, TCP, Socket"
|
||||
});
|
||||
|
||||
const portFeld = erstelleElement("input", {
|
||||
type: "number",
|
||||
min: "1",
|
||||
max: "65535",
|
||||
placeholder: "z.B. 8081 (optional)"
|
||||
});
|
||||
|
||||
const descFeld = erstelleElement("textarea", {
|
||||
rows: "2",
|
||||
placeholder: "Beschreibung des Dienstes…"
|
||||
});
|
||||
|
||||
formular.append(
|
||||
erstelleElement("label", { text: "Dienstname:" }),
|
||||
nameFeld,
|
||||
erstelleElement("label", { text: "Diensttyp:" }),
|
||||
typFeld,
|
||||
erstelleElement("label", { text: "Port (optional):" }),
|
||||
portFeld,
|
||||
erstelleElement("label", { text: "Beschreibung:" }),
|
||||
descFeld
|
||||
);
|
||||
|
||||
const abbrechenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen(),
|
||||
text: "Abbrechen"
|
||||
});
|
||||
|
||||
const anlegenBtn = erstelleElement("button", {
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche primaer",
|
||||
text: "Dienst anlegen"
|
||||
});
|
||||
|
||||
const modal = erstelleModal("Neuen Hintergrunddienst anlegen", formular, [abbrechenBtn, anlegenBtn]);
|
||||
|
||||
formular.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
anlegenBtn.disabled = true;
|
||||
|
||||
try {
|
||||
await api("/services", {
|
||||
method: "POST",
|
||||
body: {
|
||||
name: nameFeld.value.trim(),
|
||||
type: typFeld.value.trim(),
|
||||
port: portFeld.value ? parseInt(portFeld.value, 10) : 0,
|
||||
description: descFeld.value.trim()
|
||||
}
|
||||
});
|
||||
modal.schliessen();
|
||||
zeigeMeldung(`Dienst '${nameFeld.value.trim()}' angelegt.`, "erfolg");
|
||||
await ladeDienste();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler beim Anlegen: ${err.message}`, "gefahr");
|
||||
} finally {
|
||||
anlegenBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
modal.oeffnen();
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// ClawdDotNet — Freigabestelle (Staging)
|
||||
//
|
||||
// Überprüfung und Genehmigung / Ablehnung von sensiblen Werkzeug-Aufrufen.
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
PFADE,
|
||||
erstelleKarte,
|
||||
erstelleModal,
|
||||
formatiereDatumZeit,
|
||||
zeigeMeldung
|
||||
} from "../modules/dom.js";
|
||||
|
||||
let wartendeAufrufe = [];
|
||||
|
||||
export async function holeWartendeAnzahl() {
|
||||
try {
|
||||
const liste = await api("/staging");
|
||||
return Array.isArray(liste) ? liste.length : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export async function rendereStaging() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "staging-ansicht" });
|
||||
|
||||
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
kopfZeile.append(
|
||||
erstelleElement("h1", { text: "Freigabestelle" }),
|
||||
erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: ladeFreigaben
|
||||
},
|
||||
erstelleIcon(PFADE.aktualisieren, 16),
|
||||
erstelleElement("span", { text: " Aktualisieren" })
|
||||
)
|
||||
);
|
||||
|
||||
const listeBereich = erstelleElement("div", { id: "staging-liste-bereich" });
|
||||
layout.append(kopfZeile, listeBereich);
|
||||
container.append(layout);
|
||||
|
||||
await ladeFreigaben();
|
||||
}
|
||||
|
||||
async function ladeFreigaben() {
|
||||
const container = document.getElementById("staging-liste-bereich");
|
||||
if (!container) return;
|
||||
|
||||
container.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Freigaben…" })
|
||||
);
|
||||
|
||||
try {
|
||||
wartendeAufrufe = await api("/staging");
|
||||
container.replaceChildren();
|
||||
|
||||
// Aktualisiere Badge in Sidebar
|
||||
aktualisiereStagingBadge(wartendeAufrufe.length);
|
||||
|
||||
if (!wartendeAufrufe || wartendeAufrufe.length === 0) {
|
||||
container.append(
|
||||
erstelleKarte("Keine offenen Freigaben", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Aktuell warten keine Werkzeug-Aufrufe auf eine manuelle Bestätigung."
|
||||
})
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const anfrage of wartendeAufrufe) {
|
||||
const kopfAktionen = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
|
||||
const freigebenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche primaer klein",
|
||||
onclick: () => bestaetigeFreigabe(anfrage.id)
|
||||
},
|
||||
erstelleIcon(PFADE.haken, 14),
|
||||
erstelleElement("span", { text: " Freigeben" })
|
||||
);
|
||||
|
||||
const ablehnenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche gefahr flach klein",
|
||||
onclick: () => oeffneAblehnenDialog(anfrage.id)
|
||||
},
|
||||
erstelleIcon(PFADE.abbrechen, 14),
|
||||
erstelleElement("span", { text: " Ablehnen" })
|
||||
);
|
||||
|
||||
kopfAktionen.append(freigebenBtn, ablehnenBtn);
|
||||
|
||||
const datenliste = erstelleElement("dl", { klasse: "datenliste" });
|
||||
datenliste.append(
|
||||
erstelleElement("dt", { text: "Aufruf-ID" }),
|
||||
erstelleElement("dd", { text: String(anfrage.id) }),
|
||||
erstelleElement("dt", { text: "Agent" }),
|
||||
erstelleElement("dd", { text: anfrage.agentId || "—" }),
|
||||
erstelleElement("dt", { text: "Werkzeug" }),
|
||||
erstelleElement("dd", { text: `${anfrage.toolName} → ${anfrage.methodName}` }),
|
||||
erstelleElement("dt", { text: "Zeitpunkt" }),
|
||||
erstelleElement("dd", { text: formatiereDatumZeit(anfrage.requestedAt) })
|
||||
);
|
||||
|
||||
const argBox = erstelleElement("div", { klasse: "staging-argumente" });
|
||||
argBox.append(erstelleElement("strong", { text: "Eingefrorene Argumente:" }));
|
||||
|
||||
// Strikt textContent! Argumente können Fremdtext sein
|
||||
const pre = erstelleElement("pre", {
|
||||
klasse: "code-block",
|
||||
text: JSON.stringify(anfrage.arguments, null, 2)
|
||||
});
|
||||
argBox.append(pre);
|
||||
|
||||
const karte = erstelleKarte(`${anfrage.toolName}.${anfrage.methodName}`, [datenliste, argBox], {
|
||||
aktionen: kopfAktionen
|
||||
});
|
||||
|
||||
container.append(karte);
|
||||
}
|
||||
} catch (err) {
|
||||
container.replaceChildren(
|
||||
erstelleKarte("Freigabestelle nicht aktiv", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: err.message || "In dieser Instanz ist keine Freigabestelle eingerichtet."
|
||||
})
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function aktualisiereStagingBadge(anzahl) {
|
||||
const badge = document.getElementById("staging-badge");
|
||||
if (!badge) return;
|
||||
if (anzahl > 0) {
|
||||
badge.textContent = String(anzahl);
|
||||
badge.hidden = false;
|
||||
} else {
|
||||
badge.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function bestaetigeFreigabe(id) {
|
||||
try {
|
||||
const res = await api(`/staging/${id}/approve`, { method: "POST" });
|
||||
zeigeMeldung(`Aufruf freigegeben: ${res?.message ?? "Erfolgreich ausgeführt"}`, "erfolg");
|
||||
await ladeFreigaben();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Freigabe fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
function oeffneAblehnenDialog(id) {
|
||||
const formular = erstelleElement("form", { klasse: "formular-raster" });
|
||||
const grundInput = erstelleElement("input", {
|
||||
type: "text",
|
||||
placeholder: "Begründung für die Ablehnung an den Agenten…"
|
||||
});
|
||||
|
||||
formular.append(
|
||||
erstelleElement("label", { text: "Begründung (optional):" }),
|
||||
grundInput
|
||||
);
|
||||
|
||||
const abbrechenBtn = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen(),
|
||||
text: "Abbrechen"
|
||||
});
|
||||
|
||||
const ablehnenBtn = erstelleElement("button", {
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche gefahr",
|
||||
text: "Aufruf ablehnen"
|
||||
});
|
||||
|
||||
const modal = erstelleModal("Werkzeug-Aufruf ablehnen", formular, [abbrechenBtn, ablehnenBtn]);
|
||||
|
||||
formular.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
ablehnenBtn.disabled = true;
|
||||
|
||||
try {
|
||||
await api(`/staging/${id}/reject`, {
|
||||
method: "POST",
|
||||
body: { reason: grundInput.value.trim() }
|
||||
});
|
||||
modal.schliessen();
|
||||
zeigeMeldung("Aufruf wurde abgelehnt.", "info");
|
||||
await ladeFreigaben();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Ablehnen fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
} finally {
|
||||
ablehnenBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
modal.oeffnen();
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
// ClawdDotNet — Aufgaben & Geplante Läufe
|
||||
//
|
||||
// Zwei Bereiche: Taskboard (Aufgaben) und Geplante Läufe (Jobs mit Cron & Sofortausführung).
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { abonnieren } from "../modules/events.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
PFADE,
|
||||
erstellePlakette,
|
||||
erstelleKarte,
|
||||
erstelleModal,
|
||||
formatiereDatumZeit,
|
||||
zeigeMeldung
|
||||
} from "../modules/dom.js";
|
||||
|
||||
let aktiverReiter = "jobs"; // "jobs" oder "taskboard"
|
||||
let jobsListe = [];
|
||||
let entladeEreignisse = [];
|
||||
|
||||
export async function rendereAufgaben() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
entladeEreignisse.forEach((c) => c());
|
||||
entladeEreignisse = [];
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "aufgaben-ansicht" });
|
||||
|
||||
// Kopf mit Reiter-Umschaltung und Aktionen
|
||||
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
|
||||
const titel = erstelleElement("h1", { text: "Aufgaben & Jobs" });
|
||||
|
||||
const reiterLeiste = erstelleElement("div", { klasse: "reiter-leiste" });
|
||||
const reiterJobs = erstelleElement("button", {
|
||||
klasse: `reiter-schaltflaeche ${aktiverReiter === "jobs" ? "aktiv" : ""}`,
|
||||
text: "Geplante Läufe (Jobs)",
|
||||
onclick: () => wechsleReiter("jobs")
|
||||
});
|
||||
const reiterTaskboard = erstelleElement("button", {
|
||||
klasse: `reiter-schaltflaeche ${aktiverReiter === "taskboard" ? "aktiv" : ""}`,
|
||||
text: "Taskboard",
|
||||
onclick: () => wechsleReiter("taskboard")
|
||||
});
|
||||
reiterLeiste.append(reiterJobs, reiterTaskboard);
|
||||
|
||||
const aktionen = erstelleElement("div", { klasse: "gruppe-horizontal" });
|
||||
const neuerJobBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche primaer",
|
||||
onclick: oeffneNeuerJobDialog
|
||||
},
|
||||
erstelleIcon(PFADE.plus, 16),
|
||||
erstelleElement("span", { text: " Neuer Job" })
|
||||
);
|
||||
aktionen.append(neuerJobBtn);
|
||||
|
||||
kopfZeile.append(titel, reiterLeiste, aktionen);
|
||||
|
||||
const inhaltBereich = erstelleElement("div", { id: "aufgaben-bereich-inhalt" });
|
||||
layout.append(kopfZeile, inhaltBereich);
|
||||
container.append(layout);
|
||||
|
||||
await ladeAktivenBereich();
|
||||
|
||||
entladeEreignisse.push(
|
||||
abonnieren("instance", (daten) => {
|
||||
if (daten.kind === "Jobs" && aktiverReiter === "jobs") {
|
||||
ladeAktivenBereich();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function wechsleReiter(reiter) {
|
||||
aktiverReiter = reiter;
|
||||
document.querySelectorAll(".reiter-schaltflaeche").forEach((r) => r.classList.remove("aktiv"));
|
||||
if (reiter === "jobs") {
|
||||
document.querySelector(".reiter-schaltflaeche:first-child")?.classList.add("aktiv");
|
||||
} else {
|
||||
document.querySelector(".reiter-schaltflaeche:last-child")?.classList.add("aktiv");
|
||||
}
|
||||
ladeAktivenBereich();
|
||||
}
|
||||
|
||||
async function ladeAktivenBereich() {
|
||||
const container = document.getElementById("aufgaben-bereich-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
if (aktiverReiter === "jobs") {
|
||||
await rendereJobs(container);
|
||||
} else {
|
||||
await rendereTaskboard(container);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Jobs / Geplante Läufe ───
|
||||
|
||||
async function rendereJobs(container) {
|
||||
container.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade geplante Läufe…" })
|
||||
);
|
||||
|
||||
try {
|
||||
jobsListe = await api("/jobs");
|
||||
container.replaceChildren();
|
||||
|
||||
if (!jobsListe || jobsListe.length === 0) {
|
||||
container.append(
|
||||
erstelleKarte("Keine geplanten Läufe", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Es sind noch keine regelmäßigen Agenten-Läufe oder Tool-Jobs eingerichtet."
|
||||
})
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const tabelle = erstelleElement("table", { klasse: "daten-tabelle" });
|
||||
const thead = erstelleElement("thead");
|
||||
const trKopf = erstelleElement("tr");
|
||||
trKopf.append(
|
||||
erstelleElement("th", { text: "Status" }),
|
||||
erstelleElement("th", { text: "Kennung / Art" }),
|
||||
erstelleElement("th", { text: "Agent" }),
|
||||
erstelleElement("th", { text: "Cron-Muster" }),
|
||||
erstelleElement("th", { text: "Nächster Lauf (UTC)" }),
|
||||
erstelleElement("th", { text: "Letzter Lauf (Lokal)" }),
|
||||
erstelleElement("th", { text: "Aktionen", stil: "text-align: right;" })
|
||||
);
|
||||
thead.append(trKopf);
|
||||
|
||||
const tbody = erstelleElement("tbody");
|
||||
|
||||
for (const job of jobsListe) {
|
||||
const tr = erstelleElement("tr");
|
||||
|
||||
// Schalter Ein/Aus
|
||||
const tdStatus = erstelleElement("td");
|
||||
const schalterLabel = erstelleElement("label", { klasse: "schalter" });
|
||||
const schalterInput = erstelleElement("input", {
|
||||
type: "checkbox",
|
||||
checked: Boolean(job.enabled),
|
||||
onchange: async (e) => {
|
||||
const neuAktiv = e.target.checked;
|
||||
try {
|
||||
await api(
|
||||
`/jobs/${encodeURIComponent(job.jobId)}/enabled?agentId=${encodeURIComponent(job.agentId)}&enabled=${neuAktiv}`,
|
||||
{ method: "PUT" }
|
||||
);
|
||||
job.enabled = neuAktiv;
|
||||
zeigeMeldung(`Job '${job.jobId}' ${neuAktiv ? "aktiviert" : "pausiert"}.`, "info");
|
||||
} catch (err) {
|
||||
e.target.checked = !neuAktiv;
|
||||
zeigeMeldung(`Fehler: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
});
|
||||
schalterLabel.append(schalterInput, erstelleElement("span", { klasse: "schalter-regler" }));
|
||||
tdStatus.append(schalterLabel);
|
||||
|
||||
// Name / Art
|
||||
const tdName = erstelleElement("td");
|
||||
tdName.append(
|
||||
erstelleElement("strong", { text: job.jobId }),
|
||||
erstelleElement("div", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: job.kind === "toolJob" ? `Werkzeug (${job.toolName || ""})` : "Agent-Aufruf"
|
||||
})
|
||||
);
|
||||
|
||||
// Agent
|
||||
const tdAgent = erstelleElement("td", { text: job.agentId });
|
||||
|
||||
// Cron
|
||||
const tdCron = erstelleElement("td");
|
||||
tdCron.append(erstelleElement("code", { text: job.cron }));
|
||||
|
||||
// Zeiten
|
||||
const tdNext = erstelleElement("td", {
|
||||
text: job.nextRunUtc ? formatiereDatumZeit(job.nextRunUtc) : "—"
|
||||
});
|
||||
const tdLast = erstelleElement("td", {
|
||||
text: job.lastRunLocal ? formatiereDatumZeit(job.lastRunLocal) : "—"
|
||||
});
|
||||
|
||||
// Aktionen (Sofortlauf, Löschen)
|
||||
const tdAktionen = erstelleElement("td", { stil: "text-align: right;" });
|
||||
const aktionenGruppe = erstelleElement("div", { klasse: "gruppe-horizontal", stil: "justify-content: flex-end;" });
|
||||
|
||||
const ausfuehrenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche flach klein",
|
||||
title: "Job jetzt sofort manuell ausführen",
|
||||
onclick: () => fuehreJobAus(job.jobId)
|
||||
},
|
||||
erstelleIcon(PFADE.abspielen, 14),
|
||||
erstelleElement("span", { text: " Start" })
|
||||
);
|
||||
|
||||
const loeschenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
klasse: "schaltflaeche gefahr flach klein",
|
||||
title: "Job löschen",
|
||||
onclick: () => loescheJob(job)
|
||||
},
|
||||
erstelleIcon(PFADE.muelleimer, 14)
|
||||
);
|
||||
|
||||
aktionenGruppe.append(ausfuehrenBtn, loeschenBtn);
|
||||
tdAktionen.append(aktionenGruppe);
|
||||
|
||||
tr.append(tdStatus, tdName, tdAgent, tdCron, tdNext, tdLast, tdAktionen);
|
||||
tbody.append(tr);
|
||||
}
|
||||
|
||||
tabelle.append(thead, tbody);
|
||||
container.append(tabelle);
|
||||
} catch (err) {
|
||||
container.replaceChildren(
|
||||
erstelleElement("div", { klasse: "fehler-zentriert", text: err.message })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function fuehreJobAus(jobId) {
|
||||
try {
|
||||
const res = await api(`/jobs/${encodeURIComponent(jobId)}/run`, { method: "POST" });
|
||||
zeigeMeldung(`Job '${jobId}' angestoßen: ${res?.message ?? "Erfolgreich"}`, "erfolg");
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Ausführung fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
async function loescheJob(job) {
|
||||
if (!confirm(`Soll der geplante Job '${job.jobId}' gelöscht werden?`)) return;
|
||||
|
||||
try {
|
||||
await api(
|
||||
`/jobs/${encodeURIComponent(job.jobId)}?agentId=${encodeURIComponent(job.agentId)}&kind=${encodeURIComponent(job.kind)}`,
|
||||
{ method: "DELETE" }
|
||||
);
|
||||
zeigeMeldung(`Job '${job.jobId}' wurde gelöscht.`, "info");
|
||||
await ladeAktivenBereich();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Löschen fehlgeschlagen: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
async function oeffneNeuerJobDialog() {
|
||||
let agenten = [];
|
||||
try {
|
||||
agenten = await api("/agents");
|
||||
} catch {}
|
||||
|
||||
const formular = erstelleElement("form", { klasse: "formular-raster" });
|
||||
|
||||
const agentSelect = erstelleElement("select", { required: true });
|
||||
for (const a of agenten) {
|
||||
agentSelect.append(erstelleElement("option", { value: a.agentId, text: `${a.displayName} (${a.agentId})` }));
|
||||
}
|
||||
|
||||
const kindSelect = erstelleElement("select");
|
||||
kindSelect.append(
|
||||
erstelleElement("option", { value: "agentWakeup", text: "Agent Wakeup (Regelmäßiger Aufruf)" }),
|
||||
erstelleElement("option", { value: "toolJob", text: "Tool Job (Werkzeug-Ausführung)" })
|
||||
);
|
||||
|
||||
const cronInput = erstelleElement("input", {
|
||||
type: "text",
|
||||
required: true,
|
||||
value: "0 * * * *",
|
||||
placeholder: "z.B. 0 * * * * (stündlich)"
|
||||
});
|
||||
|
||||
const taskMsgInput = erstelleElement("textarea", {
|
||||
rows: "2",
|
||||
placeholder: "Optionale Start-Nachricht / Anweisung an den Agenten…"
|
||||
});
|
||||
|
||||
formular.append(
|
||||
erstelleElement("label", { text: "Ziel-Agent:" }),
|
||||
agentSelect,
|
||||
erstelleElement("label", { text: "Art des Jobs:" }),
|
||||
kindSelect,
|
||||
erstelleElement("label", { text: "Cron-Ausdruck:" }),
|
||||
cronInput,
|
||||
erstelleElement("label", { text: "Aufgaben-Nachricht:" }),
|
||||
taskMsgInput
|
||||
);
|
||||
|
||||
const abbrechenBtn = erstelleElement(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
onclick: () => modal.schliessen(),
|
||||
text: "Abbrechen"
|
||||
}
|
||||
);
|
||||
|
||||
const anlegenBtn = erstelleElement("button", {
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche primaer",
|
||||
text: "Job erstellen"
|
||||
});
|
||||
|
||||
const modal = erstelleModal("Neuen geplanten Job anlegen", formular, [abbrechenBtn, anlegenBtn]);
|
||||
|
||||
formular.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
anlegenBtn.disabled = true;
|
||||
|
||||
try {
|
||||
await api("/jobs", {
|
||||
method: "POST",
|
||||
body: {
|
||||
agentId: agentSelect.value,
|
||||
kind: kindSelect.value,
|
||||
cron: cronInput.value.trim(),
|
||||
taskMessage: taskMsgInput.value.trim(),
|
||||
runOnStart: false
|
||||
}
|
||||
});
|
||||
modal.schliessen();
|
||||
zeigeMeldung("Geplanter Job wurde erstellt.", "erfolg");
|
||||
await ladeAktivenBereich();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler beim Erstellen: ${err.message}`, "gefahr");
|
||||
} finally {
|
||||
anlegenBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
// ─── Taskboard ───
|
||||
|
||||
async function rendereTaskboard(container) {
|
||||
container.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Taskboard…" })
|
||||
);
|
||||
|
||||
try {
|
||||
const tasks = await api("/tasks?limit=50");
|
||||
container.replaceChildren();
|
||||
|
||||
if (!tasks || tasks.length === 0) {
|
||||
container.append(
|
||||
erstelleKarte("Taskboard leer", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Aktuell liegen keine offenen Aufgaben im gemeinsamen Arbeitsbereich."
|
||||
})
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const raster = erstelleElement("div", { klasse: "taskboard-raster" });
|
||||
|
||||
for (const task of tasks) {
|
||||
const karte = erstelleKarte(task.title || `Aufgabe ${task.id}`, [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: `ID: ${task.id} · Agent: ${task.agentId || "—"}` }),
|
||||
erstelleElement("div", {
|
||||
klasse: "task-status-zeile",
|
||||
stil: "margin-top: 8px;"
|
||||
}, erstellePlakette(task.status || "Offen", task.status === "Done" ? "erfolg" : "neutral")),
|
||||
erstelleElement("div", {
|
||||
klasse: "gedaempft text-klein",
|
||||
stil: "margin-top: 8px;",
|
||||
text: `Erstellt: ${formatiereDatumZeit(task.createdAt)}`
|
||||
})
|
||||
]);
|
||||
raster.append(karte);
|
||||
}
|
||||
|
||||
container.append(raster);
|
||||
} catch (err) {
|
||||
container.replaceChildren(
|
||||
erstelleKarte("Taskboard nicht verfügbar", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: err.message || "In dieser Instanz ist kein Taskboard aktiv (kein gemeinsamer Arbeitsbereich)."
|
||||
})
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// ClawdDotNet — Token-Verbrauch & Analyse
|
||||
//
|
||||
// Diagramme (CSS/DOM-Balken ohne externe Bibliothek), Statistiken und Läufetabelle.
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleKarte,
|
||||
formatiereDatumZeit,
|
||||
formatiereTokens,
|
||||
formatiereKosten
|
||||
} from "../modules/dom.js";
|
||||
|
||||
export async function rendereVerbrauch() {
|
||||
const container = document.getElementById("ansicht-inhalt");
|
||||
if (!container) return;
|
||||
container.replaceChildren();
|
||||
|
||||
const layout = erstelleElement("div", { klasse: "verbrauch-ansicht" });
|
||||
layout.append(erstelleElement("h1", { text: "Token-Verbrauch & Analyse" }));
|
||||
|
||||
const inhaltBereich = erstelleElement("div", { id: "verbrauch-inhalt-bereich" });
|
||||
layout.append(inhaltBereich);
|
||||
container.append(layout);
|
||||
|
||||
await ladeVerbrauch();
|
||||
}
|
||||
|
||||
async function ladeVerbrauch() {
|
||||
const container = document.getElementById("verbrauch-inhalt-bereich");
|
||||
if (!container) return;
|
||||
|
||||
container.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Verbrauchsdaten…" })
|
||||
);
|
||||
|
||||
try {
|
||||
const laeufe = await api("/usage?limit=100");
|
||||
container.replaceChildren();
|
||||
|
||||
if (!laeufe || laeufe.length === 0) {
|
||||
container.append(
|
||||
erstelleKarte("Keine Daten", [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Noch keine Token-Verbrauchsdaten aufgezeichnet."
|
||||
})
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Statistiken berechnen
|
||||
let summeTokens = 0;
|
||||
let summePrompt = 0;
|
||||
let summeCompletion = 0;
|
||||
let summeKosten = 0;
|
||||
const nachAgent = new Map();
|
||||
const nachModell = new Map();
|
||||
|
||||
for (const l of laeufe) {
|
||||
summeTokens += l.totalTokens || 0;
|
||||
summePrompt += l.promptTokens || 0;
|
||||
summeCompletion += l.completionTokens || 0;
|
||||
summeKosten += Number(l.cost || 0);
|
||||
|
||||
const agentKey = l.agentId || "Unbekannt";
|
||||
nachAgent.set(agentKey, (nachAgent.get(agentKey) || 0) + (l.totalTokens || 0));
|
||||
|
||||
const modellKey = l.model || "Unbekannt";
|
||||
nachModell.set(modellKey, (nachModell.get(modellKey) || 0) + (l.totalTokens || 0));
|
||||
}
|
||||
|
||||
// 1. Kacheln (Statistiken)
|
||||
const kachelGitter = erstelleElement("div", { klasse: "kachel-gitter" });
|
||||
|
||||
kachelGitter.append(
|
||||
erstelleStatKachel("Gesamtkosten (100 Läufe)", formatiereKosten(summeKosten)),
|
||||
erstelleStatKachel("Tokens gesamt", formatiereTokens(summeTokens)),
|
||||
erstelleStatKachel("Eingabe-Tokens (Prompt)", formatiereTokens(summePrompt)),
|
||||
erstelleStatKachel("Ausgabe-Tokens (Completion)", formatiereTokens(summeCompletion))
|
||||
);
|
||||
|
||||
// 2. Balkendiagramme (rein CSS/DOM)
|
||||
const diagrammGitter = erstelleElement("div", { klasse: "diagramm-gitter" });
|
||||
|
||||
const agentBalken = erstelleBalkenDiagramm("Tokens nach Agent", nachAgent, summeTokens);
|
||||
const modellBalken = erstelleBalkenDiagramm("Tokens nach Modell", nachModell, summeTokens);
|
||||
diagrammGitter.append(agentBalken, modellBalken);
|
||||
|
||||
// 3. Läufe-Tabelle
|
||||
const tabelleKarte = erstelleElement("div", { klasse: "karte" });
|
||||
tabelleKarte.append(erstelleElement("h2", { text: "Letzte Agenten-Läufe" }));
|
||||
|
||||
const tabelle = erstelleElement("table", { klasse: "daten-tabelle" });
|
||||
const thead = erstelleElement("thead");
|
||||
thead.append(
|
||||
erstelleElement("tr", {},
|
||||
erstelleElement("th", { text: "Zeitpunkt" }),
|
||||
erstelleElement("th", { text: "Agent" }),
|
||||
erstelleElement("th", { text: "Modell" }),
|
||||
erstelleElement("th", { text: "Prompt", stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: "Completion", stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: "Gesamt", stil: "text-align: right;" }),
|
||||
erstelleElement("th", { text: "Kosten", stil: "text-align: right;" })
|
||||
)
|
||||
);
|
||||
|
||||
const tbody = erstelleElement("tbody");
|
||||
|
||||
for (const lauf of laeufe) {
|
||||
const tr = erstelleElement("tr");
|
||||
tr.append(
|
||||
erstelleElement("td", { text: formatiereDatumZeit(lauf.timestamp) }),
|
||||
erstelleElement("td", {}, erstelleElement("strong", { text: lauf.agentId })),
|
||||
erstelleElement("td", { klasse: "gedaempft text-klein", text: lauf.model }),
|
||||
erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(lauf.promptTokens) }),
|
||||
erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(lauf.completionTokens) }),
|
||||
erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(lauf.totalTokens) }),
|
||||
erstelleElement("td", { stil: "text-align: right;", text: formatiereKosten(lauf.cost) })
|
||||
);
|
||||
tbody.append(tr);
|
||||
}
|
||||
|
||||
tabelle.append(thead, tbody);
|
||||
tabelleKarte.append(tabelle);
|
||||
|
||||
container.append(kachelGitter, diagrammGitter, tabelleKarte);
|
||||
} catch (err) {
|
||||
container.replaceChildren(
|
||||
erstelleKarte("Verbrauchsdaten nicht verfügbar", [
|
||||
erstelleElement("p", { klasse: "gedaempft", text: err.message })
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function erstelleStatKachel(titel, wert) {
|
||||
const kachel = erstelleElement("div", { klasse: "stat-kachel karte" });
|
||||
kachel.append(
|
||||
erstelleElement("div", { klasse: "stat-titel gedaempft", text: titel }),
|
||||
erstelleElement("div", { klasse: "stat-wert", text: wert })
|
||||
);
|
||||
return kachel;
|
||||
}
|
||||
|
||||
function erstelleBalkenDiagramm(titel, datenMap, gesamt) {
|
||||
const karte = erstelleKarte(titel);
|
||||
const container = erstelleElement("div", { klasse: "balken-liste" });
|
||||
|
||||
const sortiert = [...datenMap.entries()].sort((a, b) => b[1] - a[1]);
|
||||
|
||||
for (const [name, wert] of sortiert) {
|
||||
const prozent = gesamt > 0 ? ((wert / gesamt) * 100).toFixed(1) : "0";
|
||||
|
||||
const zeile = erstelleElement("div", { klasse: "balken-zeile" });
|
||||
const meta = erstelleElement("div", { klasse: "balken-meta" });
|
||||
meta.append(
|
||||
erstelleElement("span", { klasse: "balken-name", text: name }),
|
||||
erstelleElement("span", { klasse: "balken-wert gedaempft", text: `${formatiereTokens(wert)} (${prozent}%)` })
|
||||
);
|
||||
|
||||
const spur = erstelleElement("div", { klasse: "balken-spur" });
|
||||
const fuellung = erstelleElement("div", {
|
||||
klasse: "balken-fuellung",
|
||||
stil: `width: ${Math.max(1, Number(prozent))}%;`
|
||||
});
|
||||
spur.append(fuellung);
|
||||
|
||||
zeile.append(meta, spur);
|
||||
container.append(zeile);
|
||||
}
|
||||
|
||||
karte.append(container);
|
||||
return karte;
|
||||
}
|
||||
Reference in New Issue
Block a user