feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user