From 39c021ba566ff6aad4d9cb6ab97fd04a738d61e1 Mon Sep 17 00:00:00 2001 From: JustAGhosT Date: Tue, 10 Mar 2026 23:10:11 +0200 Subject: [PATCH 01/38] Phase 14 foundation: Zustand stores, navigation, routing, SignalR, skeletons FE-005: 5 Zustand stores - useAuthStore: mirrors AuthContext for non-React consumers - useAgentStore: agent registry with real agenticApi integration - useDashboardStore: dashboard data (fetch-based, pending backend endpoints) - useNotificationStore: in-app notifications with unread tracking - usePreferencesStore: persisted user preferences (theme, accessibility) FE-022: Navigation components - Sidebar with collapsible sections, active route highlighting - TopBar with breadcrumbs, notification bell, connection indicator - MobileMenu responsive drawer (<768px) FE-021: Multi-page routing - (app) route group with shared layout (sidebar + topbar + ProtectedRoute) - 6 routes: /dashboard, /agents, /analytics, /compliance, /marketplace, /settings - Per-route loading.tsx and error.tsx boundaries - Dashboard page wired to useDashboardStore - Agents page wired to useAgentStore with table view - Settings page wired to usePreferencesStore with toggle controls FE-003: SignalR real-time client - useSignalR hook with auto-reconnect (exponential backoff) - subscribe/unsubscribe/invoke/joinGroup/leaveGroup methods - ConnectionIndicator component shows live status FE-007: Skeleton loading components - Skeleton, SkeletonCard, SkeletonTable, SkeletonMetric, SkeletonDashboard Dependencies: zustand@5.0.11, @microsoft/signalr@10.0.0 Co-Authored-By: Claude Opus 4.6 --- src/UILayer/web/package-lock.json | 252 ++++++++++++++++-- src/UILayer/web/package.json | 4 +- src/UILayer/web/src/app/(app)/agents/page.tsx | 101 +++++++ .../web/src/app/(app)/analytics/page.tsx | 17 ++ .../web/src/app/(app)/compliance/page.tsx | 17 ++ .../web/src/app/(app)/dashboard/page.tsx | 111 ++++++++ src/UILayer/web/src/app/(app)/error.tsx | 28 ++ src/UILayer/web/src/app/(app)/layout.tsx | 25 ++ src/UILayer/web/src/app/(app)/loading.tsx | 5 + .../web/src/app/(app)/marketplace/page.tsx | 17 ++ .../web/src/app/(app)/settings/page.tsx | 114 ++++++++ .../src/components/Navigation/Breadcrumbs.tsx | 41 +++ .../Navigation/ConnectionIndicator.tsx | 33 +++ .../src/components/Navigation/MobileMenu.tsx | 84 ++++++ .../web/src/components/Navigation/Sidebar.tsx | 90 +++++++ .../web/src/components/Navigation/TopBar.tsx | 36 +++ .../web/src/components/Navigation/index.ts | 5 + .../web/src/components/Navigation/navItems.ts | 29 ++ .../web/src/components/Skeleton/Skeleton.tsx | 60 +++++ .../web/src/components/Skeleton/index.ts | 7 + src/UILayer/web/src/hooks/useSignalR.ts | 125 +++++++++ src/UILayer/web/src/stores/index.ts | 5 + src/UILayer/web/src/stores/useAgentStore.ts | 102 +++++++ src/UILayer/web/src/stores/useAuthStore.ts | 45 ++++ .../web/src/stores/useDashboardStore.ts | 149 +++++++++++ .../web/src/stores/useNotificationStore.ts | 89 +++++++ .../web/src/stores/usePreferencesStore.ts | 66 +++++ 27 files changed, 1635 insertions(+), 22 deletions(-) create mode 100644 src/UILayer/web/src/app/(app)/agents/page.tsx create mode 100644 src/UILayer/web/src/app/(app)/analytics/page.tsx create mode 100644 src/UILayer/web/src/app/(app)/compliance/page.tsx create mode 100644 src/UILayer/web/src/app/(app)/dashboard/page.tsx create mode 100644 src/UILayer/web/src/app/(app)/error.tsx create mode 100644 src/UILayer/web/src/app/(app)/layout.tsx create mode 100644 src/UILayer/web/src/app/(app)/loading.tsx create mode 100644 src/UILayer/web/src/app/(app)/marketplace/page.tsx create mode 100644 src/UILayer/web/src/app/(app)/settings/page.tsx create mode 100644 src/UILayer/web/src/components/Navigation/Breadcrumbs.tsx create mode 100644 src/UILayer/web/src/components/Navigation/ConnectionIndicator.tsx create mode 100644 src/UILayer/web/src/components/Navigation/MobileMenu.tsx create mode 100644 src/UILayer/web/src/components/Navigation/Sidebar.tsx create mode 100644 src/UILayer/web/src/components/Navigation/TopBar.tsx create mode 100644 src/UILayer/web/src/components/Navigation/index.ts create mode 100644 src/UILayer/web/src/components/Navigation/navItems.ts create mode 100644 src/UILayer/web/src/components/Skeleton/Skeleton.tsx create mode 100644 src/UILayer/web/src/components/Skeleton/index.ts create mode 100644 src/UILayer/web/src/hooks/useSignalR.ts create mode 100644 src/UILayer/web/src/stores/index.ts create mode 100644 src/UILayer/web/src/stores/useAgentStore.ts create mode 100644 src/UILayer/web/src/stores/useAuthStore.ts create mode 100644 src/UILayer/web/src/stores/useDashboardStore.ts create mode 100644 src/UILayer/web/src/stores/useNotificationStore.ts create mode 100644 src/UILayer/web/src/stores/usePreferencesStore.ts diff --git a/src/UILayer/web/package-lock.json b/src/UILayer/web/package-lock.json index 734d5909..5d5da711 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", @@ -4095,6 +4097,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", @@ -6586,6 +6666,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", @@ -9833,6 +9924,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", @@ -10093,6 +10192,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", @@ -14812,7 +14920,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" }, @@ -14824,7 +14931,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" } @@ -14862,8 +14968,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", @@ -15215,8 +15320,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", @@ -15566,6 +15670,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", @@ -17007,7 +17116,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", @@ -17022,7 +17130,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" } @@ -17604,7 +17711,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" @@ -18434,6 +18540,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": { @@ -20934,6 +21068,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", @@ -22768,6 +22953,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", @@ -25080,6 +25273,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", @@ -25267,6 +25465,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", @@ -28522,7 +28729,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" } @@ -28530,8 +28736,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", @@ -28550,8 +28755,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", @@ -28804,8 +29008,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", @@ -29029,6 +29232,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", @@ -30027,7 +30235,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", @@ -30038,8 +30245,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==" } } }, @@ -30418,7 +30624,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" @@ -31007,6 +31212,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 752e8f88..4786cfb2 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..f44cd1e4 --- /dev/null +++ b/src/UILayer/web/src/app/(app)/agents/page.tsx @@ -0,0 +1,101 @@ +"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)} + className={`cursor-pointer border-b border-white/5 transition-colors hover:bg-white/5 ${ + 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..46d16dd7 --- /dev/null +++ b/src/UILayer/web/src/app/(app)/dashboard/page.tsx @@ -0,0 +1,111 @@ +"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.map((layer) => ( +
+
+

{layer.name}

+ + {layer.uptime.toFixed(1)}% 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..0209dbff --- /dev/null +++ b/src/UILayer/web/src/app/(app)/error.tsx @@ -0,0 +1,28 @@ +"use client" + +export default function AppError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( +
+
+

+ Something went wrong +

+

+ {error.message || "An unexpected error occurred"} +

+ +
+
+ ) +} 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..47cf889d --- /dev/null +++ b/src/UILayer/web/src/app/(app)/settings/page.tsx @@ -0,0 +1,114 @@ +"use client" + +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

+
+ + + +
+
+ + +
+ ) +} + +function ToggleRow({ + label, + checked, + onChange, +}: { + label: string + checked: boolean + onChange: (v: boolean) => void +}) { + return ( +
+ {label} + +
+ ) +} diff --git a/src/UILayer/web/src/components/Navigation/Breadcrumbs.tsx b/src/UILayer/web/src/components/Navigation/Breadcrumbs.tsx new file mode 100644 index 00000000..912cb1c6 --- /dev/null +++ b/src/UILayer/web/src/components/Navigation/Breadcrumbs.tsx @@ -0,0 +1,41 @@ +"use client" + +import Link from "next/link" +import { usePathname } from "next/navigation" +import { ChevronRight } from "lucide-react" +import { navItems } from "./navItems" + +const labelMap: Record = Object.fromEntries( + navItems.map((item) => [item.href.replace("/", ""), item.label]) +) + +export function Breadcrumbs() { + const pathname = usePathname() + const segments = pathname.split("/").filter(Boolean) + if (segments.length === 0) return null + + return ( + + ) +} diff --git a/src/UILayer/web/src/components/Navigation/ConnectionIndicator.tsx b/src/UILayer/web/src/components/Navigation/ConnectionIndicator.tsx new file mode 100644 index 00000000..20351615 --- /dev/null +++ b/src/UILayer/web/src/components/Navigation/ConnectionIndicator.tsx @@ -0,0 +1,33 @@ +"use client" + +import { Wifi, WifiOff } from "lucide-react" +import { useSignalR } from "@/hooks/useSignalR" + +const statusColors = { + connected: "bg-green-500", + connecting: "bg-yellow-500 animate-pulse", + reconnecting: "bg-yellow-500 animate-pulse", + disconnected: "bg-red-500", +} + +const statusLabels = { + connected: "Connected", + connecting: "Connecting...", + reconnecting: "Reconnecting...", + disconnected: "Disconnected", +} + +export function ConnectionIndicator() { + const { status } = useSignalR({ enabled: false }) + const Icon = status === "disconnected" ? WifiOff : Wifi + + return ( +
+ + +
+ ) +} diff --git a/src/UILayer/web/src/components/Navigation/MobileMenu.tsx b/src/UILayer/web/src/components/Navigation/MobileMenu.tsx new file mode 100644 index 00000000..5321be5f --- /dev/null +++ b/src/UILayer/web/src/components/Navigation/MobileMenu.tsx @@ -0,0 +1,84 @@ +"use client" + +import { useState, useEffect } from "react" +import Link from "next/link" +import { usePathname } from "next/navigation" +import { Menu, X } from "lucide-react" +import { navItems } from "./navItems" + +export function MobileMenu() { + const [open, setOpen] = useState(false) + const pathname = usePathname() + + // Close on route change + useEffect(() => { + setOpen(false) + }, [pathname]) + + // Prevent body scroll when open + useEffect(() => { + document.body.style.overflow = open ? "hidden" : "" + return () => { + document.body.style.overflow = "" + } + }, [open]) + + return ( +
+ + + {open && ( + <> + {/* Backdrop */} +
setOpen(false)} + /> + + {/* 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..d24ab2fb --- /dev/null +++ b/src/UILayer/web/src/components/Navigation/TopBar.tsx @@ -0,0 +1,36 @@ +"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 ( +
+
+ + +
+ +
+ + + +
+
+ ) +} 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..915496ca --- /dev/null +++ b/src/UILayer/web/src/components/Navigation/navItems.ts @@ -0,0 +1,29 @@ +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) + } + } + return groups +} 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 ( +