Weboberflaeche hinzugefuegt und Fachlogik nach App herausgeloest (#493, #494)

- ClawdDotNet.Web: Kestrel im Prozess der Anwendung, Token-Absicherung (TokenGate mit FixedTimeEquals), Endpunkte unter /api, Ereignisstrom unter /events (Server-Sent Events) und fortlaufendes Log-Streaming.

- Weboberflaeche (wwwroot/): Vollstaendiges Single-Page-Frontend mit nativem HTML5, CSS3 und ES-Modulen ohne Buildschritt (strikt textContent, #-Routing, sofortiger Login-Sprung bei fehlendem Token, Light/Dark-Theme, lokale Barlow-Schriften).

- ClawdDotNet.App: Fachlogik aus den Avalonia-ViewModels in Dienste herausgeloest (AgentAdministration, JobAdministration, ServiceAdministration, RunCoordinator) und ueber InstanceChange synchronisiert.

- Tests: ClawdDotNet.Web.Tests und ClawdDotNet.App.Tests hinzugefuegt, alle 684 Tests erfolgreich.
This commit is contained in:
Richard
2026-09-07 21:56:02 +02:00
parent cffd6f4b77
commit 1e9cb789c8
64 changed files with 10543 additions and 554 deletions
+177
View File
@@ -0,0 +1,177 @@
namespace ClawdDotNet.Web.Contracts;
// ─────────────────────────────────────────────────────────────────────────────
// Die Übertragungstypen der Weboberfläche.
//
// Warum es sie gibt, statt die Kerntypen zu serialisieren:
//
// 1. AgentConfig trägt Zugangsdaten. Ein `return agent;` hätte den
// Werkzeug-Konfigurationsbaum samt Passwörtern ins Netz geschickt — die Sorte
// Fehler, die niemandem auffällt, weil alles funktioniert.
// 2. Die Kerntypen ändern sich mit dem Kern. Ein neues Feld in TaskItem wäre sonst
// still eine API-Änderung, auf die sich jemand verlassen hat.
// 3. Die Grenze soll einen Prozesswechsel überstehen (siehe WebUiHost). Über eine
// Prozessgrenze geht nur, was hier steht.
//
// Namen auf Englisch in camelCase wie im übrigen Code; die Beschreibungen auf Deutsch.
// ─────────────────────────────────────────────────────────────────────────────
/// <summary>Antwort auf einen verwaltenden Aufruf — dieselbe Form wie <c>AdminResult</c>.</summary>
public sealed record OutcomeDto(bool Ok, string Message);
/// <summary>Version, Bau und Instanz. Was auf der Info-Seite steht.</summary>
public sealed record InfoDto(
string AppVersion,
string BuildSummary,
string InstanceId,
string InstanceName,
bool EngineActive,
bool SchedulerActive,
bool StagingActive,
bool DeploymentcenterActive);
/// <summary>Erreichbarkeit und Guthaben bei OpenRouter.</summary>
public sealed record StatusDto(
bool ApiReachable,
string StatusText,
string CreditsText,
double? CreditBalance,
double? CreditRemaining);
/// <summary>Ein Agent, ohne alles, was ein Zugangsdatum sein könnte.</summary>
public sealed record AgentDto(
string AgentId,
string DisplayName,
string Model,
string Description,
string PromptCaching,
int MaxToolResultChars,
int MaxSteps,
int MaxContextTokens,
int TimeoutSeconds,
decimal DailyCostUsd,
long DailyTokens,
bool IsRunning,
IReadOnlyList<AgentToolDto> Tools);
/// <summary>
/// Ein Werkzeug am Agenten. <b>Ohne die Einstellungen</b> — die stehen in einem freien
/// Wörterbuch und enthalten regelmäßig Passwörter und Schlüssel. Was einstellbar ist und
/// ob etwas gesetzt ist, sagt <see cref="ToolSettingsDto"/>.
/// </summary>
public sealed record AgentToolDto(string Name, string Description, bool Enabled);
/// <summary>
/// Die Einstellungen eines Werkzeugs, maskiert.
///
/// <para>Ein Wert, dessen Name ein Zugangsdatum bezeichnet, kommt nie heraus. Statt
/// seiner steht in <see cref="Secrets"/>, ob überhaupt etwas gesetzt ist — mehr braucht
/// eine Anzeige nicht, und mehr darf sie nicht wissen.</para>
/// </summary>
public sealed record ToolSettingsDto(
string Name,
IReadOnlyDictionary<string, object?> Values,
IReadOnlyDictionary<string, bool> Secrets);
/// <summary>Eine Zeile im Chatverlauf.</summary>
public sealed record ChatEntryDto(
string Role,
string Content,
DateTime Timestamp,
string? Source);
/// <summary>Was der Browser schickt, wenn jemand etwas in den Chat tippt.</summary>
public sealed record SendMessageDto(string Message);
/// <summary>Ein geplanter Lauf.</summary>
public sealed record JobDto(
string Kind,
string JobId,
string AgentId,
string AgentName,
string ToolName,
string Cron,
string Message,
bool RunOnStart,
DateTime? NextRunUtc,
DateTime? LastRunLocal,
bool CronValid,
bool Enabled);
/// <summary>Was einen geplanten Lauf ausmacht — beim Anlegen wie beim Ändern.</summary>
public sealed record JobInputDto(
string AgentId,
string Kind,
string ToolName = "",
string JobTypeId = "",
string Cron = "",
string TaskMessage = "",
bool RunOnStart = false);
/// <summary>Ein Dienst der Instanz.</summary>
public sealed record ServiceDto(
string ServiceId,
string Name,
string Type,
int Port,
bool Enabled,
bool BuiltIn,
string Description);
/// <summary>Was einen neuen Dienst ausmacht.</summary>
public sealed record ServiceInputDto(string Name, string Type, int Port, string Description = "");
/// <summary>Ein neuer Agent.</summary>
public sealed record NewAgentDto(string AgentId, string DisplayName, string Model, string Description = "");
/// <summary>Eine Aufgabe vom Taskboard.</summary>
public sealed record TaskDto(
string Id,
string Title,
string Status,
string Type,
int Priority,
string Assignee,
bool RequireApproval,
string Acceptance,
IReadOnlyList<string> BlockedBy,
string Body,
DateTime CreatedAt,
DateTime UpdatedAt);
/// <summary>Ein eingefrorener Aufruf, der auf Freigabe wartet.</summary>
public sealed record StagedCallDto(
long Id,
string RunId,
string AgentId,
string Tool,
string? Action,
string Proposal,
string Status,
string ArgumentsJson);
/// <summary>Warum ein Vorschlag abgelehnt wurde.</summary>
public sealed record RejectDto(string Reason);
/// <summary>
/// Ein Lauf mit dem, was er gekostet hat.
///
/// <para><see cref="CostIsKnown"/> ist nicht schmückendes Beiwerk: Ohne geladene
/// Preisliste steht in <see cref="CostUsd"/> eine Null, die „kostenlos" heißen könnte
/// und „unbekannt" heißt. Eine Anzeige, die das nicht unterscheidet, lügt.</para>
/// </summary>
public sealed record UsageDto(
long Id,
string AgentId,
string Model,
int PromptTokens,
int CompletionTokens,
int CachedTokens,
decimal CostUsd,
bool CostIsKnown,
string Status,
int StepCount,
DateTime OccurredAt);
/// <summary>Eine Protokollzeile.</summary>
public sealed record LogLineDto(string Module, string Level, string Text);
+156
View File
@@ -0,0 +1,156 @@
using ClawdDotNet.App.Services;
using ClawdDotNet.Core.Accounting;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Engine;
using ClawdDotNet.Core.Security;
using ClawdDotNet.Core.Staging;
using ClawdDotNet.Core.Tasks;
namespace ClawdDotNet.Web.Contracts;
/// <summary>
/// Die eine Stelle, an der Kerntypen zu Übertragungstypen werden.
///
/// <para>Eine Stelle, weil die Maskierung von Zugangsdaten hier hängt. Verstreut über
/// zehn Endpunkte wäre die Frage „kommt hier ein Passwort heraus?" nicht mehr zu
/// beantworten, sondern nur noch zu hoffen.</para>
/// </summary>
internal static class Mapping
{
public static AgentDto ToDto(
this AgentConfig agent,
IReadOnlyDictionary<string, string> toolDescriptions,
bool isRunning)
=> new(
agent.AgentId,
agent.DisplayName,
agent.Model,
agent.Description,
agent.PromptCaching,
agent.MaxToolResultChars,
agent.LoopGuard.MaxSteps,
agent.LoopGuard.MaxContextTokens,
agent.LoopGuard.TimeoutSeconds,
agent.Budget.DailyCostUsd,
agent.Budget.DailyTokens,
isRunning,
AgentAdministration.KnownTools
.Select(name => new AgentToolDto(
name,
toolDescriptions.GetValueOrDefault(name) ?? "",
agent.Tools.ContainsKey(name)))
.ToList());
/// <summary>
/// Die Einstellungen eines Werkzeugs — <b>ohne die Geheimnisse</b>.
///
/// <para>Welcher Schlüssel eines ist, entscheidet <see cref="ConfigSecrets"/>: der
/// Kern weiß es bereits, und eine zweite Liste hier wäre eine, die beim nächsten
/// neuen Werkzeug vergessen wird. Statt des Werts steht in <c>Secrets</c>, ob
/// überhaupt einer gesetzt ist.</para>
/// </summary>
public static ToolSettingsDto ToDto(
string toolName, IReadOnlyDictionary<string, object?> settings)
{
var values = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
var secrets = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
foreach (var (key, value) in settings)
{
if (ConfigSecrets.IsSecretKey(key))
secrets[key] = value is not null && value is not string { Length: 0 };
else
values[key] = value;
}
return new ToolSettingsDto(toolName, values, secrets);
}
public static ChatEntryDto ToDto(this ChatEntry entry)
=> new(entry.Role, entry.Content, entry.Timestamp, entry.Source);
public static JobDto ToDto(this JobSummary job, DateTime? lastRun)
=> new(
job.Kind == JobKind.ToolJob ? "toolJob" : "agentWakeup",
job.JobId,
job.AgentId,
job.AgentName,
job.ToolName,
job.Cron,
job.Message,
job.RunOnStart,
job.NextRunUtc,
lastRun,
job.CronValid,
job.Enabled);
public static JobDefinition ToDefinition(this JobInputDto input)
=> new(
input.AgentId,
string.Equals(input.Kind, "toolJob", StringComparison.OrdinalIgnoreCase)
? JobKind.ToolJob
: JobKind.AgentWakeup,
input.ToolName,
input.JobTypeId,
input.Cron,
input.TaskMessage,
input.RunOnStart);
public static ServiceDto ToDto(this ServiceConfig service)
=> new(
service.ServiceId,
service.Name,
service.Type,
service.Port,
service.Enabled,
service.BuiltIn,
service.Description);
public static TaskDto ToDto(this TaskItem task)
=> new(
task.Id,
task.Title,
task.Status.ToString(),
task.Type.ToString(),
task.Priority,
task.Assignee,
task.RequireApproval,
task.Acceptance,
task.BlockedBy,
task.Body,
task.CreatedAt,
task.UpdatedAt);
/// <summary>
/// Ein wartender Aufruf samt seiner eingefrorenen Argumente.
///
/// <para>Die Argumente gehen <b>roh</b> heraus, und das ist der ganze Sinn der
/// Freigabe: Wer entscheidet, muss sehen, was genau ausgeführt würde — nicht eine
/// Zusammenfassung davon. Sie sind zugleich Fremdtext aus einem Modell; die Anzeige
/// setzt sie als Text ein, nie als HTML.</para>
/// </summary>
public static StagedCallDto ToDto(this StagedCall call)
=> new(
call.Id,
call.RunId,
call.AgentId,
call.Tool,
call.Action,
call.Proposal,
call.Status.ToString(),
call.ArgumentsJson);
public static UsageDto ToDto(this RunUsage usage)
=> new(
usage.Id,
usage.AgentId,
usage.Model,
usage.PromptTokens,
usage.CompletionTokens,
usage.CachedTokens,
usage.CostUsd,
usage.CostIsKnown,
usage.Status,
usage.StepCount,
usage.OccurredAt);
}