refactor: SOLID architecture — backend service layer + frontend V2 components
## Backend — Service Layer & Repository Refactoring ### Neue Services (21 neue Dateien) **Interfaces & Implementierungen:** - `IOpenClawGatewayClient` — Interface für OpenClawGatewayClient (DIP-Fix: DashboardController hing an konkreter Klasse) - `IAgentConfigService` / `AgentConfigService` — Agent-Config-File-I/O aus AgentsController extrahiert - `IProjectService` / `ProjectService` — Projekt-CRUD + Activity-Logging (SRP) - `ITaskService` / `TaskService` — Task-State-Machine, Approve/Reject, Dashboard-Operationen (eliminiert Duplikation zwischen TasksController und DashboardController) - `IDashboardService` / `DashboardService` — Queue-Aggregation, Priority-Normalisierung, Gateway-Delegation - `IOperationsService` / `OperationsService` — Metriken-Berechnung aus OperationsController - `ITeamService` / `TeamService` — IDENTITY.md-Lesen aus TeamController - `IMemoryService` / `MemoryService` — File-I/O aus MemoryController - `IIncidentService` / `IncidentService` — File-Parsing (Regex-Source-Generatoren) aus IncidentsController - `IDocService` / `DocService` — Directory-Scan aus DocsController - `ICalendarService` / `CalendarService` — Gateway-HTTP-Calls + Fallback-Daten aus CalendarController ### Repository-Fixes **IUserRepository / UserRepository:** - `SaveChangesAsync` entfernt (leaky abstraction — Caller sollten nie SaveChanges steuern) - `RevokeTokenAsync(tokenHash)` — atomares Token-Revoke inkl. SaveChanges - `RevokeFamilyAsync(familyId)` — Batch-Revoke einer Token-Familie inkl. SaveChanges - `RemoveExpiredTokensAsync` speichert jetzt selbst (war vorher dependent auf nachfolgenden Save) ### AuthService-Fixes - `GetUserAsync`: unnötiges `Task.Run` entfernt → direkt `_users.GetByIdAsync().AsTask()` - `RevokeAsync`: delegiert jetzt an `IUserRepository.RevokeTokenAsync` - `RefreshAsync`: Token-Reuse-Detection delegiert an `IUserRepository.RevokeFamilyAsync` ### Bug-Fix - `OpenClawGatewayClient.ReadAgentGoalAsync`: pre-existing `CS1656` behoben (`reader` war `using`-Variable und wurde neu zugewiesen — in `reader2` umbenannt) ### Controller (16 Stück — alle slim) Alle Controller reduziert auf: Input validieren → Service aufrufen → HTTP-Result zurückgeben. Kein Business-Logic, kein File-I/O, keine direkte Repository-Nutzung (außer AgentsController für Activity-Log). **Program.cs — neue Registrierungen:** - `AddHttpClient<IOpenClawGatewayClient, OpenClawGatewayClient>` (war vorher konkrete Klasse) - Scoped: IDashboardService, IProjectService, ITaskService, IOperationsService, ITeamService, ICalendarService - Singleton: IAgentConfigService, IMemoryService, IIncidentService, IDocService --- ## Frontend — Dashboard V2 Components **AgentDetailModal.vue, IrisChat.vue, TaskStrip.vue:** - V2 Design-System: Dark Space Theme, Glass-Panels, Gradient-Akzente - Stores (agents, chat, tasks) nutzen Service + Mapper-Pattern - NexusLayout, FlowBoard, Topbar — Layoutfixes für fullHeight-Route-Meta Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ using Nexus.Api.Models;
|
||||
|
||||
namespace Nexus.Api.Services;
|
||||
|
||||
public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration configuration)
|
||||
public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration configuration) : IOpenClawGatewayClient
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
@@ -202,7 +202,12 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
||||
Tags: tags,
|
||||
Progress: progress,
|
||||
Workload: workload,
|
||||
Goal: goal
|
||||
Goal: goal,
|
||||
RoleBadge: DeriveRoleBadge(id),
|
||||
StatusLabel: DeriveStatusLabel(isActive, status),
|
||||
Elapsed: FormatElapsed(status),
|
||||
Think: null,
|
||||
Next: DeriveNext(isActive, currentTask)
|
||||
));
|
||||
}
|
||||
return agents;
|
||||
@@ -415,7 +420,7 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
||||
if (toolResult is null)
|
||||
return result;
|
||||
|
||||
var json = toolResult.ToJsonString(); result.Add(new MessageEntry("diag", "JSON[" + json.Substring(0, Math.Min(200, json.Length)) + "]", DateTimeOffset.UtcNow.ToString("o")));
|
||||
var json = toolResult.ToJsonString();
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
@@ -840,8 +845,8 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
||||
|
||||
// 3. Look for "## Oberstes Prinzip" as second choice
|
||||
inRoleSection = false;
|
||||
reader = new StringReader(soul);
|
||||
while ((line = reader.ReadLine()) is not null)
|
||||
using var reader2 = new StringReader(soul);
|
||||
while ((line = reader2.ReadLine()) is not null)
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.StartsWith("## ") && trimmed.IndexOf("Prinzip", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
@@ -1060,6 +1065,50 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
||||
return slash > 0 ? modelId[..slash] : "unknown";
|
||||
}
|
||||
|
||||
private static string DeriveRoleBadge(string agentId) => agentId.ToLowerInvariant() switch
|
||||
{
|
||||
"iris" => "badge-purple",
|
||||
"programmer" or "developer" => "badge-blue",
|
||||
"reviewer" => "badge-amber",
|
||||
"architekt" => "badge-cyan",
|
||||
"executor" => "badge-rose",
|
||||
"researcher" => "badge-green",
|
||||
_ => "badge-slate"
|
||||
};
|
||||
|
||||
private static string DeriveStatusLabel(bool isActive, JsonNode? status)
|
||||
{
|
||||
if (!isActive) return "Bereit";
|
||||
var statusText = status?["status"]?.GetValue<string>()?.ToLowerInvariant();
|
||||
return statusText switch
|
||||
{
|
||||
"thinking" or "think" => "Plant",
|
||||
"blocked" or "block" => "Blockiert",
|
||||
_ => "Arbeitet"
|
||||
};
|
||||
}
|
||||
|
||||
private static string? FormatElapsed(JsonNode? status)
|
||||
{
|
||||
var lastActivity = status?["lastActivity"]?.GetValue<string>()
|
||||
?? status?["lastMessage"]?.GetValue<string>();
|
||||
if (lastActivity is null) return null;
|
||||
if (!DateTimeOffset.TryParse(lastActivity, out var ts)) return null;
|
||||
var diff = DateTimeOffset.UtcNow - ts;
|
||||
if (diff.TotalSeconds < 60) return $"{(int)diff.TotalSeconds}s";
|
||||
if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m";
|
||||
if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}h";
|
||||
return $"{(int)diff.TotalDays}d";
|
||||
}
|
||||
|
||||
private static string DeriveNext(bool isActive, string? currentTask)
|
||||
{
|
||||
if (!isActive) return "Standby";
|
||||
if (!string.IsNullOrWhiteSpace(currentTask) && currentTask != "Working...")
|
||||
return currentTask.Length > 60 ? currentTask[..60] + "…" : currentTask;
|
||||
return "Aufgabe ausführen";
|
||||
}
|
||||
|
||||
private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch
|
||||
{
|
||||
"iris" => "Chief of Staff",
|
||||
|
||||
Reference in New Issue
Block a user