From 2eb08f068c7e68f70d9c35fcbcdf88b002cc0535 Mon Sep 17 00:00:00 2001 From: larchanka Date: Mon, 23 Feb 2026 08:11:47 +0100 Subject: [PATCH] AO-14: Refactor DashboardService - split UI assets into separate files for better maintainability --- AI-Agent.md | 5 +- _board/_BOARD.md | 5 + src/services/dashboard-service.ts | 869 ++---------------------------- src/services/dashboard/app.js | 299 ++++++++++ src/services/dashboard/index.html | 145 +++++ src/services/dashboard/styles.css | 379 +++++++++++++ 6 files changed, 863 insertions(+), 839 deletions(-) create mode 100644 src/services/dashboard/app.js create mode 100644 src/services/dashboard/index.html create mode 100644 src/services/dashboard/styles.css diff --git a/AI-Agent.md b/AI-Agent.md index 96e0db4..695a52e 100644 --- a/AI-Agent.md +++ b/AI-Agent.md @@ -50,7 +50,8 @@ AI-Agent/ │ │ ├── rag-service.ts # Embeddings + SQLite; sqlite-vss KNN when available, else dot-product; memory.semantic.*, node.execute semantic_search │ │ ├── tool-host.ts # shell, http_get (Playwright), http_search; sandbox; tool.execute / node.execute tool │ │ ├── cron-manager.ts # node-cron + SQLite schedules; event.cron.* to Logger -│ │ └── dashboard-service.ts # Notion-style web monitoring; reads DBs/logs on demand +│ │ ├── dashboard/ # Dashboard UI (static files: index.html, app.js, styles.css) +│ │ └── dashboard-service.ts # Serves Dashboard UI and API for system monitoring │ └── shared/ │ ├── config.ts # Central config: config.json + env overrides │ ├── protocol.ts # Zod schemas: Envelope, Response, Error, Event @@ -104,7 +105,7 @@ AI-Agent/ - **RAG Service**: Ollama embed; SQLite-backed document store; **sqlite-vss** for KNN vector search when extension loads (macOS/Linux x64), else in-DB dot-product; configurable `rag.embeddingDimensions` (768); `memory.semantic.insert`, `memory.semantic.search`; search is session-scoped by default to prevent cross-chat leakage; `node.execute` for `semantic_search` returns snippets for downstream nodes. - **Tool Host**: Registry of tools (shell, http_get, http_search); sandbox dir from config; `tool.execute` and `node.execute` for type `tool`; shell tool executes commands with sandbox restrictions; browsing supports persistent context (cookies) via `userDataDir`. - **Cron Manager**: SQLite schedule table; node-cron; `cron.schedule.add/list/remove`; emits `event.cron.started/completed/failed` to Logger. -- **Dashboard Service**: Standalone internal monitoring web dashboard; provides real-time overview of tasks, system stats, and event logs via a Notion-inspired UI. +- **Dashboard Service**: Standalone monitoring web dashboard; provides real-time overview of tasks, processes, and IPC logs. UI is served from static files in `src/services/dashboard/`. ### Adapters diff --git a/_board/_BOARD.md b/_board/_BOARD.md index 4cf4207..48ecfe8 100644 --- a/_board/_BOARD.md +++ b/_board/_BOARD.md @@ -7,6 +7,11 @@ ## Done +### AO-14 Refactor Dashboard Service (Split UI/Logic) +- Status: Completed +- Commit: (current) +- Date: 2026-02-23 + ### AO-13 Final Verification Walkthrough - Status: Completed - Commit: dbe54c2 diff --git a/src/services/dashboard-service.ts b/src/services/dashboard-service.ts index 681bcfe..1f8170e 100644 --- a/src/services/dashboard-service.ts +++ b/src/services/dashboard-service.ts @@ -12,388 +12,6 @@ const ROOT_DIR = path.join(__dirname, '..', '..'); const PORT = parseInt(process.env.DASHBOARD_PORT || '3001', 10); -const CSS = ` - :root { - --bg: #ffffff; - --text: #37352f; - --text-muted: #787774; - --subtle: #f7f6f3; - --border: rgba(55, 53, 47, 0.09); - --primary: #2383e2; - --success: #0b6e4f; - --error: #df2a5f; - --warning: #d9730d; - } - - @media (prefers-color-scheme: dark) { - :root { - --bg: #191919; - --text: #d4d4d4; - --text-muted: #8b8b8b; - --subtle: #252525; - --border: rgba(255, 255, 255, 0.09); - --primary: #2ea7ff; - --success: #529e72; - --error: #ff4d4d; - --warning: #ffdc4d; - } - } - - * { box-sizing: border-box; } - body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; - background-color: var(--bg); - color: var(--text); - margin: 0; - padding: 0; - display: flex; - justify-content: center; - line-height: 1.5; - } - - .container { - max-width: 900px; - width: 100%; - padding: 80px 40px; - } - - header { - margin-bottom: 40px; - } - - h1 { - font-size: 40px; - font-weight: 700; - margin: 0 0 10px 0; - display: flex; - align-items: center; - gap: 12px; - } - - .live-indicator { - font-size: 12px; - font-weight: 500; - padding: 2px 8px; - background: var(--subtle); - border: 1px solid var(--border); - border-radius: 4px; - color: var(--success); - display: flex; - align-items: center; - gap: 6px; - } - - .live-indicator::before { - content: ''; - width: 6px; - height: 6px; - background: var(--success); - border-radius: 50%; - display: block; - } - - .description { - color: var(--text-muted); - font-size: 16px; - margin-bottom: 40px; - } - - .grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 20px; - margin-bottom: 40px; - } - - .card { - border: 1px solid var(--border); - border-radius: 8px; - padding: 20px; - background: var(--bg); - } - - .card h2 { - font-size: 14px; - font-weight: 600; - color: var(--text-muted); - margin: 0 0 8px 0; - text-transform: uppercase; - letter-spacing: 0.03em; - } - - .metric-value { - font-size: 32px; - font-weight: 600; - } - - .chart-section { - margin-bottom: 60px; - } - - .chart-section h3 { - font-size: 18px; - font-weight: 600; - margin-bottom: 20px; - padding-bottom: 8px; - border-bottom: 1px solid var(--border); - } - - .charts-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 30px; - } - - .models-section { - margin-top: 40px; - padding-top: 20px; - border-top: 1px solid var(--border); - } - - .model-pill { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 4px 12px; - background: var(--subtle); - border: 1px solid var(--border); - border-radius: 20px; - font-size: 13px; - margin-right: 12px; - margin-bottom: 12px; - } - - .model-pill b { color: var(--primary); } - - .chart-container { - height: 240px; - display: flex; - align-items: center; - justify-content: center; - background: var(--subtle); - border-radius: 8px; - padding: 20px; - } - - .logs-section h3 { - font-size: 18px; - font-weight: 600; - margin-bottom: 20px; - padding-bottom: 8px; - border-bottom: 1px solid var(--border); - } - - table { - width: 100%; - border-collapse: collapse; - table-layout: auto; - } - - th { - text-align: left; - font-size: 12px; - font-weight: 500; - color: var(--text-muted); - padding: 12px 10px; - border-bottom: 1px solid var(--border); - white-space: nowrap; - } - - td { - padding: 12px 10px; - border-bottom: 1px solid var(--border); - font-size: 14px; - vertical-align: middle; - } - - .tag { - font-size: 12px; - padding: 2px 6px; - border-radius: 3px; - font-weight: 500; - } - - .tag.success { background: rgba(11, 110, 79, 0.1); color: var(--success); } - .tag.error { background: rgba(223, 42, 95, 0.1); color: var(--error); } - .tag.warning { background: rgba(217, 115, 13, 0.1); color: var(--warning); } - .tag.running { - background: rgba(35, 131, 226, 0.1); - color: var(--primary); - display: inline-flex; - align-items: center; - gap: 4px; - } - - .tag.complexity-small { background: var(--subtle); color: var(--text-muted); border: 1px solid var(--border); } - .tag.complexity-medium { background: rgba(35, 131, 226, 0.1); color: var(--primary); } - .tag.complexity-large { background: rgba(147, 51, 234, 0.1); color: #9333ea; } - - .pulse { - width: 6px; - height: 6px; - background: var(--primary); - border-radius: 50%; - animation: pulse-ring 1.5s infinite; - } - - @keyframes pulse-ring { - 0% { transform: scale(0.7); opacity: 1; } - 50% { transform: scale(1); opacity: 0.5; } - 100% { transform: scale(0.7); opacity: 1; } - } - - .node-map { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin-top: 8px; - align-items: center; - } - .node-stage { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; - padding: 4px; - border-radius: 6px; - border: 1px dashed var(--border); - background: rgba(0,0,0,0.02); - } - .node-chip { - font-size: 11px; - padding: 2px 8px; - border-radius: 4px; - border: 1px solid var(--border); - background: var(--bg); - color: var(--text-muted); - display: flex; - align-items: center; - gap: 6px; - white-space: nowrap; - } - .node-chip.running { - border-color: var(--primary); - color: var(--primary); - background: rgba(35, 131, 226, 0.05); - font-weight: 600; - } - .node-chip.completed { - border-color: var(--success); - color: var(--success); - background: rgba(11, 110, 79, 0.05); - } - .node-chip.failed { - border-color: var(--error); - color: var(--error); - background: rgba(223, 42, 95, 0.05); - } - - .goal-text { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - font-weight: 500; - line-height: 1.4; - max-height: 2.8em; - } - - .btn-refresh { - font-size: 14px; - font-weight: 500; - padding: 6px 12px; - background: var(--bg); - border: 1px solid var(--border); - border-radius: 4px; - cursor: pointer; - color: var(--text); - transition: background 0.2s; - } - - tr.log-row { - cursor: pointer; - transition: background 0.1s; - } - - tr.log-row:hover { - background-color: var(--subtle); - } - - .log-details-row { - display: none; - } - - .log-details-row.open { - display: table-row; - } - - .log-details-container { - padding: 0 20px 20px 20px; - } - - .log-details-content { - font-size: 11px; - color: var(--text-muted); - background: var(--subtle); - padding: 12px; - border-radius: 4px; - white-space: pre-wrap; - word-break: break-all; - border: 1px solid var(--border); - font-family: monospace; - } - - @media (max-width: 768px) { - .grid { - grid-template-columns: 1fr 1fr; - } - .charts-grid { - grid-template-columns: 1fr; - } - .container { - padding: 40px 20px; - } - } - - /* Tooltip bubble using data-title */ - [data-title] { - position: relative; - } - - [data-title]:hover::after { - content: attr(data-title); - position: absolute; - bottom: 125%; - left: 50%; - transform: translateX(-50%); - padding: 6px 10px; - background: #1e1e1e; - color: #fff; - font-size: 11px; - font-weight: 500; - border-radius: 6px; - white-space: nowrap; - z-index: 1000; - box-shadow: 0 4px 12px rgba(0,0,0,0.2); - border: 1px solid rgba(255,255,255,0.1); - pointer-events: none; - } - - [data-title]:hover::before { - content: ''; - position: absolute; - bottom: 115%; - left: 50%; - transform: translateX(-50%); - border-width: 5px; - border-style: solid; - border-color: #1e1e1e transparent transparent transparent; - z-index: 1001; - pointer-events: none; - } -`; - export class DashboardService extends BaseProcess { private readonly server: http.Server; private processStats: Record = {}; @@ -504,13 +122,7 @@ export class DashboardService extends BaseProcess { return; } - if (url.pathname === '/') { - res.setHeader('Content-Type', 'text/html'); - res.end(this.getHTML()); - return; - } - - if (url.pathname.startsWith('/api/fail-task')) { + if (url.pathname === '/api/fail-task' || url.pathname.startsWith('/api/fail-task')) { const taskId = url.searchParams.get('id'); if (taskId) { try { @@ -528,6 +140,37 @@ export class DashboardService extends BaseProcess { } } + if (url.pathname === '/styles.css') { + const cssPath = path.join(ROOT_DIR, 'src/services/dashboard/styles.css'); + if (fs.existsSync(cssPath)) { + res.setHeader('Content-Type', 'text/css'); + res.end(fs.readFileSync(cssPath, 'utf8')); + return; + } + } + + if (url.pathname === '/app.js') { + const jsPath = path.join(ROOT_DIR, 'src/services/dashboard/app.js'); + if (fs.existsSync(jsPath)) { + res.setHeader('Content-Type', 'application/javascript'); + res.end(fs.readFileSync(jsPath, 'utf8')); + return; + } + } + + if (url.pathname === '/') { + const htmlPath = path.join(ROOT_DIR, 'src/services/dashboard/index.html'); + if (fs.existsSync(htmlPath)) { + res.setHeader('Content-Type', 'text/html'); + res.end(fs.readFileSync(htmlPath, 'utf8')); + return; + } else { + res.statusCode = 404; + res.end('Dashboard UI files missing.'); + return; + } + } + res.statusCode = 404; res.end('Not Found'); } @@ -640,454 +283,6 @@ export class DashboardService extends BaseProcess { }).join(''); return `${bars}`; } - - private getHTML() { - return ` - - - - - ManBot Dashboard - - - - - - -
-
-

