Dashboard: implement tabbed interface

This commit is contained in:
larchanka
2026-02-23 08:14:19 +01:00
committed by Mikhail Larchanka
parent 2eb08f068c
commit a459de5023
3 changed files with 404 additions and 340 deletions

View File

@@ -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 = `<span style="color:var(--text-muted)">${log.fromProcess}</span> <strong style="color:var(--primary)">→</strong> <span style="color:var(--text)">${log.toProcess}</span>`;
} else {
routeHtml = `<span style="color:var(--text)">${log.fromProcess}</span> <strong style="color:var(--primary)">→</strong> <span style="color:var(--text-muted)">${log.toProcess}</span>`;
}
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 = `<span style="color:var(--text-muted)">${log.fromProcess}</span> <strong style="color:var(--primary)">→</strong> <span style="color:var(--text)">${log.toProcess}</span>`;
} else {
routeHtml = `<span style="color:var(--text)">${log.fromProcess}</span> <strong style="color:var(--primary)">→</strong> <span style="color:var(--text-muted)">${log.toProcess}</span>`;
}
tr.innerHTML = `
<td style="color: var(--text-muted); font-size: 11px; font-family: monospace; padding-left: 20px;">${tsStr}</td>
<td style="font-size: 11px; font-family: monospace;">${routeHtml}</td>
<td style="color: ${color}; font-size: 11px; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${log.message.type}">${log.message.type}</td>
<td style="font-size: 11px; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 20px;" title='${JSON.stringify(log.message.payload || {})}'>${JSON.stringify(log.message.payload || {})}</td>
`;
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 `<tr class="log-row" onclick="const next = this.nextElementSibling; if(next) next.classList.toggle('open')">
return `<tr class="log-row" onclick="const next = this.nextElementSibling; if(next) next.classList.toggle('open')">
<td style="padding-left: 20px; color: var(--text-muted); vertical-align: top; padding-top: 12px;">${fmtDate(l.time || l.timestamp || Date.now())}</td>
<td style="vertical-align: top; padding-top: 12px;"><span class="tag ${tc}">${typeLabel}</span></td>
<td style="padding-right: 20px; color: var(--text-muted); font-size: 13px; vertical-align: top; padding-top: 12px;">
@@ -136,58 +144,58 @@ let allLogs = [];
</div>
</td>
</tr>` : ""}`;
}).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" ? '<div class="pulse"></div>' : '';
const restartLabel = p.restartCount ? `<div style="font-size: 11px; color: var(--error); margin-top: 5px; font-weight: 500;">Restarts: ${p.restartCount}</div>` : '';
const mem = p.memory && p.memory.rss ? `<div style="font-size: 11px; color: var(--text-muted); margin-top: 5px;">Mem: ${Math.round(p.memory.rss / 1024 / 1024)}MB</div>` : '';
const up = p.uptime ? `<div style="font-size: 11px; color: var(--text-muted);">Uptime: ${p.uptime}s</div>` : '';
return `<div class="card" style="padding: 15px;">
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" ? '<div class="pulse"></div>' : '';
const restartLabel = p.restartCount ? `<div style="font-size: 11px; color: var(--error); margin-top: 5px; font-weight: 500;">Restarts: ${p.restartCount}</div>` : '';
const mem = p.memory && p.memory.rss ? `<div style="font-size: 11px; color: var(--text-muted); margin-top: 5px;">Mem: ${Math.round(p.memory.rss / 1024 / 1024)}MB</div>` : '';
const up = p.uptime ? `<div style="font-size: 11px; color: var(--text-muted);">Uptime: ${p.uptime}s</div>` : '';
return `<div class="card" style="padding: 15px;">
<h2 style="margin-bottom: 10px;">${pName}</h2>
<div style="display: flex; gap: 8px; flex-direction: column;">
<div><span class="tag ${tc}">${indicator}${status}</span></div>
@@ -195,43 +203,43 @@ let allLogs = [];
</div>
${restartLabel}
</div>`;
}).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]) => `<div class="model-pill"><span>${k.toUpperCase()}:</span><b>${v}</b></div>`)
.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]) => `<div class="model-pill"><span>${k.toUpperCase()}:</span><b>${v}</b></div>`)
.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 ? '<div class="pulse"></div>' : '';
return `<tr>
// 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 ? '<div class="pulse"></div>' : '';
return `<tr>
<td style="padding-left: 20px; color: var(--text-muted); white-space: nowrap; vertical-align: top; padding-top: 15px; border-bottom: none;">${fmtDate(t.updated_at)}</td>
<td style="white-space: nowrap; vertical-align: top; padding-top: 15px; border-bottom: none;"><span class="tag ${tc}">${indicator}${t.status.toUpperCase()}</span></td>
<td style="white-space: nowrap; vertical-align: top; padding-top: 15px; border-bottom: none;"><span class="tag complexity-${t.complexity || 'unknown'}">${(t.complexity || 'unknown').toUpperCase()}</span></td>
@@ -246,54 +254,54 @@ let allLogs = [];
<td colspan="5" style="padding-left: 20px; padding-right: 20px; padding-bottom: 20px; border-top: none;">
<div class="node-map">
${(() => {
const levels = getGraphLayout(t.nodes || [], t.edges || []);
return levels.map(level => `
const levels = getGraphLayout(t.nodes || [], t.edges || []);
return levels.map(level => `
<div class="node-stage">
${level.map(n => {
const activeIndicator = n.status === 'running' ? '<div class="pulse"></div>' : '';
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 `<div class="node-chip ${n.status}"${chipAttr}>${activeIndicator}${typeLabel}</div>`;
}).join('')}
const activeIndicator = n.status === 'running' ? '<div class="pulse"></div>' : '';
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 `<div class="node-chip ${n.status}"${chipAttr}>${activeIndicator}${typeLabel}</div>`;
}).join('')}
</div>
`).join('<span style="color: var(--border); font-size: 14px; font-weight: 700;">➔</span>');
})()}
})()}
</div>
</td>
</tr>`;
}).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 => `<option value="${f}" ${f === today ? 'selected' : ''}>${f}</option>`).join("");
updateDashboard();
setInterval(updateDashboard, 5000);
});
dateSelect.innerHTML = files.map(f => `<option value="${f}" ${f === today ? 'selected' : ''}>${f}</option>`).join("");
updateDashboard();
setInterval(updateDashboard, 5000);
});

View File

@@ -19,123 +19,136 @@
</div>
</header>
<div class="grid">
<div class="card">
<h2>Total Tasks</h2>
<div class="metric-value" id="task-total">0</div>
</div>
<div class="card">
<h2>Knowledge Base</h2>
<div class="metric-value"><span id="rag-count">0</span> <small style="font-size: 14px; font-weight: 400; color: var(--text-muted);">docs</small></div>
</div>
<div class="card">
<h2>Active Schedules</h2>
<div class="metric-value" id="cron-count">0</div>
</div>
<div class="card">
<h2>Peak Nodes</h2>
<div class="metric-value" id="max-nodes">0</div>
</div>
</div>
<nav class="tabs">
<button class="tab-btn active" onclick="showTab('main')">Main Overview</button>
<button class="tab-btn" onclick="showTab('ipc')">Live Internal Traffic</button>
<button class="tab-btn" onclick="showTab('pipeline')">Intelligence Pipeline</button>
</nav>
<div class="grid" style="grid-template-columns: repeat(3, 1fr); margin-bottom: 60px;">
<div class="card">
<h2>First Task</h2>
<div class="metric-value" id="time-first" style="font-size: 20px;">-</div>
</div>
<div class="card">
<h2>Last Active</h2>
<div class="metric-value" id="time-last" style="font-size: 20px;">-</div>
</div>
<div class="card">
<h2>Avg Duration</h2>
<div class="metric-value" id="time-avg" style="font-size: 20px;">-</div>
</div>
</div>
<div class="processes-section" style="margin-bottom: 60px;">
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 20px; padding-bottom: 8px; border-bottom: 1px solid var(--border);">Processes</h3>
<div id="processes-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px;">
</div>
</div>
<div class="chart-section">
<h3>Analytics</h3>
<div class="charts-grid">
<!-- MAIN TAB -->
<div id="main-tab" class="tab-content active">
<div class="grid">
<div class="card">
<h2>Task Distribution</h2>
<div id="c1" class="chart-container"></div>
<h2>Total Tasks</h2>
<div class="metric-value" id="task-total">0</div>
</div>
<div class="card">
<h2>Task Complexity</h2>
<div id="c2" class="chart-container"></div>
<h2>Knowledge Base</h2>
<div class="metric-value"><span id="rag-count">0</span> <small style="font-size: 14px; font-weight: 400; color: var(--text-muted);">docs</small></div>
</div>
<div class="card">
<h2>Active Schedules</h2>
<div class="metric-value" id="cron-count">0</div>
</div>
<div class="card">
<h2>Peak Nodes</h2>
<div class="metric-value" id="max-nodes">0</div>
</div>
</div>
<div class="models-section" id="model-mapping" style="border-top: none; margin-top: 20px;">
<div id="model-list"></div>
<div class="grid" style="grid-template-columns: repeat(3, 1fr); margin-bottom: 60px;">
<div class="card">
<h2>First Task</h2>
<div class="metric-value" id="time-first" style="font-size: 20px;">-</div>
</div>
<div class="card">
<h2>Last Active</h2>
<div class="metric-value" id="time-last" style="font-size: 20px;">-</div>
</div>
<div class="card">
<h2>Avg Duration</h2>
<div class="metric-value" id="time-avg" style="font-size: 20px;">-</div>
</div>
</div>
<div class="processes-section" style="margin-bottom: 60px;">
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 20px; padding-bottom: 8px; border-bottom: 1px solid var(--border);">Processes</h3>
<div id="processes-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px;">
</div>
</div>
<div class="logs-section" id="active-queue-section" style="margin-bottom: 60px; display: none;">
<h3>Active Tasks Queue</h3>
<div class="card" style="padding: 0;">
<table id="queue-table">
<thead>
<tr>
<th style="padding-left: 20px; width: 100px;">UPDATED</th>
<th style="width: 100px;">STATUS</th>
<th style="width: 120px;">COMPLEXITY</th>
<th>GOAL</th>
<th style="padding-right: 20px; text-align: right; width: 80px;">ACTION</th>
</tr>
</thead>
<tbody id="qt"></tbody>
</table>
</div>
</div>
<div class="chart-section">
<h3>Analytics</h3>
<div class="charts-grid">
<div class="card">
<h2>Task Distribution</h2>
<div id="c1" class="chart-container"></div>
</div>
<div class="card">
<h2>Task Complexity</h2>
<div id="c2" class="chart-container"></div>
</div>
</div>
<div class="models-section" id="model-mapping" style="border-top: none; margin-top: 20px;">
<div id="model-list"></div>
</div>
</div>
</div>
<div class="logs-section" id="ipc-stream-section" style="margin-bottom: 60px;">
<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 20px; padding-bottom: 8px;">
<h3 style="margin: 0; flex: 1;">Live Internal Traffic (IPC)</h3>
<span class="tag success" style="display: inline-flex; align-items: center; gap: 6px;"><div class="pulse" style="margin:0;"></div> Live</span>
</div>
<div class="card" style="padding: 0;">
<table id="ipc-table" style="table-layout: fixed;">
<thead>
<tr>
<th style="padding-left: 20px; width: 15%;">TIME</th>
<th style="width: 25%;">ROUTING</th>
<th style="width: 25%;">TYPE</th>
<th style="padding-right: 20px; width: 35%;">PAYLOAD</th>
</tr>
</thead>
<tbody id="ipc-table-body"></tbody>
</table>
<!-- IPC TAB -->
<div id="ipc-tab" class="tab-content">
<div class="logs-section" id="ipc-stream-section" style="margin-bottom: 60px;">
<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 20px; padding-bottom: 8px;">
<h3 style="margin: 0; flex: 1;">Live Internal Traffic (IPC)</h3>
<span class="tag success" style="display: inline-flex; align-items: center; gap: 6px;"><div class="pulse" style="margin:0;"></div> Live</span>
</div>
<div class="card" style="padding: 0;">
<table id="ipc-table" style="table-layout: fixed;">
<thead>
<tr>
<th style="padding-left: 20px; width: 15%;">TIME</th>
<th style="width: 25%;">ROUTING</th>
<th style="width: 25%;">TYPE</th>
<th style="padding-right: 20px; width: 35%;">PAYLOAD</th>
</tr>
</thead>
<tbody id="ipc-table-body"></tbody>
</table>
</div>
</div>
</div>
<div class="logs-section" id="active-queue-section" style="margin-bottom: 60px; display: none;">
<h3>Active Tasks Queue</h3>
<div class="card" style="padding: 0;">
<table id="queue-table">
<thead>
<tr>
<th style="padding-left: 20px; width: 100px;">UPDATED</th>
<th style="width: 100px;">STATUS</th>
<th style="width: 120px;">COMPLEXITY</th>
<th>GOAL</th>
<th style="padding-right: 20px; text-align: right; width: 80px;">ACTION</th>
</tr>
</thead>
<tbody id="qt"></tbody>
</table>
</div>
</div>
<div class="logs-section">
<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 20px; padding-bottom: 8px;">
<h3 style="margin: 0; flex: 1;">Intelligence Pipeline</h3>
<select id="log-date-select" onchange="updateDashboard()" style="width: 140px; padding: 4px 8px; border-radius: 6px; border: 1px solid var(--border); background: var(--subtle); color: var(--text); font-family: inherit; font-size: 13px; cursor: pointer; height: 32px;">
</select>
</div>
<div class="card" style="padding: 0;">
<table id="log-table" style="table-layout: fixed;">
<thead>
<tr>
<th style="padding-left: 20px; width: 25%;">TIME</th>
<th style="width: 25%;">TYPE</th>
<th style="padding-right: 20px; width: 50%;">CONTENT</th>
</tr>
</thead>
<tbody id="lt"></tbody>
</table>
</div>
<div style="text-align: center; margin-top: 20px;">
<button id="show-more-btn" class="btn-refresh" style="display: none;" onclick="toggleLogs()">Show More</button>
<!-- PIPELINE TAB -->
<div id="pipeline-tab" class="tab-content">
<div class="logs-section">
<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 20px; padding-bottom: 8px;">
<h3 style="margin: 0; flex: 1;">Intelligence Pipeline</h3>
<select id="log-date-select" onchange="updateDashboard()" style="width: 140px; padding: 4px 8px; border-radius: 6px; border: 1px solid var(--border); background: var(--subtle); color: var(--text); font-family: inherit; font-size: 13px; cursor: pointer; height: 32px;">
</select>
</div>
<div class="card" style="padding: 0;">
<table id="log-table" style="table-layout: fixed;">
<thead>
<tr>
<th style="padding-left: 20px; width: 25%;">TIME</th>
<th style="width: 25%;">TYPE</th>
<th style="padding-right: 20px; width: 50%;">CONTENT</th>
</tr>
</thead>
<tbody id="lt"></tbody>
</table>
</div>
<div style="text-align: center; margin-top: 20px;">
<button id="show-more-btn" class="btn-refresh" style="display: none;" onclick="toggleLogs()">Show More</button>
</div>
</div>
</div>
</div>

View File

@@ -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 {