From 8422c2f0b5670316f42be3afb8121c2da7fe1fc0 Mon Sep 17 00:00:00 2001 From: Usama Kaleem Date: Mon, 6 Apr 2026 16:58:12 +0200 Subject: [PATCH 1/2] fix: add healthcheck, document hardened overlay, update gitignore - Add compose-level healthcheck using node healthcheck.js (hits /api/status?action=health, same as Dockerfile HEALTHCHECK) - Document docker-compose.hardened.yml overlay differences as comments in the base docker-compose.yml for discoverability - Add .env.production to .gitignore Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 + docker-compose.yml | 26 +++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 30e8f42073..046d2f72d8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ dist/ .DS_Store *.pem .env*.local +.env.production # npm (project uses pnpm) package-lock.json diff --git a/docker-compose.yml b/docker-compose.yml index 8336de2630..bf6f093e5c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,13 @@ +# Base compose file with security defaults baked in. +# +# Production hardening overlay (docker-compose.hardened.yml) adds: +# - logging: json-file driver with max-size 10m / max-file 3 (prevents disk fill) +# - environment: MC_ALLOWED_HOSTS, MC_COOKIE_SECURE=1, MC_COOKIE_SAMESITE=strict, +# MC_ENABLE_HSTS=1 (forces secure cookies and HSTS headers) +# - networks: mc-internal with internal:true (no external/internet access) +# +# To use: docker compose -f docker-compose.yml -f docker-compose.hardened.yml up -d + services: mission-control: build: . @@ -31,9 +41,7 @@ services: required: false volumes: - mc-data:/app/.data - # Optional: mount your OpenClaw state directory read-only so Mission Control - # can read agent configs and memory. Uncomment and adjust the host path: - # - ${OPENCLAW_HOME:-~/.openclaw}:/run/openclaw:ro + - /Users/d509243/bubblu/openclaw-dual-stack/runtime/openclaw-state:/run/openclaw:ro # Allow the container to reach an OpenClaw gateway running on the Docker host. # Docker Desktop (macOS/Windows): host.docker.internal works out of the box. # Linux: this extra_hosts entry maps host.docker.internal to the host IP. @@ -49,6 +57,7 @@ services: - NET_BIND_SERVICE security_opt: - no-new-privileges:true + # Hardened overlay adds: logging driver json-file (max-size 10m, max-file 3) deploy: resources: limits: @@ -57,6 +66,15 @@ services: pids: 256 networks: - mc-net + # Health endpoint: /api/status?action=health (no auth required). + # The Dockerfile also defines a HEALTHCHECK; this compose-level check takes + # precedence and uses the same healthcheck.js script baked into the image. + healthcheck: + test: ["CMD", "node", "/app/healthcheck.js"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s restart: unless-stopped # Standalone mode — no gateway required. Start with: @@ -77,3 +95,5 @@ volumes: networks: mc-net: driver: bridge + # Hardened overlay replaces this with mc-internal (internal: true) + # to block all outbound internet access from the container. From 46328a497e87f5e39be9f72315e18c8b53de2966 Mon Sep 17 00:00:00 2001 From: Usama Kaleem Date: Fri, 15 May 2026 03:10:49 +0200 Subject: [PATCH 2/2] =?UTF-8?q?chore(cleanup):=20Wave=201=20=E2=80=94=20re?= =?UTF-8?q?move=2020=20orphaned=20files=20in=20mission-control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletes 20 grep-verified zero-reference dead files identified in the Wave 1 audit. All files confirmed no importers before deletion. Files deleted: - src/nav-rail.tsx (duplicate; live one is src/components/layout/nav-rail.tsx) - src/styles/design-tokens.ts - src/plugins/hyperbrowser-example.ts - update-db-port.js (one-shot DB patch, hardcoded port, untracked) - src/components/layout/promo-banner.tsx - src/components/ui/online-status.tsx - src/components/panels/documents-panel.tsx - src/components/panels/token-dashboard-panel.tsx - src/lib/plugin-loader.ts (all init was commented out) - src/components/dashboard/agent-network.tsx - src/components/ui/agent-core-node.tsx (only imported by agent-network, co-deleted) - src/components/dashboard/sessions-list.tsx - src/components/dashboard/sidebar.tsx - src/components/dashboard/stats-grid.tsx - src/components/hud/connection-status.tsx - src/components/panels/agent-cost-panel.tsx - src/components/panels/agent-history-panel.tsx - src/components/panels/session-details-panel.tsx - ops/mc-provisioner-daemon.js - scripts/smoke-staging.mjs Dep cleanup (chained from agent-network + agent-core-node deletion): - Removed @xyflow/react and reactflow from package.json (zero remaining importers) - Refreshed pnpm-lock.yaml Config cleanup: - Removed stale src/lib/plugin-loader.ts entry from vitest.config.ts coverage exclude list Validation: tsc --noEmit ✓ | vitest run ✓ (1 pre-existing better-sqlite3 native binding failure, unrelated) | next build ✓ Co-Authored-By: Claude Sonnet 4.6 --- docker-compose.yml | 4 +- ops/mc-provisioner-daemon.js | 302 ----- package.json | 2 - pnpm-lock.yaml | 383 +----- scripts/smoke-staging.mjs | 168 --- src/components/dashboard/agent-network.tsx | 317 ----- src/components/dashboard/sessions-list.tsx | 206 --- src/components/dashboard/sidebar.tsx | 201 --- src/components/dashboard/stats-grid.tsx | 185 --- src/components/hud/connection-status.tsx | 125 -- src/components/layout/promo-banner.tsx | 49 - src/components/panels/agent-cost-panel.tsx | 705 ---------- src/components/panels/agent-history-panel.tsx | 340 ----- src/components/panels/documents-panel.tsx | 323 ----- .../panels/session-details-panel.tsx | 741 ---------- .../panels/token-dashboard-panel.tsx | 1197 ----------------- src/components/ui/agent-core-node.tsx | 44 - src/components/ui/online-status.tsx | 20 - src/lib/plugin-loader.ts | 15 - src/nav-rail.tsx | 500 ------- src/plugins/hyperbrowser-example.ts | 59 - src/styles/design-tokens.ts | 95 -- vitest.config.ts | 1 - 23 files changed, 5 insertions(+), 5977 deletions(-) delete mode 100644 ops/mc-provisioner-daemon.js delete mode 100755 scripts/smoke-staging.mjs delete mode 100644 src/components/dashboard/agent-network.tsx delete mode 100644 src/components/dashboard/sessions-list.tsx delete mode 100644 src/components/dashboard/sidebar.tsx delete mode 100644 src/components/dashboard/stats-grid.tsx delete mode 100644 src/components/hud/connection-status.tsx delete mode 100644 src/components/layout/promo-banner.tsx delete mode 100644 src/components/panels/agent-cost-panel.tsx delete mode 100644 src/components/panels/agent-history-panel.tsx delete mode 100644 src/components/panels/documents-panel.tsx delete mode 100644 src/components/panels/session-details-panel.tsx delete mode 100644 src/components/panels/token-dashboard-panel.tsx delete mode 100644 src/components/ui/agent-core-node.tsx delete mode 100644 src/components/ui/online-status.tsx delete mode 100644 src/lib/plugin-loader.ts delete mode 100644 src/nav-rail.tsx delete mode 100644 src/plugins/hyperbrowser-example.ts delete mode 100644 src/styles/design-tokens.ts diff --git a/docker-compose.yml b/docker-compose.yml index bf6f093e5c..5272d9e58d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,7 +41,9 @@ services: required: false volumes: - mc-data:/app/.data - - /Users/d509243/bubblu/openclaw-dual-stack/runtime/openclaw-state:/run/openclaw:ro + # Optional: mount your OpenClaw state directory read-only so Mission Control + # can read agent configs and memory. Uncomment and adjust the host path: + # - ${OPENCLAW_HOME:-~/.openclaw}:/run/openclaw:ro # Allow the container to reach an OpenClaw gateway running on the Docker host. # Docker Desktop (macOS/Windows): host.docker.internal works out of the box. # Linux: this extra_hosts entry maps host.docker.internal to the host IP. diff --git a/ops/mc-provisioner-daemon.js b/ops/mc-provisioner-daemon.js deleted file mode 100644 index a5fb0bfff7..0000000000 --- a/ops/mc-provisioner-daemon.js +++ /dev/null @@ -1,302 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs') -const net = require('net') -const { spawn } = require('child_process') -const path = require('path') - -const SOCKET_PATH = process.env.MC_PROVISIONER_SOCKET || '/run/mc-provisioner.sock' -const TOKEN = String(process.env.MC_PROVISIONER_TOKEN || '') -const SOCKET_GROUP = process.env.MC_PROVISIONER_GROUP || 'openclaw' -const REPO_ROOT = process.env.MISSION_CONTROL_REPO_ROOT || path.resolve(__dirname, '..') -const DATA_DIR = process.env.MISSION_CONTROL_DATA_DIR || path.join(REPO_ROOT, '.data') -const TENANT_HOME_ROOT = String(process.env.MC_TENANT_HOME_ROOT || '/home').trim() || '/home' -const TENANT_WORKSPACE_DIRNAME = String(process.env.MC_TENANT_WORKSPACE_DIRNAME || 'workspace').trim() || 'workspace' -const TEMPLATE_OPENCLAW_JSON = process.env.MC_SUPER_TEMPLATE_OPENCLAW_JSON || (process.env.OPENCLAW_HOME ? path.join(process.env.OPENCLAW_HOME, 'openclaw.json') : '') -const GATEWAY_SYSTEMD_TEMPLATE = path.join(REPO_ROOT, 'ops', 'templates', 'openclaw-gateway@.service') - -if (!TOKEN) { - console.error('MC_PROVISIONER_TOKEN is required') - process.exit(1) -} - -function escapeRegExp(str) { - return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') -} - -function isSafeUser(user) { - return /^[a-z_][a-z0-9_-]{1,30}$/.test(user) -} - -function pathJoinPosix(...parts) { - // Use posix paths for allowlisting because provisioner executes linux commands. - const cleaned = parts.map((p) => String(p || '').replace(/\/+$/g, '')) - return path.posix.join(...cleaned) -} - -function isSafeHomePath(path, user, suffix) { - return path === pathJoinPosix(TENANT_HOME_ROOT, user, suffix) -} - -function validateCommand(command, args) { - const cmd = String(command || '').split('/').pop() - if (!command || !Array.isArray(args)) return 'Invalid command payload' - - if (cmd === 'useradd') { - if (args.length !== 4) return 'useradd argument mismatch' - const [a, b, shell, user] = args - if (a !== '-m' || b !== '-s' || shell !== '/bin/bash') return 'useradd args not allowed' - if (!isSafeUser(user)) return 'Invalid username' - return null - } - - if (cmd === 'install') { - if (args.length !== 8) return 'install argument mismatch' - const [d, mFlag, mode, oFlag, userA, gFlag, userB, target] = args - if (d !== '-d' || mFlag !== '-m' || oFlag !== '-o' || gFlag !== '-g') return 'install args not allowed' - if (!['0750', '0700'].includes(mode)) return 'install mode not allowed' - const isRootOwned = userA === 'root' && userB === 'root' - const isTenantOwned = isSafeUser(userA) && isSafeUser(userB) && userA === userB - if (!isRootOwned && !isTenantOwned) return 'install ownership not allowed' - const openclawPath = pathJoinPosix(TENANT_HOME_ROOT, userA, '.openclaw') - const workspacePath = pathJoinPosix(TENANT_HOME_ROOT, userA, TENANT_WORKSPACE_DIRNAME) - if (isRootOwned && target === '/etc/openclaw-tenants') return null - if (![openclawPath, workspacePath].includes(target)) return 'install path not allowed' - return null - } - - if (cmd === 'cp') { - if (args.length !== 3) return 'cp argument mismatch' - const [flag, source, target] = args - if (!['-n', '-f'].includes(flag)) return 'cp flag not allowed' - if (TEMPLATE_OPENCLAW_JSON && source === TEMPLATE_OPENCLAW_JSON) { - if (flag !== '-n') return 'openclaw config copy must use -n' - const homeRootRe = escapeRegExp(pathJoinPosix(TENANT_HOME_ROOT)) - const match = new RegExp(`^${homeRootRe}\\/([a-z_][a-z0-9_-]{1,30})\\/\\.openclaw\\/openclaw\\.json$`).exec(target) - if (!match) return 'cp target not allowed' - return null - } - if (source === GATEWAY_SYSTEMD_TEMPLATE) { - if (flag !== '-n') return 'template copy must use -n' - if (target !== '/etc/systemd/system/openclaw-gateway@.service') return 'gateway template target not allowed' - return null - } - const provisionerEnvRe = new RegExp(`^${escapeRegExp(path.join(DATA_DIR, 'provisioner'))}\\/([a-z0-9-]{3,32})\\/openclaw-gateway\\.env$`) - if (provisionerEnvRe.test(source)) { - if (flag !== '-f') return 'tenant env copy must use -f' - if (!/^\/etc\/openclaw-tenants\/[a-z_][a-z0-9_-]{1,30}\.env$/.test(target)) return 'tenant env target not allowed' - return null - } - return 'cp source not allowed' - } - - if (cmd === 'chown') { - if (args.length !== 3) return 'chown argument mismatch' - const [rFlag, owner, target] = args - if (rFlag !== '-R') return 'chown must use -R' - const [userA, userB] = owner.split(':') - if (!isSafeUser(userA) || userA !== userB) return 'chown owner not allowed' - if (target !== pathJoinPosix(TENANT_HOME_ROOT, userA)) return 'chown target not allowed' - return null - } - - if (cmd === 'rm') { - if (args.length !== 2) return 'rm argument mismatch' - const [flag, target] = args - - if (flag === '-f') { - if (!/^\/etc\/openclaw-tenants\/[a-z_][a-z0-9_-]{1,30}\.env$/.test(target)) { - return 'rm -f target not allowed' - } - return null - } - - if (flag === '-rf') { - const homeRootRe = escapeRegExp(pathJoinPosix(TENANT_HOME_ROOT)) - const ws = escapeRegExp(TENANT_WORKSPACE_DIRNAME) - const match = new RegExp(`^${homeRootRe}\\/([a-z_][a-z0-9_-]{1,30})\\/(\\.openclaw|${ws})$`).exec(target) - if (!match) return 'rm -rf target not allowed' - return null - } - - return 'rm flag not allowed' - } - - if (cmd === 'userdel') { - if (args.length !== 2) return 'userdel argument mismatch' - if (args[0] !== '-r') return 'userdel must use -r' - if (!isSafeUser(args[1])) return 'Invalid username' - return null - } - - if (cmd === 'true') { - if (args.length !== 0) return 'true takes no args' - return null - } - - if (cmd === 'systemctl') { - if (args.length === 1 && args[0] === 'daemon-reload') return null - if (args.length === 3 && args[0] === 'enable' && args[1] === '--now') { - if (/^openclaw-gateway@[a-z_][a-z0-9_-]{1,30}\.service$/.test(args[2])) return null - return 'systemctl service name not allowed' - } - if (args.length === 3 && args[0] === 'disable' && args[1] === '--now') { - if (/^openclaw-gateway@[a-z_][a-z0-9_-]{1,30}\.service$/.test(args[2])) return null - return 'systemctl service name not allowed' - } - return 'systemctl args not allowed' - } - - return `Command not allowlisted: ${command}` -} - -function run(command, args, timeoutMs) { - return new Promise((resolve) => { - const child = spawn(command, args, { shell: false }) - let stdout = '' - let stderr = '' - let timedOut = false - - const timer = setTimeout(() => { - timedOut = true - child.kill('SIGKILL') - }, Math.max(1000, Number(timeoutMs || 10000))) - - child.stdout.on('data', (d) => { stdout += d.toString('utf8') }) - child.stderr.on('data', (d) => { stderr += d.toString('utf8') }) - - child.on('close', (code) => { - clearTimeout(timer) - resolve({ - ok: !timedOut && code === 0, - code: timedOut ? 124 : code, - stdout, - stderr: timedOut ? `${stderr}\nTimed out` : stderr, - }) - }) - - child.on('error', (err) => { - clearTimeout(timer) - resolve({ ok: false, code: 1, stdout, stderr: `${stderr}\n${err.message}` }) - }) - }) -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -async function runWithRetry(command, args, timeoutMs) { - const cmd = String(command || '').split('/').pop() - const maxAttempts = cmd === 'useradd' ? 6 : 1 - let last = null - - for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - const result = await run(command, args, timeoutMs) - last = result - if (result.ok) return result - - const transientLock = - cmd === 'useradd' && - /cannot lock \/etc\/passwd/i.test(String(result.stderr || '')) - - if (!transientLock || attempt === maxAttempts) { - return result - } - await sleep(800) - } - - return last || { ok: false, code: 1, stdout: '', stderr: 'Unknown execution failure' } -} - -function writeResp(socket, obj) { - try { - socket.write(JSON.stringify(obj) + '\n') - } catch { - // no-op - } finally { - socket.end() - } -} - -if (fs.existsSync(SOCKET_PATH)) { - try { - fs.unlinkSync(SOCKET_PATH) - } catch (err) { - console.error(`Failed to remove stale socket ${SOCKET_PATH}:`, err.message) - process.exit(1) - } -} - -const server = net.createServer((socket) => { - let buf = '' - - socket.on('data', async (chunk) => { - buf += chunk.toString('utf8') - const idx = buf.indexOf('\n') - if (idx === -1) return - - const line = buf.slice(0, idx) - buf = buf.slice(idx + 1) - - let req - try { - req = JSON.parse(line) - } catch { - writeResp(socket, { ok: false, error: 'Invalid JSON' }) - return - } - - if (!req || req.token !== TOKEN) { - writeResp(socket, { ok: false, error: 'Unauthorized' }) - return - } - - const command = String(req.command || '') - const args = Array.isArray(req.args) ? req.args.map((a) => String(a)) : [] - const dryRun = !!req.dryRun - const timeoutMs = Number(req.timeoutMs || 10000) - - const validationErr = validateCommand(command, args) - if (validationErr) { - writeResp(socket, { ok: false, error: validationErr }) - return - } - - if (dryRun) { - writeResp(socket, { ok: true, code: 0, stdout: '', stderr: '', skipped: true }) - return - } - - const result = await runWithRetry(command, args, timeoutMs) - if (!result.ok) { - writeResp(socket, { ok: false, code: result.code, stdout: result.stdout, stderr: result.stderr, error: `Command failed: ${command}` }) - return - } - - writeResp(socket, { ok: true, code: result.code, stdout: result.stdout, stderr: result.stderr, skipped: false }) - }) -}) - -server.listen(SOCKET_PATH, () => { - fs.chmodSync(SOCKET_PATH, 0o660) - try { - const group = require('child_process').execSync(`getent group ${SOCKET_GROUP} | cut -d: -f3`).toString('utf8').trim() - const gid = Number(group) - if (Number.isInteger(gid)) { - fs.chownSync(SOCKET_PATH, 0, gid) - } - } catch { - // fallback: keep root:root - } - console.log(`mc-provisioner listening on ${SOCKET_PATH}`) -}) - -function shutdown() { - try { server.close() } catch {} - try { fs.unlinkSync(SOCKET_PATH) } catch {} - process.exit(0) -} - -process.on('SIGINT', shutdown) -process.on('SIGTERM', shutdown) diff --git a/package.json b/package.json index 7834e61354..85919a9ba7 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", - "@xyflow/react": "^12.10.0", "autoprefixer": "^10.4.20", "better-sqlite3": "^12.6.2", "class-variance-authority": "^0.7.1", @@ -49,7 +48,6 @@ "react": "^19.0.1", "react-dom": "^19.0.1", "react-markdown": "^10.1.0", - "reactflow": "^11.11.4", "reagraph": "^4.30.8", "recharts": "^3.7.0", "remark-gfm": "^4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b364eacbb6..b3e3e5077f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,6 @@ importers: '@xterm/xterm': specifier: ^6.0.0 version: 6.0.0 - '@xyflow/react': - specifier: ^12.10.0 - version: 12.10.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) autoprefixer: specifier: ^10.4.20 version: 10.4.24(postcss@8.5.6) @@ -71,9 +68,6 @@ importers: react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.13)(react@19.2.4) - reactflow: - specifier: ^11.11.4 - version: 11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) reagraph: specifier: ^4.30.8 version: 4.30.8(@types/react@19.2.13)(@types/three@0.183.1)(graphology-types@0.24.8)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) @@ -1070,42 +1064,6 @@ packages: react-native: optional: true - '@reactflow/background@11.3.14': - resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@reactflow/controls@11.2.14': - resolution: {integrity: sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@reactflow/core@11.11.4': - resolution: {integrity: sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@reactflow/minimap@11.7.14': - resolution: {integrity: sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@reactflow/node-resizer@2.2.14': - resolution: {integrity: sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@reactflow/node-toolbar@1.3.14': - resolution: {integrity: sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - '@reduxjs/toolkit@2.11.2': resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} peerDependencies: @@ -1526,96 +1484,30 @@ packages: '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} - '@types/d3-axis@3.0.6': - resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} - - '@types/d3-brush@3.0.6': - resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} - - '@types/d3-chord@3.0.6': - resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} - '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - '@types/d3-contour@3.0.6': - resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} - - '@types/d3-delaunay@6.0.4': - resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} - - '@types/d3-dispatch@3.0.7': - resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} - - '@types/d3-drag@3.0.7': - resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} - - '@types/d3-dsv@3.0.7': - resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} - '@types/d3-ease@3.0.2': resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - '@types/d3-fetch@3.0.7': - resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} - - '@types/d3-force@3.0.10': - resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} - - '@types/d3-format@3.0.4': - resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} - - '@types/d3-geo@3.1.0': - resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} - - '@types/d3-hierarchy@3.1.7': - resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} - '@types/d3-interpolate@3.0.4': resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} '@types/d3-path@3.1.1': resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - '@types/d3-polygon@3.0.2': - resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} - - '@types/d3-quadtree@3.0.6': - resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} - - '@types/d3-random@3.0.3': - resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} - - '@types/d3-scale-chromatic@3.1.0': - resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} - '@types/d3-scale@4.0.9': resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} - '@types/d3-selection@3.0.11': - resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} - '@types/d3-shape@3.1.8': resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} - '@types/d3-time-format@4.0.3': - resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} - '@types/d3-time@3.0.4': resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - '@types/d3-transition@3.0.9': - resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} - - '@types/d3-zoom@3.0.8': - resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} - - '@types/d3@7.4.3': - resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1628,9 +1520,6 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/geojson@7946.0.16': - resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/har-format@1.2.16': resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==} @@ -1756,6 +1645,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unhead/vue@2.1.9': resolution: {integrity: sha512-7SqqDEn5zFID1PnEdjLCLa/kOhoAlzol0JdYfVr2Ejek+H4ON4s8iyExv2QQ8bReMosbXQ/Bw41j2CF1NUuGSA==} @@ -2029,14 +1919,6 @@ packages: '@xterm/xterm@6.0.0': resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} - '@xyflow/react@12.10.0': - resolution: {integrity: sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@xyflow/system@0.0.74': - resolution: {integrity: sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==} '@yomguithereal/helpers@1.1.1': resolution: {integrity: sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==} @@ -2326,9 +2208,6 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - classcat@5.0.5: - resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} - classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} @@ -2424,10 +2303,6 @@ packages: resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} engines: {node: '>=12'} - d3-drag@3.0.0: - resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} - engines: {node: '>=12'} - d3-ease@3.0.1: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} engines: {node: '>=12'} @@ -2463,10 +2338,6 @@ packages: resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} engines: {node: '>=12'} - d3-selection@3.0.0: - resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} - engines: {node: '>=12'} - d3-shape@3.2.0: resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} engines: {node: '>=12'} @@ -2483,16 +2354,6 @@ packages: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} - d3-transition@3.0.1: - resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} - engines: {node: '>=12'} - peerDependencies: - d3-selection: 2 - 3 - - d3-zoom@3.0.0: - resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} - engines: {node: '>=12'} - damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -4084,6 +3945,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -4182,12 +4044,6 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} - reactflow@11.11.4: - resolution: {integrity: sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -6000,84 +5856,6 @@ snapshots: - '@types/react' - immer - '@reactflow/background@11.3.14(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@reactflow/core': 11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - classcat: 5.0.5 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - - immer - - '@reactflow/controls@11.2.14(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@reactflow/core': 11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - classcat: 5.0.5 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - - immer - - '@reactflow/core@11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@types/d3': 7.4.3 - '@types/d3-drag': 3.0.7 - '@types/d3-selection': 3.0.11 - '@types/d3-zoom': 3.0.8 - classcat: 5.0.5 - d3-drag: 3.0.0 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - - immer - - '@reactflow/minimap@11.7.14(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@reactflow/core': 11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@types/d3-selection': 3.0.11 - '@types/d3-zoom': 3.0.8 - classcat: 5.0.5 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - - immer - - '@reactflow/node-resizer@2.2.14(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@reactflow/core': 11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - classcat: 5.0.5 - d3-drag: 3.0.0 - d3-selection: 3.0.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - - immer - - '@reactflow/node-toolbar@1.3.14(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@reactflow/core': 11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - classcat: 5.0.5 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - - immer - '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.13)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': dependencies: '@standard-schema/spec': 1.1.0 @@ -6687,121 +6465,28 @@ snapshots: '@types/d3-array@3.2.2': {} - '@types/d3-axis@3.0.6': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-brush@3.0.6': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-chord@3.0.6': {} - '@types/d3-color@3.1.3': {} - '@types/d3-contour@3.0.6': - dependencies: - '@types/d3-array': 3.2.2 - '@types/geojson': 7946.0.16 - - '@types/d3-delaunay@6.0.4': {} - - '@types/d3-dispatch@3.0.7': {} - - '@types/d3-drag@3.0.7': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-dsv@3.0.7': {} - '@types/d3-ease@3.0.2': {} - '@types/d3-fetch@3.0.7': - dependencies: - '@types/d3-dsv': 3.0.7 - - '@types/d3-force@3.0.10': {} - - '@types/d3-format@3.0.4': {} - - '@types/d3-geo@3.1.0': - dependencies: - '@types/geojson': 7946.0.16 - - '@types/d3-hierarchy@3.1.7': {} - '@types/d3-interpolate@3.0.4': dependencies: '@types/d3-color': 3.1.3 '@types/d3-path@3.1.1': {} - '@types/d3-polygon@3.0.2': {} - - '@types/d3-quadtree@3.0.6': {} - - '@types/d3-random@3.0.3': {} - - '@types/d3-scale-chromatic@3.1.0': {} - '@types/d3-scale@4.0.9': dependencies: '@types/d3-time': 3.0.4 - '@types/d3-selection@3.0.11': {} - '@types/d3-shape@3.1.8': dependencies: '@types/d3-path': 3.1.1 - '@types/d3-time-format@4.0.3': {} - '@types/d3-time@3.0.4': {} '@types/d3-timer@3.0.2': {} - '@types/d3-transition@3.0.9': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-zoom@3.0.8': - dependencies: - '@types/d3-interpolate': 3.0.4 - '@types/d3-selection': 3.0.11 - - '@types/d3@7.4.3': - dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-axis': 3.0.6 - '@types/d3-brush': 3.0.6 - '@types/d3-chord': 3.0.6 - '@types/d3-color': 3.1.3 - '@types/d3-contour': 3.0.6 - '@types/d3-delaunay': 6.0.4 - '@types/d3-dispatch': 3.0.7 - '@types/d3-drag': 3.0.7 - '@types/d3-dsv': 3.0.7 - '@types/d3-ease': 3.0.2 - '@types/d3-fetch': 3.0.7 - '@types/d3-force': 3.0.10 - '@types/d3-format': 3.0.4 - '@types/d3-geo': 3.1.0 - '@types/d3-hierarchy': 3.1.7 - '@types/d3-interpolate': 3.0.4 - '@types/d3-path': 3.1.1 - '@types/d3-polygon': 3.0.2 - '@types/d3-quadtree': 3.0.6 - '@types/d3-random': 3.0.3 - '@types/d3-scale': 4.0.9 - '@types/d3-scale-chromatic': 3.1.0 - '@types/d3-selection': 3.0.11 - '@types/d3-shape': 3.1.8 - '@types/d3-time': 3.0.4 - '@types/d3-time-format': 4.0.3 - '@types/d3-timer': 3.0.2 - '@types/d3-transition': 3.0.9 - '@types/d3-zoom': 3.0.8 - '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -6814,8 +6499,6 @@ snapshots: '@types/estree@1.0.8': {} - '@types/geojson@7946.0.16': {} - '@types/har-format@1.2.16': {} '@types/hast@3.0.4': @@ -7220,28 +6903,6 @@ snapshots: '@xterm/xterm@6.0.0': {} - '@xyflow/react@12.10.0(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@xyflow/system': 0.0.74 - classcat: 5.0.5 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - - immer - - '@xyflow/system@0.0.74': - dependencies: - '@types/d3-drag': 3.0.7 - '@types/d3-interpolate': 3.0.4 - '@types/d3-selection': 3.0.11 - '@types/d3-transition': 3.0.9 - '@types/d3-zoom': 3.0.8 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 '@yomguithereal/helpers@1.1.1': {} @@ -7548,8 +7209,6 @@ snapshots: dependencies: clsx: 2.1.1 - classcat@5.0.5: {} - classnames@2.5.1: {} client-only@0.0.1: {} @@ -7619,11 +7278,6 @@ snapshots: d3-dispatch@3.0.1: {} - d3-drag@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-selection: 3.0.0 - d3-ease@3.0.1: {} d3-force-3d@3.0.6: @@ -7656,8 +7310,6 @@ snapshots: d3-time: 3.1.0 d3-time-format: 4.1.0 - d3-selection@3.0.0: {} - d3-shape@3.2.0: dependencies: d3-path: 3.1.0 @@ -7672,23 +7324,6 @@ snapshots: d3-timer@3.0.1: {} - d3-transition@3.0.1(d3-selection@3.0.0): - dependencies: - d3-color: 3.1.0 - d3-dispatch: 3.0.1 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-timer: 3.0.1 - - d3-zoom@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - damerau-levenshtein@1.0.8: {} data-urls@5.0.0: @@ -9833,20 +9468,6 @@ snapshots: react@19.2.4: {} - reactflow@11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@reactflow/background': 11.3.14(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@reactflow/controls': 11.2.14(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@reactflow/core': 11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@reactflow/minimap': 11.7.14(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@reactflow/node-resizer': 2.2.14(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@reactflow/node-toolbar': 1.3.14(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - - immer - read-cache@1.0.0: dependencies: pify: 2.3.0 diff --git a/scripts/smoke-staging.mjs b/scripts/smoke-staging.mjs deleted file mode 100755 index d57d6669aa..0000000000 --- a/scripts/smoke-staging.mjs +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env node -const baseUrl = (process.env.STAGING_BASE_URL || process.env.BASE_URL || '').replace(/\/$/, '') -const apiKey = process.env.STAGING_API_KEY || process.env.API_KEY || '' -const authUser = process.env.STAGING_AUTH_USER || process.env.AUTH_USER || '' -const authPass = process.env.STAGING_AUTH_PASS || process.env.AUTH_PASS || '' - -if (!baseUrl) { - console.error('Missing STAGING_BASE_URL (or BASE_URL).') - process.exit(1) -} -if (!apiKey) { - console.error('Missing STAGING_API_KEY (or API_KEY).') - process.exit(1) -} -if (!authUser || !authPass) { - console.error('Missing STAGING_AUTH_USER/STAGING_AUTH_PASS (or AUTH_USER/AUTH_PASS).') - process.exit(1) -} - -const headers = { - 'x-api-key': apiKey, - 'content-type': 'application/json', -} - -let createdProjectId = null -let createdTaskId = null -let createdAgentId = null - -async function call(path, options = {}) { - const res = await fetch(`${baseUrl}${path}`, options) - const text = await res.text() - let body = null - try { - body = text ? JSON.parse(text) : null - } catch { - body = { raw: text } - } - return { res, body } -} - -function assertStatus(actual, expected, label) { - if (actual !== expected) { - throw new Error(`${label} failed: expected ${expected}, got ${actual}`) - } - console.log(`PASS ${label}`) -} - -async function run() { - const login = await call('/api/auth/login', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ username: authUser, password: authPass }), - }) - assertStatus(login.res.status, 200, 'login') - - const workspaces = await call('/api/workspaces', { headers }) - assertStatus(workspaces.res.status, 200, 'GET /api/workspaces') - - const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}` - const ticketPrefix = `S${String(Date.now()).slice(-5)}` - - const projectCreate = await call('/api/projects', { - method: 'POST', - headers, - body: JSON.stringify({ - name: `staging-smoke-${suffix}`, - ticket_prefix: ticketPrefix, - }), - }) - assertStatus(projectCreate.res.status, 201, 'POST /api/projects') - createdProjectId = projectCreate.body?.project?.id - if (!createdProjectId) throw new Error('project id missing') - - const projectGet = await call(`/api/projects/${createdProjectId}`, { headers }) - assertStatus(projectGet.res.status, 200, 'GET /api/projects/[id]') - - const projectPatch = await call(`/api/projects/${createdProjectId}`, { - method: 'PATCH', - headers, - body: JSON.stringify({ description: 'staging smoke update' }), - }) - assertStatus(projectPatch.res.status, 200, 'PATCH /api/projects/[id]') - - const agentCreate = await call('/api/agents', { - method: 'POST', - headers, - body: JSON.stringify({ name: `smoke-agent-${suffix}`, role: 'tester' }), - }) - assertStatus(agentCreate.res.status, 201, 'POST /api/agents') - createdAgentId = agentCreate.body?.agent?.id - - const assign = await call(`/api/projects/${createdProjectId}/agents`, { - method: 'POST', - headers, - body: JSON.stringify({ agent_name: `smoke-agent-${suffix}`, role: 'member' }), - }) - assertStatus(assign.res.status, 201, 'POST /api/projects/[id]/agents') - - const projectTasksCreate = await call('/api/tasks', { - method: 'POST', - headers, - body: JSON.stringify({ - title: `smoke-task-${suffix}`, - project_id: createdProjectId, - priority: 'medium', - status: 'inbox', - }), - }) - assertStatus(projectTasksCreate.res.status, 201, 'POST /api/tasks (project scoped)') - createdTaskId = projectTasksCreate.body?.task?.id - - const projectTasksGet = await call(`/api/projects/${createdProjectId}/tasks`, { headers }) - assertStatus(projectTasksGet.res.status, 200, 'GET /api/projects/[id]/tasks') - - const unassign = await call(`/api/projects/${createdProjectId}/agents?agent_name=${encodeURIComponent(`smoke-agent-${suffix}`)}`, { - method: 'DELETE', - headers, - }) - assertStatus(unassign.res.status, 200, 'DELETE /api/projects/[id]/agents') - - if (createdTaskId) { - const deleteTask = await call(`/api/tasks/${createdTaskId}`, { - method: 'DELETE', - headers, - }) - assertStatus(deleteTask.res.status, 200, 'DELETE /api/tasks/[id]') - createdTaskId = null - } - - if (createdProjectId) { - const deleteProject = await call(`/api/projects/${createdProjectId}?mode=delete`, { - method: 'DELETE', - headers, - }) - assertStatus(deleteProject.res.status, 200, 'DELETE /api/projects/[id]?mode=delete') - createdProjectId = null - } - - if (createdAgentId) { - const deleteAgent = await call(`/api/agents/${createdAgentId}`, { - method: 'DELETE', - headers, - }) - if (deleteAgent.res.status !== 200 && deleteAgent.res.status !== 404) { - throw new Error(`DELETE /api/agents/[id] cleanup failed: ${deleteAgent.res.status}`) - } - createdAgentId = null - console.log('PASS cleanup agent') - } - - console.log(`\nSmoke test passed for ${baseUrl}`) -} - -run().catch(async (error) => { - console.error(`\nSmoke test failed: ${error.message}`) - - if (createdTaskId) { - await call(`/api/tasks/${createdTaskId}`, { method: 'DELETE', headers }).catch(() => {}) - } - if (createdProjectId) { - await call(`/api/projects/${createdProjectId}?mode=delete`, { method: 'DELETE', headers }).catch(() => {}) - } - if (createdAgentId) { - await call(`/api/agents/${createdAgentId}`, { method: 'DELETE', headers }).catch(() => {}) - } - - process.exit(1) -}) diff --git a/src/components/dashboard/agent-network.tsx b/src/components/dashboard/agent-network.tsx deleted file mode 100644 index 71490c4a48..0000000000 --- a/src/components/dashboard/agent-network.tsx +++ /dev/null @@ -1,317 +0,0 @@ -'use client' - -import { useCallback, useEffect, useState, useMemo } from 'react' -import { - ReactFlow, - Node, - Edge, - addEdge, - useNodesState, - useEdgesState, - Controls, - Background, - BackgroundVariant, - Connection, -} from '@xyflow/react' -import '@xyflow/react/dist/style.css' - -import { Agent, Session } from '@/types' -import { sessionToAgent, generateNodePosition } from '@/lib/utils' -import { AgentCoreNode } from '@/components/ui/agent-core-node' - -interface AgentNetworkProps { - agents: Agent[] - sessions: Session[] -} - -// SVG icons for agent types (16x16, stroke-based) -function CrownIcon() { - return ( - - - - ) -} - -function BotIcon() { - return ( - - - - - - - ) -} - -function ClockIcon() { - return ( - - - - - ) -} - -function GroupIcon() { - return ( - - - - - - - ) -} - -function FileIcon() { - return ( - - - - - ) -} - -// Custom node component for agents -function AgentNode({ data }: { data: any }) { - const { agent, status } = data - - const getStatusClasses = () => { - switch (status) { - case 'active': return 'border-void-cyan glow-cyan' - case 'idle': return 'border-void-amber/50' - case 'error': return 'border-void-crimson badge-glow-error' - default: return 'border-border' - } - } - - const getTypeIcon = () => { - switch (agent.type) { - case 'main': return - case 'subagent': return - case 'cron': return - case 'group': return - default: return - } - } - - const getRoleBadge = () => { - switch (agent.type) { - case 'main': - return { label: 'LEAD', color: 'bg-void-violet/20 text-void-violet border-void-violet/30' } - case 'subagent': - return { label: 'WORKER', color: 'bg-void-cyan/20 text-void-cyan border-void-cyan/30' } - case 'cron': - return { label: 'CRON', color: 'bg-void-amber/20 text-void-amber border-void-amber/30' } - default: - return { label: 'SYSTEM', color: 'bg-muted text-muted-foreground border-border' } - } - } - - const roleBadge = getRoleBadge() - const isWorking = status === 'active' - - return ( -
-
- - {getTypeIcon()} - - {isWorking && ( - - WORKING - - )} -
- -
-
-
- {agent.name} -
- - {roleBadge.label} - -
- -
- {(typeof agent.model === 'string' ? agent.model : '').split('/').pop() || 'unknown'} -
- - {agent.session && ( -
- {agent.session.key.split(':').pop()} -
- )} -
-
- ) -} - -const nodeTypes = { - agent: AgentNode, - core: AgentCoreNode, -} - -export function AgentNetwork({ agents, sessions }: AgentNetworkProps) { - const [nodes, setNodes, onNodesChange] = useNodesState([]) - const [edges, setEdges, onEdgesChange] = useEdgesState([]) - - // Convert sessions to nodes and edges - const { nodeData, edgeData } = useMemo(() => { - const agentList = sessions.map(sessionToAgent) - - // Add CORE hub node at center - const coreNode: Node = { - id: '__core__', - type: 'core', - position: { x: 0, y: 0 }, - data: { label: 'CORE', agentCount: agentList.length }, - style: { background: 'transparent', border: 'none' }, - } - - const agentNodes: Node[] = agentList.map((agent, index) => ({ - id: agent.id, - type: 'agent', - position: generateNodePosition(index, agentList.length), - data: { - agent, - status: agent.status, - label: agent.name - }, - style: { - background: 'transparent', - border: 'none', - } - })) - - const nodes = [coreNode, ...agentNodes] - - // Create edges — all agents connect to CORE, plus hierarchical edges - const edges: Edge[] = [] - const cyanStroke = 'hsl(var(--void-cyan))' - const cyanDimStroke = 'hsl(var(--void-cyan) / 0.4)' - const amberStroke = 'hsl(var(--void-amber) / 0.5)' - - const mainAgents = agentList.filter(a => a.type === 'main') - const subagents = agentList.filter(a => a.type === 'subagent') - const cronAgents = agentList.filter(a => a.type === 'cron') - - // Connect all agents to CORE hub - agentList.forEach(agent => { - edges.push({ - id: `core-${agent.id}`, - source: '__core__', - target: agent.id, - animated: agent.status === 'active', - style: { - stroke: agent.status === 'active' ? cyanStroke : cyanDimStroke, - strokeWidth: 1.5, - }, - type: 'smoothstep', - }) - }) - - // Connect main agents to subagents - mainAgents.forEach(main => { - subagents.forEach(sub => { - edges.push({ - id: `${main.id}-${sub.id}`, - source: main.id, - target: sub.id, - animated: sub.status === 'active', - style: { - stroke: cyanStroke, - strokeWidth: 2, - }, - type: 'smoothstep' - }) - }) - - // Connect main agents to cron jobs - cronAgents.forEach(cron => { - edges.push({ - id: `${main.id}-${cron.id}`, - source: main.id, - target: cron.id, - animated: false, - style: { - stroke: amberStroke, - strokeWidth: 1, - strokeDasharray: '5,5', - }, - type: 'smoothstep' - }) - }) - }) - - return { nodeData: nodes, edgeData: edges } - }, [sessions]) - - useEffect(() => { - setNodes(nodeData) - setEdges(edgeData) - }, [nodeData, edgeData, setNodes, setEdges]) - - const onConnect = useCallback( - (params: Connection) => setEdges((eds) => addEdge(params, eds)), - [setEdges] - ) - - if (sessions.length === 0) { - return ( -
-
- - - - - - - -

No agent network to display

-

Agent connections will appear here

-
-
- ) - } - - return ( -
-
-

Agent Network

-

- Visual representation of agent relationships -

-
- -
- - - - -
-
- ) -} \ No newline at end of file diff --git a/src/components/dashboard/sessions-list.tsx b/src/components/dashboard/sessions-list.tsx deleted file mode 100644 index e0c7e42d4b..0000000000 --- a/src/components/dashboard/sessions-list.tsx +++ /dev/null @@ -1,206 +0,0 @@ -'use client' - -import { Session } from '@/types' -import { formatAge, parseTokenUsage, getStatusBadgeColor } from '@/lib/utils' - -interface SessionsListProps { - sessions: Session[] -} - -interface SessionCardProps { - session: Session -} - -function SessionCard({ session }: SessionCardProps) { - const tokenUsage = parseTokenUsage(session.tokens) - const statusColor = session.active ? 'success' : 'warning' - - const getSessionTypeIcon = (key: string) => { - if (key.includes('main:main')) return '👑' - if (key.includes('subagent')) return '🤖' - if (key.includes('cron')) return '⏰' - if (key.includes('group')) return '👥' - return '📄' - } - - const getModelColor = (model: string) => { - if (model.includes('opus')) return 'text-purple-400' - if (model.includes('sonnet')) return 'text-blue-400' - if (model.includes('haiku')) return 'text-green-400' - return 'text-gray-400' - } - - const getRoleBadge = (key: string) => { - if (key.includes('main:main')) { - return { label: 'LEAD', color: 'bg-purple-500/20 text-purple-400 border-purple-500/30' } - } - if (key.includes('subagent')) { - return { label: 'WORKER', color: 'bg-blue-500/20 text-blue-400 border-blue-500/30' } - } - if (key.includes('cron')) { - return { label: 'CRON', color: 'bg-orange-500/20 text-orange-400 border-orange-500/30' } - } - return { label: 'SYSTEM', color: 'bg-gray-500/20 text-gray-400 border-gray-500/30' } - } - - const getCurrentTask = (session: Session) => { - // Extract task from session label or key - if (session.label && session.label !== session.key.split(':').pop()) { - return session.label - } - // For sub-agents, try to extract task from key - const parts = session.key.split(':') - if (parts.length > 3 && parts[2] === 'subagent') { - return parts[3] || 'Unknown task' - } - return session.active ? 'Active' : 'Idle' - } - - const roleBadge = getRoleBadge(session.key) - const currentTask = getCurrentTask(session) - - return ( -
-
-
-
- {getSessionTypeIcon(session.key)} -
-
-
-

- {session.key.split(':').pop() || session.key} -

- {/* Role Badge */} - - {roleBadge.label} - -
- - {/* Current Task/Status */} -
- {currentTask} -
- -

- {session.key} -

- -
- - {session.model} - - - • {formatAge(session.age)} - -
-
-
- -
- {/* Working/Status Badge */} -
- {session.active ? 'WORKING' : 'IDLE'} -
- - {/* Token Usage */} - {session.tokens !== '-' && ( -
-
- {session.tokens} -
- {tokenUsage.total > 0 && ( -
-
80 ? 'bg-red-400' : - tokenUsage.percentage > 60 ? 'bg-yellow-400' : - 'bg-green-400' - }`} - style={{ width: `${Math.min(tokenUsage.percentage, 100)}%` }} - /> -
- )} -
- )} -
-
- - {/* Flags */} - {session.flags.length > 0 && ( -
- {session.flags.map((flag, index) => ( - - {flag} - - ))} -
- )} -
- ) -} - -export function SessionsList({ sessions }: SessionsListProps) { - const activeSessions = sessions.filter(s => s.active) - const idleSessions = sessions.filter(s => !s.active) - - return ( -
-
-

Active Sessions

-

- {sessions.length} total • {activeSessions.length} active -

-
- -
- {sessions.length === 0 ? ( -
-
🤖
-

No sessions active

-

Sessions will appear here when agents start

-
- ) : ( -
- {/* Active Sessions */} - {activeSessions.length > 0 && ( -
-

- - Active ({activeSessions.length}) -

-
- {activeSessions.map((session) => ( - - ))} -
-
- )} - - {/* Idle Sessions */} - {idleSessions.length > 0 && ( -
-

- - Idle ({idleSessions.length}) -

-
- {idleSessions.map((session) => ( - - ))} -
-
- )} -
- )} -
-
- ) -} \ No newline at end of file diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx deleted file mode 100644 index 24b646c44d..0000000000 --- a/src/components/dashboard/sidebar.tsx +++ /dev/null @@ -1,201 +0,0 @@ -'use client' - -import Image from 'next/image' -import { useEffect, useState } from 'react' -import { useMissionControl } from '@/store' -import { useNavigateToPanel } from '@/lib/navigation' -import { createClientLogger } from '@/lib/client-logger' -import { Button } from '@/components/ui/button' - -const log = createClientLogger('Sidebar') - -type SystemStats = { - memory?: { - used: number - total: number - } - disk?: { - usage?: string - } - processes?: unknown[] -} - -function readSystemStats(value: unknown): SystemStats | null { - if (!value || typeof value !== 'object') return null - const record = value as Record - const memory = record.memory && typeof record.memory === 'object' ? record.memory as Record : null - const disk = record.disk && typeof record.disk === 'object' ? record.disk as Record : null - - return { - memory: memory && typeof memory.used === 'number' && typeof memory.total === 'number' - ? { used: memory.used, total: memory.total } - : undefined, - disk: disk - ? { usage: typeof disk.usage === 'string' ? disk.usage : undefined } - : undefined, - processes: Array.isArray(record.processes) ? record.processes : undefined, - } -} - -interface MenuItem { - id: string - label: string - icon: string - description?: string -} - -const menuItems: MenuItem[] = [ - { id: 'overview', label: 'Overview', icon: '📊', description: 'System dashboard' }, - { id: 'chat', label: 'Chat', icon: '💬', description: 'Agent chat sessions' }, - { id: 'tasks', label: 'Task Board', icon: '📋', description: 'Kanban task management' }, - { id: 'agents', label: 'Agent Squad', icon: '🤖', description: 'Agent management & status' }, - { id: 'activity', label: 'Activity Feed', icon: '📣', description: 'Real-time activity stream' }, - { id: 'notifications', label: 'Notifications', icon: '🔔', description: 'Mentions & alerts' }, - { id: 'standup', label: 'Daily Standup', icon: '📈', description: 'Generate standup reports' }, - { id: 'spawn', label: 'Spawn Agent', icon: '🚀', description: 'Launch new sub-agents' }, - { id: 'logs', label: 'Logs', icon: '📝', description: 'Real-time log viewer' }, - { id: 'cron', label: 'Cron Jobs', icon: '⏰', description: 'Automated tasks' }, - { id: 'memory', label: 'Memory', icon: '🧠', description: 'Knowledge browser' }, - { id: 'tokens', label: 'Tokens', icon: '💰', description: 'Usage & cost tracking' }, - { id: 'channels', label: 'Channels', icon: '📡', description: 'Messaging platform status' }, - { id: 'nodes', label: 'Nodes', icon: '🖥', description: 'Connected instances' }, - { id: 'exec-approvals', label: 'Approvals', icon: '✅', description: 'Exec approval queue' }, - { id: 'debug', label: 'Debug', icon: '🐛', description: 'System diagnostics' }, -] - -export function Sidebar() { - const { activeTab, connection, sessions } = useMissionControl() - const navigateToPanel = useNavigateToPanel() - const [systemStats, setSystemStats] = useState(null) - - useEffect(() => { - let cancelled = false - fetch('/api/status?action=overview') - .then(res => res.json()) - .then(data => { if (!cancelled) setSystemStats(readSystemStats(data)) }) - .catch(err => log.error('Failed to fetch system status:', err)) - return () => { cancelled = true } - }, []) - - const activeSessions = sessions.filter(s => s.active).length - const totalSessions = sessions.length - - return ( - - ) -} diff --git a/src/components/dashboard/stats-grid.tsx b/src/components/dashboard/stats-grid.tsx deleted file mode 100644 index 15e594e5f0..0000000000 --- a/src/components/dashboard/stats-grid.tsx +++ /dev/null @@ -1,185 +0,0 @@ -'use client' - -import { formatUptime } from '@/lib/utils' - -interface Stats { - totalSessions: number - activeSessions: number - totalMessages: number - uptime: number - errors: number -} - -interface StatsGridProps { - stats: Stats - systemStats?: any -} - -// SVG stat icons (16x16, stroke-based) -function MonitorIcon() { - return ( - - - - - ) -} - -function PulseCircleIcon() { - return ( - - - - - ) -} - -function ChatIcon() { - return ( - - - - - ) -} - -function UptimeIcon() { - return ( - - - - - ) -} - -function WarningTriangleIcon() { - return ( - - - - - ) -} - -function CheckCircleIcon() { - return ( - - - - - ) -} - -interface StatCardProps { - title: string - value: string | number - icon: React.ReactNode - trend?: 'up' | 'down' | 'stable' - subtitle?: string - color?: 'default' | 'success' | 'warning' | 'danger' -} - -function StatCard({ title, value, icon, trend, subtitle, color = 'default' }: StatCardProps) { - const colorClasses = { - default: 'void-panel', - success: 'void-panel border-void-mint/30', - warning: 'void-panel border-void-amber/30', - danger: 'void-panel border-void-crimson/30' - } - - const iconColorClasses = { - default: 'text-void-cyan', - success: 'text-void-mint', - warning: 'text-void-amber', - danger: 'text-void-crimson' - } - - const glowClasses = { - default: '', - success: 'badge-glow-success', - warning: 'badge-glow-warning', - danger: 'badge-glow-error' - } - - return ( -
-
-
-

{title}

-
-

{value}

- {trend && ( - - {trend === 'up' ? '\u2197' : trend === 'down' ? '\u2198' : '\u2192'} - - )} -
- {subtitle && ( -

{subtitle}

- )} -
-
- {icon} -
-
-
- ) -} - -export function StatsGrid({ stats, systemStats }: StatsGridProps) { - const uptimeFormatted = systemStats?.uptime ? - formatUptime(systemStats.uptime) : - formatUptime(Date.now() - stats.uptime) - - return ( -
- } - trend="stable" - color="default" - /> - - } - trend="up" - subtitle={`${stats.totalSessions > 0 ? Math.round((stats.activeSessions / stats.totalSessions) * 100) : 0}% active`} - color="success" - /> - - } - trend="up" - subtitle="Total processed" - color="default" - /> - - } - trend="stable" - subtitle="System running" - color="default" - /> - - 0 ? : } - trend={stats.errors > 0 ? "up" : "stable"} - subtitle="Past 24h" - color={stats.errors > 0 ? "danger" : "success"} - /> -
- ) -} \ No newline at end of file diff --git a/src/components/hud/connection-status.tsx b/src/components/hud/connection-status.tsx deleted file mode 100644 index 4123b0c2e1..0000000000 --- a/src/components/hud/connection-status.tsx +++ /dev/null @@ -1,125 +0,0 @@ -'use client' - -import { useMissionControl } from '@/store' -import { Button } from '@/components/ui/button' - -interface ConnectionStatusProps { - isConnected: boolean - onConnect: () => void - onDisconnect: () => void - onReconnect?: () => void -} - -export function ConnectionStatus({ - isConnected, - onConnect, - onDisconnect, - onReconnect -}: ConnectionStatusProps) { - const { connection } = useMissionControl() - const displayUrl = connection.url || 'ws://:' - const isGatewayOptional = process.env.NEXT_PUBLIC_GATEWAY_OPTIONAL === 'true' - - const getStatusColor = () => { - if (isConnected) return 'bg-green-500 animate-pulse' - if (connection.reconnectAttempts > 0) return 'bg-yellow-500' - if (isGatewayOptional && !isConnected) return 'bg-blue-500' - return 'bg-red-500' - } - - const getStatusText = () => { - if (isConnected) { - return 'Connected' - } - if (connection.reconnectAttempts > 0) { - return `Reconnecting... (${connection.reconnectAttempts}/10)` - } - if (isGatewayOptional && !isConnected) { - return 'Gateway Optional (Standalone)' - } - return 'Disconnected' - } - - return ( -
- {/* Connection Status Indicator */} -
-
- - {getStatusText()} - - - {displayUrl} - -
- - {/* Connection Controls */} -
- {isConnected ? ( - - ) : connection.reconnectAttempts > 0 ? ( - - ) : ( -
- - {onReconnect && ( - - )} -
- )} -
- - {/* Real-time Status */} -
- {connection.latency ? ( - <> - Latency: - {connection.latency}ms - - ) : connection.lastConnected ? ( - <> - Last connected: - - {new Date(connection.lastConnected).toLocaleTimeString()} - - - ) : ( - <> - Status: - Not connected - - )} -
-
- ) -} diff --git a/src/components/layout/promo-banner.tsx b/src/components/layout/promo-banner.tsx deleted file mode 100644 index 6bfb1be121..0000000000 --- a/src/components/layout/promo-banner.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client' - -export function PromoBanner() { - return ( -
-
- -

- Built with care by nyk · available for client and custom AI orchestration work. -

-
- - -
- ) -} diff --git a/src/components/panels/agent-cost-panel.tsx b/src/components/panels/agent-cost-panel.tsx deleted file mode 100644 index f469b47032..0000000000 --- a/src/components/panels/agent-cost-panel.tsx +++ /dev/null @@ -1,705 +0,0 @@ -'use client' - -import { useState, useEffect, useCallback, useRef } from 'react' -import { useTranslations } from 'next-intl' -import { Button } from '@/components/ui/button' -import { Loader } from '@/components/ui/loader' -import { createClientLogger } from '@/lib/client-logger' -import { - PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, CartesianGrid, - Tooltip, Legend, ResponsiveContainer, BarChart, Bar, -} from 'recharts' - -const log = createClientLogger('AgentCostPanel') - -interface TokenStats { - totalTokens: number; totalCost: number; requestCount: number - avgTokensPerRequest: number; avgCostPerRequest: number -} - -interface AgentCostData { - stats: TokenStats - models: Record - sessions: string[] - timeline: Array<{ date: string; cost: number; tokens: number }> -} - -interface AgentCostsResponse { - agents: Record - timeframe: string - recordCount: number -} - -interface ByAgentModelBreakdown { - model: string - input_tokens: number - output_tokens: number - request_count: number - cost: number -} - -interface ByAgentEntry { - agent: string - total_input_tokens: number - total_output_tokens: number - total_tokens: number - total_cost: number - session_count: number - request_count: number - last_active: string - models: ByAgentModelBreakdown[] -} - -interface ByAgentResponse { - agents: ByAgentEntry[] - summary: { - total_cost: number - total_tokens: number - agent_count: number - days: number - } -} - -interface TaskCostEntry { - taskId: number - title: string - status: string - priority: string - assignedTo?: string | null - project: { id?: number | null; name?: string | null; slug?: string | null; ticketRef?: string | null } - stats: TokenStats - models: Record -} - -interface TaskCostsResponse { - summary: TokenStats - tasks: TaskCostEntry[] - agents: Record - unattributed: TokenStats - timeframe: string -} - -const REFRESH_INTERVAL = 30_000 // 30s auto-refresh - -const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d', '#ffc658', '#ff6b6b'] - -function PerAgentBreakdown({ - data, - formatCost, - formatNumber, - onRefresh, -}: { - data: ByAgentResponse | null - formatCost: (cost: number) => string - formatNumber: (num: number) => string - onRefresh: () => void -}) { - const t = useTranslations('agentCost') - const [expandedRow, setExpandedRow] = useState(null) - - if (!data || data.agents.length === 0) { - return ( -
-
{t('noPerAgentData')}
-
{t('noPerAgentDataSubtitle')}
- -
- ) - } - - const { agents, summary } = data - const maxCost = Math.max(...agents.map((a) => a.total_cost), 0.0001) - - return ( -
- {/* Summary row */} -
-
-
{summary.agent_count}
-
{t('agentCountDB')}
-
-
-
{formatCost(summary.total_cost)}
-
{t('totalCostDays', { days: summary.days })}
-
-
-
{formatNumber(summary.total_tokens)}
-
{t('totalTokens')}
-
-
-
- {summary.total_tokens > 0 - ? `$${(summary.total_cost / summary.total_tokens * 1000).toFixed(4)}` - : '-'} -
-
{t('avgPer1kTokens')}
-
-
- - {/* Cost bar chart */} -
-

{t('perAgentCostDB')}

-
- - ({ - name: a.agent.length > 12 ? a.agent.slice(0, 11) + '\u2026' : a.agent, - cost: Number(a.total_cost.toFixed(4)), - input: a.total_input_tokens, - output: a.total_output_tokens, - }))}> - - - - - dataKey === 'cost' ? formatCost(Number(value)) : formatNumber(Number(value)) - } /> - - - - -
-
- - {/* Agent detail table */} -
-

{t('agentTokenBreakdown')}

-
- {agents.map((agent) => { - const costShare = (agent.total_cost / Math.max(summary.total_cost, 0.0001)) * 100 - const isExpanded = expandedRow === agent.agent - return ( -
- - - {isExpanded && ( -
-
-
-
{t('inputTokens')}
-
{formatNumber(agent.total_input_tokens)}
-
-
-
{t('outputTokens')}
-
{formatNumber(agent.total_output_tokens)}
-
-
-
{t('ioRatio')}
-
- {agent.total_output_tokens > 0 - ? (agent.total_input_tokens / agent.total_output_tokens).toFixed(2) - : '-'} -
-
-
-
{t('lastActive')}
-
- {new Date(agent.last_active).toLocaleDateString()} -
-
-
- - {agent.models.length > 0 && ( -
-
{t('modelBreakdown')}
-
- {agent.models.map((m) => { - const displayName = m.model.split('/').pop() || m.model - return ( -
- {displayName} -
- {formatNumber(m.input_tokens)} {t('inSuffix')} - {formatNumber(m.output_tokens)} {t('outSuffix')} - {t('reqs', { count: m.request_count })} - {formatCost(m.cost)} -
-
- ) - })} -
-
- )} -
- )} -
- ) - })} -
-
-
- ) -} - -export function AgentCostPanel() { - const t = useTranslations('agentCost') - const [selectedTimeframe, setSelectedTimeframe] = useState<'hour' | 'day' | 'week' | 'month'>('day') - const [data, setData] = useState(null) - const [taskData, setTaskData] = useState(null) - const [byAgentData, setByAgentData] = useState(null) - const [isLoading, setIsLoading] = useState(false) - const [expandedAgent, setExpandedAgent] = useState(null) - const [expandedSection, setExpandedSection] = useState<'models' | 'tasks'>('tasks') - const [activeView, setActiveView] = useState<'overview' | 'per-agent'>('overview') - const refreshTimer = useRef | null>(null) - - // Map timeframe to days param for the by-agent endpoint - const timeframeToDays = (tf: string): number => { - switch (tf) { - case 'hour': return 1 - case 'day': return 1 - case 'week': return 7 - case 'month': return 30 - default: return 30 - } - } - - const loadData = useCallback(async () => { - setIsLoading(true) - try { - const [agentRes, taskRes, byAgentRes] = await Promise.all([ - fetch(`/api/tokens?action=agent-costs&timeframe=${selectedTimeframe}`), - fetch(`/api/tokens?action=task-costs&timeframe=${selectedTimeframe}`), - fetch(`/api/tokens/by-agent?days=${timeframeToDays(selectedTimeframe)}`), - ]) - const [agentJson, taskJson, byAgentJson] = await Promise.all([ - agentRes.json(), taskRes.json(), byAgentRes.json(), - ]) - setData(agentJson) - setTaskData(taskJson) - setByAgentData(byAgentJson) - } catch (err) { - log.error('Failed to load agent costs:', err) - } finally { - setIsLoading(false) - } - }, [selectedTimeframe]) - - useEffect(() => { loadData() }, [loadData]) - - // Auto-refresh every 30s - useEffect(() => { - refreshTimer.current = setInterval(loadData, REFRESH_INTERVAL) - return () => { if (refreshTimer.current) clearInterval(refreshTimer.current) } - }, [loadData]) - - // Helper: get tasks for a specific agent from task-costs data - const getAgentTasks = useCallback((agentName: string): TaskCostEntry[] => { - if (!taskData) return [] - const agentEntry = taskData.agents[agentName] - if (!agentEntry) return [] - return taskData.tasks.filter(t => agentEntry.taskIds.includes(t.taskId)) - }, [taskData]) - - const formatNumber = (num: number) => { - if (num >= 1_000_000) return (num / 1_000_000).toFixed(1) + 'M' - if (num >= 1_000) return (num / 1_000).toFixed(1) + 'K' - return num.toString() - } - - const formatCost = (cost: number) => '$' + cost.toFixed(4) - - const agents = data?.agents ? Object.entries(data.agents) : [] - const sortedAgents = agents.sort(([, a], [, b]) => b.stats.totalCost - a.stats.totalCost) - - const totalCost = agents.reduce((sum, [, a]) => sum + a.stats.totalCost, 0) - const totalAgents = agents.length - - const mostExpensive = sortedAgents[0] - const mostEfficient = agents.length > 0 - ? agents.reduce((best, curr) => { - const currCostPer1k = curr[1].stats.totalCost / Math.max(1, curr[1].stats.totalTokens) * 1000 - const bestCostPer1k = best[1].stats.totalCost / Math.max(1, best[1].stats.totalTokens) * 1000 - return currCostPer1k < bestCostPer1k ? curr : best - }) - : null - - // Pie chart data - const pieData = sortedAgents.slice(0, 8).map(([name, a]) => ({ - name, - value: a.stats.totalCost, - })) - - // Line chart: top 5 agents over time - const top5 = sortedAgents.slice(0, 5).map(([name]) => name) - const allDates = new Set() - for (const [name, a] of agents) { - if (top5.includes(name)) { - for (const t of a.timeline) allDates.add(t.date) - } - } - const trendData = [...allDates].sort().map(date => { - const point: Record = { date: date.slice(5) } // MM-DD - for (const name of top5) { - const entry = data?.agents[name]?.timeline.find(t => t.date === date) - point[name] = entry?.cost ?? 0 - } - return point - }) - - // Efficiency bars - const efficiencyData = sortedAgents.map(([name, a]) => ({ - name, - costPer1k: a.stats.totalCost / Math.max(1, a.stats.totalTokens) * 1000, - })) - const maxCostPer1k = Math.max(...efficiencyData.map(d => d.costPer1k), 0.0001) - - return ( -
- {/* Header */} -
-
-
-

{t('title')}

-

{t('subtitle')}

-
-
-
- - -
-
- {(['hour', 'day', 'week', 'month'] as const).map((tf) => ( - - ))} -
-
-
-
- - {isLoading ? ( - - ) : activeView === 'per-agent' ? ( - - ) : !data || agents.length === 0 ? ( -
-
{t('noAgentCostData')}
-
{t('noAgentCostSubtitle')}
- -
- ) : ( -
- {/* Summary Cards */} -
-
-
{totalAgents}
-
{t('activeAgents')}
-
-
-
{formatCost(totalCost)}
-
{t('totalCost', { timeframe: selectedTimeframe })}
-
-
-
{mostExpensive?.[0] || '-'}
-
{t('mostExpensive')}
- {mostExpensive &&
{formatCost(mostExpensive[1].stats.totalCost)} ({((mostExpensive[1].stats.totalCost / Math.max(totalCost, 0.0001)) * 100).toFixed(0)}%)
} -
-
-
{mostEfficient?.[0] || '-'}
-
{t('mostEfficient')}
- {mostEfficient && ( -
- ${(mostEfficient[1].stats.totalCost / Math.max(1, mostEfficient[1].stats.totalTokens) * 1000).toFixed(4)}/1K tokens -
- )} -
-
-
- {taskData ? `${((1 - taskData.unattributed.totalCost / Math.max(totalCost, 0.0001)) * 100).toFixed(0)}%` : '-'} -
-
{t('taskAttributed')}
- {taskData && taskData.unattributed.totalCost > 0 && ( -
{t('unattributed', { cost: formatCost(taskData.unattributed.totalCost) })}
- )} -
-
- - {/* Charts */} -
- {/* Cost Distribution Pie */} -
-

{t('costDistributionByAgent')}

-
- {pieData.length === 0 ? ( -
{t('noCostData')}
- ) : ( - - - - {pieData.map((_, i) => ( - - ))} - - formatCost(Number(value))} /> - - - - )} -
-
- - {/* Cost Trend Lines */} -
-

{t('costTrends')}

-
- {trendData.length === 0 ? ( -
{t('noTrendData')}
- ) : ( - - - - - - formatCost(Number(value))} /> - - {top5.map((name, i) => ( - - ))} - - - )} -
-
-
- - {/* Agent Cost Comparison Bar Chart */} - {sortedAgents.length > 1 && ( -
-

{t('costComparison')}

-
- - ({ - name: name.length > 12 ? name.slice(0, 11) + '…' : name, - cost: Number(a.stats.totalCost.toFixed(4)), - tokens: a.stats.totalTokens, - requests: a.stats.requestCount, - }))}> - - - - - dataKey === 'cost' ? formatCost(Number(value)) : formatNumber(Number(value)) - } /> - - - - -
-
- )} - - {/* Cost Efficiency Comparison */} -
-

{t('costEfficiency')}

-
- {efficiencyData.map(({ name, costPer1k }) => ( -
-
{name}
-
-
-
-
-
-
${costPer1k.toFixed(4)}/1K
-
- ))} -
-
- - {/* Agent Cost Ranking Table */} -
-

{t('agentCostRanking')}

-
- {sortedAgents.map(([name, a], index) => { - const costShare = ((a.stats.totalCost / Math.max(totalCost, 0.0001)) * 100) - const agentTasks = getAgentTasks(name) - return ( -
- - - {expandedAgent === name && ( -
- {/* Tab switcher for expanded content */} -
- - -
- - {expandedSection === 'tasks' && ( -
- {agentTasks.length === 0 ? ( -
{t('noTaskCosts')}
- ) : ( -
- {agentTasks.map((task) => { - const taskShare = ((task.stats.totalCost / Math.max(a.stats.totalCost, 0.0001)) * 100) - return ( -
-
- {task.priority} - {task.project.ticketRef && ( - {task.project.ticketRef} - )} - {task.title} - {task.status} -
-
- {taskShare.toFixed(0)}% - {formatCost(task.stats.totalCost)} -
-
- ) - })} -
- )} -
- )} - - {expandedSection === 'models' && ( -
-
- {Object.entries(a.models) - .sort(([, x], [, y]) => y.totalCost - x.totalCost) - .map(([model, stats]) => { - const displayName = model.split('/').pop() || model - return ( -
- {displayName} -
- {formatNumber(stats.totalTokens)} {t('tokens')} - {t('reqs', { count: stats.requestCount })} - {formatCost(stats.totalCost)} -
-
- ) - })} -
-
- )} -
- )} -
- ) - })} -
-
-
- )} -
- ) -} diff --git a/src/components/panels/agent-history-panel.tsx b/src/components/panels/agent-history-panel.tsx deleted file mode 100644 index 84b9069906..0000000000 --- a/src/components/panels/agent-history-panel.tsx +++ /dev/null @@ -1,340 +0,0 @@ -'use client' - -import { useState, useEffect, useCallback } from 'react' -import { useTranslations } from 'next-intl' -import { Button } from '@/components/ui/button' -import { useMissionControl } from '@/store' -import { useSmartPoll } from '@/lib/use-smart-poll' - -interface AgentActivity { - id: number - type: string - entity_type: string - entity_id: number - actor: string - description: string - data?: any - created_at: number - entity?: any -} - -interface SessionInfo { - id: string - key: string - kind: string - age: string - model: string - tokens: string - active: boolean -} - -const typeColors: Record = { - agent_status_change: 'text-yellow-400', - task_created: 'text-green-400', - task_updated: 'text-blue-400', - task_deleted: 'text-red-400', - comment_added: 'text-purple-400', - agent_created: 'text-cyan-400', - standup_generated: 'text-orange-400', - mention: 'text-pink-400', - assignment: 'text-indigo-400', -} - -const typeIcons: Record = { - agent_status_change: '~', - task_created: '+', - task_updated: '~', - task_deleted: 'x', - comment_added: '#', - agent_created: '@', - standup_generated: '!', - mention: '>', - assignment: '=', -} - -export function AgentHistoryPanel() { - const t = useTranslations('agentHistory') - const { agents } = useMissionControl() - const [selectedAgent, setSelectedAgent] = useState('') - const [activities, setActivities] = useState([]) - const [sessions, setSessions] = useState([]) - const [loading, setLoading] = useState(false) - const [total, setTotal] = useState(0) - const [page, setPage] = useState(0) - const limit = 50 - - // Auto-select first agent - useEffect(() => { - if (!selectedAgent && agents.length > 0) { - setSelectedAgent(agents[0].name) - } - }, [agents, selectedAgent]) - - const fetchActivities = useCallback(async () => { - if (!selectedAgent) return - setLoading(true) - try { - const params = new URLSearchParams({ - actor: selectedAgent, - limit: limit.toString(), - offset: (page * limit).toString(), - }) - const res = await fetch(`/api/activities?${params}`) - if (!res.ok) return - const data = await res.json() - setActivities(data.activities || []) - setTotal(data.total || 0) - } catch { /* silent */ } finally { - setLoading(false) - } - }, [selectedAgent, page]) - - const fetchSessions = useCallback(async () => { - try { - const res = await fetch('/api/sessions') - if (!res.ok) return - const data = await res.json() - setSessions(data.sessions || []) - } catch { /* silent */ } - }, []) - - useEffect(() => { fetchActivities() }, [fetchActivities]) - useEffect(() => { fetchSessions() }, [fetchSessions]) - useSmartPoll(fetchActivities, 30000, { pauseWhenDisconnected: true }) - - const agentSessions = sessions.filter(s => s.key.includes(selectedAgent)) - const selectedAgentData = agents.find(a => a.name === selectedAgent) - const totalPages = Math.ceil(total / limit) - - function formatTime(ts: number) { - const d = new Date(ts * 1000) - return d.toLocaleString(undefined, { - month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', - }) - } - - function formatRelative(ts: number) { - const diff = Math.floor(Date.now() / 1000) - ts - if (diff < 60) return 'just now' - if (diff < 3600) return `${Math.floor(diff / 60)}m ago` - if (diff < 86400) return `${Math.floor(diff / 3600)}h ago` - return `${Math.floor(diff / 86400)}d ago` - } - - // Group activities by day - const groupedByDay: Record = {} - for (const act of activities) { - const day = new Date(act.created_at * 1000).toLocaleDateString(undefined, { - weekday: 'long', month: 'short', day: 'numeric', - }) - if (!groupedByDay[day]) groupedByDay[day] = [] - groupedByDay[day].push(act) - } - - return ( -
- {/* Header */} -
-
-

{t('title')}

-

- {t('eventCount', { count: total, agent: selectedAgent || t('noAgentSelected') })} -

-
-
- - {/* Agent selector */} -
- {agents.map(a => ( - - ))} -
- - {selectedAgent && ( -
- {/* Agent info card */} -
- {selectedAgentData && ( -
-
-
- - {selectedAgentData.name.slice(0, 2).toUpperCase()} - -
-
-

{selectedAgentData.name}

-

{selectedAgentData.role}

-
-
- -
-
- {t('status')} - {selectedAgentData.status} -
- {selectedAgentData.last_seen && ( -
- {t('lastSeen')} - {formatRelative(selectedAgentData.last_seen)} -
- )} - {selectedAgentData.last_activity && ( -
- {t('lastAction')} - - {selectedAgentData.last_activity} - -
- )} - {selectedAgentData.taskStats && ( - <> -
-
- {t('tasksAssigned')} - {selectedAgentData.taskStats.assigned} -
-
- {t('inProgress')} - {selectedAgentData.taskStats.in_progress} -
-
- {t('completed')} - {selectedAgentData.taskStats.completed} -
- - )} -
-
- )} - - {/* Active sessions for this agent */} - {agentSessions.length > 0 && ( -
-

{t('activeSessions')}

-
- {agentSessions.map(s => ( -
-
- - {s.kind} -
-
- {s.model} - {s.tokens} tokens - {s.age} -
-
- ))} -
-
- )} -
- - {/* Activity timeline */} -
- {loading ? ( -
- {[...Array(6)].map((_, i) => ( -
- ))} -
- ) : activities.length === 0 ? ( -
-

{t('noActivity', { agent: selectedAgent })}

-
- ) : ( -
- {Object.entries(groupedByDay).map(([day, dayActivities]) => ( -
-
- {day} - - {t('eventsBadge', { count: dayActivities.length })} -
-
- {dayActivities.map(act => ( -
- {/* Timeline dot */} - - - {/* Icon */} - - {typeIcons[act.type] || '?'} - - - {/* Content */} -
-

{act.description}

- {act.entity && act.entity.title && ( -

- {act.entity.type === 'task' ? `Task: ${act.entity.title}` : act.entity.title} -

- )} -
- - {/* Time */} - - {new Date(act.created_at * 1000).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })} - -
- ))} -
-
- ))} - - {/* Pagination */} - {totalPages > 1 && ( -
- - - {t('page', { current: page + 1, total: totalPages })} - - -
- )} -
- )} -
-
- )} -
- ) -} diff --git a/src/components/panels/documents-panel.tsx b/src/components/panels/documents-panel.tsx deleted file mode 100644 index d1099d71e6..0000000000 --- a/src/components/panels/documents-panel.tsx +++ /dev/null @@ -1,323 +0,0 @@ -'use client' - -import { useCallback, useEffect, useMemo, useState } from 'react' -import { useTranslations } from 'next-intl' -import { MarkdownRenderer } from '@/components/markdown-renderer' - -interface DocsTreeNode { - path: string - name: string - type: 'file' | 'directory' - size?: number - modified?: number - children?: DocsTreeNode[] -} - -interface DocsTreeResponse { - roots: string[] - tree: DocsTreeNode[] - error?: string -} - -interface DocsContentResponse { - path: string - content: string - size: number - modified: number - error?: string -} - -interface DocsSearchResult { - path: string - name: string - matches: number -} - -interface DocsSearchResponse { - results: DocsSearchResult[] - error?: string -} - -function collectFilePaths(nodes: DocsTreeNode[]): string[] { - const filePaths: string[] = [] - for (const node of nodes) { - if (node.type === 'file') { - filePaths.push(node.path) - continue - } - if (node.children && node.children.length > 0) { - filePaths.push(...collectFilePaths(node.children)) - } - } - return filePaths -} - -function formatBytes(value: number): string { - if (value < 1024) return `${value} B` - if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB` - return `${(value / (1024 * 1024)).toFixed(1)} MB` -} - -function formatTime(value: number): string { - return new Date(value).toLocaleString() -} - -export function DocumentsPanel() { - const t = useTranslations('documents') - const [tree, setTree] = useState([]) - const [roots, setRoots] = useState([]) - const [loadingTree, setLoadingTree] = useState(true) - const [treeError, setTreeError] = useState(null) - const [selectedPath, setSelectedPath] = useState(null) - const [docContent, setDocContent] = useState('') - const [docMeta, setDocMeta] = useState<{ size: number; modified: number } | null>(null) - const [loadingDoc, setLoadingDoc] = useState(false) - const [docError, setDocError] = useState(null) - const [searchQuery, setSearchQuery] = useState('') - const [searchResults, setSearchResults] = useState([]) - const [searching, setSearching] = useState(false) - const [searchError, setSearchError] = useState(null) - const [expandedDirs, setExpandedDirs] = useState>(new Set()) - - const loadTree = useCallback(async () => { - setLoadingTree(true) - setTreeError(null) - try { - const res = await fetch('/api/docs/tree') - const data = (await res.json()) as DocsTreeResponse - if (!res.ok) throw new Error(data.error || 'Failed to load documents') - - setTree(data.tree || []) - setRoots(data.roots || []) - const defaultExpanded = new Set((data.roots || []).filter(Boolean)) - setExpandedDirs(defaultExpanded) - } catch (error) { - setTree([]) - setRoots([]) - setTreeError((error as Error).message || 'Failed to load documents') - } finally { - setLoadingTree(false) - } - }, []) - - const loadDoc = useCallback(async (path: string) => { - setLoadingDoc(true) - setDocError(null) - setSelectedPath(path) - try { - const res = await fetch(`/api/docs/content?path=${encodeURIComponent(path)}`) - const data = (await res.json()) as DocsContentResponse - if (!res.ok) throw new Error(data.error || 'Failed to load document') - setDocContent(data.content || '') - setDocMeta({ size: data.size, modified: data.modified }) - } catch (error) { - setDocContent('') - setDocMeta(null) - setDocError((error as Error).message || 'Failed to load document') - } finally { - setLoadingDoc(false) - } - }, []) - - useEffect(() => { - void loadTree() - }, [loadTree]) - - const filePaths = useMemo(() => collectFilePaths(tree), [tree]) - - useEffect(() => { - if (selectedPath) return - if (filePaths.length === 0) return - void loadDoc(filePaths[0]) - }, [filePaths, loadDoc, selectedPath]) - - useEffect(() => { - const query = searchQuery.trim() - if (query.length < 2) { - setSearchResults([]) - setSearchError(null) - setSearching(false) - return - } - - const handle = setTimeout(async () => { - setSearching(true) - setSearchError(null) - try { - const res = await fetch(`/api/docs/search?q=${encodeURIComponent(query)}&limit=100`) - const data = (await res.json()) as DocsSearchResponse - if (!res.ok) throw new Error(data.error || 'Failed to search docs') - setSearchResults(data.results || []) - } catch (error) { - setSearchResults([]) - setSearchError((error as Error).message || 'Failed to search docs') - } finally { - setSearching(false) - } - }, 250) - - return () => clearTimeout(handle) - }, [searchQuery]) - - const isShowingSearch = searchQuery.trim().length >= 2 - - const toggleDir = (path: string) => { - setExpandedDirs((prev) => { - const next = new Set(prev) - if (next.has(path)) next.delete(path) - else next.add(path) - return next - }) - } - - const renderNode = (node: DocsTreeNode, depth = 0) => { - if (node.type === 'directory') { - const isOpen = expandedDirs.has(node.path) - return ( -
- - {isOpen && node.children && ( -
- {node.children.map((child) => renderNode(child, depth + 1))} -
- )} -
- ) - } - - const active = selectedPath === node.path - return ( - - ) - } - - return ( -
-
- - -
-
-

{t('viewerTitle')}

-

- {t('viewerDescription')} -

-
- - {!selectedPath && ( -
{t('selectFile')}
- )} - - {selectedPath && ( -
-
-
{selectedPath}
- {docMeta && ( -
- {formatBytes(docMeta.size)} • {t('updated')} {formatTime(docMeta.modified)} -
- )} -
- - {loadingDoc &&
{t('loadingDocument')}
} - {docError &&
{docError}
} - - {!loadingDoc && !docError && ( -
- -
- )} -
- )} -
-
-
- ) -} diff --git a/src/components/panels/session-details-panel.tsx b/src/components/panels/session-details-panel.tsx deleted file mode 100644 index 6019f93e6d..0000000000 --- a/src/components/panels/session-details-panel.tsx +++ /dev/null @@ -1,741 +0,0 @@ -'use client' - -import { useState, useCallback, useRef } from 'react' -import { useTranslations } from 'next-intl' -import { Button } from '@/components/ui/button' -import { useMissionControl } from '@/store' -import { useSmartPoll } from '@/lib/use-smart-poll' -import { createClientLogger } from '@/lib/client-logger' - -const log = createClientLogger('SessionDetails') - -type TimeWindow = '1h' | '6h' | '24h' | '7d' | 'all' -type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' -type VerboseLevel = 'off' | 'on' | 'full' -type ReasoningLevel = 'off' | 'on' | 'stream' - -const selectClass = - 'px-2 py-1 border border-border rounded bg-background text-foreground text-xs focus:outline-none focus:ring-2 focus:ring-primary/50' - -export function SessionDetailsPanel() { - const t = useTranslations('sessionDetails') - const { - sessions, - selectedSession, - setSelectedSession, - setSessions, - availableModels - } = useMissionControl() - - // Smart polling for sessions (60s, visibility-aware) - const loadSessions = useCallback(async () => { - try { - const response = await fetch('/api/sessions') - const data = await response.json() - setSessions(data.sessions || data) - } catch (error) { - log.error('Failed to load sessions:', error) - } - }, [setSessions]) - - useSmartPoll(loadSessions, 60000, { pauseWhenConnected: true }) - - const [controllingSession, setControllingSession] = useState(null) - const [sessionFilter, setSessionFilter] = useState<'all' | 'active' | 'idle'>('all') - const [sortBy, setSortBy] = useState<'age' | 'tokens' | 'model'>('age') - const [expandedSession, setExpandedSession] = useState(null) - - // Time window and toggle filters - const [timeWindow, setTimeWindow] = useState('all') - const [includeGlobal, setIncludeGlobal] = useState(true) - const [includeUnknown, setIncludeUnknown] = useState(true) - - // Inline label editing - const [editingLabel, setEditingLabel] = useState(null) - const [labelValue, setLabelValue] = useState('') - const labelInputRef = useRef(null) - - // Delete confirmation - const [confirmingDelete, setConfirmingDelete] = useState(null) - - const getModelInfo = (modelName: string) => { - const matchedAlias = availableModels - .map(m => m.alias) - .find(alias => modelName.toLowerCase().includes(alias.toLowerCase())) - - return availableModels.find(m => - m.name === modelName || - m.alias === modelName || - m.alias === matchedAlias - ) || { alias: modelName, name: modelName, provider: 'unknown', description: 'Unknown model' } - } - - const parseTokenUsage = (tokenString: string) => { - const match = tokenString.match(/(\d+(?:\.\d+)?)(k|m)?\/(\d+(?:\.\d+)?)(k|m)?\s*\((\d+(?:\.\d+)?)%\)/) - if (!match) return { used: 0, total: 0, percentage: 0 } - - const used = parseFloat(match[1]) * (match[2] === 'k' ? 1000 : match[2] === 'm' ? 1000000 : 1) - const total = parseFloat(match[3]) * (match[4] === 'k' ? 1000 : match[4] === 'm' ? 1000000 : 1) - const percentage = parseFloat(match[5]) - - return { used, total, percentage } - } - - const getSessionTypeIcon = (sessionKey: string) => { - if (sessionKey.includes(':main:main')) return '👑' - if (sessionKey.includes(':subagent:')) return '🤖' - if (sessionKey.includes(':cron:')) return '⏰' - if (sessionKey.includes(':group:')) return '👥' - if (sessionKey.includes(':global:')) return '🌐' - return '💬' - } - - const getSessionType = (sessionKey: string) => { - if (sessionKey.includes(':main:main')) return 'Main' - if (sessionKey.includes(':subagent:')) return 'Sub-agent' - if (sessionKey.includes(':cron:')) return 'Cron' - if (sessionKey.includes(':group:')) return 'Group' - if (sessionKey.includes(':global:')) return 'Global' - return 'Unknown' - } - - const getSessionStatus = (session: any) => { - if (!session.active) return 'idle' - const tokenUsage = parseTokenUsage(session.tokens) - if (tokenUsage.percentage > 95) return 'critical' - if (tokenUsage.percentage > 80) return 'warning' - return 'active' - } - - const getStatusColor = (status: string) => { - switch (status) { - case 'active': return 'text-green-400' - case 'warning': return 'text-yellow-400' - case 'critical': return 'text-red-400' - case 'idle': return 'text-muted-foreground' - default: return 'text-muted-foreground' - } - } - - // Time window filter - const timeWindowMs: Record = { - '1h': 60 * 60 * 1000, - '6h': 6 * 60 * 60 * 1000, - '24h': 24 * 60 * 60 * 1000, - '7d': 7 * 24 * 60 * 60 * 1000, - 'all': Infinity, - } - - const filteredSessions = sessions.filter(session => { - // Status filter - switch (sessionFilter) { - case 'active': if (!session.active) return false; break - case 'idle': if (session.active) return false; break - } - - // Time window filter - if (timeWindow !== 'all' && session.lastActivity) { - const cutoff = Date.now() - timeWindowMs[timeWindow] - if (session.lastActivity < cutoff) return false - } - - // Global filter - if (!includeGlobal && session.key?.includes(':global:')) return false - - // Unknown type filter - if (!includeUnknown && getSessionType(session.key) === 'Unknown') return false - - return true - }) - - const sortedSessions = [...filteredSessions].sort((a, b) => { - switch (sortBy) { - case 'tokens': - return parseTokenUsage(b.tokens).percentage - parseTokenUsage(a.tokens).percentage - case 'model': - return a.model.localeCompare(b.model) - case 'age': - default: - if (a.age === 'just now') return -1 - if (b.age === 'just now') return 1 - return a.age.localeCompare(b.age) - } - }) - - const handleSessionSelect = (session: any) => { - setSelectedSession(session.id) - setExpandedSession(expandedSession === session.id ? null : session.id) - } - - const sendSessionAction = async ( - action: string, - sessionKey: string, - payload: Record, - method: 'POST' | 'DELETE' = 'POST' - ) => { - const lockKey = `${action}-${sessionKey}` - setControllingSession(lockKey) - try { - const url = method === 'DELETE' - ? '/api/sessions' - : `/api/sessions?action=${action}` - const res = await fetch(url, { - method, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ sessionKey, ...payload }), - }) - const data = await res.json() - if (!res.ok) { - alert(data.error || `Failed: ${action}`) - return false - } - return true - } catch { - alert(`Failed: ${action}`) - return false - } finally { - setControllingSession(null) - } - } - - const handleLabelSave = async (sessionKey: string) => { - if (editingLabel !== sessionKey) return - await sendSessionAction('set-label', sessionKey, { label: labelValue }) - setEditingLabel(null) - } - - const handleDeleteSession = async (sessionKey: string) => { - const ok = await sendSessionAction('delete', sessionKey, {}, 'DELETE') - if (ok) { - setConfirmingDelete(null) - loadSessions() - } - } - - return ( -
-
-

{t('title')}

-

- {t('subtitle')} -

-
- - {/* Filters and Controls */} -
-
- {/* Filter by Status */} -
- - -
- - {/* Sort by */} -
- - -
- - {/* Time Window */} -
- - -
- - {/* Toggles */} - - - - {/* Session Stats (pushed right) */} -
- {t('sessionCount', { filtered: filteredSessions.length, total: sessions.length })} - {' '}• {t('activeCount', { count: sessions.filter(s => s.active).length })} -
-
-
- -
- {/* Sessions List */} -
- {sortedSessions.length === 0 ? ( -
-
- {t('noSessionsMatch')} -
-
- ) : ( - sortedSessions.map((session) => { - const modelInfo = getModelInfo(session.model) - const tokenUsage = parseTokenUsage(session.tokens) - const status = getSessionStatus(session) - const isExpanded = expandedSession === session.id - - return ( -
handleSessionSelect(session)} - > -
- {/* Header */} -
-
-
- {getSessionTypeIcon(session.key)} -
-

- {session.key} -

-
- {getSessionType(session.key)} - - - {status.charAt(0).toUpperCase() + status.slice(1)} - - - {session.age} -
-
-
-
-
- {session.flags.map((flag: string, index: number) => ( - - {flag} - - ))} -
-
-
- - {/* Model and Token Usage */} -
-
-
{t('model')}
-
{modelInfo.alias}
-
{modelInfo.provider}
-
-
-
{t('tokenUsage')}
-
{session.tokens}
-
-
95 ? 'bg-red-500' : - tokenUsage.percentage > 80 ? 'bg-yellow-500' : 'bg-green-500' - }`} - style={{ width: `${Math.min(tokenUsage.percentage, 100)}%` }} - >
-
-
-
- - {/* Expanded Details */} - {isExpanded && ( -
-
-

{t('sessionDetails')}

-
-
- {t('kind')}: - {session.kind} -
-
- {t('id')}: - {session.id} -
- {session.lastActivity && ( -
- {t('lastActivity')}: - - {new Date(session.lastActivity).toLocaleTimeString()} - -
- )} - {session.messageCount && ( -
- {t('messages')}: - {session.messageCount} -
- )} -
-
- - {/* Editable Label */} -
-

{t('label')}

- {editingLabel === session.key ? ( -
e.stopPropagation()}> - setLabelValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') handleLabelSave(session.key) - if (e.key === 'Escape') setEditingLabel(null) - }} - onBlur={() => handleLabelSave(session.key)} - maxLength={100} - className="flex-1 px-2 py-1 border border-border rounded bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/50" - autoFocus - /> -
- ) : ( - - )} -
- - {/* Session Controls */} -
-

{t('sessionControls')}

-
e.stopPropagation()}> - {/* Thinking Level */} -
- - -
- - {/* Verbose Level */} -
- - -
- - {/* Reasoning Level */} -
- - -
-
-
- - {/* Model Information */} -
-

{t('modelInformation')}

-
-
-
- {t('fullName')}: -
{modelInfo.name}
-
-
- {t('provider')}: -
{modelInfo.provider}
-
-
- {t('description')}: -
{modelInfo.description}
-
-
-
-
- - {/* Actions */} -
- - - - - {/* Delete Button */} - {confirmingDelete === session.key ? ( -
e.stopPropagation()}> - {t('deleteConfirm')} - - -
- ) : ( - - )} -
-
- )} -
-
- ) - }) - )} -
- - {/* Session Summary */} -
-
-

{t('sessionOverview')}

- -
-
- {t('totalSessions')}: - {sessions.length} -
-
- {t('active')}: - - {sessions.filter(s => s.active).length} - -
-
- {t('idle')}: - - {sessions.filter(s => !s.active).length} - -
-
- {t('subAgents')}: - - {sessions.filter(s => s.key.includes(':subagent:')).length} - -
-
- {t('cronJobs')}: - - {sessions.filter(s => s.key.includes(':cron:')).length} - -
-
-
- - {/* Model Distribution */} -
-

{t('modelDistribution')}

- -
- {Object.entries( - sessions.reduce((acc, session) => { - const model = getModelInfo(session.model).alias - acc[model] = (acc[model] || 0) + 1 - return acc - }, {} as Record) - ).map(([model, count]) => ( -
- {model} -
- {count} -
-
-
-
-
- ))} -
-
- - {/* High Token Usage Alert */} - {sessions.some(s => parseTokenUsage(s.tokens).percentage > 80) && ( -
-

{t('highTokenUsage')}

-
- {t('highTokenUsageDesc', { count: sessions.filter(s => parseTokenUsage(s.tokens).percentage > 80).length })} -
-
- )} -
-
-
- ) -} diff --git a/src/components/panels/token-dashboard-panel.tsx b/src/components/panels/token-dashboard-panel.tsx deleted file mode 100644 index 9561a8d4a1..0000000000 --- a/src/components/panels/token-dashboard-panel.tsx +++ /dev/null @@ -1,1197 +0,0 @@ -'use client' - -import { useState, useEffect, useCallback, useMemo } from 'react' -import { useTranslations } from 'next-intl' -import { Button } from '@/components/ui/button' -import { Loader } from '@/components/ui/loader' -import { useMissionControl } from '@/store' -import { createClientLogger } from '@/lib/client-logger' -import { detectProvider } from '@/lib/token-utils' -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell } from 'recharts' - -const log = createClientLogger('TokenDashboard') - -interface UsageStats { - summary: { - totalTokens: number - totalCost: number - requestCount: number - avgTokensPerRequest: number - avgCostPerRequest: number - } - models: Record - sessions: Record - timeframe: string - recordCount: number -} - -interface TrendData { - trends: Array<{ timestamp: string; tokens: number; cost: number; requests: number }> - timeframe: string -} - -type DashboardView = 'overview' | 'sessions' - -interface SessionCostEntry { - sessionId: string - sessionKey?: string - model: string - totalTokens: number - inputTokens: number - outputTokens: number - totalCost: number - requestCount: number - firstSeen: string - lastSeen: string -} - -type TimezoneOption = { label: string; offset: number } - -const TIMEZONE_OPTIONS: TimezoneOption[] = [ - { label: 'Local', offset: NaN }, - { label: 'UTC', offset: 0 }, - { label: 'UTC-8 (PST)', offset: -8 }, - { label: 'UTC-7 (MST)', offset: -7 }, - { label: 'UTC-6 (CST)', offset: -6 }, - { label: 'UTC-5 (EST)', offset: -5 }, - { label: 'UTC+1 (CET)', offset: 1 }, - { label: 'UTC+5:30 (IST)', offset: 5.5 }, - { label: 'UTC+8 (CST)', offset: 8 }, - { label: 'UTC+9 (JST)', offset: 9 }, -] - -const deriveProvider = detectProvider - -export function TokenDashboardPanel() { - const { sessions } = useMissionControl() - const t = useTranslations('tokenDashboard') - - const [selectedTimeframe, setSelectedTimeframe] = useState<'hour' | 'day' | 'week' | 'month'>('day') - const [usageStats, setUsageStats] = useState(null) - const [trendData, setTrendData] = useState(null) - const [isLoading, setIsLoading] = useState(false) - const [isExporting, setIsExporting] = useState(false) - const [view, setView] = useState('overview') - const [sessionCosts, setSessionCosts] = useState([]) - const [sessionSort, setSessionSort] = useState<'cost' | 'tokens' | 'requests' | 'recent'>('cost') - const [chartMode, setChartMode] = useState<'incremental' | 'cumulative'>('incremental') - - // Filter state - const [modelFilters, setModelFilters] = useState>(new Set()) - const [sessionFilters, setSessionFilters] = useState>(new Set()) - - // Timezone state - const [selectedTimezone, setSelectedTimezone] = useState(TIMEZONE_OPTIONS[0]) - - const loadUsageStats = useCallback(async () => { - setIsLoading(true) - try { - const response = await fetch(`/api/tokens?action=stats&timeframe=${selectedTimeframe}`) - const data = await response.json() - setUsageStats(data) - } catch (error) { - log.error('Failed to load usage stats:', error) - } finally { - setIsLoading(false) - } - }, [selectedTimeframe]) - - const loadTrendData = useCallback(async () => { - try { - const response = await fetch(`/api/tokens?action=trends&timeframe=${selectedTimeframe}`) - const data = await response.json() - setTrendData(data) - } catch (error) { - log.error('Failed to load trend data:', error) - } - }, [selectedTimeframe]) - - const loadSessionCosts = useCallback(async () => { - try { - const response = await fetch(`/api/tokens?action=session-costs&timeframe=${selectedTimeframe}`) - const data = await response.json() - if (Array.isArray(data?.sessions)) { - setSessionCosts(data.sessions) - } else if (usageStats?.sessions) { - const entries: SessionCostEntry[] = Object.entries(usageStats.sessions).map(([sessionId, stats]) => { - const info = sessions.find(s => s.id === sessionId) - return { - sessionId, - sessionKey: info?.key, - model: '', - totalTokens: stats.totalTokens, - inputTokens: 0, - outputTokens: 0, - totalCost: stats.totalCost, - requestCount: stats.requestCount, - firstSeen: '', - lastSeen: '', - } - }) - setSessionCosts(entries) - } - } catch { - if (usageStats?.sessions) { - const entries: SessionCostEntry[] = Object.entries(usageStats.sessions).map(([sessionId, stats]) => { - const info = sessions.find(s => s.id === sessionId) - return { - sessionId, - sessionKey: info?.key, - model: '', - totalTokens: stats.totalTokens, - inputTokens: 0, - outputTokens: 0, - totalCost: stats.totalCost, - requestCount: stats.requestCount, - firstSeen: '', - lastSeen: '', - } - }) - setSessionCosts(entries) - } - } - }, [selectedTimeframe, usageStats, sessions]) - - useEffect(() => { - loadUsageStats() - loadTrendData() - }, [loadUsageStats, loadTrendData]) - - useEffect(() => { - if (view === 'sessions') loadSessionCosts() - }, [view, loadSessionCosts]) - - // Filtered stats based on active filter chips - const filteredUsageStats = useMemo((): UsageStats | null => { - if (!usageStats) return null - if (modelFilters.size === 0 && sessionFilters.size === 0) return usageStats - - const filteredModels: typeof usageStats.models = {} - const filteredSessions: typeof usageStats.sessions = {} - - // Filter models - for (const [model, stats] of Object.entries(usageStats.models)) { - if (modelFilters.size > 0 && !modelFilters.has(model)) continue - filteredModels[model] = stats - } - - // Filter sessions - for (const [sessionId, stats] of Object.entries(usageStats.sessions)) { - if (sessionFilters.size > 0 && !sessionFilters.has(sessionId)) continue - filteredSessions[sessionId] = stats - } - - // Recalculate summary from filtered models - const sourceEntries = Object.values(modelFilters.size > 0 ? filteredModels : usageStats.models) - const totalTokens = sourceEntries.reduce((sum, s) => sum + s.totalTokens, 0) - const totalCost = sourceEntries.reduce((sum, s) => sum + s.totalCost, 0) - const requestCount = sourceEntries.reduce((sum, s) => sum + s.requestCount, 0) - - return { - ...usageStats, - summary: { - totalTokens, - totalCost, - requestCount, - avgTokensPerRequest: requestCount > 0 ? Math.round(totalTokens / requestCount) : 0, - avgCostPerRequest: requestCount > 0 ? totalCost / requestCount : 0, - }, - models: filteredModels, - sessions: filteredSessions, - } - }, [usageStats, modelFilters, sessionFilters]) - - // Client-side CSV export from currently displayed data - const exportClientCsv = useCallback(() => { - if (!filteredUsageStats) return - setIsExporting(true) - try { - const headers = ['timestamp', 'model', 'session', 'inputTokens', 'outputTokens', 'totalTokens', 'cost'] - const rows: string[] = [headers.join(',')] - - // Export model-level rows - for (const [model, stats] of Object.entries(filteredUsageStats.models)) { - rows.push([ - new Date().toISOString(), - `"${model}"`, - '', - '', - '', - stats.totalTokens, - stats.totalCost.toFixed(4), - ].join(',')) - } - - // Export session-level rows - for (const [sessionId, stats] of Object.entries(filteredUsageStats.sessions)) { - rows.push([ - new Date().toISOString(), - '', - `"${sessionId}"`, - '', - '', - stats.totalTokens, - stats.totalCost.toFixed(4), - ].join(',')) - } - - // Export session cost detail rows if available - for (const entry of sessionCosts) { - rows.push([ - entry.lastSeen || new Date().toISOString(), - `"${entry.model}"`, - `"${entry.sessionId}"`, - entry.inputTokens, - entry.outputTokens, - entry.totalTokens, - entry.totalCost.toFixed(4), - ].join(',')) - } - - const csv = rows.join('\n') - const blob = new Blob([csv], { type: 'text/csv' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.style.display = 'none' - a.href = url - a.download = `usage-${selectedTimeframe}-${new Date().toISOString().split('T')[0]}.csv` - document.body.appendChild(a) - a.click() - URL.revokeObjectURL(url) - document.body.removeChild(a) - } catch (error) { - log.error('Client CSV export failed:', error) - } finally { - setIsExporting(false) - } - }, [filteredUsageStats, sessionCosts, selectedTimeframe]) - - const exportData = async (format: 'json' | 'csv') => { - setIsExporting(true) - try { - const response = await fetch(`/api/tokens?action=export&timeframe=${selectedTimeframe}&format=${format}`) - - if (!response.ok) { - throw new Error('Export failed') - } - - const blob = await response.blob() - const url = window.URL.createObjectURL(blob) - const a = document.createElement('a') - a.style.display = 'none' - a.href = url - a.download = `token-usage-${selectedTimeframe}-${new Date().toISOString().split('T')[0]}.${format}` - document.body.appendChild(a) - a.click() - window.URL.revokeObjectURL(url) - document.body.removeChild(a) - } catch (error) { - log.error('Export failed:', error) - alert('Export failed: ' + error) - } finally { - setIsExporting(false) - } - } - - const formatNumber = (num: number) => { - if (num >= 1000000) { - return (num / 1000000).toFixed(1) + 'M' - } - if (num >= 1000) { - return (num / 1000).toFixed(1) + 'K' - } - return num.toString() - } - - const formatCost = (cost: number) => { - return '$' + cost.toFixed(4) - } - - const getModelDisplayName = (modelName: string) => { - const parts = modelName.split('/') - return parts[parts.length - 1] || modelName - } - - const formatTimestamp = useCallback((isoString: string) => { - const date = new Date(isoString) - if (isNaN(selectedTimezone.offset)) { - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - } - const utcMs = date.getTime() + date.getTimezoneOffset() * 60000 - const adjusted = new Date(utcMs + selectedTimezone.offset * 3600000) - return adjusted.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - }, [selectedTimezone]) - - const toggleModelFilter = (model: string) => { - setModelFilters(prev => { - const next = new Set(prev) - if (next.has(model)) next.delete(model) - else next.add(model) - return next - }) - } - - const toggleSessionFilter = (sessionId: string) => { - setSessionFilters(prev => { - const next = new Set(prev) - if (next.has(sessionId)) next.delete(sessionId) - else next.add(sessionId) - return next - }) - } - - const clearAllFilters = () => { - setModelFilters(new Set()) - setSessionFilters(new Set()) - } - - const hasActiveFilters = modelFilters.size > 0 || sessionFilters.size > 0 - - const prepareModelChartData = () => { - if (!filteredUsageStats?.models) return [] - return Object.entries(filteredUsageStats.models) - .map(([model, stats]) => ({ - name: getModelDisplayName(model), - tokens: stats.totalTokens, - cost: stats.totalCost, - requests: stats.requestCount - })) - .sort((a, b) => b.cost - a.cost) - } - - const preparePieChartData = () => { - if (!filteredUsageStats?.models) return [] - const data = Object.entries(filteredUsageStats.models) - .map(([model, stats]) => ({ - name: getModelDisplayName(model), - value: stats.totalCost, - tokens: stats.totalTokens - })) - .sort((a, b) => b.value - a.value) - .slice(0, 6) - - return data - } - - const prepareProviderPieData = () => { - if (!filteredUsageStats?.models) return [] - const providerMap: Record = {} - for (const [model, stats] of Object.entries(filteredUsageStats.models)) { - const provider = deriveProvider(model) - if (!providerMap[provider]) providerMap[provider] = { cost: 0, tokens: 0 } - providerMap[provider].cost += stats.totalCost - providerMap[provider].tokens += stats.totalTokens - } - return Object.entries(providerMap) - .map(([name, data]) => ({ name, value: data.cost, tokens: data.tokens })) - .sort((a, b) => b.value - a.value) - } - - const prepareTrendChartData = () => { - if (!trendData?.trends) return [] - const raw = trendData.trends.map(trend => ({ - time: formatTimestamp(trend.timestamp), - tokens: trend.tokens, - cost: trend.cost, - requests: trend.requests - })) - - if (chartMode === 'cumulative') { - let cumTokens = 0 - let cumCost = 0 - let cumRequests = 0 - return raw.map(d => { - cumTokens += d.tokens - cumCost += d.cost - cumRequests += d.requests - return { ...d, tokens: cumTokens, cost: cumCost, requests: cumRequests } - }) - } - - return raw - } - - // Find peak error/request hour for trend highlighting - const peakTrendHour = useMemo(() => { - if (!trendData?.trends || trendData.trends.length === 0) return null - let peak = trendData.trends[0] - for (const t of trendData.trends) { - if (t.requests > peak.requests) peak = t - } - return formatTimestamp(peak.timestamp) - }, [trendData, formatTimestamp]) - - const sortedSessionCosts = [...sessionCosts].sort((a, b) => { - switch (sessionSort) { - case 'cost': return b.totalCost - a.totalCost - case 'tokens': return b.totalTokens - a.totalTokens - case 'requests': return b.requestCount - a.requestCount - case 'recent': return (b.lastSeen || '').localeCompare(a.lastSeen || '') - default: return 0 - } - }) - - const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d'] - const PROVIDER_COLORS: Record = { - Anthropic: '#d97706', - OpenAI: '#10b981', - Google: '#3b82f6', - Mistral: '#f97316', - Meta: '#6366f1', - DeepSeek: '#06b6d4', - Cohere: '#ec4899', - Other: '#6b7280', - } - - // Enhanced performance metrics - const getPerformanceMetrics = () => { - if (!filteredUsageStats?.models) return null - - const models = Object.entries(filteredUsageStats.models) - if (models.length === 0) return null - - let mostEfficient = { model: models[0][0], stats: models[0][1] } - for (const [model, stats] of models) { - const costPerToken = stats.totalCost / Math.max(1, stats.totalTokens) - const bestCostPerToken = mostEfficient.stats.totalCost / Math.max(1, mostEfficient.stats.totalTokens) - if (costPerToken < bestCostPerToken) { - mostEfficient = { model, stats } - } - } - - let mostUsed = { model: models[0][0], stats: models[0][1] } - for (const [model, stats] of models) { - if (stats.requestCount > mostUsed.stats.requestCount) { - mostUsed = { model, stats } - } - } - - let mostExpensive = { model: models[0][0], stats: models[0][1] } - for (const [model, stats] of models) { - const costPerToken = stats.totalCost / Math.max(1, stats.totalTokens) - const bestCostPerToken = mostExpensive.stats.totalCost / Math.max(1, mostExpensive.stats.totalTokens) - if (costPerToken > bestCostPerToken) { - mostExpensive = { model, stats } - } - } - - const totalTokens = filteredUsageStats.summary.totalTokens - const currentCost = filteredUsageStats.summary.totalCost - const efficientCostPerToken = mostEfficient.stats.totalCost / Math.max(1, mostEfficient.stats.totalTokens) - const potentialCost = totalTokens * efficientCostPerToken - const potentialSavings = Math.max(0, currentCost - potentialCost) - - return { - mostEfficient, - mostUsed, - mostExpensive, - potentialSavings, - savingsPercentage: currentCost > 0 ? (potentialSavings / currentCost) * 100 : 0 - } - } - - const performanceMetrics = getPerformanceMetrics() - - const getAlerts = () => { - const alerts = [] - - if (filteredUsageStats && filteredUsageStats.summary.totalCost !== undefined && filteredUsageStats.summary.totalCost > 100) { - alerts.push({ - type: 'warning', - title: 'High Usage Cost', - message: `Total cost of ${formatCost(filteredUsageStats.summary.totalCost)} exceeds $100 threshold`, - suggestion: 'Consider using more cost-effective models for routine tasks' - }) - } - - if (performanceMetrics && performanceMetrics.savingsPercentage !== undefined && performanceMetrics.savingsPercentage > 20) { - alerts.push({ - type: 'info', - title: 'Optimization Opportunity', - message: `Using ${getModelDisplayName(performanceMetrics.mostEfficient.model)} could save ${formatCost(performanceMetrics.potentialSavings)} (${performanceMetrics.savingsPercentage.toFixed(1)}%)`, - suggestion: 'Consider switching routine tasks to more efficient models' - }) - } - - if (filteredUsageStats && filteredUsageStats.summary.requestCount !== undefined && filteredUsageStats.summary.requestCount > 1000) { - alerts.push({ - type: 'info', - title: 'High Request Volume', - message: `${filteredUsageStats.summary.requestCount} requests in selected timeframe`, - suggestion: 'Consider implementing request batching or caching for efficiency' - }) - } - - return alerts - } - - const alerts = getAlerts() - - // Available models and sessions for filter chips - const availableModels = useMemo(() => { - if (!usageStats?.models) return [] - return Object.keys(usageStats.models).sort() - }, [usageStats]) - - const availableSessions = useMemo(() => { - if (!usageStats?.sessions) return [] - return Object.keys(usageStats.sessions).sort() - }, [usageStats]) - - // Cache token stats from session costs (if available in the data) - const cacheStats = useMemo(() => { - // Aggregate from session cost entries if they have cache token info - // For now show zeroes; real data flows once backend provides cacheReadTokens/cacheWriteTokens - let cacheRead = 0 - let cacheWrite = 0 - for (const entry of sessionCosts) { - const e = entry as unknown as Record - if (typeof e.cacheReadTokens === 'number') cacheRead += e.cacheReadTokens - if (typeof e.cacheWriteTokens === 'number') cacheWrite += e.cacheWriteTokens - } - return cacheRead > 0 || cacheWrite > 0 ? { cacheRead, cacheWrite } : null - }, [sessionCosts]) - - return ( -
-
-
-
-

{t('title')}

-

- {t('subtitle')} -

-
-
-
- - -
-
- {(['hour', 'day', 'week', 'month'] as const).map((timeframe) => ( - - ))} -
-
-
-
- - {/* Filter Chips Bar */} - {view === 'overview' && usageStats && (availableModels.length > 0 || availableSessions.length > 0) && ( -
- {t('filtersLabel')} - {availableModels.map(model => ( - - ))} - {availableSessions.length > 0 && availableModels.length > 0 && ( - | - )} - {availableSessions.slice(0, 8).map(sessionId => { - const info = sessions.find(s => s.id === sessionId) - const label = info?.key || sessionId.split(':')[0] || sessionId - return ( - - ) - })} - {hasActiveFilters && ( - - )} -
- )} - - {/* Timezone Selector */} - {view === 'overview' && ( -
- {t('timezoneLabel')} - -
- )} - - {view === 'sessions' ? ( -
-
- {t('sortByLabel')} - {(['cost', 'tokens', 'requests', 'recent'] as const).map(s => ( - - ))} -
- - {sortedSessionCosts.length === 0 ? ( -
-

{t('noSessionCostData')}

-

{t('noSessionCostSubtitle')}

-
- ) : ( -
- {sortedSessionCosts.map((entry) => { - const sessionInfo = sessions.find(s => s.id === entry.sessionId) - return ( -
-
-
-
- {entry.sessionKey || sessionInfo?.key || entry.sessionId} -
-
- {sessionInfo?.active && } - {sessionInfo?.active ? t('sessionActive') : t('sessionInactive')} - {entry.model && | {getModelDisplayName(entry.model)}} - {sessionInfo?.kind && | {sessionInfo.kind}} -
-
-
-
{formatCost(entry.totalCost)}
-
{formatNumber(entry.totalTokens)} tokens
-
-
-
-
{entry.requestCount} {t('requests')}
-
{formatNumber(entry.inputTokens || 0)} {t('inSuffix')}
-
{formatNumber(entry.outputTokens || 0)} {t('outSuffix')}
-
- {entry.totalTokens > 0 - ? {formatCost(entry.totalCost / entry.requestCount)} - : '-' - }{' '}{t('avgPerRequest')} -
-
-
- ) - })} -
- )} -
- ) : isLoading ? ( - - ) : filteredUsageStats ? ( -
- {/* Overview Stats */} -
-
-
- {formatNumber(filteredUsageStats.summary.totalTokens)} -
-
- {t('totalTokens', { timeframe: selectedTimeframe })} -
-
- -
-
- {formatCost(filteredUsageStats.summary.totalCost)} -
-
- {t('totalCost', { timeframe: selectedTimeframe })} -
-
- -
-
- {formatNumber(filteredUsageStats.summary.requestCount)} -
-
- {t('apiRequests')} -
-
- -
-
- {formatNumber(filteredUsageStats.summary.avgTokensPerRequest)} -
-
- {t('avgTokensPerRequest')} -
-
- - {cacheStats && ( - <> -
-
- {formatNumber(cacheStats.cacheRead)} -
-
- {t('cacheReadTokens')} -
-
- -
-
- {formatNumber(cacheStats.cacheWrite)} -
-
- {t('cacheWriteTokens')} -
-
- - )} -
- - {/* Charts Section */} -
- {/* Usage Trends Chart */} -
-
-

{t('usageTrends', { timeframe: selectedTimeframe })}

-
- {peakTrendHour && ( - - {t('peakLabel')} {peakTrendHour} - - )} -
- - -
-
-
-
- {prepareTrendChartData().length === 0 ? ( -
{t('noTrendData')}
- ) : ( - - - - - - - - - - - - )} -
-
- - {/* Model Usage Bar Chart */} -
-

{t('tokenUsageByModel')}

-
- {prepareModelChartData().length === 0 ? ( -
{t('noModelUsageData')}
- ) : ( - - - - - - [formatNumber(Number(value)), name]} /> - - - - )} -
-
- - {/* Cost Distribution Pie Chart */} -
-

{t('costDistributionByModel')}

-
- {preparePieChartData().length === 0 ? ( -
{t('noCostData')}
- ) : ( - - - - {preparePieChartData().map((_, index) => ( - - ))} - - formatCost(Number(value))} /> - - - - )} -
-
- - {/* Cost by Provider Pie Chart */} -
-

{t('costByProvider')}

-
- {prepareProviderPieData().length === 0 ? ( -
{t('noProviderData')}
- ) : ( -
-
- - - - {prepareProviderPieData().map((entry) => ( - - ))} - - formatCost(Number(value))} /> - - - -
-
- {prepareProviderPieData().map(entry => ( -
-
- - {entry.name} -
- {formatCost(entry.value)} -
- ))} -
-
- )} -
-
-
- - {/* Export Section */} -
-
-

{t('exportData')}

-
- - - -
-
-

- Export token usage data for analysis. "Filtered" exports only the currently displayed data; "Full" exports all records from the server. -

-
- - {/* Performance Insights */} - {performanceMetrics && ( -
-

{t('performanceInsights')}

- - {/* Alerts */} - {alerts.length > 0 && ( -
- {alerts.map((alert, index) => ( -
-
-
- {alert.type === 'warning' ? '!!' : 'i'} -
-
-

{alert.title}

-

{alert.message}

-

{alert.suggestion}

-
-
-
- ))} -
- )} - - {/* Performance Metrics Grid */} -
-
-

{t('mostEfficientModel')}

-
- {getModelDisplayName(performanceMetrics.mostEfficient.model)} -
-
- ${(performanceMetrics.mostEfficient.stats.totalCost / Math.max(1, performanceMetrics.mostEfficient.stats.totalTokens) * 1000).toFixed(4)}/1K tokens -
-
- -
-

{t('mostUsedModel')}

-
- {getModelDisplayName(performanceMetrics.mostUsed.model)} -
-
- {performanceMetrics.mostUsed.stats.requestCount} requests -
-
- -
-

{t('optimizationPotential')}

-
- {formatCost(performanceMetrics.potentialSavings)} -
-
- {t('savingsPossible', { pct: performanceMetrics.savingsPercentage.toFixed(1) })} -
-
-
- - {/* Model Efficiency Comparison */} -
-

{t('modelEfficiencyComparison')}

-
- {Object.entries(filteredUsageStats?.models || {}) - .map(([model, stats]) => { - const costPerToken = stats.totalCost / Math.max(1, stats.totalTokens) * 1000 - const efficiency = 1 / costPerToken - const maxEfficiency = Math.max(...Object.values(filteredUsageStats?.models || {}).map(s => 1 / (s.totalCost / Math.max(1, s.totalTokens) * 1000))) - const barWidth = (efficiency / maxEfficiency) * 100 - - return ( -
-
- {getModelDisplayName(model)} -
-
-
-
-
-
-
- ${costPerToken.toFixed(4)}/1K -
-
- ) - })} -
-
-
- )} - - {/* Detailed Statistics */} -
- {/* Model Statistics */} -
-

{t('modelPerformance')}

- -
- {Object.entries(filteredUsageStats.models) - .sort(([,a], [,b]) => b.totalCost - a.totalCost) - .map(([model, stats]) => { - const avgCostPerRequest = stats.totalCost / Math.max(1, stats.requestCount) - const avgTokensPerRequest = stats.totalTokens / Math.max(1, stats.requestCount) - - return ( -
-
-
- {getModelDisplayName(model)} -
-
-
- {formatCost(stats.totalCost)} -
-
- {formatNumber(stats.totalTokens)} tokens -
-
-
-
-
-
{stats.requestCount}
-
{t('requestsLabel')}
-
-
-
{formatCost(avgCostPerRequest)}
-
{t('avgCost')}
-
-
-
{formatNumber(avgTokensPerRequest)}
-
{t('avgTokens')}
-
-
-
- ) - })} -
-
- - {/* Session Statistics */} -
-

{t('topSessionsByCost')}

- -
- {Object.entries(filteredUsageStats.sessions) - .sort(([,a], [,b]) => b.totalCost - a.totalCost) - .slice(0, 10) - .map(([sessionId, stats]) => { - const sessionInfo = sessions.find(s => s.id === sessionId) - const avgCostPerRequest = stats.totalCost / Math.max(1, stats.requestCount) - - return ( -
-
-
-
- {sessionInfo?.key || sessionId} -
-
- {sessionInfo?.active ? t('sessionActive') : t('sessionInactive')} -
-
-
-
- {formatCost(stats.totalCost)} -
-
- {formatNumber(stats.totalTokens)} tokens -
-
-
-
-
-
{stats.requestCount}
-
{t('requestsLabel')}
-
-
-
{formatCost(avgCostPerRequest)}
-
{t('avgCost')}
-
-
-
- ) - })} -
-
-
-
- ) : ( -
-
{t('noUsageData')}
-
{t('noUsageDataSubtitle')}
- -
- )} -
- ) -} diff --git a/src/components/ui/agent-core-node.tsx b/src/components/ui/agent-core-node.tsx deleted file mode 100644 index c162c183e3..0000000000 --- a/src/components/ui/agent-core-node.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import { memo } from 'react' -import type { NodeProps } from '@xyflow/react' - -interface CoreNodeData { - label: string - agentCount: number -} - -/** - * Central CORE orchestration node for the agent network graph. - * Pulsing concentric cyan rings — the visual identity of Mission Control. - */ -function AgentCoreNodeInner({ data }: NodeProps & { data: CoreNodeData }) { - const { label = 'CORE', agentCount = 0 } = data ?? {} - - return ( -
- {/* Outer ring — slowest pulse */} -
- - {/* Middle ring */} -
- - {/* Inner ring */} -
- - {/* Core circle */} -
- - {label} - - {agentCount > 0 && ( - - {agentCount} - - )} -
-
- ) -} - -export const AgentCoreNode = memo(AgentCoreNodeInner) diff --git a/src/components/ui/online-status.tsx b/src/components/ui/online-status.tsx deleted file mode 100644 index a9cf4032fd..0000000000 --- a/src/components/ui/online-status.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client' - -interface OnlineStatusProps { - isConnected: boolean -} - -export function OnlineStatus({ isConnected }: OnlineStatusProps) { - return ( -
-
- - {isConnected ? 'ONLINE' : 'OFFLINE'} - -
- ) -} \ No newline at end of file diff --git a/src/lib/plugin-loader.ts b/src/lib/plugin-loader.ts deleted file mode 100644 index 88cc74a7e2..0000000000 --- a/src/lib/plugin-loader.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Plugin Loader - * - * Simple explicit loader following the initPro() pattern. - * Plugins register via direct import + init() call. - * - * Dynamic MC_PLUGINS env-based loading can be added later. - */ - -export function loadPlugins(): void { - // Plugins register via direct import + init() call. - // Example: - // import { initHyperbrowserPlugin } from '@/plugins/hyperbrowser' - // initHyperbrowserPlugin() -} diff --git a/src/nav-rail.tsx b/src/nav-rail.tsx deleted file mode 100644 index f7169f8bb6..0000000000 --- a/src/nav-rail.tsx +++ /dev/null @@ -1,500 +0,0 @@ -'use client' - -import Image from 'next/image' -import { useState, useEffect } from 'react' -import { useMissionControl } from '@/store' -import { Button } from '@/components/ui/button' - -interface NavItem { - id: string - label: string - icon: React.ReactNode - priority: boolean // Show in mobile bottom bar -} - -interface NavGroup { - id: string - label?: string // undefined = no header (core group) - items: NavItem[] -} - -const navGroups: NavGroup[] = [ - { - id: 'core', - items: [ - { id: 'overview', label: 'Overview', icon: , priority: true }, - { id: 'agents', label: 'Agents', icon: , priority: true }, - { id: 'tasks', label: 'Tasks', icon: , priority: true }, - { id: 'chat', label: 'Chat', icon: , priority: false }, - ], - }, - { - id: 'observe', - label: 'OBSERVE', - items: [ - { id: 'activity', label: 'Activity', icon: , priority: true }, - { id: 'logs', label: 'Logs', icon: , priority: true }, - { id: 'tokens', label: 'Tokens', icon: , priority: false }, - { id: 'memory', label: 'Memory', icon: , priority: false }, - ], - }, - { - id: 'automate', - label: 'AUTOMATE', - items: [ - { id: 'cron', label: 'Cron', icon: , priority: false }, - { id: 'spawn', label: 'Spawn', icon: , priority: false }, - { id: 'webhooks', label: 'Webhooks', icon: , priority: false }, - { id: 'alerts', label: 'Alerts', icon: , priority: false }, - ], - }, - { - id: 'admin', - label: 'ADMIN', - items: [ - { id: 'users', label: 'Users', icon: , priority: false }, - { id: 'audit', label: 'Audit', icon: , priority: false }, - { id: 'history', label: 'History', icon: , priority: false }, - { id: 'gateways', label: 'Gateways', icon: , priority: false }, - { id: 'gateway-config', label: 'Config', icon: , priority: false }, - { id: 'settings', label: 'Settings', icon: , priority: false }, - ], - }, -] - -// Flat list for mobile bar -const allNavItems = navGroups.flatMap(g => g.items) - -export function NavRail() { - const { activeTab, setActiveTab, connection, sidebarExpanded, collapsedGroups, toggleSidebar, toggleGroup } = useMissionControl() - - // Keyboard shortcut: [ to toggle sidebar - useEffect(() => { - function handleKey(e: KeyboardEvent) { - if (e.key === '[' && !(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || (e.target as HTMLElement)?.isContentEditable)) { - e.preventDefault() - toggleSidebar() - } - } - window.addEventListener('keydown', handleKey) - return () => window.removeEventListener('keydown', handleKey) - }, [toggleSidebar]) - - return ( - <> - {/* Desktop: Grouped sidebar */} - - - {/* Mobile: Bottom tab bar (unchanged) */} - - - ) -} - -function NavButton({ item, active, expanded, onClick }: { - item: NavItem - active: boolean - expanded: boolean - onClick: () => void -}) { - if (expanded) { - return ( - - ) - } - - return ( - - ) -} - -function MobileMoreMenu({ items, activeTab, setActiveTab }: { - items: NavItem[] - activeTab: string - setActiveTab: (tab: string) => void -}) { - const [open, setOpen] = useState(false) - - return ( -
- - - {open && ( - <> -
setOpen(false)} /> -
- {items.map((item) => ( - - ))} -
- - )} -
- ) -} - -// SVG Icons (16x16 viewbox, stroke-based) -function OverviewIcon() { - return ( - - - - - - - ) -} - -function AgentsIcon() { - return ( - - - - - ) -} - -function TasksIcon() { - return ( - - - - - ) -} - -function SessionsIcon() { - return ( - - - - ) -} - -function ActivityIcon() { - return ( - - - - ) -} - -function LogsIcon() { - return ( - - - - - ) -} - -function SpawnIcon() { - return ( - - - - - ) -} - -function CronIcon() { - return ( - - - - - ) -} - -function MemoryIcon() { - return ( - - - - - - ) -} - -function TokensIcon() { - return ( - - - - - ) -} - -function UsersIcon() { - return ( - - - - - - - ) -} - -function HistoryIcon() { - return ( - - - - - - ) -} - -function AuditIcon() { - return ( - - - - - ) -} - -function WebhookIcon() { - return ( - - - - - - - - ) -} - -function GatewayConfigIcon() { - return ( - - - - - - - ) -} - -function GatewaysIcon() { - return ( - - - - - - - - ) -} - -function AlertIcon() { - return ( - - - - - ) -} - -function SettingsIcon() { - return ( - - - - - ) -} diff --git a/src/plugins/hyperbrowser-example.ts b/src/plugins/hyperbrowser-example.ts deleted file mode 100644 index 709367670c..0000000000 --- a/src/plugins/hyperbrowser-example.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Example Plugin: Hyperbrowser - * - * Reference showing how to register Hyperbrowser via the plugin system. - * The actual Hyperbrowser integration lives as a built-in in the - * integrations route (Phase 2). This file demonstrates the plugin API - * for documentation purposes. - * - * Usage: - * import { initHyperbrowserPlugin } from '@/plugins/hyperbrowser-example' - * initHyperbrowserPlugin() - */ - -import { - registerIntegrations, - registerCategories, - registerToolProviders, -} from '@/lib/plugins' - -export function initHyperbrowserPlugin(): void { - registerCategories([ - { id: 'browser', label: 'Browser Automation', order: 8 }, - ]) - - registerIntegrations([ - { - id: 'hyperbrowser', - name: 'Hyperbrowser', - category: 'browser', - envVars: ['HYPERBROWSER_API_KEY'], - testable: true, - recommendation: 'Cloud browser automation for AI agents. Get a key at hyperbrowser.ai', - testHandler: async (envMap: Map) => { - const key = envMap.get('HYPERBROWSER_API_KEY') || process.env.HYPERBROWSER_API_KEY || '' - if (!key) return { ok: false, detail: 'API key not set' } - try { - const res = await fetch('https://app.hyperbrowser.ai/api/v2/sessions', { - headers: { 'x-api-key': key }, - signal: AbortSignal.timeout(5000), - }) - return res.ok - ? { ok: true, detail: 'API key valid' } - : { ok: false, detail: `HTTP ${res.status}` } - } catch (err: any) { - return { ok: false, detail: err.message || 'Connection failed' } - } - }, - }, - ]) - - registerToolProviders([ - { - id: 'hyperbrowser', - name: 'Hyperbrowser', - tools: ['browser', 'web'], - requiredIntegration: 'hyperbrowser', - }, - ]) -} diff --git a/src/styles/design-tokens.ts b/src/styles/design-tokens.ts deleted file mode 100644 index 8b36eae92d..0000000000 --- a/src/styles/design-tokens.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Design tokens for Mission Control "Void" aesthetic. - * Server-safe — no 'use client' directive needed. - * - * Use the `hsl()` helper when you need inline styles (ReactFlow nodes, recharts), - * and reference CSS variables via Tailwind classes everywhere else. - */ - -// --------------------------------------------------------------------------- -// HSL triplet type -// --------------------------------------------------------------------------- -export interface HSL { - h: number - s: number - l: number -} - -// --------------------------------------------------------------------------- -// Void palette -// --------------------------------------------------------------------------- -export const voidPalette = { - background: { h: 215, s: 27, l: 4 }, // #07090C — deepest void - card: { h: 220, s: 30, l: 8 }, // #0F141C - primary: { h: 187, s: 82, l: 53 }, // #22D3EE — cyan - secondary: { h: 220, s: 25, l: 11 }, - muted: { h: 220, s: 20, l: 14 }, - border: { h: 220, s: 20, l: 14 }, - ring: { h: 187, s: 82, l: 53 }, -} as const satisfies Record - -export const voidAccents = { - cyan: { h: 187, s: 82, l: 53 }, // #22D3EE - mint: { h: 160, s: 60, l: 52 }, // #34D399 - amber: { h: 38, s: 92, l: 50 }, // #F59E0B - violet: { h: 263, s: 90, l: 66 }, // #A78BFA - crimson: { h: 0, s: 72, l: 51 }, // #DC2626 -} as const satisfies Record - -export const statusColors = { - success: { h: 160, s: 60, l: 52 }, // mint - warning: { h: 38, s: 92, l: 50 }, // amber - error: { h: 0, s: 72, l: 51 }, // crimson - info: { h: 187, s: 82, l: 53 }, // cyan -} as const satisfies Record - -export const surfaces = { - 0: { h: 215, s: 27, l: 4 }, // deepest void - 1: { h: 222, s: 35, l: 7 }, // dark navy - 2: { h: 220, s: 30, l: 10 }, - 3: { h: 220, s: 25, l: 14 }, -} as const - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** Convert an HSL triplet to a CSS `hsl(...)` string. */ -export function hsl(color: HSL, alpha?: number): string { - if (alpha !== undefined) { - return `hsl(${color.h} ${color.s}% ${color.l}% / ${alpha})` - } - return `hsl(${color.h} ${color.s}% ${color.l}%)` -} - -/** Return the raw HSL string for a CSS variable value (no `hsl()` wrapper). */ -export function hslRaw(color: HSL): string { - return `${color.h} ${color.s}% ${color.l}%` -} - -// --------------------------------------------------------------------------- -// Spacing, radius & typography constants -// --------------------------------------------------------------------------- -export const spacing = { - unit: 4, // base grid unit in px - xs: 4, - sm: 8, - md: 16, - lg: 24, - xl: 32, - '2xl': 48, -} as const - -export const radius = { - xs: 6, - sm: 8, - md: 10, - lg: 12, - xl: 16, - full: 9999, -} as const - -export const fonts = { - sans: 'var(--font-sans)', - mono: 'var(--font-mono)', -} as const diff --git a/vitest.config.ts b/vitest.config.ts index f97fe64dc3..b2f4db40b6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -73,7 +73,6 @@ export default defineConfig(async () => { 'src/lib/device-identity.ts', 'src/lib/utils.ts', 'src/lib/version.ts', - 'src/lib/plugin-loader.ts', 'src/lib/plugins.ts', 'src/lib/office-layout.ts', 'src/lib/skill-registry.ts',