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
+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);
}