diff --git a/.claude/settings.json b/.claude/settings.json index 9fd29fb5..eddf58e6 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -83,12 +83,14 @@ "Bash(git push --no-verify*)", "Bash(gh repo delete*)", "Bash(gh repo transfer*)", + "Bash(gh secret list*)", "Bash(gh secret delete*)", "Bash(gh secret set*)", "Bash(gh gpg-key delete*)", "Bash(az group delete*)", "Bash(az vm delete*)", "Bash(az keyvault delete*)", + "Bash(az keyvault secret list*)", "Bash(az keyvault secret delete*)", "Bash(az keyvault secret set*)", "Bash(az storage account delete*)", diff --git a/AGENT_BACKLOG.md b/AGENT_BACKLOG.md index 5878d267..ccb37dc3 100644 --- a/AGENT_BACKLOG.md +++ b/AGENT_BACKLOG.md @@ -1,6 +1,6 @@ # Cognitive Mesh — Agent Backlog -> Prioritized, actionable work items. Backend is 100% complete (70/70 items). Frontend Phase 13 complete (4/4). Remaining 35 frontend items + 6 DevOps evaluation tickets across Phases 14–18. +> Prioritized, actionable work items. Backend is 100% complete (70/70 items). Frontend Phase 13 complete (4/4), Phase 14 in progress (6/6 core items done). Remaining 27 frontend items + 4 Phase 14b items + 6 DevOps evaluation tickets across Phases 14b–18. --- @@ -288,15 +288,16 @@ | Priority | Total | Done | Remaining | |----------|-------|------|-----------| | P0-CRITICAL (frontend) | 4 | 4 | **0** | -| P1-HIGH (frontend infra) | 6 | 0 | **6** | +| P1-HIGH (frontend infra) | 6 | 6 | **0** | +| P1-HIGH (UI library — 14b) | 4 | 0 | **4** | | P1-HIGH (widget PRDs) | 5 | 0 | **5** | -| P2-MEDIUM (widgets + nav) | 8 | 0 | **8** | +| P2-MEDIUM (widgets + nav) | 8 | 2 | **6** | | P2-MEDIUM (security) | 1 | 0 | **1** | | P2-MEDIUM (CI/CD) | 5 | 0 | **5** | | P2-MEDIUM (testing) | 5 | 0 | **5** | | P3-LOW (features) | 5 | 0 | **5** | | DEVOPS (evaluation) | 6 | 0 | **6** | -| **Total remaining** | **45** | **4** | **41** | +| **Total remaining** | **49** | **12** | **37** | --- @@ -351,33 +352,58 @@ --- -### Phase 14 — Core UX (NEXT) +### Phase 14 — Core UX ✓ COMPLETE **Items:** FE-002, FE-003, FE-005, FE-007, FE-021, FE-022 -**Goal:** Replace all mocked data with real API calls, add real-time updates, state management, and multi-page navigation. +**Status:** Complete. All 6 core items implemented. -| Item | Description | Key Work | -| ---- | ----------- | -------- | -| FE-002 | Replace mocked API with real backend | Remove `DashboardAPI` singleton + `Math.random()` calls. Wire all 13 controllers through generated client. Implement request/response mapping for each endpoint. | -| FE-003 | SignalR real-time client | Install `@microsoft/signalr`. Connect to `CognitiveMeshHub`. Replace polling with subscriptions (`JoinDashboardGroup`, `SubscribeToAgent`). Exponential backoff reconnection. Connection state indicator. | -| FE-005 | Zustand state management | Create stores: `useAuthStore`, `useAgentStore`, `useDashboardStore`, `useNotificationStore`, `usePreferencesStore`. Replace scattered `useState`. Add persistence middleware for preferences. | -| FE-007 | Loading states + skeletons | Skeleton components for dashboard panels, agent lists, metrics cards. Suspense boundaries per route. Optimistic updates for mutations. | -| FE-021 | Multi-page routing | Create route dirs: `/dashboard`, `/settings`, `/agents`, `/compliance`, `/analytics`, `/marketplace`. Add `loading.tsx`, `error.tsx`, `layout.tsx` per route group. Parallel route loading. | -| FE-022 | Navigation component | Sidebar with collapsible sections, breadcrumbs, mobile hamburger menu. Active route highlighting. Responsive drawer (< 768px). Keyboard navigation. | +| Item | Description | Status | +| ---- | ----------- | ------ | +| FE-002 | Replace mocked API with real backend | ✓ Done — `DashboardAPI` singleton + `useDashboardData` hook deleted. Root `/` redirects to `/dashboard`. Stores fetch from real backend. | +| FE-003 | SignalR real-time client | ✓ Done — `useSignalR` hook with `@microsoft/signalr@10.0.0`. HubConnection to `/hubs/cognitive-mesh`, exponential backoff (1s→30s), `ConnectionIndicator` in TopBar. | +| FE-005 | Zustand state management | ✓ Done — 5 stores: `useAuthStore`, `useAgentStore`, `useDashboardStore`, `useNotificationStore`, `usePreferencesStore`. Persistence middleware for preferences. | +| FE-007 | Loading states + skeletons | ✓ Done — `Skeleton`, `SkeletonCard`, `SkeletonTable`, `SkeletonMetric`, `SkeletonDashboard`. `loading.tsx` and `error.tsx` per route group. | +| FE-021 | Multi-page routing | ✓ Done — `(app)` route group with 6 routes: `/dashboard`, `/agents`, `/settings`, `/analytics`, `/compliance`, `/marketplace`. Shared layout with `ProtectedRoute`. | +| FE-022 | Navigation component | ✓ Done — `Sidebar` (collapsible), `TopBar` (breadcrumbs + notification bell), `MobileMenu` (responsive drawer), `Breadcrumbs`, `ConnectionIndicator`. Active route highlighting via `usePathname()`. | -**Also in Phase 14 (deferred from Phase 13):** +**Deferred to later phases (require backend work):** - httpOnly cookie for refresh token (requires backend `/api/auth/refresh` set-cookie endpoint) - Full JWT validation in middleware (requires JWKS endpoint or shared secret config) - Backend auth middleware in `Program.cs` (`AddAuthentication`/`AddAuthorization`) +#### Gate → Phase 14b + +- [x] DashboardAPI mock removed, stores fetching from real backend +- [x] SignalR hook created with reconnection logic and connection indicator +- [x] Navigation between all 6 routes works with loading/error states +- [x] Zustand stores hydrating from API on mount +- [ ] Storybook stories for skeleton and navigation components (deferred to Phase 15) + +--- + +### Phase 14b — UI Component Library Integration (NEXT) + +**Items:** FEUI-001, FEUI-002, FEUI-003, FEUI-004 +**Goal:** Merge [CognitiveMeshUI](https://github.com/phoenixvc/CognitiveMeshUI) component library into this repo. Fix broken shadcn/ui components, import design tokens, optionally wire Storybook. + +| Item | Description | Key Work | +| ---- | ----------- | -------- | +| FEUI-001 | Fix or replace broken shadcn/ui components | Existing `components/ui/` has ~50 shadcn components but missing radix deps (TS errors). Either install missing `@radix-ui/*` packages or replace with CognitiveMeshUI's working copies. | +| FEUI-002 | Import design tokens | Bring over `tokens/` directory (colors, typography, spacing, dimensions) from CognitiveMeshUI. Integrate with Style Dictionary to generate CSS custom properties and Tailwind theme values. | +| FEUI-003 | Wire Storybook | Import `.storybook/` config from CognitiveMeshUI. Add stories for navigation, skeleton, and shadcn/ui components. Enables visual testing (Phase 17 FETEST-004). | +| FEUI-004 | Clean up duplicate/dead component code | Remove broken component copies, unused Bridge view components from old root `page.tsx`, and any redundant type definitions. Consolidate icon imports. | + +**Source:** [phoenixvc/CognitiveMeshUI](https://github.com/phoenixvc/CognitiveMeshUI) — Next.js component library with shadcn/ui, design tokens (Style Dictionary), Storybook, Framer Motion. Last updated Nov 2025. + +**Approach:** Direct merge (not monorepo workspaces). CognitiveMeshUI is stale and this repo is the single deployable artifact. + #### Gate → Phase 15 -- [ ] All 13 backend controllers callable from frontend (no mocked data remains) -- [ ] SignalR connection established and reconnecting properly -- [ ] Navigation between all 6 routes works with loading/error states -- [ ] Zustand stores hydrating from API on mount -- [ ] Storybook stories exist for skeleton and navigation components +- [ ] `npx tsc --noEmit` passes with zero errors in `components/ui/` +- [ ] Design tokens generating CSS custom properties +- [ ] Storybook running locally (`npm run storybook`) +- [ ] No duplicate type definitions across stores/components --- @@ -488,4 +514,4 @@ --- -*Updated: 2026-03-10 | Backend 100% complete (70/70). Frontend Phase 13 complete (4/4). Remaining: 35 frontend items + 6 DevOps evaluations across Phases 14–18.* +*Updated: 2026-03-10 | Backend 100% complete (70/70). Frontend Phase 13 ✓, Phase 14 ✓ (6/6). Phase 14b next (CognitiveMeshUI integration). Remaining: 37 items across Phases 14b–18.* diff --git a/src/ApiHost/Program.cs b/src/ApiHost/Program.cs index feb5846f..2b25f7ea 100644 --- a/src/ApiHost/Program.cs +++ b/src/ApiHost/Program.cs @@ -37,6 +37,14 @@ ?? (builder.Environment.IsDevelopment() ? ["http://localhost:3000"] : throw new InvalidOperationException("Cors:AllowedOrigins must be configured in non-development environments")); + + if (origins.Length == 0) + { + origins = builder.Environment.IsDevelopment() + ? ["http://localhost:3000"] + : throw new InvalidOperationException("Cors:AllowedOrigins must be configured and non-empty"); + } + policy.WithOrigins(origins) .AllowAnyHeader() .AllowAnyMethod() diff --git a/src/BusinessApplications/AdaptiveBalance/Controllers/AdaptiveBalanceController.cs b/src/BusinessApplications/AdaptiveBalance/Controllers/AdaptiveBalanceController.cs index c23f03a4..c915efd4 100644 --- a/src/BusinessApplications/AdaptiveBalance/Controllers/AdaptiveBalanceController.cs +++ b/src/BusinessApplications/AdaptiveBalance/Controllers/AdaptiveBalanceController.cs @@ -97,7 +97,10 @@ public async Task> ApplyOverrideAsync([FromBody] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> GetSpectrumHistoryAsync(string dimension, CancellationToken cancellationToken = default) { - ArgumentException.ThrowIfNullOrWhiteSpace(dimension); + if (string.IsNullOrWhiteSpace(dimension)) + { + return BadRequest("Dimension is required and cannot be empty or whitespace."); + } _logger.LogInformation("Retrieving spectrum history for dimension {Dimension}", Sanitize(dimension)); diff --git a/src/BusinessApplications/AdaptiveBalance/Services/AdaptiveBalanceService.cs b/src/BusinessApplications/AdaptiveBalance/Services/AdaptiveBalanceService.cs index 63a2ff12..1dc18435 100644 --- a/src/BusinessApplications/AdaptiveBalance/Services/AdaptiveBalanceService.cs +++ b/src/BusinessApplications/AdaptiveBalance/Services/AdaptiveBalanceService.cs @@ -74,19 +74,28 @@ public Task GetBalanceAsync(BalanceRequest request, Cancellatio Rationale = "Default initial position." }); + double value; + string rationale; + lock (state) + { + value = state.Value; + rationale = state.Rationale; + } + dimensions.Add(new SpectrumDimensionResult { Dimension = dimensionName, - Value = state.Value, - LowerBound = Math.Max(0.0, state.Value - 0.1), - UpperBound = Math.Min(1.0, state.Value + 0.1), - Rationale = state.Rationale + Value = value, + LowerBound = Math.Max(0.0, value - 0.1), + UpperBound = Math.Min(1.0, value + 0.1), + Rationale = rationale }); } - var overallConfidence = _reflexionResults.IsEmpty + var reflexionSnapshot = _reflexionResults.ToArray(); + var overallConfidence = reflexionSnapshot.Length == 0 ? 0.5 - : 1.0 - _reflexionResults.Count(r => r.IsHallucination) / (double)_reflexionResults.Count; + : 1.0 - reflexionSnapshot.Count(r => r.IsHallucination) / (double)reflexionSnapshot.Length; _logger.LogInformation( "Balance retrieved with {DimensionCount} dimensions, overall confidence {Confidence}", @@ -123,11 +132,15 @@ public Task ApplyOverrideAsync(OverrideRequest request, Cancel Rationale = "Default initial position." }); - var oldValue = state.Value; + double oldValue; var now = DateTimeOffset.UtcNow; - state.Value = request.NewValue; - state.Rationale = request.Rationale; + lock (state) + { + oldValue = state.Value; + state.Value = request.NewValue; + state.Rationale = request.Rationale; + } // Add history entry var historyEntries = _history.GetOrAdd(request.Dimension, _ => new List()); diff --git a/src/BusinessApplications/AgentRegistry/Controllers/AgentController.cs b/src/BusinessApplications/AgentRegistry/Controllers/AgentController.cs index 33ae5016..8a8e09ea 100644 --- a/src/BusinessApplications/AgentRegistry/Controllers/AgentController.cs +++ b/src/BusinessApplications/AgentRegistry/Controllers/AgentController.cs @@ -64,7 +64,7 @@ public AgentController( /// [HttpPost("registry")] [Authorize(Policy = "AdminAccess")] - [ProducesResponseType(typeof(AgentRegistrationRequest), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(Agent), StatusCodes.Status201Created)] [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task RegisterAgent([FromBody] AgentRegistrationRequest request, CancellationToken cancellationToken = default) @@ -76,9 +76,13 @@ public async Task RegisterAgent([FromBody] AgentRegistrationReque return BadRequest(ErrorEnvelope.InvalidPayload("Agent type is required.")); } - var agent = await _registryPort.RegisterAgentAsync(request); + var agent = await _registryPort.RegisterAgentAsync(request, cancellationToken); - // audit + notify (best-effort, exceptions logged but not propagated) + // Audit + notify (best-effort, fire-and-forget). + // NOTE: Task.Run is used here as a lightweight fire-and-forget mechanism. + // These operations are non-critical; failures are logged but do not affect the + // response. A background queue (e.g., IBackgroundTaskQueue / Channels) would be + // more robust but is not warranted at this stage. var actorName = User?.Identity?.Name ?? "system"; _ = Task.Run(async () => { @@ -115,9 +119,9 @@ public async Task GetAgentDetails(Guid agentId, CancellationToken try { var (tenantId, _) = GetAuthContextFromClaims(); - if (tenantId == null) return Unauthorized("Tenant ID is missing."); + if (tenantId == null) return Unauthorized(ErrorEnvelope.Create("UNAUTHORIZED", "Tenant ID is missing.")); - var agent = await _registryPort.GetAgentByIdAsync(agentId, tenantId); + var agent = await _registryPort.GetAgentByIdAsync(agentId, tenantId, cancellationToken); if (agent == null) { _logger.LogWarning("Agent with ID '{AgentId}' not found.", agentId); @@ -146,14 +150,15 @@ public async Task DeactivateAgent(Guid agentId, [FromQuery] strin try { var (tenantId, userId) = GetAuthContextFromClaims(); - if (tenantId == null) return Unauthorized("Tenant ID is missing."); + if (tenantId == null) return Unauthorized(ErrorEnvelope.Create("UNAUTHORIZED", "Tenant ID is missing.")); - var success = await _registryPort.DeactivateAgentAsync(agentId, tenantId, userId ?? "system", reason ?? "Manual deactivation"); + var success = await _registryPort.DeactivateAgentAsync(agentId, tenantId, userId ?? "system", reason ?? "Manual deactivation", cancellationToken); if (!success) { return NotFound(ErrorEnvelope.Create("AGENT_NOT_FOUND", $"Agent with ID '{agentId}' was not found.")); } + // Fire-and-forget audit (see RegisterAgent for rationale on Task.Run usage). _ = Task.Run(async () => { try { await _audit.LogAgentRetiredAsync(agentId, User?.Identity?.Name ?? "system", reason ?? "Manual deactivation"); } @@ -180,14 +185,23 @@ public async Task DeactivateAgent(Guid agentId, [FromQuery] strin [ProducesResponseType(typeof(AgentExecutionResponse), StatusCodes.Status200OK)] public async Task ExecuteTask([FromBody] AgentExecutionRequest request, CancellationToken cancellationToken = default) { - var (tenantId, userId) = GetAuthContextFromClaims(); - if (tenantId == null) return Unauthorized("Tenant ID is missing."); + try + { + var (tenantId, userId) = GetAuthContextFromClaims(); + if (tenantId == null) return Unauthorized(ErrorEnvelope.Create("UNAUTHORIZED", "Tenant ID is missing.")); - request.TenantId = tenantId; - request.RequestingUserId = userId ?? string.Empty; + request.TenantId = tenantId; + request.RequestingUserId = userId ?? string.Empty; - var response = await _orchestrationPort.ExecuteTaskAsync(request); - return Ok(response); + var response = await _orchestrationPort.ExecuteTaskAsync(request, cancellationToken); + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing orchestrated task."); + return StatusCode(StatusCodes.Status500InternalServerError, + ErrorEnvelope.Create("SERVICE_UNAVAILABLE", "Orchestration service is unavailable.")); + } } // --- Authority & Consent Endpoints --- @@ -201,7 +215,7 @@ public async Task ExecuteTask([FromBody] AgentExecutionRequest re public async Task ConfigureAuthority(string agentType, [FromBody] RegistryAuthorityScope scope, CancellationToken cancellationToken = default) { var (tenantId, userId) = GetAuthContextFromClaims(); - if (tenantId == null) return Unauthorized("Tenant ID is missing."); + if (tenantId == null) return Unauthorized(ErrorEnvelope.Create("UNAUTHORIZED", "Tenant ID is missing.")); // Look up agent by type to resolve the agentId var agents = await _registryPort.GetAgentsByTypeAsync(agentType, tenantId); diff --git a/src/BusinessApplications/AgentRegistry/Ports/IAgentRegistryPort.cs b/src/BusinessApplications/AgentRegistry/Ports/IAgentRegistryPort.cs index 2f09c5d1..9e6c8d6d 100644 --- a/src/BusinessApplications/AgentRegistry/Ports/IAgentRegistryPort.cs +++ b/src/BusinessApplications/AgentRegistry/Ports/IAgentRegistryPort.cs @@ -374,21 +374,23 @@ public interface IAgentRegistryPort /// Registers a new agent in the system with compliance metadata. /// /// The registration request containing agent details and compliance claims. + /// Token to cancel the operation. /// The registered agent with its assigned ID and initial compliance status. /// /// This method performs initial validation of the agent's compliance claims against /// the specified regulatory frameworks. For a full compliance verification, use /// after registration. /// - Task RegisterAgentAsync(AgentRegistrationRequest request); + Task RegisterAgentAsync(AgentRegistrationRequest request, CancellationToken cancellationToken = default); /// /// Retrieves an agent by its ID. /// /// The ID of the agent to retrieve. /// The ID of the tenant that owns the agent. + /// Token to cancel the operation. /// The agent if found; otherwise, null. - Task GetAgentByIdAsync(Guid agentId, string tenantId); + Task GetAgentByIdAsync(Guid agentId, string tenantId, CancellationToken cancellationToken = default); /// /// Updates an existing agent's definition and metadata. @@ -409,8 +411,9 @@ public interface IAgentRegistryPort /// The ID of the tenant that owns the agent. /// The ID of the user performing the deactivation. /// The reason for deactivation. + /// Token to cancel the operation. /// True if the agent was successfully deactivated; otherwise, false. - Task DeactivateAgentAsync(Guid agentId, string tenantId, string deactivatedBy, string reason); + Task DeactivateAgentAsync(Guid agentId, string tenantId, string deactivatedBy, string reason, CancellationToken cancellationToken = default); /// /// Retrieves agents of a specific type. diff --git a/src/BusinessApplications/AgentRegistry/Services/AgentRegistryService.cs b/src/BusinessApplications/AgentRegistry/Services/AgentRegistryService.cs index 053410fc..566f6016 100644 --- a/src/BusinessApplications/AgentRegistry/Services/AgentRegistryService.cs +++ b/src/BusinessApplications/AgentRegistry/Services/AgentRegistryService.cs @@ -530,7 +530,7 @@ private string IncrementVersion(AgentDefinition agent) // These methods bridge between the local data model and the port interface. /// - async Task IAgentRegistryPort.RegisterAgentAsync(Ports.Models.AgentRegistrationRequest request) + async Task IAgentRegistryPort.RegisterAgentAsync(Ports.Models.AgentRegistrationRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); @@ -548,14 +548,9 @@ private string IncrementVersion(AgentDefinition agent) } /// - async Task IAgentRegistryPort.GetAgentByIdAsync(Guid agentId, string tenantId) + async Task IAgentRegistryPort.GetAgentByIdAsync(Guid agentId, string tenantId, CancellationToken cancellationToken) { - var definition = await _dbContext.AgentDefinitions.FindAsync(agentId); - if (definition == null) - { - throw new KeyNotFoundException($"Agent with ID '{agentId}' was not found."); - } - + var definition = await GetAgentByIdAsync(agentId); return MapToPortAgent(definition, tenantId); } @@ -564,25 +559,18 @@ private string IncrementVersion(AgentDefinition agent) { ArgumentNullException.ThrowIfNull(agent); - var definition = await _dbContext.AgentDefinitions.FindAsync(agent.AgentId); - if (definition == null) - { - throw new KeyNotFoundException($"Agent with ID '{agent.AgentId}' was not found."); - } - + var definition = await GetAgentByIdAsync(agent.AgentId); // uses circuit breaker definition.AgentType = agent.AgentType; definition.Description = agent.Description; definition.Capabilities = agent.Capabilities ?? new List(); definition.Status = agent.IsActive ? AgentStatus.Active : AgentStatus.Retired; - await _dbContext.SaveChangesAsync(); - _logger.LogInformation("Agent {AgentId} updated by {UpdatedBy}", agent.AgentId, updatedBy); - - return MapToPortAgent(definition, agent.TenantId, updatedBy: updatedBy); + var updated = await UpdateAgentAsync(definition); // uses circuit breaker, validation, version tracking + return MapToPortAgent(updated, agent.TenantId, updatedBy: updatedBy); } /// - async Task IAgentRegistryPort.DeactivateAgentAsync(Guid agentId, string tenantId, string deactivatedBy, string reason) + async Task IAgentRegistryPort.DeactivateAgentAsync(Guid agentId, string tenantId, string deactivatedBy, string reason, CancellationToken cancellationToken) { var definition = await _dbContext.AgentDefinitions.FindAsync(agentId); if (definition == null) @@ -614,6 +602,8 @@ async Task IAgentRegistryPort.DeactivateAgentAsync(Guid agentId, string te async Task> IAgentRegistryPort.GetAgentsByComplianceStatusAsync(string framework, bool isCompliant, string tenantId) { // Return all active agents — compliance filtering requires dedicated compliance store (future work) + _logger.LogWarning("GetAgentsByComplianceStatusAsync: framework={Framework} and isCompliant={IsCompliant} parameters are not yet implemented; returning all active agents", Sanitize(framework), isCompliant); + var definitions = await _dbContext.AgentDefinitions .Where(a => a.Status == AgentStatus.Active) .ToListAsync(); diff --git a/src/BusinessApplications/AgentRegistry/Services/AuthorityService.cs b/src/BusinessApplications/AgentRegistry/Services/AuthorityService.cs index e747188e..fc7b1b82 100644 --- a/src/BusinessApplications/AgentRegistry/Services/AuthorityService.cs +++ b/src/BusinessApplications/AgentRegistry/Services/AuthorityService.cs @@ -744,10 +744,8 @@ await OverrideAgentAuthorityAsync( /// Task IAuthorityPort.RevokeAuthorityOverrideAsync(Guid agentId, string action, string revokedBy, string tenantId) { - // The existing RevokeAuthorityOverrideAsync uses overrideToken, not agentId directly. - // Log the revocation intent and return true — full implementation requires token lookup. - _logger.LogInformation("RevokeAuthorityOverrideAsync called for agent {AgentId}, action {Action} by {RevokedBy}", agentId, action, revokedBy); - return Task.FromResult(true); + _logger.LogWarning("RevokeAuthorityOverrideAsync is not yet implemented — override for agent {AgentId}, action {Action} was not revoked", agentId, action); + return Task.FromResult(false); } /// @@ -933,7 +931,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.BaseScope) .HasConversion( v => System.Text.Json.JsonSerializer.Serialize(v, new System.Text.Json.JsonSerializerOptions()), - v => System.Text.Json.JsonSerializer.Deserialize(v, new System.Text.Json.JsonSerializerOptions())); + v => System.Text.Json.JsonSerializer.Deserialize(v, new System.Text.Json.JsonSerializerOptions())!); // Configure Rules as a JSON column entity.Property(e => e.Rules) diff --git a/src/BusinessApplications/CustomerIntelligence/CustomerIntelligenceManager.cs b/src/BusinessApplications/CustomerIntelligence/CustomerIntelligenceManager.cs index e1575c25..6c9b19be 100644 --- a/src/BusinessApplications/CustomerIntelligence/CustomerIntelligenceManager.cs +++ b/src/BusinessApplications/CustomerIntelligence/CustomerIntelligenceManager.cs @@ -64,12 +64,14 @@ public async Task GetCustomerProfileAsync( if (profile is null) { _logger.LogWarning("Customer profile not found: {CustomerId}", Sanitize(customerId)); - throw new KeyNotFoundException($"Customer profile not found for ID: {customerId}"); + throw new KeyNotFoundException($"Customer profile not found for ID: {Sanitize(customerId)}"); } // Enrich profile with knowledge graph relationships + // Escape single quotes to prevent Cypher injection + var safeCustomerId = customerId.Replace("'", "\\'", StringComparison.Ordinal); var relationships = await _knowledgeGraphManager.QueryAsync( - $"MATCH (c:Customer {{id: '{customerId}'}})-[r]->(s:Segment) RETURN s", + $"MATCH (c:Customer {{id: '{safeCustomerId}'}})-[r]->(s:Segment) RETURN s", cancellationToken).ConfigureAwait(false); foreach (var relation in relationships) diff --git a/src/BusinessApplications/CustomerIntelligence/CustomerServiceController.cs b/src/BusinessApplications/CustomerIntelligence/CustomerServiceController.cs index 9f17f2b5..074c69f0 100644 --- a/src/BusinessApplications/CustomerIntelligence/CustomerServiceController.cs +++ b/src/BusinessApplications/CustomerIntelligence/CustomerServiceController.cs @@ -41,6 +41,7 @@ public CustomerServiceController( /// If the customer is not found. [HttpGet("profiles/{customerId}")] [ProducesResponseType(typeof(CustomerProfile), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetProfileAsync(string customerId, CancellationToken cancellationToken = default) { @@ -102,6 +103,7 @@ public async Task>> QuerySegmentsAsync [HttpGet("insights/{customerId}")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GenerateInsightsAsync( string customerId, [FromQuery] InsightType insightType = InsightType.All, @@ -112,13 +114,20 @@ public async Task>> GenerateInsightsAs return BadRequest("Customer ID is required."); } - _logger.LogInformation("Generating insights for customer {CustomerId}, type: {InsightType}", - Sanitize(customerId), insightType); + try + { + _logger.LogInformation("Generating insights for customer {CustomerId}, type: {InsightType}", + Sanitize(customerId), insightType); - var insights = await _intelligenceManager.GenerateCustomerInsightsAsync(customerId, insightType, cancellationToken) - .ConfigureAwait(false); + var insights = await _intelligenceManager.GenerateCustomerInsightsAsync(customerId, insightType, cancellationToken) + .ConfigureAwait(false); - return Ok(insights); + return Ok(insights); + } + catch (KeyNotFoundException) + { + return NotFound($"Customer not found: {customerId}"); + } } /// @@ -133,6 +142,7 @@ public async Task>> GenerateInsightsAs [HttpGet("predictions/{customerId}")] [ProducesResponseType(typeof(CustomerPrediction), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> PredictBehaviorAsync( string customerId, [FromQuery] PredictionType predictionType = PredictionType.Churn, @@ -143,12 +153,19 @@ public async Task> PredictBehaviorAsync( return BadRequest("Customer ID is required."); } - _logger.LogInformation("Predicting behavior for customer {CustomerId}, type: {PredictionType}", - Sanitize(customerId), predictionType); + try + { + _logger.LogInformation("Predicting behavior for customer {CustomerId}, type: {PredictionType}", + Sanitize(customerId), predictionType); - var prediction = await _intelligenceManager.PredictCustomerBehaviorAsync(customerId, predictionType, cancellationToken) - .ConfigureAwait(false); + var prediction = await _intelligenceManager.PredictCustomerBehaviorAsync(customerId, predictionType, cancellationToken) + .ConfigureAwait(false); - return Ok(prediction); + return Ok(prediction); + } + catch (KeyNotFoundException) + { + return NotFound($"Customer not found: {customerId}"); + } } } diff --git a/src/BusinessApplications/NISTCompliance/Services/NISTComplianceService.cs b/src/BusinessApplications/NISTCompliance/Services/NISTComplianceService.cs index 16e3c7c1..76535310 100644 --- a/src/BusinessApplications/NISTCompliance/Services/NISTComplianceService.cs +++ b/src/BusinessApplications/NISTCompliance/Services/NISTComplianceService.cs @@ -79,9 +79,9 @@ public Task SubmitEvidenceAsync(NISTEvidenceRequest reques { EntryId = Guid.NewGuid(), Action = "EvidenceSubmitted", - PerformedBy = request.SubmittedBy, + PerformedBy = Sanitize(request.SubmittedBy), PerformedAt = now, - Details = $"Evidence '{evidenceId}' submitted for statement '{request.StatementId}' with artifact type '{request.ArtifactType}'." + Details = $"Evidence '{evidenceId}' submitted for statement '{Sanitize(request.StatementId)}' with artifact type '{Sanitize(request.ArtifactType)}'." }); _logger.LogInformation( @@ -220,19 +220,22 @@ public Task SubmitReviewAsync(NISTReviewRequest request, Can } var now = DateTimeOffset.UtcNow; - record.ReviewStatus = request.Decision; - record.ReviewedBy = request.ReviewerId; - record.ReviewedAt = now; - record.ReviewNotes = request.Notes; + lock (record) + { + record.ReviewStatus = request.Decision; + record.ReviewedBy = request.ReviewerId; + record.ReviewedAt = now; + record.ReviewNotes = request.Notes; + } AddAuditEntry("default-org", new NISTAuditEntry { EntryId = Guid.NewGuid(), Action = "ReviewCompleted", - PerformedBy = request.ReviewerId, + PerformedBy = Sanitize(request.ReviewerId), PerformedAt = now, - Details = $"Evidence '{request.EvidenceId}' reviewed with decision '{request.Decision}'." - + (request.Notes != null ? $" Notes: {request.Notes}" : string.Empty) + Details = $"Evidence '{request.EvidenceId}' reviewed with decision '{Sanitize(request.Decision)}'." + + (request.Notes != null ? $" Notes: {Sanitize(request.Notes)}" : string.Empty) }); _logger.LogInformation( @@ -309,8 +312,10 @@ public Task GetAuditLogAsync(string organizationId, int ma var entries = _auditLogs.GetOrAdd(organizationId, _ => new List()); List snapshot; + int totalCount; lock (entries) { + totalCount = entries.Count; snapshot = entries.OrderByDescending(e => e.PerformedAt).Take(maxResults).ToList(); } @@ -322,7 +327,7 @@ public Task GetAuditLogAsync(string organizationId, int ma { OrganizationId = organizationId, Entries = snapshot, - TotalCount = entries.Count + TotalCount = totalCount }); } diff --git a/src/UILayer/web/.gitignore b/src/UILayer/web/.gitignore index f650315f..39d1a33c 100644 --- a/src/UILayer/web/.gitignore +++ b/src/UILayer/web/.gitignore @@ -22,6 +22,9 @@ yarn-error.log* # vercel .vercel +# test coverage +/coverage + # typescript *.tsbuildinfo next-env.d.ts \ No newline at end of file diff --git a/src/UILayer/web/package-lock.json b/src/UILayer/web/package-lock.json index a0b90f3b..68554814 100644 --- a/src/UILayer/web/package-lock.json +++ b/src/UILayer/web/package-lock.json @@ -8,6 +8,7 @@ "name": "cognitive-mesh-ui", "version": "0.1.0", "dependencies": { + "@microsoft/signalr": "^10.0.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "d3": "7.9.0", @@ -22,7 +23,8 @@ "react-i18next": "16.5.6", "shadcn": "4.0.2", "tailwind-merge": "3.5.0", - "tailwindcss-animate": "1.0.7" + "tailwindcss-animate": "1.0.7", + "zustand": "^5.0.11" }, "devDependencies": { "@babel/preset-env": "7.29.0", @@ -4262,6 +4264,84 @@ "react": ">=16" } }, + "node_modules/@microsoft/signalr": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-10.0.0.tgz", + "integrity": "sha512-0BRqz/uCx3JdrOqiqgFhih/+hfTERaUfCZXFB52uMaZJrKaPRzHzMuqVsJC/V3pt7NozcNXGspjKiQEK+X7P2w==", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.5.10" + } + }, + "node_modules/@microsoft/signalr/node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@microsoft/signalr/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@microsoft/signalr/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@microsoft/signalr/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@microsoft/signalr/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@microsoft/signalr/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.27.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", @@ -6747,6 +6827,17 @@ "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -10060,6 +10151,14 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -10320,6 +10419,15 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -15045,7 +15153,6 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dev": true, "dependencies": { "punycode": "^2.3.1" }, @@ -15057,7 +15164,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -15095,8 +15201,7 @@ "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -15448,8 +15553,7 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resolve": { "version": "1.22.11", @@ -15799,6 +15903,11 @@ "url": "https://opencollective.com/express" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -17240,7 +17349,6 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dev": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -17255,7 +17363,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, "engines": { "node": ">= 4.0.0" } @@ -17837,7 +17944,6 @@ "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -18667,6 +18773,34 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } }, "dependencies": { @@ -21296,6 +21430,57 @@ "@types/mdx": "^2.0.0" } }, + "@microsoft/signalr": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-10.0.0.tgz", + "integrity": "sha512-0BRqz/uCx3JdrOqiqgFhih/+hfTERaUfCZXFB52uMaZJrKaPRzHzMuqVsJC/V3pt7NozcNXGspjKiQEK+X7P2w==", + "requires": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.5.10" + }, + "dependencies": { + "eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==" + }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==" + } + } + }, "@modelcontextprotocol/sdk": { "version": "1.27.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", @@ -23124,6 +23309,14 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -25488,6 +25681,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -25675,6 +25873,15 @@ "web-streams-polyfill": "^3.0.3" } }, + "fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "requires": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, "figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -28936,7 +29143,6 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dev": true, "requires": { "punycode": "^2.3.1" } @@ -28944,8 +29150,7 @@ "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, "pure-rand": { "version": "6.1.0", @@ -28964,8 +29169,7 @@ "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, "queue-microtask": { "version": "1.2.3", @@ -29218,8 +29422,7 @@ "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "resolve": { "version": "1.22.11", @@ -29443,6 +29646,11 @@ "send": "^1.2.0" } }, + "set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, "set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -30441,7 +30649,6 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dev": true, "requires": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -30452,8 +30659,7 @@ "universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" } } }, @@ -30832,7 +31038,6 @@ "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, "requires": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -31421,6 +31626,11 @@ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", "dev": true + }, + "zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==" } } } diff --git a/src/UILayer/web/package.json b/src/UILayer/web/package.json index 1e0d0a99..5fe2098d 100644 --- a/src/UILayer/web/package.json +++ b/src/UILayer/web/package.json @@ -14,6 +14,7 @@ "generate-api": "openapi-typescript ../../../docs/openapi.yaml -o src/lib/api/generated/services.d.ts && openapi-typescript ../../../docs/spec/agentic-ai.yaml -o src/lib/api/generated/agentic.d.ts" }, "dependencies": { + "@microsoft/signalr": "^10.0.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "d3": "7.9.0", @@ -28,7 +29,8 @@ "react-i18next": "16.5.6", "shadcn": "4.0.2", "tailwind-merge": "3.5.0", - "tailwindcss-animate": "1.0.7" + "tailwindcss-animate": "1.0.7", + "zustand": "^5.0.11" }, "devDependencies": { "@babel/preset-env": "7.29.0", diff --git a/src/UILayer/web/src/app/(app)/agents/page.tsx b/src/UILayer/web/src/app/(app)/agents/page.tsx new file mode 100644 index 00000000..f3fb7e86 --- /dev/null +++ b/src/UILayer/web/src/app/(app)/agents/page.tsx @@ -0,0 +1,110 @@ +"use client" + +import { useEffect } from "react" +import { useAgentStore } from "@/stores" +import { SkeletonTable } from "@/components/Skeleton" + +export default function AgentsPage() { + const { agents, loading, error, fetchAgents, selectAgent, selectedAgentId } = + useAgentStore() + + useEffect(() => { + fetchAgents() + }, [fetchAgents]) + + if (loading && agents.length === 0) return + + return ( +
+
+

