mirror of
https://github.com/larchanka/manbot.git
synced 2026-05-13 21:42:08 +00:00
Dashboard: implement tabbed interface
This commit is contained in:
committed by
Mikhail Larchanka
parent
2eb08f068c
commit
a459de5023
@@ -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);
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user