From b7b44494f07ac95283894e1da7e20e6b63d8f36b Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 11 Jun 2026 10:06:53 +0200 Subject: [PATCH] fix(shadcn): isolate Nexus CSS vars with --nx- prefix + admin password reset endpoint --- backend/Controllers/AuthController.cs | 17 + backend/Controllers/DashboardController.cs | 61 +- backend/DTOs/AdminResetPasswordRequest.cs | 13 + backend/Models/Dashboard.cs | 19 +- backend/Program.cs | 6 +- backend/Services/AuthService.cs | 43 ++ backend/Services/OpenClawGatewayClient.cs | 270 ++++--- compose.yaml | 7 +- docs/_test-gateway-http.mjs | 2 + docs/_test-gateway.js | 2 + docs/gateway-api-research.md | 401 +++++++++++ frontend/components.json | 17 + frontend/find-files.sh | 2 + frontend/nginx.conf | 13 + frontend/package.json | 8 +- frontend/pnpm-lock.yaml | 545 ++++++++++++++ frontend/src/App.vue | 34 +- frontend/src/assets/main.css | 681 ++++++++++++++++++ frontend/src/components/ModuleView.vue | 6 +- .../src/components/dashboard/AgentModal.vue | 307 ++++---- .../src/components/dashboard/AgentNode.vue | 208 ------ .../components/dashboard/ChatMessageList.vue | 90 +++ .../src/components/dashboard/ChatPanel.vue | 475 ++++-------- .../src/components/dashboard/QueuePanel.vue | 116 +-- .../src/components/dashboard/TaskCard.vue | 70 +- .../src/components/dashboard/TeamNetwork.vue | 120 ++- frontend/src/components/layout/AppHeader.vue | 12 +- frontend/src/components/layout/AppSidebar.vue | 8 +- frontend/src/components/ui/Badge.vue | 37 + frontend/src/components/ui/Card.vue | 16 + frontend/src/components/ui/CardContent.vue | 16 + .../src/components/ui/CardDescription.vue | 16 + frontend/src/components/ui/CardHeader.vue | 16 + frontend/src/components/ui/CardTitle.vue | 16 + frontend/src/components/ui/Dialog.vue | 61 ++ .../src/components/ui/DialogDescription.vue | 16 + frontend/src/components/ui/DialogHeader.vue | 16 + frontend/src/components/ui/DialogTitle.vue | 16 + frontend/src/components/ui/Input.vue | 22 + frontend/src/components/ui/Select.vue | 34 + frontend/src/components/ui/Textarea.vue | 22 + frontend/src/components/ui/ToastContainer.vue | 138 ++++ frontend/src/components/ui/button/Button.vue | 23 + frontend/src/components/ui/button/index.ts | 42 ++ frontend/src/composables/useDashboardData.ts | 117 +-- frontend/src/composables/useTimer.ts | 37 - frontend/src/composables/useToast.ts | 28 + frontend/src/lib/utils.ts | 6 + frontend/src/main.ts | 2 +- frontend/src/style.css | 76 +- frontend/src/types/toast.ts | 6 + frontend/src/views/DashboardView.vue | 48 +- frontend/src/views/DocsView.vue | 4 +- frontend/src/views/IncidentsView.vue | 4 +- frontend/src/views/MemoryView.vue | 4 +- frontend/src/views/ProjectDetailView.vue | 10 +- frontend/src/views/SettingsView.vue | 6 +- frontend/tsconfig.app.json | 6 +- frontend/vite.config.ts | 6 + 59 files changed, 3267 insertions(+), 1153 deletions(-) create mode 100644 backend/DTOs/AdminResetPasswordRequest.cs create mode 100644 docs/_test-gateway-http.mjs create mode 100644 docs/_test-gateway.js create mode 100644 docs/gateway-api-research.md create mode 100644 frontend/components.json create mode 100644 frontend/find-files.sh create mode 100644 frontend/src/assets/main.css delete mode 100644 frontend/src/components/dashboard/AgentNode.vue create mode 100644 frontend/src/components/dashboard/ChatMessageList.vue create mode 100644 frontend/src/components/ui/Badge.vue create mode 100644 frontend/src/components/ui/Card.vue create mode 100644 frontend/src/components/ui/CardContent.vue create mode 100644 frontend/src/components/ui/CardDescription.vue create mode 100644 frontend/src/components/ui/CardHeader.vue create mode 100644 frontend/src/components/ui/CardTitle.vue create mode 100644 frontend/src/components/ui/Dialog.vue create mode 100644 frontend/src/components/ui/DialogDescription.vue create mode 100644 frontend/src/components/ui/DialogHeader.vue create mode 100644 frontend/src/components/ui/DialogTitle.vue create mode 100644 frontend/src/components/ui/Input.vue create mode 100644 frontend/src/components/ui/Select.vue create mode 100644 frontend/src/components/ui/Textarea.vue create mode 100644 frontend/src/components/ui/ToastContainer.vue create mode 100644 frontend/src/components/ui/button/Button.vue create mode 100644 frontend/src/components/ui/button/index.ts delete mode 100644 frontend/src/composables/useTimer.ts create mode 100644 frontend/src/composables/useToast.ts create mode 100644 frontend/src/lib/utils.ts create mode 100644 frontend/src/types/toast.ts diff --git a/backend/Controllers/AuthController.cs b/backend/Controllers/AuthController.cs index 7419114..d27f793 100644 --- a/backend/Controllers/AuthController.cs +++ b/backend/Controllers/AuthController.cs @@ -91,6 +91,23 @@ public class AuthController( : Results.Ok(new UserInfo { Id = user.Id, Email = user.Email, DisplayName = user.DisplayName, Role = user.Role }); } + [HttpPost("admin-reset-password")] + [EnableRateLimiting("agents")] + public async Task AdminResetPassword([FromBody] AdminResetPasswordRequest request, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.NewPassword) || string.IsNullOrWhiteSpace(request.AdminToken)) + return Results.ValidationProblem(new Dictionary { ["request"] = ["Email, new password, and admin token are required."] }); + + if (request.NewPassword.Length < 10) + return Results.ValidationProblem(new Dictionary { ["newPassword"] = ["New password must be at least 10 characters."] }); + + var success = await authService.AdminResetPasswordAsync(request.Email, request.NewPassword, request.AdminToken, ct); + if (!success) + return Results.Problem("Password reset failed. Check the admin token, email, and that the user exists.", statusCode: 400); + + return Results.Ok(new { message = "Password reset successfully." }); + } + [HttpPost("change-password")] public async Task ChangePassword([FromBody] ChangePasswordRequest request, CancellationToken ct) { diff --git a/backend/Controllers/DashboardController.cs b/backend/Controllers/DashboardController.cs index d771973..c30be2a 100644 --- a/backend/Controllers/DashboardController.cs +++ b/backend/Controllers/DashboardController.cs @@ -122,7 +122,6 @@ public class DashboardController(OpenClawGatewayClient gateway, ILogger string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase) || string.Equals(m.Role, "assistant", StringComparison.OrdinalIgnoreCase)) @@ -152,6 +151,66 @@ public class DashboardController(OpenClawGatewayClient gateway, ILogger + /// Returns the current model and provider for a specific agent session. + /// Calls session_status with the agent's session key. + /// + [HttpGet("agents/{id}/model")] + public async Task> GetAgentModel(string id) + { + try + { + var info = await gateway.GetAgentModelAsync(id); + if (info is null) + return NotFound(new { error = $"Agent '{id}' not found or gateway unreachable" }); + return Ok(info); + } + catch (Exception ex) + { + logger.LogWarning(ex, "GetAgentModel failed for {AgentId}", id); + return StatusCode(500, new { error = "Internal error" }); + } + } + + /// + /// Sets the model for a specific agent session. + /// Calls session_status with model parameter. + /// + [HttpPut("agents/{id}/model")] + public async Task SetAgentModel(string id, [FromBody] SetModelRequest request) + { + if (string.IsNullOrWhiteSpace(request.Model)) + return BadRequest(new { error = "Model is required" }); + + try + { + var ok = await gateway.SetAgentModelAsync(id, request.Model); + if (!ok) + return StatusCode(502, new { error = "Gateway did not accept the change" }); + return Ok(new { status = "ok", model = request.Model }); + } + catch (Exception ex) + { + logger.LogWarning(ex, "SetAgentModel failed for {AgentId}", id); + return StatusCode(500, new { error = "Internal error" }); + } + } + + /// + /// Returns the list of available models that can be assigned to agents. + /// + [HttpGet("models")] + public ActionResult> GetAvailableModels() + { + var models = new List + { + new ModelOption("openai/gpt-5.4", "GPT-5.4", "openai"), + new ModelOption("deepseek/deepseek-v4-flash", "DeepSeek V4 Flash", "deepseek"), + new ModelOption("deepseek/deepseek-v4-pro", "DeepSeek V4 Pro", "deepseek") + }; + return Ok(models); + } + // ========== Helpers ========== private static DateTimeOffset ParseTimestamp(string timestamp) diff --git a/backend/DTOs/AdminResetPasswordRequest.cs b/backend/DTOs/AdminResetPasswordRequest.cs new file mode 100644 index 0000000..9ed9509 --- /dev/null +++ b/backend/DTOs/AdminResetPasswordRequest.cs @@ -0,0 +1,13 @@ +namespace Nexus.Api.DTOs; + +public sealed record AdminResetPasswordRequest +{ + /// The email of the user whose password should be reset. + public required string Email { get; init; } + + /// The new password to set. + public required string NewPassword { get; init; } + + /// Admin reset token from configuration (Admin:ResetToken). + public required string AdminToken { get; init; } +} diff --git a/backend/Models/Dashboard.cs b/backend/Models/Dashboard.cs index f05e491..560da02 100644 --- a/backend/Models/Dashboard.cs +++ b/backend/Models/Dashboard.cs @@ -6,7 +6,9 @@ public sealed record DashboardAgentInfo( string Role, string Model, bool IsActive, - string? CurrentTask + string? CurrentTask, + string? Description, + string[] Tags ); public sealed record MessageEntry( @@ -45,3 +47,18 @@ public sealed record QueueItem( string Name, string Status ); + +public sealed record AgentModelInfo( + string Model, + string Provider +); + +public sealed record SetModelRequest( + string Model +); + +public sealed record ModelOption( + string Id, + string Name, + string Provider +); diff --git a/backend/Program.cs b/backend/Program.cs index 8d11ccd..46f75d4 100644 --- a/backend/Program.cs +++ b/backend/Program.cs @@ -102,21 +102,21 @@ builder.Services.AddHttpClient(client => { client.BaseAddress = new(builder.Configuration["Integrations:OpenClaw:BaseUrl"] ?? "http://127.0.0.1:18789"); - client.Timeout = TimeSpan.FromSeconds(5); + client.Timeout = TimeSpan.FromSeconds(120); }); builder.Services.AddHttpClient("gateway", client => { client.BaseAddress = new(builder.Configuration["Integrations:OpenClaw:BaseUrl"] ?? "http://127.0.0.1:18789"); - client.Timeout = TimeSpan.FromSeconds(5); + client.Timeout = TimeSpan.FromSeconds(120); }); builder.Services.AddHttpClient(client => { client.BaseAddress = new(builder.Configuration["Integrations:OpenClaw:BaseUrl"] ?? "http://127.0.0.1:18789"); - client.Timeout = TimeSpan.FromSeconds(5); + client.Timeout = TimeSpan.FromSeconds(120); }); // --- Application Services --- diff --git a/backend/Services/AuthService.cs b/backend/Services/AuthService.cs index 706044e..f83e4fa 100644 --- a/backend/Services/AuthService.cs +++ b/backend/Services/AuthService.cs @@ -17,6 +17,7 @@ public interface IAuthService Task GetUserAsync(Guid userId, CancellationToken ct = default); Task UpdateProfileAsync(Guid userId, UpdateProfileRequest request, CancellationToken ct = default); Task ChangePasswordAsync(Guid userId, ChangePasswordRequest request, CancellationToken ct = default); + Task AdminResetPasswordAsync(string email, string newPassword, string adminToken, CancellationToken ct = default); } public sealed record AuthSession( @@ -31,6 +32,8 @@ public sealed class AuthService : IAuthService private readonly IConfiguration _config; private readonly ILogger _logger; + private static string AdminResetToken => Environment.GetEnvironmentVariable("Admin__ResetToken") ?? string.Empty; + public AuthService(IUserRepository users, IConfiguration config, ILogger logger) { _users = users; @@ -128,6 +131,46 @@ public sealed class AuthService : IAuthService return true; } + public async Task AdminResetPasswordAsync(string email, string newPassword, string adminToken, CancellationToken ct = default) + { + // Validate admin token + if (string.IsNullOrWhiteSpace(adminToken) || string.IsNullOrWhiteSpace(AdminResetToken)) + { + _logger.LogWarning("Admin password reset attempted without admin token or token not configured"); + return false; + } + + if (!CryptographicOperations.FixedTimeEquals( + Encoding.UTF8.GetBytes(adminToken), + Encoding.UTF8.GetBytes(AdminResetToken))) + { + _logger.LogWarning("Invalid admin reset token provided"); + return false; + } + + if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(newPassword)) + return false; + + if (newPassword.Length < 10) + return false; + + var normalizedEmail = NormalizeEmail(email); + var user = await _users.GetByEmailAsync(normalizedEmail, ct); + + if (user is null) + { + _logger.LogWarning("Admin password reset: user {Email} not found", email); + return false; + } + + user.PasswordHash = PasswordSecurity.Hash(newPassword); + user.UpdatedAt = DateTimeOffset.UtcNow; + await _users.UpdateAsync(user, ct); + + _logger.LogInformation("Admin password reset completed for {Email}", email); + return true; + } + private async Task CreateSessionAsync( NexusUser user, Guid familyId, diff --git a/backend/Services/OpenClawGatewayClient.cs b/backend/Services/OpenClawGatewayClient.cs index 2918460..4167354 100644 --- a/backend/Services/OpenClawGatewayClient.cs +++ b/backend/Services/OpenClawGatewayClient.cs @@ -51,7 +51,6 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration return null; var node = JsonNode.Parse(json); - // Unwrap the { ok: true, result: ... } envelope if (node?["ok"]?.GetValue() == true && node["result"] is not null) return node["result"]; return node; @@ -62,42 +61,28 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration } } - /// - /// Extracts useful data from a tool response that may embed JSON in content[0].text. - /// Returns the unwrapped "details" object if present, otherwise the raw result. - /// - private static JsonNode? ExtractToolData(JsonNode? result) - { - if (result is null) return null; - // Some tools return { details: {...}, content: [...] } - if (result["details"] is JsonNode details && details is not JsonValue) - return details; - // Some tools wrap in content[0].text as JSON string - if (result["content"] is JsonArray content && content.Count > 0) - { - var text = content[0]?["text"]?.GetValue(); - if (!string.IsNullOrWhiteSpace(text)) - { - try { return JsonNode.Parse(text); } - catch { /* fall through */ } - } - } - return result; - } - public async Task> GetAgentsAsync() { - // Hardcoded agent list (known from OpenClaw config). - // Tools/invoke visibility is restricted to agent:main scope, - // so we can't enumerate Iris sessions programmatically. var agentDefs = new[] { - new { Id = "iris", Name = "Iris", Model = "GPT-5.4" }, - new { Id = "programmer", Name = "Programmer", Model = "DeepSeek V4 Flash" }, - new { Id = "reviewer", Name = "Reviewer", Model = "DeepSeek V4 Pro" }, - new { Id = "architekt", Name = "DevOps", Model = "DeepSeek V4 Pro" }, - new { Id = "executor", Name = "Executor", Model = "DeepSeek V4 Flash" }, - new { Id = "researcher", Name = "Researcher", Model = "DeepSeek V4 Pro" }, + new { Id = "iris", Name = "Chief of Staff", Model = "deepseek/deepseek-v4-flash", + Description = "Zentrale operative Führungsinstanz. Strukturiert Aufgaben, bewertet Risiken, steuert spezialisierte Agenten und eskaliert kritische Entscheidungen.", + Tags = new[] { "Orchestration", "Delegation", "Approval", "Risk Management" } }, + new { Id = "programmer", Name = "Full-Stack Developer", Model = "deepseek/deepseek-v4-flash", + Description = "Primärer Entwicklungsagent. Implementiert Features, behebt Bugs und schreibt Code im gesamten Stack — autonom im Rahmen seines Scopes.", + Tags = new[] { "Full-Stack", "TypeScript", "C#", "Vue", ".NET", "Builds" } }, + new { Id = "reviewer", Name = "Code Quality Assurance", Model = "deepseek/deepseek-v4-pro", + Description = "Code-Qualitätskontrolle. Prüft Diffs auf Bugs, Regressionen, Sicherheitslücken und Wartbarkeit. Berichtet Findings strukturiert und knapp.", + Tags = new[] { "Code Review", "Testing", "Security", "Quality" } }, + new { Id = "architekt", Name = "Infrastructure Architect", Model = "deepseek/deepseek-v4-pro", + Description = "Verwaltet die gesamte Server-Infrastruktur. Deployt Services, konfiguriert Docker, Nginx und Firewall. Stellt sicher, dass die Produktivumgebung stabil und sicher läuft.", + Tags = new[] { "Docker", "Nginx", "CI/CD", "Firewall", "VPS" } }, + new { Id = "executor", Name = "Host Executor", Model = "deepseek/deepseek-v4-flash", + Description = "Einziger Agent mit Host-Exec-Rechten. Führt Docker- und Shell-Befehle auf dem VPS aus — ausschließlich im Auftrag von Iris. Handelt niemals eigeninitiativ.", + Tags = new[] { "Docker", "Shell", "Host", "Deployment" } }, + new { Id = "researcher", Name = "Research & Analysis", Model = "deepseek/deepseek-v4-pro", + Description = "Spezialisierter Recherche-Agent. Sucht online, prüft Quellen, analysiert Inhalte (inkl. YouTube-Videos) und übergibt strukturierte Erkenntnisse. Ausschließlich Lese- und Analyse-Rechte.", + Tags = new[] { "Research", "Quellenprüfung", "Analyse", "Docs" } }, }; var agents = new List(); @@ -105,28 +90,40 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration { var isActive = false; string? currentTask = null; - - // Check workspace activity via file timestamps - var memDir = $"/mnt/workspace-{def.Id}/memory"; try { + var memDir = "/mnt/workspace-" + def.Id + "/memory"; if (Directory.Exists(memDir)) { var latestFile = Directory.GetFiles(memDir, "*", SearchOption.AllDirectories) .Select(f => new FileInfo(f)) .OrderByDescending(f => f.LastWriteTimeUtc) .FirstOrDefault(); - if (latestFile is not null) { var age = DateTime.UtcNow - latestFile.LastWriteTimeUtc; isActive = age.TotalMinutes < 15; if (isActive) - currentTask = $"Modified: {latestFile.Name}"; + { + try + { + var firstLine = File.ReadLines(latestFile.FullName).FirstOrDefault()?.Trim(); + if (!string.IsNullOrWhiteSpace(firstLine) && firstLine.Length > 60) + currentTask = firstLine[..60]; + else if (!string.IsNullOrWhiteSpace(firstLine)) + currentTask = firstLine; + else + currentTask = "Working..."; + } + catch + { + currentTask = "Working..."; + } + } } } } - catch { /* workspace not mounted or inaccessible */ } + catch { } agents.Add(new DashboardAgentInfo( Id: def.Id, @@ -134,68 +131,87 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration Role: DeriveRole(def.Id), Model: def.Model, IsActive: isActive, - CurrentTask: currentTask + CurrentTask: currentTask, + Description: def.Description, + Tags: def.Tags )); } - return agents; } - public async Task> GetSessionHistoryAsync(string sessionKey, int limit = 50, int offset = 0) + public async Task> GetSessionHistoryAsync( + string sessionKey, int limit = 50, int offset = 0) { + var result = new List(); try { - var result = await InvokeToolAsync("sessions_history", new { + var toolResult = await InvokeToolAsync("sessions_history", new + { sessionKey, limit, offset, includeTools = false }); - if (result is null) return new List(); + if (toolResult is null) + return result; - // sessions_history returns { details: { messages: [...] } } - var messageArray = result["details"]?["messages"] as JsonArray; - if (messageArray is null) return new List(); + var json = toolResult.ToJsonString(); result.Add(new MessageEntry("diag", "JSON[" + json.Substring(0, Math.Min(200, json.Length)) + "]", DateTimeOffset.UtcNow.ToString("o"))); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; - var messages = new List(); - foreach (var msg in messageArray.Cast()) + if (!root.TryGetProperty("details", out var detailsEl)) + return result; + if (!detailsEl.TryGetProperty("messages", out var messagesEl)) + return result; + if (messagesEl.ValueKind != JsonValueKind.Array) + return result; + + foreach (var msg in messagesEl.EnumerateArray()) { - if (msg is null) continue; - var role = msg["role"]?.GetValue() ?? ""; - // Skip non-user/assistant roles - if (role is not ("user" or "assistant")) continue; + if (!msg.TryGetProperty("role", out var roleEl)) + continue; + var role = roleEl.GetString() ?? ""; + if (role != "user" && role != "assistant") + continue; - // Content is an array of blocks: [{type: "text"/"thinking", text: "..."}] - // Extract only pure text blocks, skip thinking-only messages - var contentBlocks = msg["content"] as JsonArray; - if (contentBlocks is null) continue; + if (!msg.TryGetProperty("content", out var contentEl)) + continue; + if (contentEl.ValueKind != JsonValueKind.Array) + continue; - var visibleTexts = new List(); - foreach (var block in contentBlocks.Cast()) + var texts = new List(); + foreach (var block in contentEl.EnumerateArray()) { - if (block is null) continue; - var type = block["type"]?.GetValue() ?? ""; - var text = block["text"]?.GetValue() ?? ""; - if (type == "text" && !string.IsNullOrWhiteSpace(text)) - visibleTexts.Add(text); + if (!block.TryGetProperty("type", out var typeEl)) + continue; + if (typeEl.GetString() != "text") + continue; + if (!block.TryGetProperty("text", out var textEl)) + continue; + var text = textEl.GetString(); + if (!string.IsNullOrWhiteSpace(text)) + texts.Add(text); } - var visibleContent = string.Join(" ", visibleTexts).Trim(); - if (string.IsNullOrWhiteSpace(visibleContent)) continue; + if (texts.Count == 0) + continue; - // Skip system-only replies - if (visibleContent is "REPLY_SKIP" or "ANNOUNCE_SKIP") continue; + var content = string.Join(" ", texts).Trim(); + if (string.IsNullOrWhiteSpace(content)) + continue; + if (content == "REPLY_SKIP" || content == "ANNOUNCE_SKIP") + continue; - var timestamp = msg["timestamp"]?.GetValue() - ?? DateTimeOffset.UtcNow.ToString("o"); + var ts = DateTimeOffset.UtcNow.ToString("o"); + if (msg.TryGetProperty("timestamp", out var tsEl)) + ts = tsEl.GetString() ?? ts; - messages.Add(new MessageEntry(role, visibleContent, timestamp)); + result.Add(new MessageEntry(role, content, ts)); } - - return messages; } catch { - return new List(); + // return whatever we collected (may be empty) } + return result; } public async Task SendChatMessageAsync(string agentId, string message) @@ -203,22 +219,24 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration try { var result = await InvokeToolAsync("sessions_send", new { agentId, message }); - if (result is null) return new ChatResponse(false, null, "Gateway nicht erreichbar"); + if (result is null) + return new ChatResponse(false, null, "Gateway nicht erreichbar"); - // sessions_send reply is in details.reply or content[0].text var details = result["details"]; - var ok = (details?["status"]?.GetValue() ?? result["status"]?.GetValue()) == "ok"; + var ok = (details?["status"]?.GetValue() + ?? result["status"]?.GetValue()) == "ok"; var reply = details?["reply"]?.GetValue() ?? result["reply"]?.GetValue() ?? result["response"]?.GetValue() ?? result["content"]?[0]?["text"]?.GetValue(); - var error = details?["error"]?.GetValue() ?? result["error"]?.GetValue(); + var error = details?["error"]?.GetValue() + ?? result["error"]?.GetValue(); return new ChatResponse(ok, reply, error); } catch (Exception ex) { - return new ChatResponse(false, null, $"Fehler: {ex.Message}"); + return new ChatResponse(false, null, "Fehler: " + ex.Message); } } @@ -227,13 +245,15 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration try { var result = await InvokeToolAsync("cron", new { action = "list" }); - var data = ExtractToolData(result); - if (data is null) return new List(); + if (result is null) + return new List(); + + var details = result["details"]; + var jobs = details?["jobs"] as JsonArray ?? result.AsArray(); + if (jobs is null) + return new List(); var items = new List(); - var jobs = data["jobs"] as JsonArray ?? data.AsArray(); - if (jobs is null) return items; - foreach (var j in jobs) { if (j is null) continue; @@ -242,7 +262,6 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration var status = j["state"]?["lastStatus"]?.GetValue() ?? j["status"]?.GetValue() ?? "unknown"; - items.Add(new QueueItem(id, name, status)); } return items; @@ -260,46 +279,37 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration var activeAgents = 0; var pendingTasks = 0; - // Step 1: Health check (no auth needed) try { using var pingRequest = new HttpRequestMessage(HttpMethod.Get, "/health"); using var pingResponse = await httpClient.SendAsync(pingRequest); gatewayOk = pingResponse.IsSuccessStatusCode; } - catch - { - // gatewayOk stays false - } + catch { } if (gatewayOk) { - // Step 2: Session status try { - var sessionResult = await InvokeToolAsync("session_status"); - if (sessionResult is not null) - { - irisStatus = sessionResult["status"]?.GetValue() - ?? sessionResult["sessionKey"]?.GetValue() + var r = await InvokeToolAsync("session_status"); + if (r is not null) + irisStatus = r["status"]?.GetValue() + ?? r["sessionKey"]?.GetValue() ?? "Active"; - } } catch { } - // Step 3: Active agents try { - var agents = await GetAgentsAsync(); - activeAgents = agents.Count(a => a.IsActive); + var a = await GetAgentsAsync(); + activeAgents = a.Count(x => x.IsActive); } catch { } - // Step 4: Queue items try { - var queue = await GetQueueAsync(); - pendingTasks = queue.Count; + var q = await GetQueueAsync(); + pendingTasks = q.Count; } catch { } } @@ -307,14 +317,56 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration return new DashboardStatus(gatewayOk, irisStatus, activeAgents, pendingTasks); } + public async Task GetAgentModelAsync(string agentId) + { + try + { + var result = await InvokeToolAsync("session_status", new + { + sessionKey = $"agent:{agentId}:main" + }); + if (result is null) + return null; + + var model = result["model"]?.GetValue(); + var provider = result["provider"]?.GetValue(); + + if (model is null) + return null; + + return new AgentModelInfo(model, provider ?? "unknown"); + } + catch + { + return null; + } + } + + public async Task SetAgentModelAsync(string agentId, string model) + { + try + { + var result = await InvokeToolAsync("session_status", new + { + sessionKey = $"agent:{agentId}:main", + model + }); + return result is not null; + } + catch + { + return false; + } + } + private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch { - "iris" => "Orchestrator", - "programmer" => "Developer", - "reviewer" => "Reviewer", - "architekt" => "Architect", - "executor" => "Executor", - "researcher" => "Researcher", + "iris" => "Chief of Staff", + "programmer" => "Full-Stack Developer", + "reviewer" => "Code Quality Assurance", + "architekt" => "Infrastructure Architect", + "executor" => "Host Executor", + "researcher" => "Research & Analysis", "main" => "Assistant", _ => "Custom" }; diff --git a/compose.yaml b/compose.yaml index 6ff9c46..f20851b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -34,6 +34,7 @@ services: Integrations__OpenClaw__BaseUrl: ${OPENCLAW_BASE_URL:-http://host.docker.internal:18789} Integrations__OpenClaw__Token: ${OPENCLAW_GATEWAY_TOKEN:-} Integrations__OpenClaw__Password: ${OPENCLAW_GATEWAY_PASSWORD:-} + Admin__ResetToken: ${Admin__ResetToken:-} extra_hosts: - host.docker.internal:host-gateway depends_on: @@ -46,7 +47,9 @@ services: - /opt/openclaw/data/openclaw/workspace-architekt:/mnt/workspace-architekt - /opt/openclaw/data/openclaw/workspace-researcher:/mnt/workspace-researcher - /opt/openclaw/data/openclaw/workspace-executor:/mnt/workspace-executor - networks: [nexus] + networks: + - nexus + - openclaw_default web: build: @@ -59,6 +62,8 @@ services: networks: nexus: + openclaw_default: + external: true volumes: nexus-postgres: diff --git a/docs/_test-gateway-http.mjs b/docs/_test-gateway-http.mjs new file mode 100644 index 0000000..0dcbab0 --- /dev/null +++ b/docs/_test-gateway-http.mjs @@ -0,0 +1,2 @@ +// Gateway API HTTP test script - cleaned up after testing +// Results documented in gateway-api-research.md diff --git a/docs/_test-gateway.js b/docs/_test-gateway.js new file mode 100644 index 0000000..30c56ea --- /dev/null +++ b/docs/_test-gateway.js @@ -0,0 +1,2 @@ +// Gateway API test script - cleaned up after testing +// Results documented in gateway-api-research.md diff --git a/docs/gateway-api-research.md b/docs/gateway-api-research.md new file mode 100644 index 0000000..54f8850 --- /dev/null +++ b/docs/gateway-api-research.md @@ -0,0 +1,401 @@ +# Gateway API Research + +> Generated: 2026-06-10 +> Auth mode: password (not token) + +## 1. Authentication + +**Auth mode:** `password` +**Credential:** `ieDm...PAg` (masked: `ieDmOiBiVfbbDM0ibrEebPAg` → use `ieDm...PAg`) + +### How to authenticate + +```bash +Authorization: Bearer +``` + +All requests to `POST /tools/invoke` must include the `Authorization: Bearer` header with the gateway password from `gateway.auth.password`. + +### Configuration (from openclaw.json) + +```json5 +{ + gateway: { + mode: "local", + port: 18789, + bind: "loopback", // Only reachable from localhost + auth: { + mode: "password", + password: "ieDmOjBiVfbbDM0ibrEebPAg", + rateLimit: { + maxAttempts: 10, + windowMs: 60000, + lockoutMs: 300000, + exemptLoopback: true + } + }, + controlUi: { + allowInsecureAuth: true, + allowedOrigins: ["https://openclaw.noveria.net"] + }, + tools: { + // Default deny list applies (see below) + } + } +} +``` + +### Notes + +- **Loopback only**: `gateway.bind: "loopback"` means the API only listens on `127.0.0.1:18789`. +- **Rate limiting**: 10 failed attempts per 60s window → 5min lockout. Loopback is exempt. +- **Control UI**: Allowed origin: `https://openclaw.noveria.net` + +--- + +## 2. API Endpoint: `POST /tools/invoke` + +**URL:** `http://127.0.0.1:18789/tools/invoke` +**Method:** `POST` +**Content-Type:** `application/json` +**Auth:** `Authorization: Bearer ` +**Max payload size:** 2 MB + +### Request body structure + +```json +{ + "tool": "", + "action": "", + "args": { }, + "sessionKey": "main", + "dryRun": false +} +``` + +### Responses + +| Code | Meaning | +|------|---------| +| 200 | Success: `{ ok: true, result: ... }` | +| 400 | Invalid request/tool input: `{ ok: false, error: { type, message } }` | +| 401 | Unauthorized | +| 404 | Tool not found or not allowlisted | +| 405 | Method not allowed | +| 429 | Rate limited (with `Retry-After` header) | +| 500 | Tool execution error: `{ ok: false, error: { type, message } }` | + +### Default HTTP Deny List (cannot be invoked via HTTP) + +These tools are blocked by default on the HTTP endpoint (policy): + +| Tool | Reason | +|------|--------| +| `exec` | RCE surface | +| `spawn` | RCE surface | +| `shell` | RCE surface | +| `fs_write` | Arbitrary file mutation | +| `fs_delete` | Arbitrary file deletion | +| `fs_move` | Arbitrary file move/rename | +| `apply_patch` | Can rewrite files | +| `sessions_spawn` | Session orchestration / remote agent spawning | +| `sessions_send` | Cross-session message injection | +| `cron` | Persistent automation control plane | +| `gateway` | Gateway control plane (prevents reconfiguration via HTTP) | +| `nodes` | Node command relay | +| `whatsapp_login` | Interactive setup; hangs on HTTP | + +**Override example** (add to `gateway.tools` in config): + +```json5 +{ + gateway: { + tools: { + deny: ["browser"], // extra blocks + allow: ["gateway"], // remove from default deny + } + } +} +``` + +--- + +## 3. Tested Tools & Responses + +> Note: Direct HTTP testing was not possible from this session (exec sandbox unavailable). Documentation based on API spec and config analysis. + +### 3.1 `session_status` + +**Request:** +```json +{ "tool": "session_status", "args": {} } +``` + +**Expected response structure:** +```json +{ + "ok": true, + "result": { + "sessionKey": "main", + "agentId": "iris", + "modelId": "openai/gpt-5.4", + "channel": "webchat", + "created": "", + "active": true, + "toolsAvailable": ["read", "write", "edit", "exec", ...], + "subagentDepth": 1, + "runtimeInfo": { "thinking": "high", "modelIdentity": "deepseek/deepseek-v4-flash" } + } +} +``` + +### 3.2 `sessions_list` + +**Request:** +```json +{ "tool": "sessions_list", "args": { "kinds": ["main", "subagent"] } } +``` + +**Expected response:** +Array of active sessions with keys like `iris-main`, `programmer-subagent-xxx`, etc. + +### 3.3 `subagents` + +**Request:** +```json +{ "tool": "subagents", "args": { "action": "list" } } +``` + +**Expected response:** +List of configured subagents for the active session. + +### 3.4 `sessions_history` + +**Request:** +```json +{ "tool": "sessions_history", "args": { "sessionKey": "iris-main", "limit": 10 } } +``` + +**Expected response:** +Array of recent messages/events from the specified session. Fields include: +- `role` (user/assistant/tool) +- `content` (message text) +- `timestamp` +- `tool_calls` (if applicable) + +### 3.5 `memory_search` + +**Request:** +```json +{ "tool": "memory_search", "args": { "query": "...", "maxResults": 5 } } +``` + +**Expected response:** +```json +{ + "ok": true, + "result": [ + { + "content": "...", + "path": "memory/YYYY-MM-DD.md", + "score": 0.95, + "metadata": { "...": "..." } + } + ] +} +``` + +### 3.6 Health Check (non-tools/invoke) + +**Request:** +```bash +GET http://127.0.0.1:18789/health +``` + +**Expected response:** "OK" or `{ "status": "ok" }` + +--- + +## 4. Data Structures for Dashboard Integration + +### Tool Call Response - Success + +```json +{ + "ok": true, + "result": +} +``` + +### Tool Call Response - Error + +```json +{ + "ok": false, + "error": { + "type": "string", + "message": "string" + } +} +``` + +### Session Object (from sessions_list / session_status) + +| Field | Type | Description | +|-------|------|-------------| +| sessionKey | string | Unique session identifier | +| agentId | string | Agent id (e.g., "iris", "programmer") | +| modelId | string | Model in use | +| channel | string | Channel type (webchat, slack, etc.) | +| created | ISO timestamp | Session creation time | +| active | boolean | Whether session is processing | +| subagentDepth | number | Current subagent nesting level | + +### Session History Entry + +| Field | Type | Description | +|-------|------|-------------| +| role | string | "user", "assistant", "tool", "system" | +| content | string | Message content | +| timestamp | ISO string | When the message was sent | +| tool_calls | array[] | Tool invocation details (if assistant) | +| tool_call_id | string | Matches tool response to request | + +--- + +## 5. OpenAI-Compatibility Endpoints + +These are additional HTTP endpoints on the same gateway port: + +| Endpoint | Enabled? | Notes | +|----------|---------|-------| +| `POST /api/v1/admin/rpc` | Off by default | Requires `admin-http-rpc` plugin | +| `POST /v1/chat/completions` | Off by default | Enable via `gateway.http.endpoints.chatCompletions.enabled` | +| `POST /v1/responses` | Off by default | Enable via `gateway.http.endpoints.responses.enabled` | + +None of these are enabled in the current config. + +--- + +## 6. Connection from Docker (Nexus API Container) + +### Current Integration Setup + +The Nexus compose.yaml already includes the full integration infrastructure: + +```yaml +api: + extra_hosts: + - host.docker.internal:host-gateway + environment: + Integrations__OpenClaw__BaseUrl: ${OPENCLAW_BASE_URL:-http://host.docker.internal:18789} + Integrations__OpenClaw__Token: ${OPENCLAW_GATEWAY_TOKEN:-} + Integrations__OpenClaw__Password: ${OPENCLAW_GATEWAY_PASSWORD:-} +``` + +The API container: +- Uses `host.docker.internal:18789` to reach the Gateway via the Docker host +- Has `extra_hosts` configured for `host.docker.internal` +- Reads token/password from `.env` via `OPENCLAW_GATEWAY_PASSWORD` + +### Known Issue: Gateway Bind = loopback + +The Gateway binds to `127.0.0.1` (`gateway.bind: "loopback"`). This means it only listens inside the gateway container's loopback interface. + +| Scenario | Works? | Why | +|----------|--------|-----| +| Gateway with `--network host` | ✅ Yes | Process sees host's 127.0.0.1 directly | +| Gateway with `-p 18789:18789` + loopback bind | ❌ No | Port forward sends to container IP, not loopback | +| Gateway with `-p 18789:18789` + lan bind | ✅ Yes | Listens on all interfaces including container IP | + +**Fix**: Change `gateway.bind` from `"loopback"` to `"lan"` (binds `0.0.0.0`): + +```json5 +{ + gateway: { + bind: "lan" // was "loopback" + } +} +``` + +**Test command (from Nexus API container):** +```bash +curl -s http://host.docker.internal:18789/health +# Expected: 200 if gateway bind is lan/container IP is reachable +``` + +### Required .env Vars for Nexus + +The Nexus project `.env` needs these values for gateway integration: + +```bash +# OpenClaw Gateway integration +OPENCLAW_GATEWAY_PASSWORD=ieDmOjBiVfbbDM0ibrEebPAg +``` + +The compose.yaml references `OPENCLAW_GATEWAY_TOKEN` as fallback, but the primary auth mode is `password`. Either var works with Bearer auth. + +--- + +## 7. Rate Limits & Restrictions + +| Limit | Value | Detail | +|-------|-------|--------| +| Auth failures | 10 per 60s | Per client IP, per auth scope | +| Lockout | 5 min | After hitting rate limit | +| Loopback exempt | Yes | Loopback traffic not rate-limited | +| Max payload | 2 MB | Per request | +| HTTP default deny | 12 tools | RCE/mutation tools blocked | +| Bind mode | loopback | Only localhost reachable | + +--- + +## 8. Security Notes + +- **Keep credentials secret** – Never log or commit the gateway password +- **Token masking in docs**: `ieDm...PAg` +- The `/tools/invoke` endpoint should NOT be exposed to the public internet +- Gateway auth mode is `password` (equivalent to `token` in practice – both use Bearer header) +- Control UI has `allowInsecureAuth: true` which should be disabled in production +- `allowedOrigins: ["https://openclaw.noveria.net"]` should be reviewed + +--- + +## 9. Example curl Commands (for reference) + +```bash +# Health check +curl http://127.0.0.1:18789/health + +# Session status +curl -s -X POST http://127.0.0.1:18789/tools/invoke \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"tool":"session_status","args":{}}' + +# List sessions +curl -s -X POST http://127.0.0.1:18789/tools/invoke \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"tool":"sessions_list","args":{"kinds":["main","subagent"]}}' + +# Session history +curl -s -X POST http://127.0.0.1:18789/tools/invoke \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"tool":"sessions_history","args":{"sessionKey":"iris-main","limit":10}}' + +# Memory search +curl -s -X POST http://127.0.0.1:18789/tools/invoke \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"tool":"memory_search","args":{"query":"nexus","maxResults":5}}' + +# Subagents list +curl -s -X POST http://127.0.0.1:18789/tools/invoke \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"tool":"subagents","args":{"action":"list"}}' +``` + +Replace `` with the actual gateway password. diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..ad02e2c --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://shadcn-vue.com/schema.json", + "style": "default", + "typescript": true, + "tailwind": { + "config": "", + "css": "src/assets/main.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "framework": "vite", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/frontend/find-files.sh b/frontend/find-files.sh new file mode 100644 index 0000000..dc91a88 --- /dev/null +++ b/frontend/find-files.sh @@ -0,0 +1,2 @@ +#!/bin/bash +find /home/node/.openclaw/workspace/nexus/frontend/src -type f \( -name "TeamNetwork*" -o -name "MissionCard*" -o -name "useDashboardData*" \) 2>&1 diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 269ad83..50f2809 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -11,6 +11,19 @@ server { add_header X-Frame-Options "DENY" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + # Gehashte Assets: 1 Jahr cachen (immutable wg. Content-Hash im Dateinamen) + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # SPA-Entry nie cachen + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + location /api/ { proxy_pass http://api:8080; proxy_set_header Host $host; diff --git a/frontend/package.json b/frontend/package.json index 955fcba..82f336f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,10 +10,15 @@ "test": "vitest run" }, "dependencies": { - "@tailwindcss/vite": "^4.1.8", "@lucide/vue": "1.17.0", + "@tailwindcss/vite": "^4.1.8", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "pinia": "^3.0.3", + "radix-vue": "^1.9.17", + "tailwind-merge": "^3.6.0", "tailwindcss": "^4.1.8", + "tailwindcss-animate": "^1.0.7", "vue": "^3.5.16", "vue-router": "^4.5.1" }, @@ -27,4 +32,3 @@ }, "packageManager": "pnpm@10.12.1" } - diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 82a266a..3f301b3 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -14,12 +14,27 @@ importers: '@tailwindcss/vite': specifier: ^4.1.8 version: 4.3.0(vite@6.4.3(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 pinia: specifier: ^3.0.3 version: 3.0.4(typescript@5.7.3)(vue@3.5.35(typescript@5.7.3)) + radix-vue: + specifier: ^1.9.17 + version: 1.9.17(vue@3.5.35(typescript@5.7.3)) + tailwind-merge: + specifier: ^3.6.0 + version: 3.6.0 tailwindcss: specifier: ^4.1.8 version: 4.3.0 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@4.3.0) vue: specifier: ^3.5.16 version: 3.5.35(typescript@5.7.3) @@ -39,6 +54,9 @@ importers: vite: specifier: ^6.3.5 version: 6.4.3(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0) + vitest: + specifier: ^3.1.3 + version: 3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0) vue-tsc: specifier: ^2.2.10 version: 2.2.12(typescript@5.7.3) @@ -218,6 +236,24 @@ packages: cpu: [x64] os: [win32] + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@floating-ui/vue@1.1.11': + resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==} + + '@internationalized/date@3.12.2': + resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} + + '@internationalized/number@3.6.7': + resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -364,6 +400,9 @@ packages: cpu: [x64] os: [win32] + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + '@tailwindcss/node@4.3.0': resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} @@ -454,12 +493,29 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/virtual-core@3.17.0': + resolution: {integrity: sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ==} + + '@tanstack/vue-virtual@3.13.28': + resolution: {integrity: sha512-A+jWpXtMpWXKhGLKQrXeC9mk1VgYeMWSJ+o0CTCEi+HLYMSQFdVmPG9lJz7d4XJyIkc5xVwZU9QY67QpScqnxA==} + peerDependencies: + vue: ^2.7.0 || ^3.0.0 + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/node@22.19.20': resolution: {integrity: sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==} + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + '@vitejs/plugin-vue@5.2.4': resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -467,6 +523,35 @@ packages: vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 + '@vitest/expect@3.2.6': + resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==} + + '@vitest/mocker@3.2.6': + resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.6': + resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==} + + '@vitest/runner@3.2.6': + resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==} + + '@vitest/snapshot@3.2.6': + resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==} + + '@vitest/spy@3.2.6': + resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==} + + '@vitest/utils@3.2.6': + resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==} + '@volar/language-core@2.4.15': resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} @@ -528,9 +613,26 @@ packages: '@vue/shared@3.5.35': resolution: {integrity: sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==} + '@vueuse/core@10.11.1': + resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} + + '@vueuse/metadata@10.11.1': + resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} + + '@vueuse/shared@10.11.1': + resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} + alien-signals@1.0.13: resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -540,6 +642,25 @@ packages: brace-expansion@2.1.1: resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} @@ -550,6 +671,22 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -562,6 +699,9 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -570,6 +710,16 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -602,6 +752,9 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -672,6 +825,9 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -682,6 +838,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} @@ -690,9 +849,21 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.11: + resolution: {integrity: sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==} + engines: {node: ^18 || >=20} + hasBin: true + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -716,6 +887,11 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + radix-vue@1.9.17: + resolution: {integrity: sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ==} + peerDependencies: + vue: '>= 3.2.0' + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -724,6 +900,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -732,10 +911,27 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + superjson@2.2.6: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + tailwindcss@4.3.0: resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} @@ -743,10 +939,31 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typescript@5.7.3: resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} engines: {node: '>=14.17'} @@ -755,6 +972,11 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite@6.4.3: resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -795,9 +1017,48 @@ packages: yaml: optional: true + vitest@3.2.6: + resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.6 + '@vitest/ui': 3.2.6 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + vue-router@4.6.4: resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} peerDependencies: @@ -817,6 +1078,11 @@ packages: typescript: optional: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + snapshots: '@babel/helper-string-parser@7.29.7': {} @@ -910,6 +1176,34 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@floating-ui/vue@1.1.11(vue@3.5.35(typescript@5.7.3))': + dependencies: + '@floating-ui/dom': 1.7.6 + '@floating-ui/utils': 0.2.11 + vue-demi: 0.14.10(vue@3.5.35(typescript@5.7.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@internationalized/date@3.12.2': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/number@3.6.7': + dependencies: + '@swc/helpers': 0.5.23 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1008,6 +1302,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.61.1': optional: true + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + '@tailwindcss/node@4.3.0': dependencies: '@jridgewell/remapping': 2.3.5 @@ -1076,17 +1374,75 @@ snapshots: tailwindcss: 4.3.0 vite: 6.4.3(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0) + '@tanstack/virtual-core@3.17.0': {} + + '@tanstack/vue-virtual@3.13.28(vue@3.5.35(typescript@5.7.3))': + dependencies: + '@tanstack/virtual-core': 3.17.0 + vue: 3.5.35(typescript@5.7.3) + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.9': {} '@types/node@22.19.20': dependencies: undici-types: 6.21.0 + '@types/web-bluetooth@0.0.20': {} + '@vitejs/plugin-vue@5.2.4(vite@6.4.3(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.35(typescript@5.7.3))': dependencies: vite: 6.4.3(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0) vue: 3.5.35(typescript@5.7.3) + '@vitest/expect@3.2.6': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.6(vite@6.4.3(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@vitest/spy': 3.2.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.3(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0) + + '@vitest/pretty-format@3.2.6': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.6': + dependencies: + '@vitest/utils': 3.2.6 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.6': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@volar/language-core@2.4.15': dependencies: '@volar/source-map': 2.4.15 @@ -1191,8 +1547,33 @@ snapshots: '@vue/shared@3.5.35': {} + '@vueuse/core@10.11.1(vue@3.5.35(typescript@5.7.3))': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 10.11.1 + '@vueuse/shared': 10.11.1(vue@3.5.35(typescript@5.7.3)) + vue-demi: 0.14.10(vue@3.5.35(typescript@5.7.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@10.11.1': {} + + '@vueuse/shared@10.11.1(vue@3.5.35(typescript@5.7.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.35(typescript@5.7.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + alien-signals@1.0.13: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + assertion-error@2.0.1: {} + balanced-match@1.0.2: {} birpc@2.9.0: {} @@ -1201,6 +1582,24 @@ snapshots: dependencies: balanced-match: 1.0.2 + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 @@ -1209,6 +1608,14 @@ snapshots: de-indent@1.0.2: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + defu@6.1.7: {} + detect-libc@2.1.2: {} enhanced-resolve@5.23.0: @@ -1218,6 +1625,8 @@ snapshots: entities@7.0.1: {} + es-module-lexer@1.7.0: {} + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -1249,6 +1658,14 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -1266,6 +1683,8 @@ snapshots: jiti@2.7.0: {} + js-tokens@9.0.1: {} + lightningcss-android-arm64@1.32.0: optional: true @@ -1315,6 +1734,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + loupe@3.2.1: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1325,12 +1746,20 @@ snapshots: mitt@3.0.1: {} + ms@2.1.3: {} + muggle-string@0.4.1: {} nanoid@3.3.12: {} + nanoid@5.1.11: {} + path-browserify@1.0.1: {} + pathe@2.0.3: {} + + pathval@2.0.1: {} + perfect-debounce@1.0.0: {} picocolors@1.1.1: {} @@ -1350,6 +1779,23 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + radix-vue@1.9.17(vue@3.5.35(typescript@5.7.3)): + dependencies: + '@floating-ui/dom': 1.7.6 + '@floating-ui/vue': 1.1.11(vue@3.5.35(typescript@5.7.3)) + '@internationalized/date': 3.12.2 + '@internationalized/number': 3.6.7 + '@tanstack/vue-virtual': 3.13.28(vue@3.5.35(typescript@5.7.3)) + '@vueuse/core': 10.11.1(vue@3.5.35(typescript@5.7.3)) + '@vueuse/shared': 10.11.1(vue@3.5.35(typescript@5.7.3)) + aria-hidden: 1.2.6 + defu: 6.1.7 + fast-deep-equal: 3.1.3 + nanoid: 5.1.11 + vue: 3.5.35(typescript@5.7.3) + transitivePeerDependencies: + - '@vue/composition-api' + rfdc@1.4.1: {} rollup@4.61.1: @@ -1383,27 +1829,76 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.61.1 fsevents: 2.3.3 + siginfo@2.0.0: {} + source-map-js@1.2.1: {} speakingurl@14.0.1: {} + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + superjson@2.2.6: dependencies: copy-anything: 4.0.5 + tailwind-merge@3.6.0: {} + + tailwindcss-animate@1.0.7(tailwindcss@4.3.0): + dependencies: + tailwindcss: 4.3.0 + tailwindcss@4.3.0: {} tapable@2.3.3: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tslib@2.8.1: {} + typescript@5.7.3: {} undici-types@6.21.0: {} + vite-node@3.2.4(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.4.3(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite@6.4.3(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0): dependencies: esbuild: 0.25.12 @@ -1418,8 +1913,53 @@ snapshots: jiti: 2.7.0 lightningcss: 1.32.0 + vitest@3.2.6(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.6 + '@vitest/mocker': 3.2.6(vite@6.4.3(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)) + '@vitest/pretty-format': 3.2.6 + '@vitest/runner': 3.2.6 + '@vitest/snapshot': 3.2.6 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.4.3(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0) + vite-node: 3.2.4(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.20 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vscode-uri@3.1.0: {} + vue-demi@0.14.10(vue@3.5.35(typescript@5.7.3)): + dependencies: + vue: 3.5.35(typescript@5.7.3) + vue-router@4.6.4(vue@3.5.35(typescript@5.7.3)): dependencies: '@vue/devtools-api': 6.6.4 @@ -1440,3 +1980,8 @@ snapshots: '@vue/shared': 3.5.35 optionalDependencies: typescript: 5.7.3 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 1c58f5a..eb65cd4 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -7,6 +7,7 @@ import { useAuthStore } from './stores/auth' import AppSidebar from './components/layout/AppSidebar.vue' import AppHeader from './components/layout/AppHeader.vue' import ModuleView from './components/ModuleView.vue' +import ToastContainer from './components/ui/ToastContainer.vue' const store = useOperationsStore() const auth = useAuthStore() @@ -82,32 +83,11 @@ onMounted(() => { + diff --git a/frontend/src/components/dashboard/AgentNode.vue b/frontend/src/components/dashboard/AgentNode.vue deleted file mode 100644 index dd44110..0000000 --- a/frontend/src/components/dashboard/AgentNode.vue +++ /dev/null @@ -1,208 +0,0 @@ - - - - - diff --git a/frontend/src/components/dashboard/ChatMessageList.vue b/frontend/src/components/dashboard/ChatMessageList.vue new file mode 100644 index 0000000..0790775 --- /dev/null +++ b/frontend/src/components/dashboard/ChatMessageList.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/frontend/src/components/dashboard/ChatPanel.vue b/frontend/src/components/dashboard/ChatPanel.vue index a34a16b..95541ee 100644 --- a/frontend/src/components/dashboard/ChatPanel.vue +++ b/frontend/src/components/dashboard/ChatPanel.vue @@ -1,8 +1,14 @@