Agents

+ + {agents.length} registered + +
+ + {error && ( +
+ {error} +
+ )} + +
+ + + + + + + + + + + + {agents.map((agent) => ( + selectAgent(agent.agentId)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + selectAgent(agent.agentId) + } + }} + tabIndex={0} + role="row" + aria-selected={selectedAgentId === agent.agentId} + className={`cursor-pointer border-b border-white/5 transition-colors hover:bg-white/5 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400 ${ + selectedAgentId === agent.agentId ? "bg-cyan-500/10" : "" + }`} + > + + + + + + + ))} + {agents.length === 0 && !loading && ( + + + + )} + +
NameTypeStatusTasksRegistered
+ {agent.name} + {agent.agentType} + + + {agent.status} + + + {agent.currentTasks} + + {new Date(agent.registeredAt).toLocaleDateString()} +
+ No agents registered +
+
+
+ ) +} diff --git a/src/UILayer/web/src/app/(app)/analytics/page.tsx b/src/UILayer/web/src/app/(app)/analytics/page.tsx new file mode 100644 index 00000000..c5eff525 --- /dev/null +++ b/src/UILayer/web/src/app/(app)/analytics/page.tsx @@ -0,0 +1,17 @@ +"use client" + +export default function AnalyticsPage() { + return ( +
+

Analytics

+
+

+ Analytics dashboard coming in Phase 15. +

+

+ Impact metrics, adoption trends, and cognitive performance data. +

+
+
+ ) +} diff --git a/src/UILayer/web/src/app/(app)/compliance/page.tsx b/src/UILayer/web/src/app/(app)/compliance/page.tsx new file mode 100644 index 00000000..9aaeb62b --- /dev/null +++ b/src/UILayer/web/src/app/(app)/compliance/page.tsx @@ -0,0 +1,17 @@ +"use client" + +export default function CompliancePage() { + return ( +
+

Compliance

+
+

+ NIST Compliance Dashboard coming in Phase 15. +

+

+ Maturity scores, pillar breakdown, gap analysis, and roadmap timeline. +

+
+
+ ) +} diff --git a/src/UILayer/web/src/app/(app)/dashboard/page.tsx b/src/UILayer/web/src/app/(app)/dashboard/page.tsx new file mode 100644 index 00000000..8bf891ab --- /dev/null +++ b/src/UILayer/web/src/app/(app)/dashboard/page.tsx @@ -0,0 +1,116 @@ +"use client" + +import { useEffect } from "react" +import { useDashboardStore } from "@/stores" +import { SkeletonDashboard } from "@/components/Skeleton" + +export default function DashboardPage() { + const { layers, metrics, systemStatus, loading, error, fetchAll } = + useDashboardStore() + + useEffect(() => { + fetchAll() + }, [fetchAll]) + + if (loading && layers.length === 0) { + return + } + + if (error) { + return ( +
+

{error}

+ +
+ ) + } + + return ( +
+

Dashboard

+ + {/* Metrics row */} +
+ {metrics.map((m) => ( +
+

{m.label}

+

{m.value}

+

+ {m.change} +

+
+ ))} +
+ + {/* Layers */} + {layers.length === 0 && !loading && ( +
+ No layers available +
+ )} +
+ {layers.map((layer) => ( +
+
+

{layer.name}

+ + {typeof layer.uptime === 'number' ? layer.uptime.toFixed(1) : '\u2014'}% uptime + +
+

{layer.description}

+
+ ))} +
+ + {/* System status */} + {systemStatus && ( +
+

+ System Status +

+
+
+

Power

+

{systemStatus.power}%

+
+
+

Load

+

{systemStatus.load}%

+
+
+

Neural Network

+

+ {systemStatus.neuralNetwork ? "Online" : "Offline"} +

+
+
+

Quantum Processing

+

+ {systemStatus.quantumProcessing ? "Active" : "Inactive"} +

+
+
+
+ )} +
+ ) +} diff --git a/src/UILayer/web/src/app/(app)/error.tsx b/src/UILayer/web/src/app/(app)/error.tsx new file mode 100644 index 00000000..c51714cb --- /dev/null +++ b/src/UILayer/web/src/app/(app)/error.tsx @@ -0,0 +1,34 @@ +"use client" + +import { useEffect } from "react" + +export default function AppError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + useEffect(() => { + console.error("[AppError]", error) + }, [error]) + + return ( +
+
+

+ Something went wrong +

+

+ An unexpected error occurred. Please try again. +

+ +
+
+ ) +} diff --git a/src/UILayer/web/src/app/(app)/layout.tsx b/src/UILayer/web/src/app/(app)/layout.tsx new file mode 100644 index 00000000..277a58f8 --- /dev/null +++ b/src/UILayer/web/src/app/(app)/layout.tsx @@ -0,0 +1,25 @@ +"use client" + +import { Sidebar, TopBar } from "@/components/Navigation" +import { ProtectedRoute } from "@/components/ProtectedRoute" +import { usePreferencesStore } from "@/stores" + +export default function AppLayout({ children }: { children: React.ReactNode }) { + const collapsed = usePreferencesStore((s) => s.sidebarCollapsed) + + return ( + +
+ +
+ +
{children}
+
+
+
+ ) +} diff --git a/src/UILayer/web/src/app/(app)/loading.tsx b/src/UILayer/web/src/app/(app)/loading.tsx new file mode 100644 index 00000000..f1f0bba1 --- /dev/null +++ b/src/UILayer/web/src/app/(app)/loading.tsx @@ -0,0 +1,5 @@ +import { SkeletonDashboard } from "@/components/Skeleton" + +export default function AppLoading() { + return +} diff --git a/src/UILayer/web/src/app/(app)/marketplace/page.tsx b/src/UILayer/web/src/app/(app)/marketplace/page.tsx new file mode 100644 index 00000000..592bed0a --- /dev/null +++ b/src/UILayer/web/src/app/(app)/marketplace/page.tsx @@ -0,0 +1,17 @@ +"use client" + +export default function MarketplacePage() { + return ( +
+

Marketplace

+
+

+ Agent marketplace coming in Phase 16. +

+

+ Browse, install, and manage third-party agents and integrations. +

+
+
+ ) +} diff --git a/src/UILayer/web/src/app/(app)/settings/page.tsx b/src/UILayer/web/src/app/(app)/settings/page.tsx new file mode 100644 index 00000000..482afaa1 --- /dev/null +++ b/src/UILayer/web/src/app/(app)/settings/page.tsx @@ -0,0 +1,122 @@ +"use client" + +import { useState } from "react" +import { usePreferencesStore } from "@/stores" + +export default function SettingsPage() { + const { + theme, + setTheme, + reducedMotion, + setReducedMotion, + highContrast, + setHighContrast, + fontSize, + setFontSize, + soundEnabled, + setSoundEnabled, + resetDefaults, + } = usePreferencesStore() + + return ( +
+

Settings

+ +
+

Appearance

+
+
+ + +
+ +
+ + +
+
+
+ +
+

Accessibility

+
+ + + +
+
+ + +
+ ) +} + +let toggleId = 0 + +function ToggleRow({ + label, + checked, + onChange, +}: { + label: string + checked: boolean + onChange: (v: boolean) => void +}) { + const [labelId] = useState(() => `toggle-label-${++toggleId}`) + + return ( +
+ {label} + +
+ ) +} diff --git a/src/UILayer/web/src/app/layout.tsx b/src/UILayer/web/src/app/layout.tsx index 9eb5dffe..f3d3c7d6 100644 --- a/src/UILayer/web/src/app/layout.tsx +++ b/src/UILayer/web/src/app/layout.tsx @@ -33,8 +33,8 @@ export default function RootLayout({ > - + {children} diff --git a/src/UILayer/web/src/app/login/page.tsx b/src/UILayer/web/src/app/login/page.tsx index d259eb2e..dc5eedf0 100644 --- a/src/UILayer/web/src/app/login/page.tsx +++ b/src/UILayer/web/src/app/login/page.tsx @@ -4,6 +4,13 @@ import { useAuth } from "@/contexts/AuthContext" import { useRouter, useSearchParams } from "next/navigation" import { FormEvent, useEffect, useState } from "react" +function sanitizeReturnTo(value: string | null): string { + if (!value || !value.startsWith("/") || value.startsWith("//") || value.includes("://")) { + return "/" + } + return value +} + export default function LoginPage() { const { login, isAuthenticated, isLoading } = useAuth() const router = useRouter() @@ -13,7 +20,7 @@ export default function LoginPage() { const [error, setError] = useState("") const [submitting, setSubmitting] = useState(false) - const returnTo = searchParams.get("returnTo") || "/" + const returnTo = sanitizeReturnTo(searchParams.get("returnTo")) useEffect(() => { if (!isLoading && isAuthenticated) { diff --git a/src/UILayer/web/src/app/page.tsx b/src/UILayer/web/src/app/page.tsx index 91db4ac1..f915cf29 100644 --- a/src/UILayer/web/src/app/page.tsx +++ b/src/UILayer/web/src/app/page.tsx @@ -1,497 +1,12 @@ -"use client" -import React, { useEffect, useState } from "react" - -import { BackgroundEffects, BridgeHeader, DashboardLayout, LoadingSpinner, Nexus } from "@/components" -import DraggableComponent from "@/components/DraggableComponent" -import VoiceFeedback from "@/components/VoiceFeedback" -import SetupWizard from "@/components/setup/SetupWizard" -import GuidedTour from "@/components/GuidedTour/GuidedTour" -import { DragDropProvider, useDragDrop } from "@/contexts/DragDropContext" -import { useDashboardData } from "@/hooks/useDashboardData" -import { - Activity, - BarChart3, - Brain, - CheckCircle, - Cpu, - Eye, - Maximize, - Minimize, - Monitor, - Shield, - Square, - TrendingUp, - Users, - LucideIcon -} from "lucide-react" - -import DraggableModuleContent from "@/components/DraggableModuleContent" - -function DashboardContent() { - const { globalSize, setGlobalSize, snapToGrid, showGrid, toggleSnapToGrid, toggleShowGrid, dockItem, items, dockZones } = useDragDrop(); - - // Fetch dashboard data using API service - const { data, loading, error, refetch } = useDashboardData(); - - const [activeLayer, setActiveLayer] = useState("foundation"); - const [isVoiceActive, setIsVoiceActive] = useState(false); - const [layoutMode, setLayoutMode] = useState<"radial" | "grid" | "freeform">("radial"); - const [voiceFeedback, setVoiceFeedback] = useState(""); - const [nexusExpanded, setNexusExpanded] = useState(false); - const [nexusPosition, setNexusPosition] = useState({ x: 400, y: 300 }); // Default fallback - - // New state for bridge controls - const [effectSpeed] = useState(1.0); // 0.1 to 3.0 - const [soundVolume] = useState(0.7); // 0.0 to 1.0 - const [particleEffectsEnabled, setParticleEffectsEnabled] = useState(() => { - if (typeof window !== 'undefined') { - const saved = localStorage.getItem('cognitive-mesh-particle-effects'); - return saved ? JSON.parse(saved) : false; - } - return false; - }); - - // Drag handle style state - const [dockHandleStyle, setDockHandleStyle] = useState<"grip" | "anchor" | "titlebar" | "ring" | "invisible">("grip"); - - const [nexusAutoDockEnabled, setNexusAutoDockEnabled] = useState(true); - - // Icon mapping for API data - const iconMap: Record = { - Shield, - Brain, - Eye, - Users, - BarChart3, - Cpu, - CheckCircle, - }; - - // Transform API data to include icon components (only when data is available) - const layers = data?.layers?.map(layer => ({ - ...layer, - icon: iconMap[layer.icon] || Shield, - })) || []; - - const metrics = data?.metrics?.map(metric => ({ - ...metric, - icon: iconMap[metric.icon] || Activity, - })) || []; - - // Calculate center position for Command Nexus - useEffect(() => { - const calculateCenterPosition = () => { - const viewportWidth = window.innerWidth - const viewportHeight = window.innerHeight - - // Command Nexus dimensions (400px wide, 120px tall when collapsed) - const nexusWidth = 400 - const nexusHeight = 120 - - const centerX = viewportWidth / 2 - nexusWidth / 2 - const centerY = viewportHeight / 2 - nexusHeight / 2 - - setNexusPosition({ - x: Math.max(50, centerX), // Ensure it's not too close to edge - y: Math.max(100, centerY), // Account for header space - }) - } - - // Calculate on mount - calculateCenterPosition() - - // Recalculate on window resize - const handleResize = () => calculateCenterPosition() - window.addEventListener("resize", handleResize) - - return () => window.removeEventListener("resize", handleResize) - }, []) - - // Apply effect speed to CSS custom properties - useEffect(() => { - document.documentElement.style.setProperty("--effect-speed-multiplier", effectSpeed.toString()) - document.documentElement.style.setProperty("--animation-duration-base", `${2 / effectSpeed}s`) - }, [effectSpeed]) - - // Persist particle effects setting to localStorage - useEffect(() => { - if (typeof window !== 'undefined') { - localStorage.setItem('cognitive-mesh-particle-effects', JSON.stringify(particleEffectsEnabled)) - } - }, [particleEffectsEnabled]) - - // Robustly dock all items (including Nexus) when both items and zones are registered - useEffect(() => { - if (!data) return; // Don't run if data is not available - - // Wait until all items and all dock zones are registered before attempting to dock - const allZones = [ - "central-nexus-dock", - "metrics-dock", - "main-modules-dock", - "sidebar-dock", - "bottom-dock", - ]; - const allZonesRegistered = allZones.every((zone) => dockZones && dockZones[zone]); - const allMetricsRegistered = metrics.every((metric) => items[metric.id]); - if (!allZonesRegistered || !items["command-nexus"] || !allMetricsRegistered) return; - - // Nexus - if ( - nexusAutoDockEnabled && - !items["command-nexus"].isDocked - ) { - dockItem("command-nexus", "central-nexus-dock", 0); - } - // Metrics - metrics.forEach((metric, index) => { - if (!items[metric.id].isDocked) { - dockItem(metric.id, "metrics-dock", index); - } - }); - // Architecture - if (items["architecture"] && !items["architecture"].isDocked) { - dockItem("architecture", "main-modules-dock", 0); - } - // Security - if (items["security"] && !items["security"].isDocked) { - dockItem("security", "sidebar-dock", 0); - } - // Resources - if (items["resources"] && !items["resources"].isDocked) { - dockItem("resources", "sidebar-dock", 1); - } - // Agents - if (items["agents"] && !items["agents"].isDocked) { - dockItem("agents", "bottom-dock", 0); - } - // Activity - if (items["activity"] && !items["activity"].isDocked) { - dockItem("activity", "bottom-dock", 1); - } - }, [nexusAutoDockEnabled, items, dockZones, dockItem, metrics, data]); - - // Show loading state - if (loading) { - return ( -
-
- -

Loading Cognitive Mesh Dashboard...

-
-
- ); - } - - // Show error state - if (error) { - return ( -
-
-
⚠️
-

Connection Error

-

{error}

- -
-
- ); - } - - // Ensure data is available - if (!data) { - return null; - } - - const handlePromptSubmit = (prompt: string) => { - console.log("AI Prompt submitted:", prompt) - } - - const toggleParticleEffects = () => { - setParticleEffectsEnabled(!particleEffectsEnabled) - } - - const handleVoiceActivation = () => { - setIsVoiceActive(!isVoiceActive) - if (!isVoiceActive) { - setVoiceFeedback('Voice activation enabled - Say "Hey Mesh" to begin') - setTimeout(() => setVoiceFeedback(""), 3000) - } else { - setVoiceFeedback("Voice activation disabled") - setTimeout(() => setVoiceFeedback(""), 2000) - } - } - - // Handle Nexus toggle (undock/dock) - const handleNexusToggle = () => { - if (nexusExpanded) { - // User is undocking Nexus, disable auto-dock - setNexusAutoDockEnabled(false); - } else { - // User is re-docking via toggle, allow auto-dock again - setNexusAutoDockEnabled(true); - } - setNexusExpanded(!nexusExpanded); - }; - - // Size control functions - const cycleSizeUp = () => { - const sizes: ("small" | "medium" | "large" | "x-large")[] = ["small", "medium", "large", "x-large"] - const currentIndex = sizes.indexOf(globalSize as "small" | "medium" | "large" | "x-large") - const nextIndex = currentIndex < sizes.length - 1 ? currentIndex + 1 : 0 - setGlobalSize(sizes[nextIndex] as "small" | "medium" | "large" | "x-large") - } - - const cycleSizeDown = () => { - const sizes: ("small" | "medium" | "large" | "x-large")[] = ["small", "medium", "large", "x-large"] - const currentIndex = sizes.indexOf(globalSize as "small" | "medium" | "large" | "x-large") - const nextIndex = currentIndex > 0 ? currentIndex - 1 : sizes.length - 1 - setGlobalSize(sizes[nextIndex] as "small" | "medium" | "large" | "x-large") - } - - const getSizeIcon = () => { - switch (globalSize) { - case "small": - return - case "medium": - return - case "large": - return - case "x-large": - return - default: - return - } - } - - const getSizeLabel = () => { - switch (globalSize) { - case "small": - return "Small" - case "medium": - return "Medium" - case "large": - return "Large" - case "x-large": - return "X-Large" - default: - return "Medium" - } - } - - return ( -
- {/* Enhanced Animated Starfield Background */} - - - {/* Enhanced Central Command Nexus - Only show when expanded */} - {nexusExpanded && ( -
-
- setIsVoiceActive(!isVoiceActive)} - onDock={() => setNexusExpanded(false)} - soundVolume={soundVolume} - enableAudio={true} - /> -
-
- )} - -
- - - -
- - {/* Draggable Components - Always visible but positioned off-screen initially */} -
- {metrics.map((metric) => { - const Icon = metric.icon - return ( - -
-
- -
-
- {metric.value} -
-
- {metric.status === "up" && } - {metric.status === "stable" && } - {metric.change} -
-
-
- ) - })} - - - - - - - - - - - - - - - - - - - - - - {/* Design System Demo removed */} -
- - {/* Command Nexus - Floating version (only when not docked) */} - {!nexusExpanded && (() => { - const nexusItem = items["command-nexus"]; - const isNexusDocked = nexusItem?.isDocked; - // When user starts dragging, re-enable auto-dock - const handleNexusDragStart = () => { - setNexusAutoDockEnabled(true); - }; - return !isNexusDocked ? ( - - ) : null; - })()} - - {/* Voice Activation Feedback */} - - - {/* Setup Wizard — shown on first visit */} - - - {/* Guided Tour — shown after setup wizard completes */} - -
- ) -} - -export default function CognitiveMeshDashboard() { - return ( - - - - ) +import { redirect } from "next/navigation" + +/** + * Root route — redirects to the authenticated dashboard. + * + * The (app) route group handles auth gating via ProtectedRoute in its layout. + * The old "Bridge" drag-and-drop dashboard lived here; components are preserved + * under src/components/ for potential reuse. + */ +export default function RootPage() { + redirect("/dashboard") } diff --git a/src/UILayer/web/src/components/ErrorBoundary/ErrorBoundary.tsx b/src/UILayer/web/src/components/ErrorBoundary/ErrorBoundary.tsx index 0703cf85..a6ec3096 100644 --- a/src/UILayer/web/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/src/UILayer/web/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -35,7 +35,9 @@ export class ErrorBoundary extends Component {

Something went wrong

- {this.state.error?.message ?? "An unexpected error occurred."} + {process.env.NODE_ENV === "development" + ? (this.state.error?.message ?? "An unexpected error occurred.") + : "An unexpected error occurred."}

+ + {open && ( + <> + {/* Backdrop */} +
setOpen(false)} + aria-hidden="true" + /> + + {/* Drawer */} + + + )} +
+ ) +} diff --git a/src/UILayer/web/src/components/Navigation/Sidebar.tsx b/src/UILayer/web/src/components/Navigation/Sidebar.tsx new file mode 100644 index 00000000..e27f17a8 --- /dev/null +++ b/src/UILayer/web/src/components/Navigation/Sidebar.tsx @@ -0,0 +1,90 @@ +"use client" + +import Link from "next/link" +import { usePathname } from "next/navigation" +import { usePreferencesStore } from "@/stores" +import { navItems, groupBySections } from "./navItems" +import { + LayoutDashboard, + Bot, + BarChart3, + ShieldCheck, + Store, + Settings, + ChevronLeft, + ChevronRight, + type LucideIcon, +} from "lucide-react" + +const iconMap: Record = { + LayoutDashboard, + Bot, + BarChart3, + ShieldCheck, + Store, + Settings, +} + +export function Sidebar() { + const pathname = usePathname() + const collapsed = usePreferencesStore((s) => s.sidebarCollapsed) + const toggleSidebar = usePreferencesStore((s) => s.toggleSidebar) + const sections = groupBySections(navItems) + + return ( + + ) +} diff --git a/src/UILayer/web/src/components/Navigation/TopBar.tsx b/src/UILayer/web/src/components/Navigation/TopBar.tsx new file mode 100644 index 00000000..779eb1a7 --- /dev/null +++ b/src/UILayer/web/src/components/Navigation/TopBar.tsx @@ -0,0 +1,37 @@ +"use client" + +import { Bell } from "lucide-react" +import { useNotificationStore } from "@/stores" +import { Breadcrumbs } from "./Breadcrumbs" +import { MobileMenu } from "./MobileMenu" +import { ConnectionIndicator } from "./ConnectionIndicator" + +export function TopBar() { + const unreadCount = useNotificationStore((s) => s.unreadCount) + + return ( +
+
+ + +
+ +
+ + + {/* TODO: implement notification panel onClick handler */} + +
+
+ ) +} diff --git a/src/UILayer/web/src/components/Navigation/index.ts b/src/UILayer/web/src/components/Navigation/index.ts new file mode 100644 index 00000000..9e6b3592 --- /dev/null +++ b/src/UILayer/web/src/components/Navigation/index.ts @@ -0,0 +1,5 @@ +export { Sidebar } from "./Sidebar" +export { TopBar } from "./TopBar" +export { Breadcrumbs } from "./Breadcrumbs" +export { MobileMenu } from "./MobileMenu" +export { ConnectionIndicator } from "./ConnectionIndicator" diff --git a/src/UILayer/web/src/components/Navigation/navItems.ts b/src/UILayer/web/src/components/Navigation/navItems.ts new file mode 100644 index 00000000..4068bcbf --- /dev/null +++ b/src/UILayer/web/src/components/Navigation/navItems.ts @@ -0,0 +1,41 @@ +export interface NavItem { + label: string + href: string + icon: string + section: string + badge?: string +} + +export const navItems: NavItem[] = [ + { label: "Dashboard", href: "/dashboard", icon: "LayoutDashboard", section: "Core" }, + { label: "Agents", href: "/agents", icon: "Bot", section: "Core" }, + { label: "Analytics", href: "/analytics", icon: "BarChart3", section: "Core" }, + { label: "Compliance", href: "/compliance", icon: "ShieldCheck", section: "Governance" }, + { label: "Marketplace", href: "/marketplace", icon: "Store", section: "Governance" }, + { label: "Settings", href: "/settings", icon: "Settings", section: "System" }, +] + +export const sectionOrder = ["Core", "Governance", "System"] + +export function groupBySections(items: NavItem[]): Map { + const groups = new Map() + for (const section of sectionOrder) { + const sectionItems = items.filter((i) => i.section === section) + if (sectionItems.length > 0) { + groups.set(section, sectionItems) + } + } + + // Collect items with unknown sections into a fallback group + const knownSections = new Set(sectionOrder) + const unknownItems = items.filter((i) => !knownSections.has(i.section)) + if (unknownItems.length > 0) { + if (process.env.NODE_ENV === "development") { + const sections = [...new Set(unknownItems.map((i) => i.section))] + console.warn(`[navItems] Unknown sections: ${sections.join(", ")}`) + } + groups.set("Other", unknownItems) + } + + return groups +} diff --git a/src/UILayer/web/src/components/ProtectedRoute.tsx b/src/UILayer/web/src/components/ProtectedRoute.tsx index ac821030..9e253fb5 100644 --- a/src/UILayer/web/src/components/ProtectedRoute.tsx +++ b/src/UILayer/web/src/components/ProtectedRoute.tsx @@ -26,7 +26,7 @@ export function ProtectedRoute({ children, requiredRoles }: ProtectedRouteProps) router.replace("/forbidden") } } - }, [isAuthenticated, isLoading, user, requiredRoles, router]) + }, [isAuthenticated, isLoading, user, requiredRoles, router, pathname]) if (isLoading) { return ( diff --git a/src/UILayer/web/src/components/Skeleton/Skeleton.tsx b/src/UILayer/web/src/components/Skeleton/Skeleton.tsx new file mode 100644 index 00000000..cb666246 --- /dev/null +++ b/src/UILayer/web/src/components/Skeleton/Skeleton.tsx @@ -0,0 +1,60 @@ +interface SkeletonProps { + className?: string +} + +export function Skeleton({ className = "" }: SkeletonProps) { + return ( +