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
+73 -4
View File
@@ -63,6 +63,12 @@ public sealed class AppHost : IAsyncDisposable
public TaskScanner? Scanner { get; private init; }
public StagingService? Staging { get; private init; }
/// <summary>
/// Das Taskboard. Null, wenn den Agenten dieser Instanz ein gemeinsamer
/// Arbeitsbereich fehlt — dann gibt es keinen Ort, an dem Aufgaben lägen.
/// </summary>
public TaskboardService? Board { get; private init; }
public SqliteUsageRepository? Usage { get; private init; }
public TelegramClientManager? TelegramClient { get; private init; }
public OpenRouterStatusService? Status { get; private init; }
@@ -98,6 +104,68 @@ public sealed class AppHost : IAsyncDisposable
/// </summary>
public LicenseWatch? LicenseWatch { get; private set; }
/// <summary>
/// Meldet, dass die Instanzkonfiguration geschrieben wurde.
///
/// <para>Es gibt dieses Ereignis erst, seit es zwei Oberflächen gibt.
/// <see cref="Instance"/> ist ein veränderliches Objekt, das beide teilen — die
/// Ansichtsmodelle lesen es aber einmal beim Aufbau in ihre Listen. Ohne eine
/// Meldung erschiene ein in der Weboberfläche angelegter Agent im laufenden Fenster
/// nicht, und umgekehrt genauso.</para>
///
/// <para><b>Kommt auf dem Faden des Verursachers</b>, nicht auf dem der Oberfläche.
/// Wer eine <c>ObservableCollection</c> daran hängt, geht über den Dispatcher.</para>
/// </summary>
public event Action<InstanceChange>? InstanceChanged;
/// <summary>
/// Schreibt die Instanzkonfiguration und sagt Bescheid.
///
/// <para>Der einzige Weg, auf dem <see cref="Instance"/> gespeichert werden soll.
/// <c>Directories.SaveInstanceConfig</c> von Hand zu rufen schreibt zwar dieselbe
/// Datei, lässt aber die andere Oberfläche in Unkenntnis.</para>
/// </summary>
public void SaveInstance(InstanceChange change)
{
Directories.SaveInstanceConfig(InstancePath, Instance);
InstanceChanged?.Invoke(change);
}
// ─── Verwaltung ───
//
// Die Regeln zum Anlegen, Ändern und Ausführen — bis zum 2026-09-06 in den
// Avalonia-Ansichtsmodellen, jetzt hier. Der Grund ist die zweite Oberfläche: Was
// dort stand, hätte sie ein zweites Mal schreiben müssen.
//
// Je Aufbau genau eine Ausprägung, und deshalb hier und nicht beim Aufrufer: Der
// RunCoordinator führt den Verlauf und merkt sich die letzten Handläufe. Zwei
// Ausprägungen hiessen zwei Verläufe, von denen jede Oberfläche einen anderen zeigt.
private readonly Lazy<AgentAdministration> _agentAdmin;
private readonly Lazy<ServiceAdministration> _serviceAdmin;
private readonly Lazy<JobAdministration> _jobAdmin;
private readonly Lazy<RunCoordinator> _runs;
public AppHost()
{
_agentAdmin = new Lazy<AgentAdministration>(() => new AgentAdministration(this));
_serviceAdmin = new Lazy<ServiceAdministration>(() => new ServiceAdministration(this));
_jobAdmin = new Lazy<JobAdministration>(() => new JobAdministration(this));
_runs = new Lazy<RunCoordinator>(() => new RunCoordinator(this));
}
/// <summary>Agenten anlegen, entfernen, Werkzeuge zulassen und einstellen.</summary>
public AgentAdministration AgentAdmin => _agentAdmin.Value;
/// <summary>Dienste der Instanz anlegen, entfernen, umschalten.</summary>
public ServiceAdministration ServiceAdmin => _serviceAdmin.Value;
/// <summary>Geplante Läufe anlegen, ändern, entfernen, abschalten.</summary>
public JobAdministration JobAdmin => _jobAdmin.Value;
/// <summary>„Jetzt ausführen" samt Verlauf.</summary>
public RunCoordinator Runs => _runs.Value;
/// <summary>Was der Aufrufer beim Start erfragen muss.</summary>
public sealed class Callbacks
{
@@ -362,7 +430,7 @@ public sealed class AppHost : IAsyncDisposable
engine.LoadPersistedChats();
var (scanner, staging) = BuildTaskboard(
var (scanner, staging, board) = BuildTaskboard(
instance, taskRepository, engine, tools, stateStore, audit,
stagingRepository, loggerFactory, logger);
@@ -378,6 +446,7 @@ public sealed class AppHost : IAsyncDisposable
Engine = engine,
Scanner = scanner,
Staging = staging,
Board = board,
Usage = usage,
TelegramClient = telegram,
Status = new OpenRouterStatusService(instance.OpenRouterApiKey)
@@ -394,7 +463,7 @@ public sealed class AppHost : IAsyncDisposable
/// Taskboard und Scanner. Der Scanner ist der einzige periodische Treiber (A1) —
/// geplante Agentenläufe wie Tool-Job-Polls sind Tasks.
/// </summary>
private static (TaskScanner?, StagingService?) BuildTaskboard(
private static (TaskScanner?, StagingService?, TaskboardService?) BuildTaskboard(
InstanceConfig instance,
SqliteTaskRepository taskRepository,
AgentEngine engine,
@@ -410,7 +479,7 @@ public sealed class AppHost : IAsyncDisposable
.FirstOrDefault(p => !string.IsNullOrWhiteSpace(p));
if (string.IsNullOrWhiteSpace(sharedWorkspace))
return (null, null);
return (null, null, null);
var board = new TaskboardService(taskRepository, Path.Combine(sharedWorkspace, "tasks"));
@@ -451,7 +520,7 @@ public sealed class AppHost : IAsyncDisposable
}
});
return (scanner, staging);
return (scanner, staging, board);
}
private async Task ConnectTelegramAsync(Callbacks callbacks, ILogger logger)