diff --git a/src/services/dashboard/app.js b/src/services/dashboard/app.js index 4de3645..c7ce535 100644 --- a/src/services/dashboard/app.js +++ b/src/services/dashboard/app.js @@ -1,128 +1,136 @@ 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}`; - } +let showingAll = false; +let lastDashboardState = {}; - tr.innerHTML = ` +function showTab(tabId) { + document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + + document.getElementById(tabId + '-tab').classList.add('active'); + event.currentTarget.classList.add('active'); +} + +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(); + tbody.insertBefore(tr, tbody.firstChild); + }); - 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++; + 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; } }); - - // 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); } - - 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 toggleLogs() { + showingAll = !showingAll; + const btn = document.getElementById("show-more-btn"); + btn.textContent = showingAll ? "Show Less" : "Show More"; + renderLogs(); +} - 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 renderLogs() { + const lt = document.getElementById("lt"); + const logsToRender = showingAll ? allLogs : allLogs.slice(0, 20); - function toggleLogs() { - showingAll = !showingAll; - const btn = document.getElementById("show-more-btn"); - btn.textContent = showingAll ? "Show Less" : "Show More"; - renderLogs(); - } + // Check if logs actually changed before rendering + const currentLogsHash = JSON.stringify(logsToRender); + if (lastDashboardState.logsHash === currentLogsHash) return; + lastDashboardState.logsHash = currentLogsHash; - 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) : ""; - 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 ` + return ` ${fmtDate(l.time || l.timestamp || Date.now())} ${typeLabel} @@ -136,58 +144,58 @@ let allLogs = []; ` : ""}`; - }).join(""); + }).join(""); - document.getElementById("show-more-btn").style.display = allLogs.length > 20 ? "inline-block" : "none"; - } + 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); +function updateDashboard() { + const dateSelect = document.getElementById("log-date-select"); + const date = dateSelect.value; - 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; - } + 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); - // 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 `
+ 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}
@@ -195,43 +203,43 @@ let allLogs = [];
${restartLabel}
`; - }).join(""); - pg.innerHTML = html; - } - lastDashboardState.procsHash = procsHash; - } + }).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; - } + // 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; + } - // 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 ` + // 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()} @@ -246,54 +254,54 @@ let allLogs = [];
${(() => { - const levels = getGraphLayout(t.nodes || [], t.edges || []); - return levels.map(level => ` + 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('')} + 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(); - }); + }).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); } - // 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 + 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 index a2e4c00..6f67d76 100644 --- a/src/services/dashboard/index.html +++ b/src/services/dashboard/index.html @@ -19,123 +19,136 @@
-
-
-

Total Tasks

-
0
-
-
-

Knowledge Base

-
0 docs
-
-
-

Active Schedules

-
0
-
-
-

Peak Nodes

-
0
-
-
+ -
-
-

First Task

-
-
-
-
-

Last Active

-
-
-
-
-

Avg Duration

-
-
-
-
- -
-

Processes

-
-
-
- -
- -

Analytics

-
+ +
+
-

Task Distribution

-
+

Total Tasks

+
0
-

Task Complexity

-
+

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
+ +
+
+
+

Live Internal Traffic (IPC)

+
Live
+
+
+ + + + + + + + + + +
TIMEROUTINGTYPEPAYLOAD
+
- - -
-
-

Intelligence Pipeline

- -
-
- - - - - - - - - -
TIMETYPECONTENT
-
-
- + +
+
+
+

Intelligence Pipeline

+ +
+
+ + + + + + + + + +
TIMETYPECONTENT
+
+
+ +
diff --git a/src/services/dashboard/styles.css b/src/services/dashboard/styles.css index e534c81..6799e56 100644 --- a/src/services/dashboard/styles.css +++ b/src/services/dashboard/styles.css @@ -39,7 +39,50 @@ .container { max-width: 900px; width: 100%; - padding: 80px 40px; + padding: 60px 40px; + } + + .tabs { + display: flex; + gap: 30px; + margin-bottom: 40px; + border-bottom: 1px solid var(--border); + } + + .tab-btn { + padding: 8px 4px; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + color: var(--text-muted); + font-size: 15px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + margin-bottom: -1px; + } + + .tab-btn:hover { + color: var(--text); + } + + .tab-btn.active { + color: var(--primary); + border-bottom-color: var(--primary); + } + + .tab-content { + display: none; + animation: fadeIn 0.3s ease-in-out; + } + + .tab-content.active { + display: block; + } + + @keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } } header {