🍱 ManBot Dashboard LIVE

-

Real-time internal monitoring for AI-Agent orchestration, memory, and events.

-
- -
-
- -
-
-

Total Tasks

-
0
-
-
-

Knowledge Base

-
0 docs
-
-
-

Active Schedules

-
0
-
-
-

Peak Nodes

-
0
-
-
- -
-
-

First Task

-
-
-
-
-

Last Active

-
-
-
-
-

Avg Duration

-
-
-
-
- -
-

Processes

-
-
-
- -
- -

Analytics

-
-
-

Task Distribution

-
-
-
-

Task Complexity

-
-
-
- -
-
-
-
- -
-
-

Live Internal Traffic (IPC)

-
Live
-
-
- - - - - - - - - - -
TIMEROUTINGTYPEPAYLOAD
-
-
- - - -
-
-

Intelligence Pipeline

- -
-
- - - - - - - - - -
TIMETYPECONTENT
-
-
- -
-
-
- - - -`; - } } if (process.argv[1] === fileURLToPath(import.meta.url)) { diff --git a/src/services/dashboard/app.js b/src/services/dashboard/app.js new file mode 100644 index 0000000..4de3645 --- /dev/null +++ b/src/services/dashboard/app.js @@ -0,0 +1,299 @@ +let allLogs = []; + let showingAll = false; + let lastDashboardState = {}; + + let lastIpcLogTimestamp = 0; + function updateIpcLogs() { + fetch(`/api/ipc-logs?since=${lastIpcLogTimestamp}`) + .then(r => r.json()) + .then(logs => { + if (!logs || !logs.length) return; + lastIpcLogTimestamp = Math.max(...logs.map(l => l.message.timestamp)); + const tbody = document.getElementById("ipc-table-body"); + if (!tbody) return; + + logs.forEach(log => { + const tr = document.createElement("tr"); + const ts = new Date(log.message.timestamp); + const tsStr = ts.getHours().toString().padStart(2, '0') + ':' + ts.getMinutes().toString().padStart(2, '0') + ':' + ts.getSeconds().toString().padStart(2, '0') + '.' + ts.getMilliseconds().toString().padStart(3, '0'); + + let color = "var(--text)"; + if (log.message.type && log.message.type.includes("error")) color = "var(--error)"; + else if (log.message.type && log.message.type.includes("heartbeat")) color = "var(--text-muted)"; + else if (log.message.type && log.message.type.includes("event")) color = "var(--warning)"; + else if (log.message.type && log.message.type.includes("response")) color = "var(--success)"; + + let routeHtml = ''; + if (log.direction === '←') { + routeHtml = `${log.fromProcess} ${log.toProcess}`; + } else { + routeHtml = `${log.fromProcess} ${log.toProcess}`; + } + + tr.innerHTML = ` + ${tsStr} + ${routeHtml} + ${log.message.type} + ${JSON.stringify(log.message.payload || {})} + `; + tbody.insertBefore(tr, tbody.firstChild); + }); + + while (tbody.children.length > 200) tbody.removeChild(tbody.lastChild); + }).catch(e => console.error(e)); + } + setInterval(updateIpcLogs, 1000); + updateIpcLogs(); + + function getGraphLayout(nodes, edges) { + if (!nodes || nodes.length === 0) return []; + const nodeMap = {}; + nodes.forEach(n => nodeMap[n.id] = { ...n, incoming: 0, outgoing: [], level: 0 }); + (edges || []).forEach(e => { + if (nodeMap[e.from] && nodeMap[e.to]) { + nodeMap[e.from].outgoing.push(e.to); + nodeMap[e.to].incoming++; + } + }); + + // Assign levels + let changed = true; + let iterations = 0; + while (changed && iterations < 100) { + changed = false; + iterations++; + nodes.forEach(n => { + const current = nodeMap[n.id]; + current.outgoing.forEach(nextId => { + const next = nodeMap[nextId]; + if (next.level <= current.level) { + next.level = current.level + 1; + changed = true; + } + }); + }); + } + + const levels = []; + Object.values(nodeMap).forEach(n => { + if (!levels[n.level]) levels[n.level] = []; + levels[n.level].push(n); + }); + return levels.filter(l => l && l.length > 0); + } + + function fmtDate(d) { + const date = new Date(d); + return `${date.getDate()} ${date.toLocaleString('en-US', { month: 'short' })}, ${date.getFullYear()} ${date.toLocaleTimeString('en-GB')}`; + } + + function failTask(id) { + if (!confirm("Mark this task as failed?")) return; + fetch(`/api/fail-task?id=${id}`) + .then(r => r.json()) + .then(d => { + if (d.status === 'success') { + updateDashboard(); + } else { + alert("Error: " + d.message); + } + }); + } + + function toggleLogs() { + showingAll = !showingAll; + const btn = document.getElementById("show-more-btn"); + btn.textContent = showingAll ? "Show Less" : "Show More"; + renderLogs(); + } + + function renderLogs() { + const lt = document.getElementById("lt"); + const logsToRender = showingAll ? allLogs : allLogs.slice(0, 20); + + // Check if logs actually changed before rendering + const currentLogsHash = JSON.stringify(logsToRender); + if (lastDashboardState.logsHash === currentLogsHash) return; + lastDashboardState.logsHash = currentLogsHash; + + lt.innerHTML = logsToRender.map(l => { + const tc = l.type?.includes("failed") ? "error" : (l.type?.includes("completed") ? "success" : "warning"); + const typeLabel = (l.type || "EVENT").split(".").pop(); + const mainContent = l.payload?.toolName || l.payload?.nodeId || l.message || "-"; + const args = l.payload ? JSON.stringify(l.payload, null, 2) : ""; + + return ` + ${fmtDate(l.time || l.timestamp || Date.now())} + ${typeLabel} + +
${mainContent}
+ + + ${args ? ` + +
+
${args}
+
+ + ` : ""}`; + }).join(""); + + document.getElementById("show-more-btn").style.display = allLogs.length > 20 ? "inline-block" : "none"; + } + + function updateDashboard() { + const dateSelect = document.getElementById("log-date-select"); + const date = dateSelect.value; + + fetch(`/api/stats${date ? '?date=' + date : ''}`) + .then(r => r.json()) + .then(d => { + // 1. Update Stats + const statsChanged = + lastDashboardState.rag !== d.rag || + lastDashboardState.cron !== d.cron || + lastDashboardState.maxNodes !== d.maxNodes || + JSON.stringify(lastDashboardState.tasks) !== JSON.stringify(d.tasks) || + JSON.stringify(lastDashboardState.timing) !== JSON.stringify(d.timing); + + if (statsChanged) { + const total = Object.values(d.tasks).reduce((a, b) => a + b, 0); + document.getElementById("task-total").textContent = total; + document.getElementById("rag-count").textContent = d.rag; + document.getElementById("cron-count").textContent = d.cron; + document.getElementById("max-nodes").textContent = d.maxNodes || 0; + document.getElementById("time-first").textContent = d.timing.first; + document.getElementById("time-last").textContent = d.timing.last; + document.getElementById("time-avg").textContent = d.timing.avg; + + lastDashboardState.rag = d.rag; + lastDashboardState.cron = d.cron; + lastDashboardState.maxNodes = d.maxNodes; + lastDashboardState.tasks = d.tasks; + lastDashboardState.timing = d.timing; + } + + // 1.5 Update Processes + const procsHash = JSON.stringify(d.processes); + if (lastDashboardState.procsHash !== procsHash) { + const pg = document.getElementById("processes-grid"); + if (pg && d.processes) { + const html = Object.keys(d.processes).sort().map(pName => { + const p = d.processes[pName]; + const isDown = (Date.now() - (p.lastHeartbeat || 0)) > 30000; + const status = isDown ? "OFFLINE" : (p.status || "UNKNOWN").toUpperCase(); + const tc = status === "OFFLINE" ? "error" : (status === "READY" ? "success" : "warning"); + const indicator = status === "READY" ? '
' : ''; + const restartLabel = p.restartCount ? `
Restarts: ${p.restartCount}
` : ''; + const mem = p.memory && p.memory.rss ? `
Mem: ${Math.round(p.memory.rss / 1024 / 1024)}MB
` : ''; + const up = p.uptime ? `
Uptime: ${p.uptime}s
` : ''; + return `
+

