add worker event tracking; implement worker rates display in admin panel
This commit is contained in:
+69
-11
@@ -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(); } }); }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user