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:
@@ -161,7 +161,7 @@ public sealed class InstanceHttpHost : IAsyncDisposable
|
||||
|
||||
// Die Protokollierung der Anwendung uebernehmen, statt eine zweite aufzumachen.
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddProvider(new SharedLoggerProvider(loggerFactory));
|
||||
builder.Logging.AddProvider(new Core.Logging.SharedLoggerProvider(loggerFactory));
|
||||
|
||||
// ASP.NET protokolliert auf Information jede einzelne Anfrage - mit eigener
|
||||
// Zeile fuer Start und Ende, und das auch fuer jede Schriftart und jedes
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Reicht die Protokollierung des Webhosts an die Protokollierung der Anwendung durch.
|
||||
///
|
||||
/// <para>Ohne diese Zwischenschicht gäbe es zwei Möglichkeiten, und beide wären falsch:
|
||||
/// Entweder der Webhost bekommt eigene Ausgabewege — dann stünde die Hälfte der
|
||||
/// Meldungen woanders als die andere. Oder man reicht ihm die <c>ILoggerFactory</c> der
|
||||
/// Anwendung direkt — dann räumt er sie beim Herunterfahren mit ab, und der Rest der
|
||||
/// Anwendung protokolliert ab da ins Leere.</para>
|
||||
///
|
||||
/// <para>Deshalb: durchreichen, und beim Abräumen <b>nichts</b> tun. Die Fabrik gehört
|
||||
/// dem <c>AppHost</c>, und der räumt sie selbst ab.</para>
|
||||
/// </summary>
|
||||
internal sealed class SharedLoggerProvider(ILoggerFactory shared) : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(string categoryName) => shared.CreateLogger(categoryName);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Absicht. Siehe Klassenbeschreibung.
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,178 +0,0 @@
|
||||
// ClawdDotNet — WebUI Haupteinstiegspunkt
|
||||
//
|
||||
// Initialisiert Authentifizierung, Router, Ereignisstrom, Farbschema und Kopfzeile.
|
||||
|
||||
import { api, hatToken, loescheToken, beiUnberechtigt } 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";
|
||||
|
||||
// Ansichten
|
||||
import { rendereChat } from "./views/chat.js";
|
||||
import { rendereAgenten } from "./views/agents.js";
|
||||
import { rendereAufgaben } from "./views/tasks.js";
|
||||
import { rendereDienste } from "./views/services.js";
|
||||
import { rendereStaging, holeWartendeAnzahl } from "./views/staging.js";
|
||||
import { rendereVerbrauch } from "./views/usage.js";
|
||||
import { rendereProtokoll, trenneLogStream } from "./views/logs.js";
|
||||
import { rendereInfo } from "./views/info.js";
|
||||
import { rendereSicherung, rendereEinstellungen, rendereAgentenChats } from "./views/placeholders.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(); });
|
||||
|
||||
// ─── 2. DOM-Verdrahtung ───
|
||||
|
||||
const seitenleiste = document.getElementById("seitenleiste");
|
||||
const sidebarToggle = document.getElementById("sidebar-toggle");
|
||||
const mobilMenueBtn = document.getElementById("mobil-menue-btn");
|
||||
const seitenleisteBackdrop = document.getElementById("seitenleiste-backdrop");
|
||||
const themeSchalter = document.getElementById("theme-schalter");
|
||||
const abmeldenBtn = document.getElementById("abmelden-btn");
|
||||
const verbindungPunkt = document.getElementById("verbindung-punkt");
|
||||
const kopfInstanz = document.getElementById("kopf-instanz");
|
||||
const kopfOpenRouter = document.getElementById("kopf-openrouter-status");
|
||||
|
||||
// Seitenleiste kollabieren (Desktop)
|
||||
sidebarToggle?.addEventListener("click", () => {
|
||||
seitenleiste?.classList.toggle("kollabiert");
|
||||
});
|
||||
|
||||
// Mobil-Menü öffnen
|
||||
mobilMenueBtn?.addEventListener("click", () => {
|
||||
seitenleiste?.classList.add("mobil-offen");
|
||||
if (seitenleisteBackdrop) seitenleisteBackdrop.hidden = false;
|
||||
});
|
||||
|
||||
// Mobil-Menü schließen bei Klick auf Backdrop oder Link
|
||||
function schliesseMobilMenue() {
|
||||
seitenleiste?.classList.remove("mobil-offen");
|
||||
if (seitenleisteBackdrop) seitenleisteBackdrop.hidden = true;
|
||||
}
|
||||
|
||||
seitenleisteBackdrop?.addEventListener("click", schliesseMobilMenue);
|
||||
document.querySelectorAll(".nav-link").forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
if (window.innerWidth <= 768) schliesseMobilMenue();
|
||||
});
|
||||
});
|
||||
|
||||
// Farbschema umschalten
|
||||
themeSchalter?.addEventListener("click", () => {
|
||||
wechsleTheme();
|
||||
});
|
||||
|
||||
// Abmelden
|
||||
abmeldenBtn?.addEventListener("click", () => {
|
||||
loescheToken();
|
||||
trenneStrom();
|
||||
trenneLogStream();
|
||||
zeigeAnmeldung();
|
||||
});
|
||||
|
||||
// Bei unberechtigtem API-Aufruf (401) sofort zur Anmeldung springen
|
||||
beiUnberechtigt(() => {
|
||||
trenneStrom();
|
||||
trenneLogStream();
|
||||
zeigeAnmeldung();
|
||||
});
|
||||
|
||||
// Verbindungsstatus des Ereignisstroms
|
||||
beiVerbindungsStatus((istOffen) => {
|
||||
if (!verbindungPunkt) return;
|
||||
if (istOffen) {
|
||||
verbindungPunkt.classList.add("offen");
|
||||
verbindungPunkt.title = "Ereignisstrom: verbunden";
|
||||
} else {
|
||||
verbindungPunkt.classList.remove("offen");
|
||||
verbindungPunkt.title = "Ereignisstrom: getrennt";
|
||||
}
|
||||
});
|
||||
|
||||
// ─── 3. Start & Initialisierung ───
|
||||
|
||||
initialisiereTheme();
|
||||
initialisiereLogin(starteHauptAnwendung);
|
||||
|
||||
async function aktualisiereKopfStatus() {
|
||||
try {
|
||||
const status = await api("/status");
|
||||
if (kopfOpenRouter && status) {
|
||||
kopfOpenRouter.textContent = status.creditsText
|
||||
? `OpenRouter: ${status.creditsText}`
|
||||
: (status.isApiReachable ? "OpenRouter: OK" : "OpenRouter: offline");
|
||||
}
|
||||
} catch {
|
||||
if (kopfOpenRouter) kopfOpenRouter.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function aktualisiereStagingZaehler() {
|
||||
const anzahl = await holeWartendeAnzahl();
|
||||
const badge = document.getElementById("staging-badge");
|
||||
if (badge) {
|
||||
if (anzahl > 0) {
|
||||
badge.textContent = String(anzahl);
|
||||
badge.hidden = false;
|
||||
} else {
|
||||
badge.hidden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function starteHauptAnwendung(info) {
|
||||
versteckeAnmeldung();
|
||||
|
||||
if (kopfInstanz && info) {
|
||||
kopfInstanz.textContent = `${info.instanceName} (${info.instanceId})`;
|
||||
}
|
||||
|
||||
// Ereignisstrom verbinden
|
||||
verbindeStrom();
|
||||
|
||||
// Status & Zähler laden
|
||||
await aktualisiereKopfStatus();
|
||||
await aktualisiereStagingZaehler();
|
||||
|
||||
// Ereignisse für Kopf-Updates
|
||||
abonnieren("status", () => {
|
||||
aktualisiereKopfStatus();
|
||||
});
|
||||
|
||||
abonnieren("instance", (daten) => {
|
||||
if (daten.kind === "Settings") {
|
||||
aktualisiereKopfStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// Router initialisieren
|
||||
initialisiereRouter();
|
||||
}
|
||||
|
||||
// Prüfe bestehende Session
|
||||
if (hatToken()) {
|
||||
api("/info")
|
||||
.then((info) => {
|
||||
starteHauptAnwendung(info);
|
||||
})
|
||||
.catch(() => {
|
||||
loescheToken();
|
||||
zeigeAnmeldung();
|
||||
});
|
||||
} else {
|
||||
zeigeAnmeldung();
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,93 +0,0 @@
|
||||
Copyright 2017 The Barlow Project Authors (https://github.com/jpt/barlow)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,172 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ClawdDotNet</title>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ─── Anmelde-Ansicht ─── -->
|
||||
<div id="anmeldung">
|
||||
<div class="karte anmelde-kasten">
|
||||
<div class="karte-kopf">
|
||||
<h2>ClawdDotNet</h2>
|
||||
</div>
|
||||
<p class="hinweis">
|
||||
Bitte gib das Zugangstoken der Instanz ein (<code>webUiToken</code> in der Instanzdatei).
|
||||
</p>
|
||||
<form id="anmeldeformular" class="formular-raster">
|
||||
<input type="password" id="token-eingabe" placeholder="Zugangstoken (mind. 20 Zeichen)" autocomplete="current-password" required>
|
||||
<button type="submit" class="primaer">Anmelden</button>
|
||||
<p id="anmeldefehler" class="fehler" hidden></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── Hauptanwendung ─── -->
|
||||
<div id="anwendung" hidden>
|
||||
<div id="seitenleiste-backdrop" class="seitenleiste-backdrop" hidden></div>
|
||||
|
||||
<!-- Seitenleiste / Navigation -->
|
||||
<aside id="seitenleiste" class="seitenleiste">
|
||||
<div class="seitenleiste-kopf">
|
||||
<span class="marke">ClawdDotNet</span>
|
||||
<button id="sidebar-toggle" class="schaltflaeche-icon flach" title="Seitenleiste ein-/ausklappen" type="button">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M15 18l-6-6 6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="seitenleiste-inhalt">
|
||||
<div class="nav-gruppe">
|
||||
<div class="nav-gruppe-titel">Arbeiten</div>
|
||||
<a href="#/chat" class="nav-link" title="Chat">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
|
||||
</svg>
|
||||
<span class="nav-label">Chat</span>
|
||||
</a>
|
||||
<a href="#/agenten" class="nav-link" title="Agenten">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2 M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M23 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
<span class="nav-label">Agenten</span>
|
||||
</a>
|
||||
<a href="#/aufgaben" class="nav-link" title="Aufgaben">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M9 11l3 3L22 4 M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
|
||||
</svg>
|
||||
<span class="nav-label">Aufgaben</span>
|
||||
</a>
|
||||
<a href="#/dienste" class="nav-link" title="Dienste">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M4 14a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-6z M4 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4z M12 6v6 M8 8h.01 M16 8h.01"/>
|
||||
</svg>
|
||||
<span class="nav-label">Dienste</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-gruppe">
|
||||
<div class="nav-gruppe-titel">Analyse</div>
|
||||
<a href="#/verbrauch" class="nav-link" title="Tokennutzung">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M3 3v18h18 M7 15v4 M11 11v8 M15 7v12 M19 12v7"/>
|
||||
</svg>
|
||||
<span class="nav-label">Tokennutzung</span>
|
||||
</a>
|
||||
<a href="#/agenten-chats" class="nav-link" title="Agenten-Chats">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M8 12h.01M12 12h.01M16 12h.01 M21 12c0 4.4-4 8-9 8-1.3 0-2.6-.2-3.7-.7L3 20l1-4.2C3.4 14.6 3 13.3 3 12c0-4.4 4-8 9-8s9 3.6 9 8Z"/>
|
||||
</svg>
|
||||
<span class="nav-label">Agenten-Chats</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-gruppe">
|
||||
<div class="nav-gruppe-titel">System</div>
|
||||
<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"/>
|
||||
</svg>
|
||||
<span class="nav-label">Freigaben</span>
|
||||
<span id="staging-badge" class="nav-badge" hidden>0</span>
|
||||
</a>
|
||||
<a href="#/protokoll" class="nav-link" title="Protokoll">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20 M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z M8 7h8 M8 11h6"/>
|
||||
</svg>
|
||||
<span class="nav-label">Protokoll</span>
|
||||
</a>
|
||||
<a href="#/sicherung" class="nav-link" title="Sicherung">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z M17 21v-8H7v8 M7 3v5h8"/>
|
||||
</svg>
|
||||
<span class="nav-label">Sicherung</span>
|
||||
</a>
|
||||
<a href="#/einstellungen" class="nav-link" title="Einstellungen">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
<span class="nav-label">Einstellungen</span>
|
||||
</a>
|
||||
<a href="#/info" class="nav-link" title="Info">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="16" x2="12" y2="12"/>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
||||
</svg>
|
||||
<span class="nav-label">Info</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Hauptinhalt -->
|
||||
<div class="hauptbereich">
|
||||
<header class="kopfzeile">
|
||||
<div class="kopfzeile-links">
|
||||
<button id="mobil-menue-btn" class="schaltflaeche-icon flach" title="Menü öffnen" 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>
|
||||
|
||||
<div class="kopfzeile-rechts">
|
||||
<span id="kopf-openrouter-status" class="gedaempft text-klein"></span>
|
||||
<span id="verbindung-punkt" class="status-punkt" title="Ereignisstrom: getrennt"></span>
|
||||
<button id="theme-schalter" class="schaltflaeche-icon flach" title="Farbschema umschalten" type="button">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<line x1="12" y1="1" x2="12" y2="3"/>
|
||||
<line x1="12" y1="21" x2="12" y2="23"/>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
||||
<line x1="1" y1="12" x2="3" y2="12"/>
|
||||
<line x1="21" y1="12" x2="23" y2="12"/>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="abmelden-btn" class="schaltflaeche flach klein" title="Abmelden" type="button">
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="inhalt-scroll">
|
||||
<div id="ansicht-inhalt"></div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,115 +0,0 @@
|
||||
// ClawdDotNet — API-Zugriffsschicht
|
||||
//
|
||||
// Verwaltet das Zugangstoken im sessionStorage und kapselt fetch-Aufrufe.
|
||||
|
||||
export const TOKEN_SCHLUESSEL = "clawd.token";
|
||||
|
||||
let token = sessionStorage.getItem(TOKEN_SCHLUESSEL) ?? "";
|
||||
const unberechtigtCallbacks = new Set();
|
||||
|
||||
/**
|
||||
* Gibt das aktuell gespeicherte Token zurück.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function holeToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert ein neues Token im Speicher und sessionStorage.
|
||||
* @param {string} neuesToken
|
||||
*/
|
||||
export function setzeToken(neuesToken) {
|
||||
token = (neuesToken || "").trim();
|
||||
if (token) {
|
||||
sessionStorage.setItem(TOKEN_SCHLUESSEL, token);
|
||||
} else {
|
||||
sessionStorage.removeItem(TOKEN_SCHLUESSEL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht das Token (Abmeldung).
|
||||
*/
|
||||
export function loescheToken() {
|
||||
token = "";
|
||||
sessionStorage.removeItem(TOKEN_SCHLUESSEL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Token im Speicher liegt.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hatToken() {
|
||||
return Boolean(token && token.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registriert einen Rückruf für den Fall einer 401-Ablehnung (sofortiger Login-Sprung).
|
||||
* @param {() => void} callback
|
||||
*/
|
||||
export function beiUnberechtigt(callback) {
|
||||
unberechtigtCallbacks.add(callback);
|
||||
return () => unberechtigtCallbacks.delete(callback);
|
||||
}
|
||||
|
||||
function meldeUnberechtigt() {
|
||||
loescheToken();
|
||||
for (const cb of unberechtigtCallbacks) {
|
||||
try {
|
||||
cb();
|
||||
} catch (err) {
|
||||
console.error("Fehler im Unberechtigt-Callback:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen Aufruf gegen die API aus.
|
||||
* @param {string} pfad Pfad relativ zu /api, z.B. "/info" oder "/agents"
|
||||
* @param {RequestInit} [optionen={}]
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function api(pfad, optionen = {}) {
|
||||
const kopfzeilen = {
|
||||
...(optionen.headers ?? {}),
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
|
||||
if (optionen.body && typeof optionen.body === "object" && !(optionen.body instanceof FormData)) {
|
||||
kopfzeilen["Content-Type"] = "application/json";
|
||||
optionen.body = JSON.stringify(optionen.body);
|
||||
}
|
||||
|
||||
const antwort = await fetch(`/api${pfad}`, {
|
||||
...optionen,
|
||||
headers: kopfzeilen
|
||||
});
|
||||
|
||||
if (antwort.status === 401) {
|
||||
meldeUnberechtigt();
|
||||
const fehler = new Error("Kein gültiges Token. Bitte neu anmelden.");
|
||||
fehler.abgelehnt = true;
|
||||
fehler.status = 401;
|
||||
throw fehler;
|
||||
}
|
||||
|
||||
if (!antwort.ok) {
|
||||
let meldung = `HTTP ${antwort.status}`;
|
||||
try {
|
||||
const rumpf = await antwort.json();
|
||||
meldung = rumpf?.message ?? 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;
|
||||
throw fehler;
|
||||
}
|
||||
|
||||
if (antwort.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return antwort.json();
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
// ClawdDotNet — Sichere DOM-Helferfunktionen
|
||||
//
|
||||
// Leitfaden-Regel 3.1: Fremdtext ist nie HTML. Kein innerHTML im gesamten Baum.
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
/**
|
||||
* Erzeugt ein HTML-Element sicher ohne innerHTML.
|
||||
* @param {string} tag
|
||||
* @param {object} [eigenschaften]
|
||||
* @param {...(Node|string|null|undefined)} kinder
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function erstelleElement(tag, eigenschaften = {}, ...kinder) {
|
||||
const el = document.createElement(tag);
|
||||
|
||||
for (const [schluessel, wert] of Object.entries(eigenschaften)) {
|
||||
if (wert === undefined || wert === null) continue;
|
||||
|
||||
if (schluessel === "klasse" || schluessel === "className") {
|
||||
el.className = wert;
|
||||
} else if (schluessel === "text") {
|
||||
el.textContent = wert;
|
||||
} else if (schluessel === "stil" || schluessel === "style") {
|
||||
if (typeof wert === "string") {
|
||||
el.style.cssText = wert;
|
||||
} else {
|
||||
Object.assign(el.style, wert);
|
||||
}
|
||||
} else if (schluessel.startsWith("on") && typeof wert === "function") {
|
||||
const eventName = schluessel.slice(2).toLowerCase();
|
||||
el.addEventListener(eventName, wert);
|
||||
} else if (schluessel === "dataset" && typeof wert === "object") {
|
||||
Object.assign(el.dataset, wert);
|
||||
} else if (typeof wert === "boolean") {
|
||||
if (wert) el.setAttribute(schluessel, "");
|
||||
else el.removeAttribute(schluessel);
|
||||
} else {
|
||||
el.setAttribute(schluessel, String(wert));
|
||||
}
|
||||
}
|
||||
|
||||
for (const kind of kinder) {
|
||||
if (kind === null || kind === undefined) continue;
|
||||
if (typeof kind === "string" || typeof kind === "number") {
|
||||
el.append(document.createTextNode(String(kind)));
|
||||
} else if (kind instanceof Node) {
|
||||
el.append(kind);
|
||||
}
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt ein SVG-Icon sicher über den SVG-Namensraum ohne innerHTML.
|
||||
* @param {string} pfadDaten SVG path d attribute
|
||||
* @param {number} [groesse=16]
|
||||
* @param {string} [klasse=""]
|
||||
* @returns {SVGSVGElement}
|
||||
*/
|
||||
export function erstelleIcon(pfadDaten, groesse = 16, klasse = "") {
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 24 24");
|
||||
svg.setAttribute("width", String(groesse));
|
||||
svg.setAttribute("height", String(groesse));
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("stroke", "currentColor");
|
||||
svg.setAttribute("stroke-width", "1.75");
|
||||
svg.setAttribute("stroke-linecap", "round");
|
||||
svg.setAttribute("stroke-linejoin", "round");
|
||||
if (klasse) svg.setAttribute("class", `icon ${klasse}`);
|
||||
else svg.setAttribute("class", "icon");
|
||||
|
||||
const path = document.createElementNS(SVG_NS, "path");
|
||||
path.setAttribute("d", pfadDaten);
|
||||
svg.appendChild(path);
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Häufig genutzte SVG-Pfade (Lucide-kompatibel, 24x24).
|
||||
*/
|
||||
export const PFADE = {
|
||||
chat: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z",
|
||||
agenten: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2 M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M23 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75",
|
||||
aufgaben: "M9 11l3 3L22 4 M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11",
|
||||
dienste: "M4 14a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-6z M4 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4z M12 6v6 M8 8h.01 M16 8h.01",
|
||||
verbrauch: "M3 3v18h18 M7 15v4 M11 11v8 M15 7v12 M19 12v7",
|
||||
staging: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z M9 12l2 2 4-4",
|
||||
protokoll: "M4 19.5A2.5 2.5 0 0 1 6.5 17H20 M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z M8 7h8 M8 11h6",
|
||||
sicherung: "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z M17 21v-8H7v8 M7 3v5h8",
|
||||
einstellungen: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
|
||||
info: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z M12 16v-4 M12 8h.01",
|
||||
plus: "M12 5v14 M5 12h14",
|
||||
muelleimer: "M3 6h18 M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2 M10 11v6 M14 11v6",
|
||||
abspielen: "M5 3l14 9-14 9V3z",
|
||||
abbrechen: "M18 6L6 18 M6 6l12 12",
|
||||
senden: "M22 2L11 13 M22 2l-7 20-4-9-9-4 20-7z",
|
||||
leeren: "M19 7l-.867 12.142A2 2 0 0 1 16.138 21H7.862a2 2 0 0 1-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v3M4 7h16",
|
||||
aktualisieren: "M21.5 2v6h-6 M2.5 22v-6h6 M2 11.5a10 10 0 0 1 18.8-4.3L21.5 8 M22 12.5a10 10 0 0 1-18.8 4.2L2.5 16",
|
||||
sonne: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0z",
|
||||
mond: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z",
|
||||
menue: "M3 12h18 M3 6h18 M3 18h18",
|
||||
schliessen: "M18 6L6 18 M6 6l12 12",
|
||||
pfeilLinks: "M19 12H5 M12 19l-7-7 7-7",
|
||||
pfeilRechts: "M5 12h14 M12 5l7 7-7 7",
|
||||
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"
|
||||
};
|
||||
|
||||
/**
|
||||
* Erzeugt eine Statusplakette (Badge).
|
||||
* @param {string} text
|
||||
* @param {"erfolg"|"warnung"|"gefahr"|"neutral"|"laufend"} art
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function erstellePlakette(text, art = "neutral") {
|
||||
return erstelleElement("span", { klasse: `plakette plakette-${art}`, text });
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Karte mit optionalem Blueprint-Rahmen.
|
||||
* @param {string} [titel]
|
||||
* @param {HTMLElement[]} [elemente]
|
||||
* @param {object} [optionen]
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function erstelleKarte(titel, elemente = [], optionen = {}) {
|
||||
const card = erstelleElement("div", {
|
||||
klasse: `karte ${optionen.klasse ?? ""}`.trim()
|
||||
});
|
||||
|
||||
if (titel) {
|
||||
const kopf = erstelleElement("div", { klasse: "karte-kopf" });
|
||||
const h = erstelleElement("h2", { text: titel });
|
||||
kopf.append(h);
|
||||
if (optionen.aktionen) kopf.append(optionen.aktionen);
|
||||
card.append(kopf);
|
||||
}
|
||||
|
||||
for (const el of elemente) {
|
||||
if (el) card.append(el);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt einen standardisierten Modaldialog.
|
||||
* @param {string} titel
|
||||
* @param {HTMLElement} inhalt
|
||||
* @param {HTMLElement[]} [aktionen]
|
||||
* @returns {{ dialog: HTMLElement, oeffnen: () => void, schliessen: () => void }}
|
||||
*/
|
||||
export function erstelleModal(titel, inhalt, aktionen = []) {
|
||||
const overlay = erstelleElement("div", { klasse: "modal-overlay", hidden: true });
|
||||
const box = erstelleElement("div", { klasse: "modal-box karte" });
|
||||
|
||||
const kopf = erstelleElement("div", { klasse: "modal-kopf" });
|
||||
const h = erstelleElement("h2", { text: titel });
|
||||
const schliessenBtn = erstelleElement(
|
||||
"button",
|
||||
{ klasse: "schaltflaeche-icon flach", title: "Schließen", type: "button" },
|
||||
erstelleIcon(PFADE.schliessen, 16)
|
||||
);
|
||||
kopf.append(h, schliessenBtn);
|
||||
|
||||
const koerper = erstelleElement("div", { klasse: "modal-koerper" }, inhalt);
|
||||
|
||||
const fuss = erstelleElement("div", { klasse: "modal-fuss" });
|
||||
for (const aktion of aktionen) fuss.append(aktion);
|
||||
|
||||
box.append(kopf, koerper, fuss);
|
||||
overlay.append(box);
|
||||
document.body.append(overlay);
|
||||
|
||||
const schliessen = () => {
|
||||
overlay.hidden = true;
|
||||
overlay.classList.remove("sichtbar");
|
||||
document.removeEventListener("keydown", tasteHandler);
|
||||
};
|
||||
|
||||
const oeffnen = () => {
|
||||
overlay.hidden = false;
|
||||
overlay.classList.add("sichtbar");
|
||||
document.addEventListener("keydown", tasteHandler);
|
||||
const erstesInput = box.querySelector("input, select, textarea, button:not(.schaltflaeche-icon)");
|
||||
erstesInput?.focus();
|
||||
};
|
||||
|
||||
const tasteHandler = (e) => {
|
||||
if (e.key === "Escape") schliessen();
|
||||
};
|
||||
|
||||
schliessenBtn.addEventListener("click", schliessen);
|
||||
overlay.addEventListener("click", (e) => {
|
||||
if (e.target === overlay) schliessen();
|
||||
});
|
||||
|
||||
return { dialog: overlay, oeffnen, schliessen };
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert Zeitangaben (z.B. "14:32:05").
|
||||
* @param {string|Date|number|null} wert
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatiereZeit(wert) {
|
||||
if (!wert) return "—";
|
||||
const d = wert instanceof Date ? wert : new Date(wert);
|
||||
if (isNaN(d.getTime())) return String(wert);
|
||||
return d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert Datum und Uhrzeit.
|
||||
* @param {string|Date|number|null} wert
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatiereDatumZeit(wert) {
|
||||
if (!wert) return "—";
|
||||
const d = wert instanceof Date ? wert : new Date(wert);
|
||||
if (isNaN(d.getTime())) return String(wert);
|
||||
return d.toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert Tokenzahlen mit Tausendertrennzeichen.
|
||||
* @param {number|null} zahl
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatiereTokens(zahl) {
|
||||
if (zahl === null || zahl === undefined) return "0";
|
||||
return new Intl.NumberFormat("de-DE").format(zahl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert Kosten (USD / EUR).
|
||||
* @param {number|null} kosten
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatiereKosten(kosten) {
|
||||
if (kosten === null || kosten === undefined) return "$0.00";
|
||||
return "$" + Number(kosten).toFixed(4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt ein kurzes Benachrichtigungsbanner oben im Fenster.
|
||||
* @param {string} text
|
||||
* @param {"info"|"erfolg"|"warnung"|"gefahr"} [art="info"]
|
||||
* @param {number} [dauerMs=4000]
|
||||
*/
|
||||
export function zeigeMeldung(text, art = "info", dauerMs = 4000) {
|
||||
let container = document.getElementById("toast-container");
|
||||
if (!container) {
|
||||
container = erstelleElement("div", { id: "toast-container", klasse: "toast-container" });
|
||||
document.body.append(container);
|
||||
}
|
||||
|
||||
const toast = erstelleElement("div", { klasse: `toast toast-${art}` });
|
||||
const iconArt = art === "erfolg" ? PFADE.haken : art === "warnung" || art === "gefahr" ? PFADE.ausrufezeichen : PFADE.info;
|
||||
toast.append(erstelleIcon(iconArt, 16), erstelleElement("span", { text }));
|
||||
|
||||
container.append(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add("ausblenden");
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, dauerMs);
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
// ClawdDotNet — Ereignisstrom (Server-Sent Events)
|
||||
//
|
||||
// Hört auf /events und verteilt anwendungsintern Ereignisse (chat, step, run, instance, status).
|
||||
|
||||
import { holeToken } from "./api.js";
|
||||
|
||||
let strom = null;
|
||||
const abonnenten = new Map();
|
||||
const verbindungsStatusCallbacks = new Set();
|
||||
let verbunden = false;
|
||||
|
||||
/**
|
||||
* Registriert einen Rückruf für Änderungen des Verbindungsstatus (offen / getrennt).
|
||||
* @param {(status: boolean) => void} callback
|
||||
*/
|
||||
export function beiVerbindungsStatus(callback) {
|
||||
verbindungsStatusCallbacks.add(callback);
|
||||
callback(verbunden);
|
||||
return () => verbindungsStatusCallbacks.delete(callback);
|
||||
}
|
||||
|
||||
function meldeStatus(istOffen) {
|
||||
if (verbunden === istOffen) return;
|
||||
verbunden = istOffen;
|
||||
for (const cb of verbindungsStatusCallbacks) {
|
||||
try {
|
||||
cb(verbunden);
|
||||
} catch (err) {
|
||||
console.error("Fehler im Verbindungsstatus-Callback:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abonniert ein bestimmtes Server-Ereignis.
|
||||
* @param {string} ereignis Name des Ereignisses ("chat", "step", "run", "instance", "status")
|
||||
* @param {(daten: any) => void} callback
|
||||
* @returns {() => void} Abmeldefunktion
|
||||
*/
|
||||
export function abonnieren(ereignis, callback) {
|
||||
if (!abonnenten.has(ereignis)) {
|
||||
abonnenten.set(ereignis, new Set());
|
||||
}
|
||||
abonnenten.get(ereignis).add(callback);
|
||||
|
||||
return () => {
|
||||
const s = abonnenten.get(ereignis);
|
||||
if (s) {
|
||||
s.delete(callback);
|
||||
if (s.size === 0) abonnenten.delete(ereignis);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function verteileEreignis(ereignis, rohdaten) {
|
||||
const gruppe = abonnenten.get(ereignis);
|
||||
if (!gruppe || gruppe.size === 0) return;
|
||||
|
||||
let daten = rohdaten;
|
||||
try {
|
||||
daten = JSON.parse(rohdaten);
|
||||
} catch {
|
||||
// Falls Rohdaten kein JSON waren, unverändert übergeben
|
||||
}
|
||||
|
||||
for (const cb of gruppe) {
|
||||
try {
|
||||
cb(daten);
|
||||
} catch (err) {
|
||||
console.error(`Fehler bei Verarbeitung von ${ereignis}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet den Ereignisstrom, falls ein Token vorliegt.
|
||||
*/
|
||||
export function verbindeStrom() {
|
||||
trenneStrom();
|
||||
|
||||
const token = holeToken();
|
||||
if (!token) return;
|
||||
|
||||
// EventSource kann keine Header setzen: Token wird über die Query übergeben (siehe TokenGate.cs)
|
||||
strom = new EventSource(`/events?token=${encodeURIComponent(token)}`);
|
||||
|
||||
strom.onopen = () => {
|
||||
meldeStatus(true);
|
||||
};
|
||||
|
||||
// EventSource verbindet automatisch wieder bei Abbruch.
|
||||
strom.onerror = () => {
|
||||
meldeStatus(false);
|
||||
};
|
||||
|
||||
const bekannteEreignisse = ["chat", "step", "run", "instance", "status"];
|
||||
for (const art of bekannteEreignisse) {
|
||||
strom.addEventListener(art, (e) => verteileEreignis(art, e.data));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schließt die bestehende Verbindung zum Ereignisstrom.
|
||||
*/
|
||||
export function trenneStrom() {
|
||||
if (strom) {
|
||||
strom.close();
|
||||
strom = null;
|
||||
}
|
||||
meldeStatus(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt an, ob der Ereignisstrom aktuell aktiv verbunden ist.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function istVerbunden() {
|
||||
return verbunden;
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
// ClawdDotNet — Hash-basierter Router
|
||||
//
|
||||
// Leitfaden-Regel 3.2: Navigation über #. Kein history.pushState, keine Serverrouten.
|
||||
|
||||
const routen = new Map();
|
||||
let aktiverPfad = "";
|
||||
let navigationsListener = new Set();
|
||||
|
||||
/**
|
||||
* Registriert einen Handler für eine Route.
|
||||
* @param {string} muster z.B. "/chat", "/chat/:agentId", "/agenten"
|
||||
* @param {(parameter: object) => Promise<void>|void} handler
|
||||
*/
|
||||
export function registriereRoute(muster, handler) {
|
||||
routen.set(muster, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registriert einen allgemeinen Listener, der bei jedem Routenwechsel aufgerufen wird.
|
||||
* @param {(pfad: string, parameter: object) => void} listener
|
||||
*/
|
||||
export function beiRoutenwechsel(listener) {
|
||||
navigationsListener.add(listener);
|
||||
return () => navigationsListener.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigiert zu einem bestimmten Hash.
|
||||
* @param {string} hash z.B. "#/agenten" oder "/agenten"
|
||||
*/
|
||||
export function navigiere(hash) {
|
||||
const bereinigt = hash.startsWith("#") ? hash : `#${hash}`;
|
||||
if (window.location.hash !== bereinigt) {
|
||||
window.location.hash = bereinigt;
|
||||
} else {
|
||||
fuehreRouteAus();
|
||||
}
|
||||
}
|
||||
|
||||
function parseHash() {
|
||||
const hash = window.location.hash || "#/chat";
|
||||
const pfad = hash.startsWith("#") ? hash.slice(1) : hash;
|
||||
return pfad.startsWith("/") ? pfad : `/${pfad}`;
|
||||
}
|
||||
|
||||
function findePassendeRoute(pfad) {
|
||||
const pfadTeile = pfad.split("/").filter(Boolean);
|
||||
|
||||
for (const [muster, handler] of routen.entries()) {
|
||||
const musterTeile = muster.split("/").filter(Boolean);
|
||||
if (pfadTeile.length !== musterTeile.length) continue;
|
||||
|
||||
const params = {};
|
||||
let passt = true;
|
||||
|
||||
for (let i = 0; i < musterTeile.length; i++) {
|
||||
if (musterTeile[i].startsWith(":")) {
|
||||
const paramName = musterTeile[i].slice(1);
|
||||
params[paramName] = decodeURIComponent(pfadTeile[i]);
|
||||
} else if (musterTeile[i] !== pfadTeile[i]) {
|
||||
passt = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (passt) {
|
||||
return { handler, params, muster };
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Erste Route oder 404
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fuehreRouteAus() {
|
||||
const pfad = parseHash();
|
||||
aktiverPfad = pfad;
|
||||
|
||||
aktualisiereAktivenNavEintrag(pfad);
|
||||
|
||||
const match = findePassendeRoute(pfad);
|
||||
if (match) {
|
||||
for (const listener of navigationsListener) {
|
||||
listener(pfad, match.params);
|
||||
}
|
||||
await match.handler(match.params);
|
||||
} else if (routen.has("/chat")) {
|
||||
navigiere("#/chat");
|
||||
}
|
||||
}
|
||||
|
||||
function aktualisiereAktivenNavEintrag(pfad) {
|
||||
const basisPfad = "/" + pfad.split("/").filter(Boolean)[0];
|
||||
const navLinks = document.querySelectorAll(".nav-link[href]");
|
||||
|
||||
for (const link of navLinks) {
|
||||
const href = link.getAttribute("href") || "";
|
||||
const zielPfad = href.startsWith("#") ? href.slice(1) : href;
|
||||
const zielBasis = "/" + zielPfad.split("/").filter(Boolean)[0];
|
||||
|
||||
if (basisPfad === zielBasis) {
|
||||
link.classList.add("aktiv");
|
||||
} else {
|
||||
link.classList.remove("aktiv");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert den Router und startet die Überwachung.
|
||||
*/
|
||||
export function initialisiereRouter() {
|
||||
window.addEventListener("hashchange", fuehreRouteAus);
|
||||
fuehreRouteAus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den aktuellen Pfad zurück.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function holeAktuellenPfad() {
|
||||
return aktiverPfad;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// ClawdDotNet — Theme-Verwaltung (Hell / Dunkel)
|
||||
//
|
||||
// Farbschema nach Theme.axaml / Mockup. Unterstützt system, hell, dunkel.
|
||||
|
||||
const THEME_SCHLUESSEL = "clawd.theme";
|
||||
|
||||
let aktuellerModus = localStorage.getItem(THEME_SCHLUESSEL) || "system";
|
||||
|
||||
const systemQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
function wendeThemeAn() {
|
||||
const istDunkel =
|
||||
aktuellerModus === "dunkel" ||
|
||||
(aktuellerModus === "system" && systemQuery.matches);
|
||||
|
||||
if (istDunkel) {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
}
|
||||
}
|
||||
|
||||
systemQuery.addEventListener("change", () => {
|
||||
if (aktuellerModus === "system") {
|
||||
wendeThemeAn();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialisiert das Theme beim Start.
|
||||
*/
|
||||
export function initialisiereTheme() {
|
||||
wendeThemeAn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schaltet zwischen Hell und Dunkel um.
|
||||
* @returns {string} Neues aktives Theme
|
||||
*/
|
||||
export function wechsleTheme() {
|
||||
const istJetztDunkel = document.documentElement.getAttribute("data-theme") === "dark";
|
||||
aktuellerModus = istJetztDunkel ? "hell" : "dunkel";
|
||||
localStorage.setItem(THEME_SCHLUESSEL, aktuellerModus);
|
||||
wendeThemeAn();
|
||||
return aktuellerModus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt das aktuell gewählte Theme zurück.
|
||||
* @returns {"system"|"hell"|"dunkel"}
|
||||
*/
|
||||
export function holeAktuellesTheme() {
|
||||
return aktuellerModus;
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
// 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}` })
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,394 +0,0 @@
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// 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 })
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
// 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();
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
// 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();
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
// 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)."
|
||||
})
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
// 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