feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar

This commit is contained in:
Richard
2026-08-10 10:48:34 +02:00
parent a0e18d2a57
commit b5bf97ae74
187 changed files with 20054 additions and 882 deletions
@@ -0,0 +1,182 @@
using System.Globalization;
using ClawdDotNet.Core.Storage;
using Microsoft.Data.Sqlite;
namespace ClawdDotNet.Core.Audit;
/// <summary>
/// Das Audit-Log in der Instanz-Datenbank. Bewusst nur Einfügen und Lesen — es gibt keine
/// Update-/Delete-Methoden, weil die Unveränderlichkeit die eigentliche Zusage ist.
/// </summary>
public sealed class SqliteAuditRepository : IAuditRepository
{
private readonly SqliteStorage _storage;
public SqliteAuditRepository(SqliteStorage storage) => _storage = storage;
public Task AppendAsync(AuditEntry entry, CancellationToken ct)
=> _storage.WriteAsync(async conn =>
{
using var cmd = conn.CreateCommand();
cmd.CommandText = """
INSERT INTO AuditLog
(RunId, AgentId, Model, Source, Tool, Arguments, Status, Summary, DurationMs, OccurredAt)
VALUES
(@runId, @agentId, @model, @source, @tool, @arguments, @status, @summary, @durationMs, @occurredAt)
""";
cmd.Parameters.AddWithValue("@runId", entry.RunId);
cmd.Parameters.AddWithValue("@agentId", entry.AgentId);
cmd.Parameters.AddWithValue("@model", entry.Model);
cmd.Parameters.AddWithValue("@source", AuditSource.Normalize(entry.Source));
cmd.Parameters.AddWithValue("@tool", entry.Tool);
cmd.Parameters.AddWithValue("@arguments", entry.Arguments);
cmd.Parameters.AddWithValue("@status", entry.Status.ToString());
cmd.Parameters.AddWithValue("@summary", entry.Summary);
cmd.Parameters.AddWithValue("@durationMs", entry.DurationMs);
cmd.Parameters.AddWithValue("@occurredAt", Format(entry.OccurredAt));
await cmd.ExecuteNonQueryAsync(ct);
}, ct);
public Task RecordReceiptAsync(RunReceipt receipt, CancellationToken ct)
=> _storage.WriteAsync(async conn =>
{
using var cmd = conn.CreateCommand();
cmd.CommandText = """
INSERT INTO RunReceipts
(RunId, AgentId, Model, Source, TaskId, Status, StepCount,
PromptTokens, CompletionTokens, CachedTokens, CostUsd, CostIsKnown,
DurationMs, ResultRef, OccurredAt)
VALUES
(@runId, @agentId, @model, @source, @taskId, @status, @stepCount,
@prompt, @completion, @cached, @cost, @costKnown,
@durationMs, @resultRef, @occurredAt)
""";
cmd.Parameters.AddWithValue("@runId", receipt.RunId);
cmd.Parameters.AddWithValue("@agentId", receipt.AgentId);
cmd.Parameters.AddWithValue("@model", receipt.Model);
cmd.Parameters.AddWithValue("@source", AuditSource.Normalize(receipt.Source));
cmd.Parameters.AddWithValue("@taskId", (object?)receipt.TaskId ?? DBNull.Value);
cmd.Parameters.AddWithValue("@status", receipt.Status);
cmd.Parameters.AddWithValue("@stepCount", receipt.StepCount);
cmd.Parameters.AddWithValue("@prompt", receipt.PromptTokens);
cmd.Parameters.AddWithValue("@completion", receipt.CompletionTokens);
cmd.Parameters.AddWithValue("@cached", receipt.CachedTokens);
cmd.Parameters.AddWithValue("@cost", receipt.CostUsd.ToString(CultureInfo.InvariantCulture));
cmd.Parameters.AddWithValue("@costKnown", receipt.CostIsKnown ? 1 : 0);
cmd.Parameters.AddWithValue("@durationMs", receipt.DurationMs);
cmd.Parameters.AddWithValue("@resultRef", receipt.ResultRef);
cmd.Parameters.AddWithValue("@occurredAt", Format(receipt.OccurredAt));
await cmd.ExecuteNonQueryAsync(ct);
}, ct);
public async Task<IReadOnlyList<AuditEntry>> ListRecentAsync(int limit, CancellationToken ct)
{
await using var conn = await _storage.OpenConnectionAsync(ct);
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT " + AuditColumns + " FROM AuditLog ORDER BY Id DESC LIMIT @limit";
cmd.Parameters.AddWithValue("@limit", Math.Clamp(limit, 1, 1000));
return await ReadEntriesAsync(cmd, ct);
}
public async Task<IReadOnlyList<AuditEntry>> ListForRunAsync(string runId, CancellationToken ct)
{
await using var conn = await _storage.OpenConnectionAsync(ct);
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT " + AuditColumns + " FROM AuditLog WHERE RunId = @runId ORDER BY Id";
cmd.Parameters.AddWithValue("@runId", runId);
return await ReadEntriesAsync(cmd, ct);
}
public async Task<RunReceipt?> GetReceiptForRunAsync(string runId, CancellationToken ct)
{
await using var conn = await _storage.OpenConnectionAsync(ct);
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT " + ReceiptColumns + " FROM RunReceipts WHERE RunId = @runId ORDER BY Id DESC LIMIT 1";
cmd.Parameters.AddWithValue("@runId", runId);
await using var reader = await cmd.ExecuteReaderAsync(ct);
return await reader.ReadAsync(ct) ? ReadReceipt(reader) : null;
}
public async Task<IReadOnlyList<RunReceipt>> ListReceiptsForTaskAsync(string taskId, CancellationToken ct)
{
await using var conn = await _storage.OpenConnectionAsync(ct);
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT " + ReceiptColumns + " FROM RunReceipts WHERE TaskId = @taskId ORDER BY Id";
cmd.Parameters.AddWithValue("@taskId", taskId);
var results = new List<RunReceipt>();
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
results.Add(ReadReceipt(reader));
return results;
}
public async Task<int> CountAsync(CancellationToken ct)
{
await using var conn = await _storage.OpenConnectionAsync(ct);
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT COUNT(*) FROM AuditLog";
return Convert.ToInt32(await cmd.ExecuteScalarAsync(ct));
}
// ─── Hilfsfunktionen ───
private const string AuditColumns =
"Id, RunId, AgentId, Model, Source, Tool, Arguments, Status, Summary, DurationMs, OccurredAt";
private const string ReceiptColumns =
"Id, RunId, AgentId, Model, Source, TaskId, Status, StepCount, PromptTokens, " +
"CompletionTokens, CachedTokens, CostUsd, CostIsKnown, DurationMs, ResultRef, OccurredAt";
private static async Task<IReadOnlyList<AuditEntry>> ReadEntriesAsync(SqliteCommand cmd, CancellationToken ct)
{
var results = new List<AuditEntry>();
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
results.Add(ReadEntry(reader));
return results;
}
private static AuditEntry ReadEntry(SqliteDataReader r) => new()
{
Id = r.GetInt64(0),
RunId = r.GetString(1),
AgentId = r.GetString(2),
Model = r.GetString(3),
Source = r.GetString(4),
Tool = r.GetString(5),
Arguments = r.GetString(6),
Status = Enum.TryParse<AuditStatus>(r.GetString(7), out var s) ? s : AuditStatus.Ok,
Summary = r.GetString(8),
DurationMs = r.GetInt64(9),
OccurredAt = Parse(r.GetString(10))
};
private static RunReceipt ReadReceipt(SqliteDataReader r) => new()
{
Id = r.GetInt64(0),
RunId = r.GetString(1),
AgentId = r.GetString(2),
Model = r.GetString(3),
Source = r.GetString(4),
TaskId = r.IsDBNull(5) ? null : r.GetString(5),
Status = r.GetString(6),
StepCount = r.GetInt32(7),
PromptTokens = r.GetInt32(8),
CompletionTokens = r.GetInt32(9),
CachedTokens = r.GetInt32(10),
CostUsd = decimal.TryParse(r.GetString(11), NumberStyles.Any, CultureInfo.InvariantCulture, out var c) ? c : 0m,
CostIsKnown = r.GetInt32(12) != 0,
DurationMs = r.GetInt64(13),
ResultRef = r.GetString(14),
OccurredAt = Parse(r.GetString(15))
};
private static string Format(DateTime value) => value.ToUniversalTime().ToString("O");
private static DateTime Parse(string value)
=> DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dt)
? dt
: DateTime.MinValue;
}