add worker event tracking; implement worker rates display in admin panel

This commit is contained in:
ImBenji
2026-04-23 17:13:57 +01:00
parent 16d95f5392
commit 30104c6e66
6 changed files with 145 additions and 23 deletions
+69 -11
View File
@@ -715,6 +715,11 @@
<span>Show untracked entities</span>
</label>
<label style="display:flex; align-items:center; gap:6px; margin-left:16px; user-select:none">
<span>Min confirmations</span>
<input type="number" id="graph-min-count" value="2" min="1" style="width:52px; min-width:unset; padding:3px 6px; font-size:12px" />
</label>
<span style="margin-left:auto; color:var(--muted-dark)">Scroll to zoom · drag nodes · click for facts</span>
</div>
</div>
@@ -739,6 +744,14 @@
<!-- Stats tab -->
<div id="tab-stats" style="display:none">
<div style="margin-bottom:28px">
<div class="section-heading">Worker throughput</div>
<div id="worker-rates-row" style="display:flex; gap:12px; flex-wrap:wrap; margin-top:10px">
<span style="color:var(--muted); font-size:13px">Loading...</span>
</div>
</div>
<div style="display:flex; gap:32px; flex-wrap:wrap; padding-top:4px">
<div>
<div class="section-heading">By source</div>
@@ -887,6 +900,34 @@ async function loadStats() {
document.getElementById('statusTable').innerHTML = data.byStatus
.map(r => `<tr><td>${badgeHtml(r.status === 'null' ? null : r.status)}</td><td style="text-align:right; padding-left:24px">${r.n.toLocaleString()}</td></tr>`).join('');
renderWorkerRates();
}
function renderWorkerRates() {
const rates = window._workerRates || [];
const el = document.getElementById('worker-rates-row');
if (!el) return;
const workerLabels = { augor: 'Augor (events)', consolidation: 'Consolidation (companies)', graph: 'Graph (edges)' };
if (rates.length === 0) {
el.innerHTML = '<span style="color:var(--muted); font-size:13px">No data yet — worker_events table may not exist until workers restart.</span>';
return;
}
el.innerHTML = rates.map(r => {
const label = workerLabels[r.worker] || r.worker;
const avg5m = (r.n5m / 5).toFixed(1);
const last1m = r.n1m;
return `
<div class="intel-stat-card" style="min-width:180px">
<span class="label">${label}</span>
<span class="value" style="font-size:18px">${avg5m}<span style="font-size:12px; font-weight:400; color:var(--muted)">/min</span></span>
<span style="font-size:11px; color:var(--muted-dark); margin-top:4px">${last1m} in last 1m</span>
</div>`;
}).join('');
}
// ── source dropdown ────────────────────────────────────────────────────────
@@ -1109,9 +1150,6 @@ async function loadIntelligenceStats() {
const queueMap = {};
(data.queue || []).forEach(r => queueMap[r.status] = r.n);
const rate5m = (data.processed5m / 5).toFixed(1);
const rate1m = data.processed1m;
document.getElementById('intel-stats-row').innerHTML = [
['Queue pending', (queueMap.pending || 0).toLocaleString()],
['Processed', (queueMap.processed || 0).toLocaleString()],
@@ -1119,8 +1157,6 @@ async function loadIntelligenceStats() {
['Knowledge rows', data.knowledge.toLocaleString()],
['Predictions', data.predictions.toLocaleString()],
['Companies', `${data.embeddings}/${data.companies} embedded`],
['Rate (5m avg)', `${rate5m}/min`],
['Rate (last 1m)', `${rate1m}/min`],
].map(([label, value]) => `
<div class="intel-stat-card">
<span class="label">${label}</span>
@@ -1128,6 +1164,9 @@ async function loadIntelligenceStats() {
</div>
`).join('');
// stash worker rates for the stats tab
window._workerRates = data.workerRates || [];
return true;
}
@@ -1324,6 +1363,10 @@ document.getElementById('graph-show-untracked').addEventListener('change', () =>
if (graphAllNodes.length) renderIntelGraph();
});
document.getElementById('graph-min-count').addEventListener('change', () => {
if (graphAllNodes.length) renderIntelGraph();
});
function toggleGraphExpand() {
const wrap = document.getElementById('intel-graph-svg-wrap');
@@ -1337,13 +1380,25 @@ function toggleGraphExpand() {
function renderIntelGraph() {
const showUntracked = document.getElementById('graph-show-untracked').checked;
const minCount = parseInt(document.getElementById('graph-min-count').value, 10) || 1;
const nodes = showUntracked
? graphAllNodes
: graphAllNodes.filter(n => n.tracked);
const filteredLinks = graphAllLinks.filter(l => (l.count || 1) >= minCount);
// only show nodes that still have at least one edge after the count filter
const activeKeys = new Set();
for (const l of filteredLinks) {
const src = typeof l.source === 'object' ? l.source.key : l.source;
const tgt = typeof l.target === 'object' ? l.target.key : l.target;
activeKeys.add(src);
activeKeys.add(tgt);
}
const nodes = graphAllNodes.filter(n =>
activeKeys.has(n.key) && (showUntracked || n.tracked)
);
const visibleKeys = new Set(nodes.map(n => n.key));
const links = graphAllLinks.filter(l => visibleKeys.has(
const links = filteredLinks.filter(l => visibleKeys.has(
typeof l.source === 'object' ? l.source.key : l.source
) && visibleKeys.has(
typeof l.target === 'object' ? l.target.key : l.target
@@ -1456,6 +1511,7 @@ function renderIntelGraph() {
// links — use <path> so textPath can align labels to the line
const linkG = g.append('g');
let linkLabels;
// stroke-width min 10 so text (7px) always fits inside the line
const strokeWidth = d => 10 + Math.pow(d.count / maxCount, 0.55) * 8;
@@ -1471,14 +1527,16 @@ function renderIntelGraph() {
.attr('stroke-opacity', d => 0.55 + (d.count / maxCount) * 0.35)
.on('mouseenter', function(ev, d) {
d3.select(this).attr('stroke', '#60a5fa').attr('stroke-opacity', 1);
linkLabels.filter(l => l === d).attr('fill', '#f8fafc');
})
.on('mouseleave', function(ev, d) {
d3.select(this).attr('stroke', '#334155')
.attr('stroke-opacity', 0.2 + (d.count / maxCount) * 0.7);
linkLabels.filter(l => l === d).attr('fill', '#94a3b8');
});
// labels along each edge via textPath — font-size slightly smaller than stroke-width
const linkLabels = linkG.selectAll('text.ig-elabel')
linkLabels = linkG.selectAll('text.ig-elabel')
.data(linksCopy)
.join('text')
.attr('class', 'ig-elabel')
@@ -1769,7 +1827,7 @@ function switchTab(tab) {
location.hash = tab;
if (tab === 'events') loadEvents();
if (tab === 'stats') loadStats();
if (tab === 'stats') { loadStats(); loadIntelligenceStats(); }
if (tab === 'intelligence') { intelOffset = 0; loadIntelligenceStats().then(ok => { if (ok) { loadIntelligenceCompanies(); loadIntelligence(); } }); }
}