4ad0f9e493
## 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>
210 lines
6.9 KiB
C#
210 lines
6.9 KiB
C#
using Nexus.Api.Models;
|
|
|
|
namespace Nexus.Api.Services;
|
|
|
|
public sealed class DashboardService(
|
|
IOpenClawGatewayClient gateway,
|
|
ITaskService taskService,
|
|
ILogger<DashboardService> logger) : IDashboardService
|
|
{
|
|
public async Task<DashboardStatus> GetStatusAsync()
|
|
{
|
|
try
|
|
{
|
|
return await gateway.GetStatusAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Dashboard status check failed");
|
|
return new DashboardStatus(false, "Offline", 0, 0);
|
|
}
|
|
}
|
|
|
|
public async Task<List<DashboardAgentInfo>> GetAgentsAsync()
|
|
{
|
|
try
|
|
{
|
|
return await gateway.GetAgentsAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Dashboard agents fetch failed");
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public async Task<List<FeedEntry>> GetOperationsAsync(int limit, string? agentFilter)
|
|
{
|
|
try
|
|
{
|
|
var entries = await gateway.GetAllAgentOperationsAsync(Math.Clamp(limit, 1, 100));
|
|
|
|
if (!string.IsNullOrWhiteSpace(agentFilter))
|
|
{
|
|
entries = entries
|
|
.Where(e => string.Equals(e.AgentId, agentFilter, StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(e.Agent, agentFilter, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Dashboard operations fetch failed");
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public async Task<ChatResponse> SendChatAsync(string agentId, string message)
|
|
{
|
|
try
|
|
{
|
|
return await gateway.SendChatMessageAsync(agentId, message);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Dashboard chat send failed");
|
|
return new ChatResponse(false, null, "Gateway nicht erreichbar");
|
|
}
|
|
}
|
|
|
|
public async Task<List<MessageEntry>> GetMessagesAsync(string? sessionKey, int limit, int offset)
|
|
{
|
|
try
|
|
{
|
|
var key = string.IsNullOrWhiteSpace(sessionKey) ? "agent:iris:main" : sessionKey.Trim();
|
|
var messages = await gateway.GetSessionHistoryAsync(key, Math.Clamp(limit, 1, 200), Math.Max(0, offset));
|
|
return messages
|
|
.Where(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(m.Role, "assistant", StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Dashboard messages fetch failed");
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public async Task<List<QueueItem>> GetQueueAsync(CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
var cronTask = gateway.GetQueueAsync();
|
|
var tasksTask = taskService.GetOpenAsync(ct);
|
|
await Task.WhenAll(cronTask, tasksTask);
|
|
|
|
var merged = new List<QueueItem>(cronTask.Result);
|
|
foreach (var t in tasksTask.Result)
|
|
{
|
|
merged.Add(new QueueItem("task-" + t.Id, t.Title, t.State, NormalizePriority(t.Priority), "task", "--"));
|
|
}
|
|
|
|
return merged
|
|
.OrderBy(q => PriorityOrder.GetValueOrDefault(q.Priority, 99))
|
|
.ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Dashboard queue fetch failed");
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public async Task<QueueDeleteResult> DeleteQueueItemAsync(string id, string? source, CancellationToken ct)
|
|
{
|
|
if (string.Equals(source, "cron", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var ok = await gateway.DeleteCronJobAsync(id);
|
|
return new QueueDeleteResult(ok ? QueueDeleteOutcome.Deleted : QueueDeleteOutcome.GatewayError);
|
|
}
|
|
|
|
if (string.Equals(source, "task", StringComparison.OrdinalIgnoreCase) || id.StartsWith("task-"))
|
|
{
|
|
if (!id.StartsWith("task-")) return new QueueDeleteResult(QueueDeleteOutcome.InvalidTaskId);
|
|
if (!Guid.TryParse(id["task-".Length..], out var guid))
|
|
return new QueueDeleteResult(QueueDeleteOutcome.InvalidTaskId);
|
|
|
|
var result = await taskService.CompleteViaQueueAsync(guid, ct);
|
|
return result.Outcome switch
|
|
{
|
|
TaskOperationOutcome.NotFound => new QueueDeleteResult(QueueDeleteOutcome.TaskNotFound),
|
|
_ => new QueueDeleteResult(QueueDeleteOutcome.Deleted)
|
|
};
|
|
}
|
|
|
|
var deleted = await gateway.DeleteCronJobAsync(id);
|
|
return new QueueDeleteResult(deleted ? QueueDeleteOutcome.Deleted : QueueDeleteOutcome.NotFound);
|
|
}
|
|
|
|
public async Task<QueuePriorityResult> CycleQueuePriorityAsync(string id, CancellationToken ct)
|
|
{
|
|
if (!id.StartsWith("task-"))
|
|
return new QueuePriorityResult(QueuePriorityOutcome.Ignored);
|
|
|
|
if (!Guid.TryParse(id["task-".Length..], out var guid))
|
|
return new QueuePriorityResult(QueuePriorityOutcome.InvalidTaskId);
|
|
|
|
var result = await taskService.CyclePriorityAsync(guid, ct);
|
|
return result.Outcome switch
|
|
{
|
|
TaskOperationOutcome.NotFound => new QueuePriorityResult(QueuePriorityOutcome.TaskNotFound),
|
|
_ => new QueuePriorityResult(QueuePriorityOutcome.Updated, result.Task?.Priority)
|
|
};
|
|
}
|
|
|
|
public async Task<AgentModelInfo?> GetAgentModelAsync(string agentId)
|
|
{
|
|
try
|
|
{
|
|
return await gateway.GetAgentModelAsync(agentId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "GetAgentModel failed for {AgentId}", agentId);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<bool> SetAgentModelAsync(string agentId, string model)
|
|
{
|
|
try
|
|
{
|
|
return await gateway.SetAgentModelAsync(agentId, model);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "SetAgentModel failed for {AgentId}", agentId);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async Task<List<AgentActivityEntry>> GetAgentActivityAsync(string agentId, int limit)
|
|
{
|
|
try
|
|
{
|
|
return await gateway.GetAgentActivityAsync(agentId, Math.Clamp(limit, 1, 20));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "GetAgentActivity failed for {AgentId}", agentId);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public List<ModelOption> GetAvailableModels() => gateway.GetAvailableModels();
|
|
|
|
private static string NormalizePriority(string priority) => priority.ToLowerInvariant() switch
|
|
{
|
|
"high" or "critical" or "urgent" => "high",
|
|
"low" or "minor" => "low",
|
|
_ => "medium"
|
|
};
|
|
|
|
private static readonly Dictionary<string, int> PriorityOrder = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["high"] = 0, ["medium"] = 1, ["low"] = 2
|
|
};
|
|
}
|