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..5272d9e58d 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: .
@@ -49,6 +59,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 +68,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 +97,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.
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]) => (
-
- ))}
-
-
-
- {/* 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',