${pName}

+
+
${indicator}${status}
+
${mem}${up}
+
+ ${restartLabel} +
`; + }).join(""); + pg.innerHTML = html; + } + lastDashboardState.procsHash = procsHash; + } + + // 2. Update Models + const modelsHash = JSON.stringify(d.models); + if (lastDashboardState.modelsHash !== modelsHash) { + const modelsSection = document.getElementById("model-list"); + modelsSection.innerHTML = Object.entries(d.models) + .filter(([k]) => ['small', 'medium', 'large'].includes(k)) + .map(([k, v]) => `
${k.toUpperCase()}:${v}
`) + .join(""); + lastDashboardState.modelsHash = modelsHash; + } + + // 3. Update Charts + const chartsHash = JSON.stringify(d.charts); + if (lastDashboardState.chartsHash !== chartsHash) { + document.getElementById("c1").innerHTML = d.charts.taskDonut; + document.getElementById("c2").innerHTML = d.charts.compBar; + lastDashboardState.chartsHash = chartsHash; + } + + // 4. Update Active Queue + const queueHash = JSON.stringify(d.pendingTasks); + if (lastDashboardState.queueHash !== queueHash) { + const qt = document.getElementById("qt"); + const qs = document.getElementById("active-queue-section"); + if (d.pendingTasks && d.pendingTasks.length > 0) { + qs.style.display = "block"; + qt.innerHTML = d.pendingTasks.map(t => { + const isRunning = t.status === 'running'; + const tc = isRunning ? "running" : "warning"; + const indicator = isRunning ? '
' : ''; + return ` + ${fmtDate(t.updated_at)} + ${indicator}${t.status.toUpperCase()} + ${(t.complexity || 'unknown').toUpperCase()} + +
${t.goal}
+ + + + + + + +
+ ${(() => { + const levels = getGraphLayout(t.nodes || [], t.edges || []); + return levels.map(level => ` +
+ ${level.map(n => { + const activeIndicator = n.status === 'running' ? '
' : ''; + const typeLabel = n.type.split('.').pop().toUpperCase(); + let chipAttr = ''; + if (n.type === 'skill' && n.input) { + try { + const input = typeof n.input === 'string' ? JSON.parse(n.input) : n.input; + const skillName = input.skillName || input.skill; + if (skillName) chipAttr = ` data-title="Skill: ${skillName}"`; + } catch(e) {} + } + return `
${activeIndicator}${typeLabel}
`; + }).join('')} +
+ `).join(''); + })()} +
+ + `; + }).join(""); + } else { + qs.style.display = "none"; + } + lastDashboardState.queueHash = queueHash; + } + + // 5. Update Logs + allLogs = d.logs; + renderLogs(); + }); + } + + // Initial load + fetch("/api/log-files") + .then(r => r.json()) + .then(files => { + const dateSelect = document.getElementById("log-date-select"); + const today = new Date().toISOString().split('T')[0]; + + if (!files.includes(today)) { + files.unshift(today); + } + + dateSelect.innerHTML = files.map(f => ``).join(""); + updateDashboard(); + + setInterval(updateDashboard, 5000); + }); \ No newline at end of file diff --git a/src/services/dashboard/index.html b/src/services/dashboard/index.html new file mode 100644 index 0000000..a2e4c00 --- /dev/null +++ b/src/services/dashboard/index.html @@ -0,0 +1,145 @@ + + + + + + ManBot Dashboard + + + + + + +
+
+

🍱 ManBot Dashboard LIVE

+

Real-time internal monitoring for AI-Agent orchestration, memory, and events.

+
+ +
+
+ +
+
+

Total Tasks

+
0
+
+
+

Knowledge Base

+
0 docs
+
+
+

Active Schedules

+
0
+
+
+

Peak Nodes

+
0
+
+
+ +
+
+

First Task

+
-
+
+
+

Last Active

+
-
+
+
+

Avg Duration

+
-
+
+
+ +
+

Processes

+
+
+
+ +
+ +

Analytics

+
+
+

Task Distribution

+
+
+
+

Task Complexity

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

Live Internal Traffic (IPC)

+
Live
+
+
+ + + + + + + + + + +
TIMEROUTINGTYPEPAYLOAD
+
+
+ + + +
+
+

Intelligence Pipeline

+ +
+
+ + + + + + + + + +
TIMETYPECONTENT
+
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/src/services/dashboard/styles.css b/src/services/dashboard/styles.css new file mode 100644 index 0000000..e534c81 --- /dev/null +++ b/src/services/dashboard/styles.css @@ -0,0 +1,379 @@ +:root { + --bg: #ffffff; + --text: #37352f; + --text-muted: #787774; + --subtle: #f7f6f3; + --border: rgba(55, 53, 47, 0.09); + --primary: #2383e2; + --success: #0b6e4f; + --error: #df2a5f; + --warning: #d9730d; + } + + @media (prefers-color-scheme: dark) { + :root { + --bg: #191919; + --text: #d4d4d4; + --text-muted: #8b8b8b; + --subtle: #252525; + --border: rgba(255, 255, 255, 0.09); + --primary: #2ea7ff; + --success: #529e72; + --error: #ff4d4d; + --warning: #ffdc4d; + } + } + + * { box-sizing: border-box; } + body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; + background-color: var(--bg); + color: var(--text); + margin: 0; + padding: 0; + display: flex; + justify-content: center; + line-height: 1.5; + } + + .container { + max-width: 900px; + width: 100%; + padding: 80px 40px; + } + + header { + margin-bottom: 40px; + } + + h1 { + font-size: 40px; + font-weight: 700; + margin: 0 0 10px 0; + display: flex; + align-items: center; + gap: 12px; + } + + .live-indicator { + font-size: 12px; + font-weight: 500; + padding: 2px 8px; + background: var(--subtle); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--success); + display: flex; + align-items: center; + gap: 6px; + } + + .live-indicator::before { + content: ''; + width: 6px; + height: 6px; + background: var(--success); + border-radius: 50%; + display: block; + } + + .description { + color: var(--text-muted); + font-size: 16px; + margin-bottom: 40px; + } + + .grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; + margin-bottom: 40px; + } + + .card { + border: 1px solid var(--border); + border-radius: 8px; + padding: 20px; + background: var(--bg); + } + + .card h2 { + font-size: 14px; + font-weight: 600; + color: var(--text-muted); + margin: 0 0 8px 0; + text-transform: uppercase; + letter-spacing: 0.03em; + } + + .metric-value { + font-size: 32px; + font-weight: 600; + } + + .chart-section { + margin-bottom: 60px; + } + + .chart-section h3 { + font-size: 18px; + font-weight: 600; + margin-bottom: 20px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border); + } + + .charts-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px; + } + + .models-section { + margin-top: 40px; + padding-top: 20px; + border-top: 1px solid var(--border); + } + + .model-pill { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 4px 12px; + background: var(--subtle); + border: 1px solid var(--border); + border-radius: 20px; + font-size: 13px; + margin-right: 12px; + margin-bottom: 12px; + } + + .model-pill b { color: var(--primary); } + + .chart-container { + height: 240px; + display: flex; + align-items: center; + justify-content: center; + background: var(--subtle); + border-radius: 8px; + padding: 20px; + } + + .logs-section h3 { + font-size: 18px; + font-weight: 600; + margin-bottom: 20px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border); + } + + table { + width: 100%; + border-collapse: collapse; + table-layout: auto; + } + + th { + text-align: left; + font-size: 12px; + font-weight: 500; + color: var(--text-muted); + padding: 12px 10px; + border-bottom: 1px solid var(--border); + white-space: nowrap; + } + + td { + padding: 12px 10px; + border-bottom: 1px solid var(--border); + font-size: 14px; + vertical-align: middle; + } + + .tag { + font-size: 12px; + padding: 2px 6px; + border-radius: 3px; + font-weight: 500; + } + + .tag.success { background: rgba(11, 110, 79, 0.1); color: var(--success); } + .tag.error { background: rgba(223, 42, 95, 0.1); color: var(--error); } + .tag.warning { background: rgba(217, 115, 13, 0.1); color: var(--warning); } + .tag.running { + background: rgba(35, 131, 226, 0.1); + color: var(--primary); + display: inline-flex; + align-items: center; + gap: 4px; + } + + .tag.complexity-small { background: var(--subtle); color: var(--text-muted); border: 1px solid var(--border); } + .tag.complexity-medium { background: rgba(35, 131, 226, 0.1); color: var(--primary); } + .tag.complexity-large { background: rgba(147, 51, 234, 0.1); color: #9333ea; } + + .pulse { + width: 6px; + height: 6px; + background: var(--primary); + border-radius: 50%; + animation: pulse-ring 1.5s infinite; + } + + @keyframes pulse-ring { + 0% { transform: scale(0.7); opacity: 1; } + 50% { transform: scale(1); opacity: 0.5; } + 100% { transform: scale(0.7); opacity: 1; } + } + + .node-map { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 8px; + align-items: center; + } + .node-stage { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + padding: 4px; + border-radius: 6px; + border: 1px dashed var(--border); + background: rgba(0,0,0,0.02); + } + .node-chip { + font-size: 11px; + padding: 2px 8px; + border-radius: 4px; + border: 1px solid var(--border); + background: var(--bg); + color: var(--text-muted); + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; + } + .node-chip.running { + border-color: var(--primary); + color: var(--primary); + background: rgba(35, 131, 226, 0.05); + font-weight: 600; + } + .node-chip.completed { + border-color: var(--success); + color: var(--success); + background: rgba(11, 110, 79, 0.05); + } + .node-chip.failed { + border-color: var(--error); + color: var(--error); + background: rgba(223, 42, 95, 0.05); + } + + .goal-text { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 500; + line-height: 1.4; + max-height: 2.8em; + } + + .btn-refresh { + font-size: 14px; + font-weight: 500; + padding: 6px 12px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 4px; + cursor: pointer; + color: var(--text); + transition: background 0.2s; + } + + tr.log-row { + cursor: pointer; + transition: background 0.1s; + } + + tr.log-row:hover { + background-color: var(--subtle); + } + + .log-details-row { + display: none; + } + + .log-details-row.open { + display: table-row; + } + + .log-details-container { + padding: 0 20px 20px 20px; + } + + .log-details-content { + font-size: 11px; + color: var(--text-muted); + background: var(--subtle); + padding: 12px; + border-radius: 4px; + white-space: pre-wrap; + word-break: break-all; + border: 1px solid var(--border); + font-family: monospace; + } + + @media (max-width: 768px) { + .grid { + grid-template-columns: 1fr 1fr; + } + .charts-grid { + grid-template-columns: 1fr; + } + .container { + padding: 40px 20px; + } + } + + /* Tooltip bubble using data-title */ + [data-title] { + position: relative; + } + + [data-title]:hover::after { + content: attr(data-title); + position: absolute; + bottom: 125%; + left: 50%; + transform: translateX(-50%); + padding: 6px 10px; + background: #1e1e1e; + color: #fff; + font-size: 11px; + font-weight: 500; + border-radius: 6px; + white-space: nowrap; + z-index: 1000; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + border: 1px solid rgba(255,255,255,0.1); + pointer-events: none; + } + + [data-title]:hover::before { + content: ''; + position: absolute; + bottom: 115%; + left: 50%; + transform: translateX(-50%); + border-width: 5px; + border-style: solid; + border-color: #1e1e1e transparent transparent transparent; + z-index: 1001; + pointer-events: none; + } \ No newline at end of file