Skip to content

Commit da61515

Browse files
authored
Merge PR #482: Queue visualization overlay
Queue: work queue visualization overlay
2 parents 2fb519b + c8d9962 commit da61515

1 file changed

Lines changed: 186 additions & 0 deletions

File tree

client/dashboard.js

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ class Dashboard {
139139
<div id="dashboard-advice-summary" class="dashboard-summary-body">Loading…</div>
140140
<div class="dashboard-summary-actions">
141141
<button class="dashboard-topbar-btn" id="dashboard-open-queue" title="Open Queue">📥 Queue</button>
142+
<button class="dashboard-topbar-btn" id="dashboard-open-queue-viz" title="Work queue visualization">🧭 Viz</button>
142143
<button class="dashboard-topbar-btn" id="dashboard-open-advice" title="Open Commander Advice">🧠 Advice</button>
143144
<button class="dashboard-topbar-btn" id="dashboard-open-suggestions" title="Open workspace suggestions">✨ Suggestions</button>
144145
<button class="dashboard-topbar-btn" id="dashboard-open-distribution" title="Suggested terminal per PR/task">🎯 Distribution</button>
@@ -224,6 +225,10 @@ class Dashboard {
224225
e.preventDefault();
225226
this.orchestrator?.showQueuePanel?.().catch?.(() => {});
226227
});
228+
document.getElementById('dashboard-open-queue-viz')?.addEventListener('click', (e) => {
229+
e.preventDefault();
230+
this.showQueueVizOverlay().catch(() => {});
231+
});
227232
document.getElementById('dashboard-open-prs')?.addEventListener('click', (e) => {
228233
e.preventDefault();
229234
try {
@@ -638,6 +643,187 @@ class Dashboard {
638643
await this.loadPerformanceDetails();
639644
}
640645

646+
async showQueueVizOverlay() {
647+
const existing = document.getElementById('dashboard-queue-viz-overlay');
648+
if (existing) {
649+
existing.classList.remove('hidden');
650+
return;
651+
}
652+
653+
const overlay = document.createElement('div');
654+
overlay.id = 'dashboard-queue-viz-overlay';
655+
overlay.className = 'dashboard-telemetry-overlay';
656+
overlay.innerHTML = `
657+
<div class="dashboard-telemetry-panel" role="dialog" aria-label="Work queue visualization">
658+
<div class="dashboard-telemetry-header">
659+
<div class="dashboard-telemetry-title">Queue — Visualization</div>
660+
<button class="dashboard-topbar-btn" id="dashboard-queue-viz-close" title="Close (Esc)">✕</button>
661+
</div>
662+
<div class="dashboard-telemetry-controls">
663+
<div class="dashboard-telemetry-actions">
664+
<button class="btn-secondary" type="button" id="dashboard-queue-viz-open-queue">📥 Open Queue</button>
665+
<button class="btn-secondary" type="button" id="dashboard-queue-viz-refresh">Refresh</button>
666+
</div>
667+
</div>
668+
<div id="dashboard-queue-viz-body" class="dashboard-telemetry-body">Loading…</div>
669+
</div>
670+
`;
671+
672+
document.body.appendChild(overlay);
673+
674+
const close = () => this.hideQueueVizOverlay();
675+
overlay.addEventListener('click', (e) => {
676+
if (e.target === overlay) close();
677+
});
678+
overlay.querySelector('#dashboard-queue-viz-close')?.addEventListener('click', close);
679+
overlay.querySelector('#dashboard-queue-viz-open-queue')?.addEventListener('click', () => {
680+
close();
681+
this.orchestrator?.showQueuePanel?.().catch?.(() => {});
682+
});
683+
overlay.querySelector('#dashboard-queue-viz-refresh')?.addEventListener('click', () => {
684+
this.loadQueueVizDetails().catch(() => {});
685+
});
686+
687+
const onKey = (e) => {
688+
if (e.key !== 'Escape') return;
689+
const el = document.getElementById('dashboard-queue-viz-overlay');
690+
if (!el || el.classList.contains('hidden')) return;
691+
close();
692+
};
693+
overlay._escHandler = onKey;
694+
document.addEventListener('keydown', onKey);
695+
696+
await this.loadQueueVizDetails();
697+
}
698+
699+
hideQueueVizOverlay() {
700+
const overlay = document.getElementById('dashboard-queue-viz-overlay');
701+
if (!overlay) return;
702+
overlay.classList.add('hidden');
703+
const handler = overlay._escHandler;
704+
if (handler) {
705+
document.removeEventListener('keydown', handler);
706+
overlay._escHandler = null;
707+
}
708+
overlay.remove();
709+
}
710+
711+
async loadQueueVizDetails() {
712+
const bodyEl = document.getElementById('dashboard-queue-viz-body');
713+
if (bodyEl) bodyEl.textContent = 'Loading…';
714+
715+
let data = null;
716+
try {
717+
const url = new URL('/api/process/tasks', window.location.origin);
718+
url.searchParams.set('mode', 'all');
719+
url.searchParams.set('state', 'open');
720+
url.searchParams.set('include', 'dependencySummary');
721+
const res = await fetch(url.toString());
722+
data = res && res.ok ? await res.json().catch(() => null) : null;
723+
} catch {
724+
data = null;
725+
}
726+
727+
if (!bodyEl) return;
728+
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
729+
if (!data || !tasks) {
730+
bodyEl.textContent = 'Failed to load.';
731+
return;
732+
}
733+
734+
const escapeHtml = (value) => String(value ?? '')
735+
.replace(/&/g, '&amp;')
736+
.replace(/</g, '&lt;')
737+
.replace(/>/g, '&gt;');
738+
739+
const tierKey = (t) => {
740+
const tier = t?.record?.tier;
741+
const n = Number(tier);
742+
return (Number.isFinite(n) && n >= 1 && n <= 4) ? `T${n}` : 'None';
743+
};
744+
745+
const counts = { T1: 0, T2: 0, T3: 0, T4: 0, None: 0 };
746+
const unclaimed = { T1: 0, T2: 0, T3: 0, T4: 0, None: 0 };
747+
const unassigned = { T1: 0, T2: 0, T3: 0, T4: 0, None: 0 };
748+
const byAssignee = {};
749+
750+
for (const t of tasks) {
751+
const k = tierKey(t);
752+
counts[k] = (counts[k] || 0) + 1;
753+
const claimedBy = String(t?.record?.claimedBy || '').trim();
754+
const assignedTo = String(t?.record?.assignedTo || '').trim();
755+
if (!claimedBy) unclaimed[k] = (unclaimed[k] || 0) + 1;
756+
if (!assignedTo) unassigned[k] = (unassigned[k] || 0) + 1;
757+
758+
const bucket = assignedTo || '(unassigned)';
759+
if (!byAssignee[bucket]) byAssignee[bucket] = { T1: 0, T2: 0, T3: 0, T4: 0, None: 0, total: 0 };
760+
byAssignee[bucket][k] = (byAssignee[bucket][k] || 0) + 1;
761+
byAssignee[bucket].total += 1;
762+
}
763+
764+
const assignees = Object.entries(byAssignee)
765+
.sort((a, b) => (b[1].total || 0) - (a[1].total || 0) || String(a[0]).localeCompare(String(b[0])));
766+
767+
const rows = assignees.map(([who, c]) => {
768+
return `
769+
<tr>
770+
<td class="mono">${escapeHtml(who)}</td>
771+
<td class="mono">${escapeHtml(c.T1 || 0)}</td>
772+
<td class="mono">${escapeHtml(c.T2 || 0)}</td>
773+
<td class="mono">${escapeHtml(c.T3 || 0)}</td>
774+
<td class="mono">${escapeHtml(c.T4 || 0)}</td>
775+
<td class="mono">${escapeHtml(c.None || 0)}</td>
776+
<td class="mono">${escapeHtml(c.total || 0)}</td>
777+
</tr>
778+
`;
779+
}).join('');
780+
781+
const sumRow = `
782+
<tr>
783+
<td class="mono"><strong>Total</strong></td>
784+
<td class="mono"><strong>${escapeHtml(counts.T1)}</strong></td>
785+
<td class="mono"><strong>${escapeHtml(counts.T2)}</strong></td>
786+
<td class="mono"><strong>${escapeHtml(counts.T3)}</strong></td>
787+
<td class="mono"><strong>${escapeHtml(counts.T4)}</strong></td>
788+
<td class="mono"><strong>${escapeHtml(counts.None)}</strong></td>
789+
<td class="mono"><strong>${escapeHtml(tasks.length)}</strong></td>
790+
</tr>
791+
`;
792+
793+
bodyEl.innerHTML = `
794+
<div class="dashboard-telemetry-muted">
795+
Items: <strong>${escapeHtml(tasks.length)}</strong> • Unclaimed: <strong>${escapeHtml(Object.values(unclaimed).reduce((a, b) => a + (b || 0), 0))}</strong> • Unassigned: <strong>${escapeHtml(Object.values(unassigned).reduce((a, b) => a + (b || 0), 0))}</strong>
796+
</div>
797+
<div style="margin-top:10px; display:flex; flex-wrap:wrap; gap:8px;">
798+
<span class="pr-badge" title="Total items per tier">T1 ${escapeHtml(counts.T1)}</span>
799+
<span class="pr-badge">T2 ${escapeHtml(counts.T2)}</span>
800+
<span class="pr-badge">T3 ${escapeHtml(counts.T3)}</span>
801+
<span class="pr-badge">T4 ${escapeHtml(counts.T4)}</span>
802+
<span class="pr-badge">None ${escapeHtml(counts.None)}</span>
803+
<span class="pr-badge" title="Unclaimed items per tier">Unclaimed T1 ${escapeHtml(unclaimed.T1)} • T2 ${escapeHtml(unclaimed.T2)} • T3 ${escapeHtml(unclaimed.T3)} • T4 ${escapeHtml(unclaimed.T4)} • None ${escapeHtml(unclaimed.None)}</span>
804+
<span class="pr-badge" title="Unassigned items per tier">Unassigned T1 ${escapeHtml(unassigned.T1)} • T2 ${escapeHtml(unassigned.T2)} • T3 ${escapeHtml(unassigned.T3)} • T4 ${escapeHtml(unassigned.T4)} • None ${escapeHtml(unassigned.None)}</span>
805+
</div>
806+
807+
<table class="worktree-inspector-table" style="margin-top:12px;">
808+
<thead>
809+
<tr>
810+
<th>Assigned to</th>
811+
<th>T1</th>
812+
<th>T2</th>
813+
<th>T3</th>
814+
<th>T4</th>
815+
<th>None</th>
816+
<th>Total</th>
817+
</tr>
818+
</thead>
819+
<tbody>
820+
${rows || `<tr><td colspan="7" style="opacity:0.8;">No items.</td></tr>`}
821+
${sumRow}
822+
</tbody>
823+
</table>
824+
`;
825+
}
826+
641827
hidePerformanceOverlay() {
642828
const overlay = document.getElementById('dashboard-performance-overlay');
643829
if (!overlay) return;

0 commit comments

Comments
 (0)