opencastle 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +0 -0
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +3 -2
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +6 -2
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +21 -0
- package/dist/cli/mcp.js.map +1 -1
- package/package.json +13 -6
- package/src/cli/dashboard.ts +3 -2
- package/src/cli/init.ts +7 -2
- package/src/cli/mcp.ts +30 -1
- package/src/dashboard/dist/_astro/index.CWVzbF4T.css +1 -0
- package/src/dashboard/dist/icon-192.png +0 -0
- package/src/dashboard/dist/index.html +992 -0
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +31 -0
- package/src/dashboard/node_modules/.vite/deps/astro___aria-query.js +6776 -0
- package/src/dashboard/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
- package/src/dashboard/node_modules/.vite/deps/astro___axobject-query.js +3754 -0
- package/src/dashboard/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
- package/src/dashboard/node_modules/.vite/deps/astro___cssesc.js +99 -0
- package/src/dashboard/node_modules/.vite/deps/astro___cssesc.js.map +7 -0
- package/src/dashboard/node_modules/.vite/deps/chunk-BUSYA2B4.js +8 -0
- package/src/dashboard/node_modules/.vite/deps/chunk-BUSYA2B4.js.map +7 -0
- package/src/dashboard/node_modules/.vite/deps/package.json +3 -0
- package/src/dashboard/src/env.d.ts +1 -0
- package/src/dashboard/src/pages/index.astro +163 -59
- package/src/dashboard/src/styles/dashboard.css +261 -0
- package/src/dashboard/tsconfig.json +1 -1
- package/src/orchestrator/agents/release-manager.agent.md +1 -1
- package/src/orchestrator/agents/team-lead.agent.md +13 -1
- package/src/orchestrator/customizations/stack/notifications-config.md +57 -0
- package/src/orchestrator/instructions/general.instructions.md +31 -2
- package/src/orchestrator/mcp.json +12 -6
- package/src/orchestrator/skills/agent-hooks/SKILL.md +13 -7
- package/src/orchestrator/skills/session-checkpoints/SKILL.md +12 -0
- package/src/orchestrator/skills/slack-notifications/SKILL.md +139 -39
- package/src/dashboard/package-lock.json +0 -5455
- package/src/dashboard/package.json +0 -14
- /package/src/dashboard/{public/data → seed-data}/delegations.ndjson +0 -0
- /package/src/dashboard/{public/data → seed-data}/panels.ndjson +0 -0
- /package/src/dashboard/{public/data → seed-data}/sessions.ndjson +0 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
<!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Observability Dashboard — OpenCastle</title><meta name="description" content="Real-time observability for OpenCastle multi-agent orchestration — sessions, delegations, model tiers, and quality gates."><meta name="theme-color" content="#0a0a0f"><link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png"><link rel="stylesheet" href="/_astro/index.CWVzbF4T.css"></head> <body> <header class="dash-header"> <div class="dash-header__inner"> <div class="dash-header__brand"> <img class="dash-header__icon" src="/icon-192.png" alt="OpenCastle" width="32" height="32"> <h1 class="dash-header__title">Observability Dashboard</h1> </div> </div> </header> <div class="dash-layout"> <!-- Sidebar Navigation --> <nav class="dash-sidebar" id="dash-sidebar"> <ul class="dash-sidebar__list"> <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#kpi-row" data-section="kpi-row">Overview</a></li> <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section">Pipeline</a></li> <li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section">Agents</a></li> <li><a class="dash-sidebar__link" href="#tier-section" data-section="tier-section">Tiers</a></li> <li><a class="dash-sidebar__link" href="#delegation-section" data-section="delegation-section">Delegations</a></li> <li><a class="dash-sidebar__link" href="#timeline-section" data-section="timeline-section">Timeline</a></li> <li><a class="dash-sidebar__link" href="#model-section" data-section="model-section">Models</a></li> <li><a class="dash-sidebar__link" href="#execution-section" data-section="execution-section">Exec Log</a></li> <li><a class="dash-sidebar__link" href="#panel-section" data-section="panel-section">Panels</a></li> <li><a class="dash-sidebar__link" href="#sessions-section" data-section="sessions-section">Sessions</a></li> </ul> </nav> <main class="dash-main"> <!-- KPI Row --> <section class="kpi-row" id="kpi-row" data-nav-section> <div class="kpi-card" id="kpi-sessions"> <span class="kpi-card__label">Total Sessions</span> <span class="kpi-card__value">—</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-success"> <span class="kpi-card__label">Success Rate</span> <span class="kpi-card__value">—</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-delegations"> <span class="kpi-card__label">Total Delegations</span> <span class="kpi-card__value">—</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-duration"> <span class="kpi-card__label">Avg Duration</span> <span class="kpi-card__value">—</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-retries"> <span class="kpi-card__label">Total Retries</span> <span class="kpi-card__value">—</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-lessons"> <span class="kpi-card__label">Lessons Added</span> <span class="kpi-card__value">—</span> <span class="kpi-card__sub"></span> </div> </section> <!-- Pipeline View (Steroids-inspired) --> <section class="chart-card" id="pipeline-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Task Pipeline</h2> <p class="chart-card__desc">Delegation flow across execution phases</p> </div> <div class="chart-card__body" id="pipeline-view"> <div class="loading-skeleton"></div> </div> </section> <!-- Charts Row 1 --> <div class="charts-row" id="agent-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Sessions by Agent</h2> <p class="chart-card__desc">Stacked by outcome</p> </div> <div class="chart-card__body" id="agent-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card" id="tier-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Tier Distribution</h2> <p class="chart-card__desc">Delegation model tiers</p> </div> <div class="chart-card__body" id="tier-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Charts Row: Delegation Insights --> <div class="charts-row" id="delegation-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Delegation Mechanism</h2> <p class="chart-card__desc">Sub-agent vs background split</p> </div> <div class="chart-card__body" id="mechanism-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Delegation Outcomes</h2> <p class="chart-card__desc">Success rate by delegation</p> </div> <div class="chart-card__body" id="delegation-outcome-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Charts Row 2 --> <div class="charts-row" id="timeline-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Timeline</h2> <p class="chart-card__desc">Sessions and delegations over time</p> </div> <div class="chart-card__body" id="timeline-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card" id="model-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Model Usage</h2> <p class="chart-card__desc">Sessions by model</p> </div> <div class="chart-card__body" id="model-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Execution Log (Duvo-inspired) --> <section class="chart-card" id="execution-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Execution Log</h2> <p class="chart-card__desc">Recent agent activity, step by step</p> </div> <div class="chart-card__body" id="execution-log"> <div class="loading-skeleton"></div> </div> </section> <!-- Panel Results --> <section class="chart-card" id="panel-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Panel Reviews</h2> <p class="chart-card__desc">Quality gate verdicts and fix items</p> </div> <div class="chart-card__body" id="panel-chart"> <div class="loading-skeleton"></div> </div> </section> <!-- Sessions Table --> <section class="chart-card" id="sessions-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Recent Sessions</h2> <p class="chart-card__desc">Last 15 sessions by timestamp</p> </div> <div class="chart-card__body chart-card__body--table" id="sessions-table"> <div class="loading-skeleton"></div> </div> </section> </main> </div> </body></html> <script>(function(){const base = "/";
|
|
2
|
+
|
|
3
|
+
// ── Data Loading ──────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
async function loadNdjson(path) {
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(path);
|
|
8
|
+
if (!res.ok) return [];
|
|
9
|
+
const text = await res.text();
|
|
10
|
+
return text
|
|
11
|
+
.trim()
|
|
12
|
+
.split('\n')
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.map((line) => JSON.parse(line));
|
|
15
|
+
} catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── Helpers ───────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const TIER_COLORS = {
|
|
23
|
+
premium: '#f59e0b',
|
|
24
|
+
standard: '#a78bfa',
|
|
25
|
+
utility: '#3b82f6',
|
|
26
|
+
economy: '#64748b',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const MODEL_COLORS = {
|
|
30
|
+
'claude-opus-4-6': '#a78bfa',
|
|
31
|
+
'gpt-5-mini': '#64748b',
|
|
32
|
+
'gpt-5.3-codex': '#3b82f6',
|
|
33
|
+
'gemini-3.1-pro': '#f59e0b',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const OUTCOME_ICONS = {
|
|
37
|
+
success: '\u2713',
|
|
38
|
+
partial: '\u25CB',
|
|
39
|
+
failed: '\u2717',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function formatTime(ts) {
|
|
43
|
+
const d = new Date(ts);
|
|
44
|
+
return d.toLocaleDateString('en-US', {
|
|
45
|
+
month: 'short',
|
|
46
|
+
day: 'numeric',
|
|
47
|
+
hour: '2-digit',
|
|
48
|
+
minute: '2-digit',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatShortDate(dateKey) {
|
|
53
|
+
const d = new Date(dateKey + 'T00:00:00Z');
|
|
54
|
+
return d.toLocaleDateString('en-US', {
|
|
55
|
+
month: 'short',
|
|
56
|
+
day: 'numeric',
|
|
57
|
+
timeZone: 'UTC',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function escapeHtml(str) {
|
|
62
|
+
const div = document.createElement('div');
|
|
63
|
+
div.textContent = str;
|
|
64
|
+
return div.innerHTML;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── SVG Icon Library (empty states) ────────────────────
|
|
68
|
+
|
|
69
|
+
const EMPTY_ICONS = {
|
|
70
|
+
welcome: '<svg width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M24 4L6 14v20l18 10 18-10V14L24 4z" opacity="0.3"/><path d="M24 4L6 14v20l18 10 18-10V14L24 4z"/><path d="M24 24v20"/><path d="M6 14l18 10 18-10"/><rect x="18" y="18" width="12" height="14" rx="1" opacity="0.5"/><path d="M21 32v-6h6v6"/></svg>',
|
|
71
|
+
agents: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="20" cy="14" r="6"/><path d="M8 34c0-6.627 5.373-12 12-12s12 5.373 12 12"/><circle cx="32" cy="12" r="4" opacity="0.4"/><path d="M36 26c0-3.5-2-6-4-7" opacity="0.4"/></svg>',
|
|
72
|
+
tiers: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="20" cy="10" rx="14" ry="5"/><path d="M6 10v8c0 2.761 6.268 5 14 5s14-2.239 14-5v-8"/><path d="M6 18v8c0 2.761 6.268 5 14 5s14-2.239 14-5v-8" opacity="0.5"/></svg>',
|
|
73
|
+
mechanism: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="20" cy="20" r="6"/><path d="M20 6v6M20 28v6M6 20h6M28 20h6"/><path d="M10.1 10.1l4.2 4.2M25.7 25.7l4.2 4.2M10.1 29.9l4.2-4.2M25.7 14.3l4.2-4.2" opacity="0.5"/></svg>',
|
|
74
|
+
outcomes: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="6" width="28" height="28" rx="4"/><polyline points="12 22 18 28 28 14" opacity="0.5"/><line x1="12" y1="16" x2="18" y2="16" opacity="0.3"/><line x1="12" y1="12" x2="24" y2="12" opacity="0.3"/></svg>',
|
|
75
|
+
timeline: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="8" width="28" height="26" rx="3"/><line x1="6" y1="16" x2="34" y2="16"/><line x1="14" y1="8" x2="14" y2="12"/><line x1="26" y1="8" x2="26" y2="12"/><circle cx="14" cy="24" r="2" opacity="0.4"/><circle cx="20" cy="28" r="2" opacity="0.4"/><circle cx="26" cy="22" r="2" opacity="0.4"/></svg>',
|
|
76
|
+
models: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="24" height="24" rx="4"/><circle cx="20" cy="20" r="4"/><path d="M20 12v4M20 24v4M12 20h4M24 20h4" opacity="0.5"/><circle cx="14" cy="14" r="1.5" opacity="0.3"/><circle cx="26" cy="14" r="1.5" opacity="0.3"/><circle cx="14" cy="26" r="1.5" opacity="0.3"/><circle cx="26" cy="26" r="1.5" opacity="0.3"/></svg>',
|
|
77
|
+
execLog: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 8h18a2 2 0 0 1 2 2v20a2 2 0 0 1-2 2H14"/><circle cx="10" cy="14" r="3"/><circle cx="10" cy="24" r="3" opacity="0.4"/><line x1="10" y1="17" x2="10" y2="21" opacity="0.3"/><line x1="18" y1="14" x2="28" y2="14" opacity="0.5"/><line x1="18" y1="24" x2="26" y2="24" opacity="0.3"/><line x1="18" y1="19" x2="24" y2="19" opacity="0.2"/></svg>',
|
|
78
|
+
panels: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6l2 4h4l-3 3 1 5-4-2.5L16 18l1-5-3-3h4l2-4z"/><rect x="8" y="22" width="24" height="12" rx="3" opacity="0.4"/><line x1="14" y1="28" x2="26" y2="28" opacity="0.3"/><line x1="14" y1="31" x2="22" y2="31" opacity="0.2"/></svg>',
|
|
79
|
+
sessions: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="6" width="28" height="28" rx="3"/><line x1="6" y1="14" x2="34" y2="14"/><line x1="14" y1="6" x2="14" y2="34" opacity="0.3"/><line x1="6" y1="22" x2="34" y2="22" opacity="0.2"/><line x1="6" y1="30" x2="34" y2="30" opacity="0.2"/></svg>',
|
|
80
|
+
pipeline: '<svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="14" width="8" height="12" rx="2" opacity="0.3"/><rect x="16" y="14" width="8" height="12" rx="2" opacity="0.3"/><rect x="28" y="14" width="8" height="12" rx="2" opacity="0.3"/><path d="M12 20h4M24 20h4" opacity="0.4"/></svg>',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function emptyStateHtml(iconKey, title, description) {
|
|
84
|
+
return '<div class="empty-state empty-state--enhanced">' +
|
|
85
|
+
'<div class="empty-state__icon-wrap">' + EMPTY_ICONS[iconKey] + '</div>' +
|
|
86
|
+
'<p class="empty-state__title">' + title + '</p>' +
|
|
87
|
+
'<p class="empty-state__desc">' + description + '</p>' +
|
|
88
|
+
'</div>';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Welcome Banner (all data empty) ────────────────────
|
|
92
|
+
|
|
93
|
+
function renderWelcomeBanner() {
|
|
94
|
+
const existing = document.getElementById('welcome-banner');
|
|
95
|
+
if (existing) existing.remove();
|
|
96
|
+
|
|
97
|
+
const banner = document.createElement('section');
|
|
98
|
+
banner.id = 'welcome-banner';
|
|
99
|
+
banner.className = 'welcome-banner';
|
|
100
|
+
banner.innerHTML =
|
|
101
|
+
'<div class="welcome-banner__glow"></div>' +
|
|
102
|
+
'<div class="welcome-banner__content">' +
|
|
103
|
+
'<div class="welcome-banner__icon">' + EMPTY_ICONS.welcome + '</div>' +
|
|
104
|
+
'<h2 class="welcome-banner__title">Welcome to your Observability Dashboard</h2>' +
|
|
105
|
+
'<p class="welcome-banner__subtitle">Agent sessions, delegations, and quality reviews will appear here as your team completes work.</p>' +
|
|
106
|
+
'<div class="welcome-banner__steps">' +
|
|
107
|
+
'<div class="welcome-step">' +
|
|
108
|
+
'<span class="welcome-step__num">1</span>' +
|
|
109
|
+
'<div class="welcome-step__text">' +
|
|
110
|
+
'<strong>Configure agents</strong>' +
|
|
111
|
+
'<span>Set up your orchestrator and specialist agents</span>' +
|
|
112
|
+
'</div>' +
|
|
113
|
+
'</div>' +
|
|
114
|
+
'<div class="welcome-step">' +
|
|
115
|
+
'<span class="welcome-step__num">2</span>' +
|
|
116
|
+
'<div class="welcome-step__text">' +
|
|
117
|
+
'<strong>Run a session</strong>' +
|
|
118
|
+
'<span>Delegate tasks — session logs are captured automatically</span>' +
|
|
119
|
+
'</div>' +
|
|
120
|
+
'</div>' +
|
|
121
|
+
'<div class="welcome-step">' +
|
|
122
|
+
'<span class="welcome-step__num">3</span>' +
|
|
123
|
+
'<div class="welcome-step__text">' +
|
|
124
|
+
'<strong>Watch insights flow in</strong>' +
|
|
125
|
+
'<span>Charts, metrics, and trends populate in real time</span>' +
|
|
126
|
+
'</div>' +
|
|
127
|
+
'</div>' +
|
|
128
|
+
'</div>' +
|
|
129
|
+
'</div>';
|
|
130
|
+
|
|
131
|
+
const main = document.querySelector('.dash-main');
|
|
132
|
+
if (main) main.prepend(banner);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function removeWelcomeBanner() {
|
|
136
|
+
const existing = document.getElementById('welcome-banner');
|
|
137
|
+
if (existing) existing.remove();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── KPI Rendering ────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function renderKpis(sessions, delegations) {
|
|
143
|
+
const total = sessions.length;
|
|
144
|
+
const isEmpty = total === 0;
|
|
145
|
+
const successCount = sessions.filter((s) => s.outcome === 'success').length;
|
|
146
|
+
const rate = total > 0 ? Math.round((successCount / total) * 100) : 0;
|
|
147
|
+
const durSessions = sessions.filter((s) => s.duration_min != null);
|
|
148
|
+
const avgDur =
|
|
149
|
+
durSessions.length > 0
|
|
150
|
+
? Math.round(
|
|
151
|
+
durSessions.reduce((sum, s) => sum + (s.duration_min || 0), 0) /
|
|
152
|
+
durSessions.length
|
|
153
|
+
)
|
|
154
|
+
: 0;
|
|
155
|
+
const uniqueAgents = new Set(delegations.map((d) => d.agent)).size;
|
|
156
|
+
|
|
157
|
+
// Toggle ghost class on KPI row
|
|
158
|
+
const kpiRow = document.querySelector('.kpi-row');
|
|
159
|
+
if (kpiRow) kpiRow.classList.toggle('kpi-row--empty', isEmpty);
|
|
160
|
+
|
|
161
|
+
const kpiSessions = document.getElementById('kpi-sessions');
|
|
162
|
+
const kpiSuccess = document.getElementById('kpi-success');
|
|
163
|
+
const kpiDelegations = document.getElementById('kpi-delegations');
|
|
164
|
+
const kpiDuration = document.getElementById('kpi-duration');
|
|
165
|
+
|
|
166
|
+
if (kpiSessions) {
|
|
167
|
+
kpiSessions.querySelector('.kpi-card__value').textContent = isEmpty ? '0' : total;
|
|
168
|
+
kpiSessions.querySelector('.kpi-card__sub').innerHTML = isEmpty
|
|
169
|
+
? '<span class="kpi-card__hint">No sessions yet</span>'
|
|
170
|
+
: '<span class="kpi-trend kpi-trend--up">\u2191</span> ' + successCount + ' successful';
|
|
171
|
+
}
|
|
172
|
+
if (kpiSuccess) {
|
|
173
|
+
if (isEmpty) {
|
|
174
|
+
kpiSuccess.querySelector('.kpi-card__value').textContent = '\u2014';
|
|
175
|
+
kpiSuccess.querySelector('.kpi-card__sub').innerHTML =
|
|
176
|
+
'<span class="kpi-card__hint">No sessions yet</span>';
|
|
177
|
+
} else {
|
|
178
|
+
const trendClass =
|
|
179
|
+
rate >= 80 ? 'up' : rate >= 60 ? 'neutral' : 'down';
|
|
180
|
+
kpiSuccess.querySelector('.kpi-card__value').textContent = rate + '%';
|
|
181
|
+
kpiSuccess.querySelector('.kpi-card__sub').innerHTML =
|
|
182
|
+
'<span class="kpi-trend kpi-trend--' +
|
|
183
|
+
trendClass +
|
|
184
|
+
'">' +
|
|
185
|
+
(trendClass === 'up' ? '\u2191' : trendClass === 'down' ? '\u2193' : '\u2192') +
|
|
186
|
+
'</span> across all sessions';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (kpiDelegations) {
|
|
190
|
+
kpiDelegations.querySelector('.kpi-card__value').textContent =
|
|
191
|
+
delegations.length === 0 ? '0' : delegations.length;
|
|
192
|
+
kpiDelegations.querySelector('.kpi-card__sub').innerHTML = isEmpty
|
|
193
|
+
? '<span class="kpi-card__hint">No delegations yet</span>'
|
|
194
|
+
: uniqueAgents + ' unique agents';
|
|
195
|
+
}
|
|
196
|
+
if (kpiDuration) {
|
|
197
|
+
kpiDuration.querySelector('.kpi-card__value').textContent = isEmpty ? '\u2014' : avgDur + 'm';
|
|
198
|
+
kpiDuration.querySelector('.kpi-card__sub').innerHTML = isEmpty
|
|
199
|
+
? '<span class="kpi-card__hint">No duration yet</span>'
|
|
200
|
+
: '<span class="kpi-trend kpi-trend--neutral">\u2192</span> per session';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Retries KPI
|
|
204
|
+
const totalRetries = sessions.reduce((sum, s) => sum + (s.retries || 0), 0);
|
|
205
|
+
const kpiRetries = document.getElementById('kpi-retries');
|
|
206
|
+
if (kpiRetries) {
|
|
207
|
+
kpiRetries.querySelector('.kpi-card__value').textContent = isEmpty ? '0' : totalRetries;
|
|
208
|
+
const retriedSessions = sessions.filter((s) => (s.retries || 0) > 0).length;
|
|
209
|
+
kpiRetries.querySelector('.kpi-card__sub').innerHTML = isEmpty
|
|
210
|
+
? '<span class="kpi-card__hint">No retries yet</span>'
|
|
211
|
+
: retriedSessions + ' sessions with retries';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Lessons KPI
|
|
215
|
+
const totalLessons = sessions.reduce(
|
|
216
|
+
(sum, s) => sum + (s.lessons_added ? s.lessons_added.length : 0),
|
|
217
|
+
0
|
|
218
|
+
);
|
|
219
|
+
const kpiLessons = document.getElementById('kpi-lessons');
|
|
220
|
+
if (kpiLessons) {
|
|
221
|
+
kpiLessons.querySelector('.kpi-card__value').textContent = isEmpty ? '0' : totalLessons;
|
|
222
|
+
const discoveryCount = sessions.reduce(
|
|
223
|
+
(sum, s) => sum + (s.discoveries ? s.discoveries.length : 0),
|
|
224
|
+
0
|
|
225
|
+
);
|
|
226
|
+
kpiLessons.querySelector('.kpi-card__sub').innerHTML = isEmpty
|
|
227
|
+
? '<span class="kpi-card__hint">No lessons yet</span>'
|
|
228
|
+
: discoveryCount + ' issues discovered';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Pipeline View ─────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
function renderPipeline(delegations) {
|
|
235
|
+
const el = document.getElementById('pipeline-view');
|
|
236
|
+
if (!el) return;
|
|
237
|
+
|
|
238
|
+
if (delegations.length === 0) {
|
|
239
|
+
el.innerHTML = emptyStateHtml('pipeline', 'No pipeline activity yet', 'Delegation phases appear here as tasks flow through Foundation, Integration, Validation, and QA stages.');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const phases = { 1: 0, 2: 0, 3: 0, 4: 0 };
|
|
244
|
+
delegations.forEach((d) => {
|
|
245
|
+
const p = d.phase || 1;
|
|
246
|
+
if (phases[p] !== undefined) phases[p]++;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const stageConfig = [
|
|
250
|
+
{
|
|
251
|
+
label: 'Foundation',
|
|
252
|
+
phase: 1,
|
|
253
|
+
iconClass: 'pending',
|
|
254
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="13" y2="14"/></svg>',
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
label: 'Integration',
|
|
258
|
+
phase: 2,
|
|
259
|
+
iconClass: 'active',
|
|
260
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
label: 'Validation',
|
|
264
|
+
phase: 3,
|
|
265
|
+
iconClass: 'review',
|
|
266
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
label: 'QA Gate',
|
|
270
|
+
phase: 4,
|
|
271
|
+
iconClass: 'done',
|
|
272
|
+
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
el.innerHTML =
|
|
277
|
+
'<div class="pipeline">' +
|
|
278
|
+
stageConfig
|
|
279
|
+
.map(
|
|
280
|
+
(stage, i) =>
|
|
281
|
+
(i > 0 ? '<div class="pipeline-arrow">\u2192</div>' : '') +
|
|
282
|
+
'<div class="pipeline-stage">' +
|
|
283
|
+
'<div class="pipeline-stage__icon pipeline-stage__icon--' +
|
|
284
|
+
stage.iconClass +
|
|
285
|
+
'">' +
|
|
286
|
+
stage.icon +
|
|
287
|
+
'</div>' +
|
|
288
|
+
'<span class="pipeline-stage__count">' +
|
|
289
|
+
(phases[stage.phase] || 0) +
|
|
290
|
+
'</span>' +
|
|
291
|
+
'<span class="pipeline-stage__label">' +
|
|
292
|
+
stage.label +
|
|
293
|
+
'</span>' +
|
|
294
|
+
'</div>'
|
|
295
|
+
)
|
|
296
|
+
.join('') +
|
|
297
|
+
'</div>';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Agent Chart ───────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
function renderAgentChart(sessions) {
|
|
303
|
+
const el = document.getElementById('agent-chart');
|
|
304
|
+
if (!el) return;
|
|
305
|
+
|
|
306
|
+
if (sessions.length === 0) {
|
|
307
|
+
el.innerHTML = emptyStateHtml('agents', 'No agent sessions yet', 'A breakdown of sessions per agent will appear here — stacked by outcome (success, partial, failed).');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const agentMap = {};
|
|
312
|
+
sessions.forEach((s) => {
|
|
313
|
+
if (!agentMap[s.agent])
|
|
314
|
+
agentMap[s.agent] = { success: 0, partial: 0, failed: 0, total: 0 };
|
|
315
|
+
agentMap[s.agent][s.outcome] = (agentMap[s.agent][s.outcome] || 0) + 1;
|
|
316
|
+
agentMap[s.agent].total++;
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const agents = Object.entries(agentMap).sort(
|
|
320
|
+
(a, b) => b[1].total - a[1].total
|
|
321
|
+
);
|
|
322
|
+
const maxTotal = Math.max(...agents.map(([, d]) => d.total));
|
|
323
|
+
|
|
324
|
+
el.innerHTML = agents
|
|
325
|
+
.map(
|
|
326
|
+
([name, data]) =>
|
|
327
|
+
'<div class="bar-row">' +
|
|
328
|
+
'<span class="bar-label">' +
|
|
329
|
+
escapeHtml(name) +
|
|
330
|
+
'</span>' +
|
|
331
|
+
'<div class="bar-track">' +
|
|
332
|
+
(data.success > 0
|
|
333
|
+
? '<div class="bar-segment bar--success" style="width: ' +
|
|
334
|
+
((data.success / maxTotal) * 100).toFixed(1) + '%"></div>'
|
|
335
|
+
: '') +
|
|
336
|
+
(data.partial > 0
|
|
337
|
+
? '<div class="bar-segment bar--partial" style="width: ' +
|
|
338
|
+
((data.partial / maxTotal) * 100).toFixed(1) + '%"></div>'
|
|
339
|
+
: '') +
|
|
340
|
+
(data.failed > 0
|
|
341
|
+
? '<div class="bar-segment bar--failed" style="width: ' +
|
|
342
|
+
((data.failed / maxTotal) * 100).toFixed(1) + '%"></div>'
|
|
343
|
+
: '') +
|
|
344
|
+
'</div>' +
|
|
345
|
+
'<span class="bar-value">' +
|
|
346
|
+
data.total +
|
|
347
|
+
'</span>' +
|
|
348
|
+
'</div>'
|
|
349
|
+
)
|
|
350
|
+
.join('');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── Tier Donut Chart ──────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
function renderTierChart(delegations) {
|
|
356
|
+
const el = document.getElementById('tier-chart');
|
|
357
|
+
if (!el) return;
|
|
358
|
+
|
|
359
|
+
if (delegations.length === 0) {
|
|
360
|
+
el.innerHTML = emptyStateHtml('tiers', 'No tier data yet', 'Model tier distribution (Premium, Standard, Utility, Economy) will be visualized as a donut chart.');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const tierCounts = {};
|
|
365
|
+
delegations.forEach((d) => {
|
|
366
|
+
tierCounts[d.tier] = (tierCounts[d.tier] || 0) + 1;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const order = ['premium', 'standard', 'utility', 'economy'];
|
|
370
|
+
const tiers = order
|
|
371
|
+
.filter((t) => tierCounts[t])
|
|
372
|
+
.map((t) => ({ name: t, count: tierCounts[t] }));
|
|
373
|
+
|
|
374
|
+
const total = delegations.length;
|
|
375
|
+
const r = 70;
|
|
376
|
+
const circumference = 2 * Math.PI * r;
|
|
377
|
+
let cumOffset = 0;
|
|
378
|
+
|
|
379
|
+
const circles = tiers.map((t) => {
|
|
380
|
+
const pct = t.count / total;
|
|
381
|
+
const dashLen = pct * circumference;
|
|
382
|
+
// Skip round linecap for single-segment donuts to avoid overlap artifact
|
|
383
|
+
const linecap = tiers.length === 1 ? 'butt' : 'round';
|
|
384
|
+
const segment =
|
|
385
|
+
'<circle cx="90" cy="90" r="' +
|
|
386
|
+
r +
|
|
387
|
+
'" fill="none" ' +
|
|
388
|
+
'stroke="' +
|
|
389
|
+
(TIER_COLORS[t.name] || '#64748b') +
|
|
390
|
+
'" stroke-width="18" ' +
|
|
391
|
+
'stroke-dasharray="' +
|
|
392
|
+
dashLen.toFixed(2) +
|
|
393
|
+
' ' +
|
|
394
|
+
(circumference - dashLen).toFixed(2) +
|
|
395
|
+
'" ' +
|
|
396
|
+
'stroke-dashoffset="' +
|
|
397
|
+
(-cumOffset).toFixed(2) +
|
|
398
|
+
'" ' +
|
|
399
|
+
'transform="rotate(-90 90 90)" ' +
|
|
400
|
+
'stroke-linecap="' + linecap + '"/>';
|
|
401
|
+
cumOffset += dashLen;
|
|
402
|
+
return segment;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const legend = tiers
|
|
406
|
+
.map(
|
|
407
|
+
(t) =>
|
|
408
|
+
'<div class="legend-item">' +
|
|
409
|
+
'<span class="legend-dot" style="background: ' +
|
|
410
|
+
(TIER_COLORS[t.name] || '#64748b') +
|
|
411
|
+
'"></span>' +
|
|
412
|
+
'<span class="legend-name">' +
|
|
413
|
+
t.name +
|
|
414
|
+
'</span>' +
|
|
415
|
+
'<span class="legend-count">' +
|
|
416
|
+
t.count +
|
|
417
|
+
' (' +
|
|
418
|
+
Math.round((t.count / total) * 100) +
|
|
419
|
+
'%)</span>' +
|
|
420
|
+
'</div>'
|
|
421
|
+
)
|
|
422
|
+
.join('');
|
|
423
|
+
|
|
424
|
+
el.innerHTML =
|
|
425
|
+
'<div class="donut-container">' +
|
|
426
|
+
'<div class="donut-wrap">' +
|
|
427
|
+
'<svg viewBox="0 0 180 180" class="donut-svg">' +
|
|
428
|
+
circles.join('') +
|
|
429
|
+
'</svg>' +
|
|
430
|
+
'<div class="donut-center">' +
|
|
431
|
+
'<span class="donut-total">' +
|
|
432
|
+
total +
|
|
433
|
+
'</span>' +
|
|
434
|
+
'<span class="donut-total-label">total</span>' +
|
|
435
|
+
'</div>' +
|
|
436
|
+
'</div>' +
|
|
437
|
+
'<div class="donut-legend">' +
|
|
438
|
+
legend +
|
|
439
|
+
'</div>' +
|
|
440
|
+
'</div>';
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ── Mechanism Donut Chart ─────────────────────────────────
|
|
444
|
+
|
|
445
|
+
function renderMechanismChart(delegations) {
|
|
446
|
+
const el = document.getElementById('mechanism-chart');
|
|
447
|
+
if (!el) return;
|
|
448
|
+
|
|
449
|
+
if (delegations.length === 0) {
|
|
450
|
+
el.innerHTML = emptyStateHtml('mechanism', 'No delegation data yet', 'The split between sub-agent (inline) and background (worktree) delegations will be shown here.');
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const mechCounts = {};
|
|
455
|
+
delegations.forEach((d) => {
|
|
456
|
+
const mech = d.mechanism || 'unknown';
|
|
457
|
+
mechCounts[mech] = (mechCounts[mech] || 0) + 1;
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
var MECH_COLORS = {
|
|
461
|
+
'sub-agent': '#3b82f6',
|
|
462
|
+
'background': '#a78bfa',
|
|
463
|
+
'unknown': '#64748b',
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
var MECH_LABELS = {
|
|
467
|
+
'sub-agent': 'Sub-agent (inline)',
|
|
468
|
+
'background': 'Background (worktree)',
|
|
469
|
+
'unknown': 'Unknown',
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
var mechOrder = ['sub-agent', 'background', 'unknown'];
|
|
473
|
+
var mechs = mechOrder
|
|
474
|
+
.filter(function (m) { return mechCounts[m]; })
|
|
475
|
+
.map(function (m) { return { name: m, count: mechCounts[m] }; });
|
|
476
|
+
|
|
477
|
+
var total = delegations.length;
|
|
478
|
+
var r = 70;
|
|
479
|
+
var circumference = 2 * Math.PI * r;
|
|
480
|
+
var cumOffset = 0;
|
|
481
|
+
|
|
482
|
+
var circles = mechs.map(function (m) {
|
|
483
|
+
var pct = m.count / total;
|
|
484
|
+
var dashLen = pct * circumference;
|
|
485
|
+
// Skip round linecap for single-segment donuts to avoid overlap artifact
|
|
486
|
+
var linecap = mechs.length === 1 ? 'butt' : 'round';
|
|
487
|
+
var segment =
|
|
488
|
+
'<circle cx="90" cy="90" r="' + r + '" fill="none" ' +
|
|
489
|
+
'stroke="' + (MECH_COLORS[m.name] || '#64748b') + '" stroke-width="18" ' +
|
|
490
|
+
'stroke-dasharray="' + dashLen.toFixed(2) + ' ' + (circumference - dashLen).toFixed(2) + '" ' +
|
|
491
|
+
'stroke-dashoffset="' + (-cumOffset).toFixed(2) + '" ' +
|
|
492
|
+
'transform="rotate(-90 90 90)" stroke-linecap="' + linecap + '"/>';
|
|
493
|
+
cumOffset += dashLen;
|
|
494
|
+
return segment;
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
var legend = mechs
|
|
498
|
+
.map(function (m) {
|
|
499
|
+
return '<div class="legend-item">' +
|
|
500
|
+
'<span class="legend-dot" style="background: ' + (MECH_COLORS[m.name] || '#64748b') + '"></span>' +
|
|
501
|
+
'<span class="legend-name">' + (MECH_LABELS[m.name] || m.name) + '</span>' +
|
|
502
|
+
'<span class="legend-count">' + m.count + ' (' + Math.round((m.count / total) * 100) + '%)</span>' +
|
|
503
|
+
'</div>';
|
|
504
|
+
})
|
|
505
|
+
.join('');
|
|
506
|
+
|
|
507
|
+
el.innerHTML =
|
|
508
|
+
'<div class="donut-container">' +
|
|
509
|
+
'<div class="donut-wrap">' +
|
|
510
|
+
'<svg viewBox="0 0 180 180" class="donut-svg">' +
|
|
511
|
+
circles.join('') +
|
|
512
|
+
'</svg>' +
|
|
513
|
+
'<div class="donut-center">' +
|
|
514
|
+
'<span class="donut-total">' + total + '</span>' +
|
|
515
|
+
'<span class="donut-total-label">total</span>' +
|
|
516
|
+
'</div>' +
|
|
517
|
+
'</div>' +
|
|
518
|
+
'<div class="donut-legend">' +
|
|
519
|
+
legend +
|
|
520
|
+
'</div>' +
|
|
521
|
+
'</div>';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ── Delegation Outcome Chart ──────────────────────────────
|
|
525
|
+
|
|
526
|
+
function renderDelegationOutcomeChart(delegations) {
|
|
527
|
+
var el = document.getElementById('delegation-outcome-chart');
|
|
528
|
+
if (!el) return;
|
|
529
|
+
|
|
530
|
+
if (delegations.length === 0) {
|
|
531
|
+
el.innerHTML = emptyStateHtml('outcomes', 'No outcome data yet', 'Delegation results — success, partial, failed, redirected — will be tracked and compared here.');
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
var OUTCOME_COLORS = {
|
|
536
|
+
success: '#22c55e',
|
|
537
|
+
partial: '#f59e0b',
|
|
538
|
+
failed: '#ef4444',
|
|
539
|
+
redirected: '#64748b',
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
var outcomeCounts = {};
|
|
543
|
+
delegations.forEach(function (d) {
|
|
544
|
+
var outcome = d.outcome || 'unknown';
|
|
545
|
+
outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
var outcomes = Object.entries(outcomeCounts).sort(function (a, b) { return b[1] - a[1]; });
|
|
549
|
+
var maxCount = Math.max.apply(null, outcomes.map(function (o) { return o[1]; }));
|
|
550
|
+
|
|
551
|
+
el.innerHTML = outcomes
|
|
552
|
+
.map(function (entry) {
|
|
553
|
+
var name = entry[0];
|
|
554
|
+
var count = entry[1];
|
|
555
|
+
return '<div class="bar-row">' +
|
|
556
|
+
'<span class="bar-label">' + escapeHtml(name) + '</span>' +
|
|
557
|
+
'<div class="bar-track">' +
|
|
558
|
+
'<div class="bar-segment" style="width: ' + ((count / maxCount) * 100).toFixed(1) + '%; background: ' + (OUTCOME_COLORS[name] || '#64748b') + '"></div>' +
|
|
559
|
+
'</div>' +
|
|
560
|
+
'<span class="bar-value">' + count + '</span>' +
|
|
561
|
+
'</div>';
|
|
562
|
+
})
|
|
563
|
+
.join('');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ── Timeline Chart ────────────────────────────────────────
|
|
567
|
+
|
|
568
|
+
function renderTimelineChart(sessions, delegations) {
|
|
569
|
+
const el = document.getElementById('timeline-chart');
|
|
570
|
+
if (!el) return;
|
|
571
|
+
|
|
572
|
+
const dateMap = {};
|
|
573
|
+
sessions.forEach((s) => {
|
|
574
|
+
const key = s.timestamp.slice(0, 10);
|
|
575
|
+
if (!dateMap[key]) dateMap[key] = { sessions: 0, delegations: 0 };
|
|
576
|
+
dateMap[key].sessions++;
|
|
577
|
+
});
|
|
578
|
+
delegations.forEach((d) => {
|
|
579
|
+
const key = d.timestamp.slice(0, 10);
|
|
580
|
+
if (!dateMap[key]) dateMap[key] = { sessions: 0, delegations: 0 };
|
|
581
|
+
dateMap[key].delegations++;
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const dates = Object.entries(dateMap).sort(([a], [b]) =>
|
|
585
|
+
a.localeCompare(b)
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
if (dates.length === 0) {
|
|
589
|
+
el.innerHTML = emptyStateHtml('timeline', 'No timeline data yet', 'A daily activity chart will build here as sessions and delegations accumulate over time.');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const maxVal = Math.max(
|
|
594
|
+
...dates.map(([, d]) => Math.max(d.sessions, d.delegations))
|
|
595
|
+
);
|
|
596
|
+
const w = 500;
|
|
597
|
+
const h = 180;
|
|
598
|
+
const pad = { top: 10, right: 10, bottom: 28, left: 10 };
|
|
599
|
+
const plotW = w - pad.left - pad.right;
|
|
600
|
+
const plotH = h - pad.top - pad.bottom;
|
|
601
|
+
// Prevent sparse layout when there are very few dates
|
|
602
|
+
const groupWidth = dates.length <= 3
|
|
603
|
+
? Math.min(100, plotW / dates.length)
|
|
604
|
+
: plotW / dates.length;
|
|
605
|
+
const barWidth = Math.min(dates.length <= 3 ? 24 : 16, groupWidth * 0.35);
|
|
606
|
+
// Center the bars when there are few dates
|
|
607
|
+
const timelineStartX = dates.length <= 3
|
|
608
|
+
? pad.left + (plotW - dates.length * groupWidth) / 2
|
|
609
|
+
: pad.left;
|
|
610
|
+
|
|
611
|
+
let rects = '';
|
|
612
|
+
let labels = '';
|
|
613
|
+
|
|
614
|
+
dates.forEach(([date, data], i) => {
|
|
615
|
+
const x = timelineStartX + i * groupWidth + groupWidth / 2;
|
|
616
|
+
const sH = maxVal > 0 ? (data.sessions / maxVal) * plotH : 0;
|
|
617
|
+
const dH = maxVal > 0 ? (data.delegations / maxVal) * plotH : 0;
|
|
618
|
+
|
|
619
|
+
rects +=
|
|
620
|
+
'<rect x="' +
|
|
621
|
+
(x - barWidth - 1).toFixed(1) +
|
|
622
|
+
'" y="' +
|
|
623
|
+
(pad.top + plotH - sH).toFixed(1) +
|
|
624
|
+
'" width="' +
|
|
625
|
+
barWidth.toFixed(1) +
|
|
626
|
+
'" height="' +
|
|
627
|
+
sH.toFixed(1) +
|
|
628
|
+
'" fill="#3b82f6" rx="3" opacity="0.85"/>';
|
|
629
|
+
rects +=
|
|
630
|
+
'<rect x="' +
|
|
631
|
+
(x + 1).toFixed(1) +
|
|
632
|
+
'" y="' +
|
|
633
|
+
(pad.top + plotH - dH).toFixed(1) +
|
|
634
|
+
'" width="' +
|
|
635
|
+
barWidth.toFixed(1) +
|
|
636
|
+
'" height="' +
|
|
637
|
+
dH.toFixed(1) +
|
|
638
|
+
'" fill="#a78bfa" rx="3" opacity="0.65"/>';
|
|
639
|
+
labels +=
|
|
640
|
+
'<text x="' +
|
|
641
|
+
x.toFixed(1) +
|
|
642
|
+
'" y="' +
|
|
643
|
+
(h - 6) +
|
|
644
|
+
'" text-anchor="middle" fill="#5a5a6e" font-size="10">' +
|
|
645
|
+
formatShortDate(date) +
|
|
646
|
+
'</text>';
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
el.innerHTML =
|
|
650
|
+
'<svg viewBox="0 0 ' +
|
|
651
|
+
w +
|
|
652
|
+
' ' +
|
|
653
|
+
h +
|
|
654
|
+
'" class="timeline-svg" preserveAspectRatio="xMidYMid meet">' +
|
|
655
|
+
rects +
|
|
656
|
+
labels +
|
|
657
|
+
'</svg>' +
|
|
658
|
+
'<div class="timeline-legend">' +
|
|
659
|
+
'<div class="timeline-legend__item">' +
|
|
660
|
+
'<span class="timeline-legend__dot" style="background: #3b82f6"></span>' +
|
|
661
|
+
'Sessions</div>' +
|
|
662
|
+
'<div class="timeline-legend__item">' +
|
|
663
|
+
'<span class="timeline-legend__dot" style="background: #a78bfa"></span>' +
|
|
664
|
+
'Delegations</div>' +
|
|
665
|
+
'</div>';
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ── Model Chart ───────────────────────────────────────────
|
|
669
|
+
|
|
670
|
+
function renderModelChart(sessions) {
|
|
671
|
+
const el = document.getElementById('model-chart');
|
|
672
|
+
if (!el) return;
|
|
673
|
+
|
|
674
|
+
if (sessions.length === 0) {
|
|
675
|
+
el.innerHTML = emptyStateHtml('models', 'No model data yet', 'Model utilization across sessions — Claude Opus, GPT-5, Gemini, etc. — will be compared here.');
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const modelCounts = {};
|
|
680
|
+
sessions.forEach((s) => {
|
|
681
|
+
modelCounts[s.model] = (modelCounts[s.model] || 0) + 1;
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
const models = Object.entries(modelCounts).sort((a, b) => b[1] - a[1]);
|
|
685
|
+
const maxCount = Math.max(...models.map(([, c]) => c));
|
|
686
|
+
|
|
687
|
+
el.innerHTML = models
|
|
688
|
+
.map(
|
|
689
|
+
([name, count]) =>
|
|
690
|
+
'<div class="bar-row">' +
|
|
691
|
+
'<span class="bar-label">' +
|
|
692
|
+
escapeHtml(name) +
|
|
693
|
+
'</span>' +
|
|
694
|
+
'<div class="bar-track">' +
|
|
695
|
+
'<div class="bar-segment" style="width: ' +
|
|
696
|
+
((count / maxCount) * 100).toFixed(1) +
|
|
697
|
+
'%; background: ' +
|
|
698
|
+
(MODEL_COLORS[name] || '#64748b') +
|
|
699
|
+
'"></div>' +
|
|
700
|
+
'</div>' +
|
|
701
|
+
'<span class="bar-value">' +
|
|
702
|
+
count +
|
|
703
|
+
'</span>' +
|
|
704
|
+
'</div>'
|
|
705
|
+
)
|
|
706
|
+
.join('');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ── Execution Log ─────────────────────────────────────────
|
|
710
|
+
|
|
711
|
+
function renderExecutionLog(sessions) {
|
|
712
|
+
const el = document.getElementById('execution-log');
|
|
713
|
+
if (!el) return;
|
|
714
|
+
|
|
715
|
+
const sorted = sessions
|
|
716
|
+
.slice()
|
|
717
|
+
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
|
718
|
+
.slice(0, 10);
|
|
719
|
+
|
|
720
|
+
if (sorted.length === 0) {
|
|
721
|
+
el.innerHTML = emptyStateHtml('execLog', 'No execution history yet', 'A step-by-step trace of agent activity — with outcomes, durations, and metadata — will appear here.');
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
el.innerHTML =
|
|
726
|
+
'<div class="exec-log">' +
|
|
727
|
+
sorted
|
|
728
|
+
.map(
|
|
729
|
+
(s, i) =>
|
|
730
|
+
'<div class="exec-step">' +
|
|
731
|
+
'<div class="exec-step__indicator">' +
|
|
732
|
+
'<div class="exec-step__dot exec-step__dot--' +
|
|
733
|
+
s.outcome +
|
|
734
|
+
'">' +
|
|
735
|
+
(OUTCOME_ICONS[s.outcome] || '') +
|
|
736
|
+
'</div>' +
|
|
737
|
+
(i < sorted.length - 1
|
|
738
|
+
? '<div class="exec-step__line"></div>'
|
|
739
|
+
: '') +
|
|
740
|
+
'</div>' +
|
|
741
|
+
'<div class="exec-step__content">' +
|
|
742
|
+
'<div class="exec-step__header">' +
|
|
743
|
+
'<span class="exec-step__agent">' +
|
|
744
|
+
escapeHtml(s.agent) +
|
|
745
|
+
'</span>' +
|
|
746
|
+
'<span class="exec-step__badge exec-step__badge--' +
|
|
747
|
+
s.outcome +
|
|
748
|
+
'">' +
|
|
749
|
+
s.outcome +
|
|
750
|
+
'</span>' +
|
|
751
|
+
'</div>' +
|
|
752
|
+
'<div class="exec-step__task">' +
|
|
753
|
+
escapeHtml(s.task) +
|
|
754
|
+
'</div>' +
|
|
755
|
+
'<div class="exec-step__meta">' +
|
|
756
|
+
'<span class="exec-step__meta-item">\uD83D\uDD52 ' +
|
|
757
|
+
formatTime(s.timestamp) +
|
|
758
|
+
'</span>' +
|
|
759
|
+
(s.duration_min != null
|
|
760
|
+
? '<span class="exec-step__meta-item">\u23F1 ' +
|
|
761
|
+
s.duration_min +
|
|
762
|
+
'm</span>'
|
|
763
|
+
: '') +
|
|
764
|
+
(s.files_changed != null
|
|
765
|
+
? '<span class="exec-step__meta-item">\uD83D\uDCC1 ' +
|
|
766
|
+
s.files_changed +
|
|
767
|
+
' files</span>'
|
|
768
|
+
: '') +
|
|
769
|
+
(s.model
|
|
770
|
+
? '<span class="exec-step__meta-item">\uD83E\uDD16 ' +
|
|
771
|
+
escapeHtml(s.model) +
|
|
772
|
+
'</span>'
|
|
773
|
+
: '') +
|
|
774
|
+
(s.retries > 0
|
|
775
|
+
? '<span class="exec-step__meta-item">\uD83D\uDD04 ' +
|
|
776
|
+
s.retries +
|
|
777
|
+
' retries</span>'
|
|
778
|
+
: '') +
|
|
779
|
+
(s.lessons_added && s.lessons_added.length > 0
|
|
780
|
+
? '<span class="exec-step__meta-item">\uD83D\uDCA1 ' +
|
|
781
|
+
s.lessons_added.length +
|
|
782
|
+
' lessons</span>'
|
|
783
|
+
: '') +
|
|
784
|
+
(s.discoveries && s.discoveries.length > 0
|
|
785
|
+
? '<span class="exec-step__meta-item">\uD83D\uDD0D ' +
|
|
786
|
+
s.discoveries.length +
|
|
787
|
+
' discoveries</span>'
|
|
788
|
+
: '') +
|
|
789
|
+
'</div>' +
|
|
790
|
+
'</div>' +
|
|
791
|
+
'</div>'
|
|
792
|
+
)
|
|
793
|
+
.join('') +
|
|
794
|
+
'</div>';
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ── Panel Chart ───────────────────────────────────────────
|
|
798
|
+
|
|
799
|
+
function renderPanelChart(panels) {
|
|
800
|
+
const el = document.getElementById('panel-chart');
|
|
801
|
+
if (!el) return;
|
|
802
|
+
|
|
803
|
+
if (panels.length === 0) {
|
|
804
|
+
el.innerHTML = emptyStateHtml('panels', 'No panel reviews yet', 'Quality gate verdicts from majority-vote panels — with pass/block counts and must-fix items — will be shown here.');
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
el.innerHTML =
|
|
809
|
+
'<div class="panel-grid">' +
|
|
810
|
+
panels
|
|
811
|
+
.map(
|
|
812
|
+
(p) =>
|
|
813
|
+
'<div class="panel-item">' +
|
|
814
|
+
'<div class="panel-item__header">' +
|
|
815
|
+
'<span class="panel-item__key">' +
|
|
816
|
+
escapeHtml(p.panel_key) +
|
|
817
|
+
'</span>' +
|
|
818
|
+
'<span class="panel-item__verdict panel-item__verdict--' +
|
|
819
|
+
p.verdict +
|
|
820
|
+
'">' +
|
|
821
|
+
p.verdict +
|
|
822
|
+
'</span>' +
|
|
823
|
+
'</div>' +
|
|
824
|
+
'<div class="panel-item__votes">' +
|
|
825
|
+
Array.from({ length: p.pass_count })
|
|
826
|
+
.map(
|
|
827
|
+
() =>
|
|
828
|
+
'<div class="panel-item__vote panel-item__vote--pass">\u2713</div>'
|
|
829
|
+
)
|
|
830
|
+
.join('') +
|
|
831
|
+
Array.from({ length: p.block_count })
|
|
832
|
+
.map(
|
|
833
|
+
() =>
|
|
834
|
+
'<div class="panel-item__vote panel-item__vote--block">\u2717</div>'
|
|
835
|
+
)
|
|
836
|
+
.join('') +
|
|
837
|
+
'</div>' +
|
|
838
|
+
'<div class="panel-item__fixes">' +
|
|
839
|
+
(p.must_fix > 0
|
|
840
|
+
? '<strong>' + p.must_fix + ' must-fix</strong>'
|
|
841
|
+
: '') +
|
|
842
|
+
(p.must_fix > 0 && p.should_fix > 0 ? ' \u00B7 ' : '') +
|
|
843
|
+
(p.should_fix > 0 ? p.should_fix + ' should-fix' : '') +
|
|
844
|
+
(p.must_fix === 0 && p.should_fix === 0 ? 'Clean' : '') +
|
|
845
|
+
'</div>' +
|
|
846
|
+
'<div class="panel-item__meta">' +
|
|
847
|
+
'<span class="panel-item__meta-item">\uD83E\uDD16 ' + escapeHtml(p.reviewer_model || 'unknown') + '</span>' +
|
|
848
|
+
(p.attempt > 1 ? '<span class="panel-item__meta-item">\uD83D\uDD04 attempt ' + p.attempt + '</span>' : '') +
|
|
849
|
+
(p.artifacts_count ? '<span class="panel-item__meta-item">\uD83D\uDCC4 ' + p.artifacts_count + ' artifacts</span>' : '') +
|
|
850
|
+
'</div>' +
|
|
851
|
+
'</div>'
|
|
852
|
+
)
|
|
853
|
+
.join('') +
|
|
854
|
+
'</div>';
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ── Sessions Table ────────────────────────────────────────
|
|
858
|
+
|
|
859
|
+
function renderSessionsTable(sessions) {
|
|
860
|
+
const el = document.getElementById('sessions-table');
|
|
861
|
+
if (!el) return;
|
|
862
|
+
|
|
863
|
+
const sorted = sessions
|
|
864
|
+
.slice()
|
|
865
|
+
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
|
866
|
+
.slice(0, 15);
|
|
867
|
+
|
|
868
|
+
if (sorted.length === 0) {
|
|
869
|
+
el.innerHTML = emptyStateHtml('sessions', 'No session records yet', 'A detailed table of recent sessions — with timestamps, agents, tasks, outcomes, and linked issues — will populate here.');
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
el.innerHTML =
|
|
874
|
+
'<table class="sessions-table">' +
|
|
875
|
+
'<thead><tr>' +
|
|
876
|
+
'<th>Timestamp</th>' +
|
|
877
|
+
'<th>Agent</th>' +
|
|
878
|
+
'<th>Task</th>' +
|
|
879
|
+
'<th>Outcome</th>' +
|
|
880
|
+
'<th>Duration</th>' +
|
|
881
|
+
'<th>Files</th>' +
|
|
882
|
+
'<th>Retries</th>' +
|
|
883
|
+
'<th>Issue</th>' +
|
|
884
|
+
'</tr></thead>' +
|
|
885
|
+
'<tbody>' +
|
|
886
|
+
sorted
|
|
887
|
+
.map(
|
|
888
|
+
(s) =>
|
|
889
|
+
'<tr>' +
|
|
890
|
+
'<td>' +
|
|
891
|
+
formatTime(s.timestamp) +
|
|
892
|
+
'</td>' +
|
|
893
|
+
'<td class="td-agent">' +
|
|
894
|
+
escapeHtml(s.agent) +
|
|
895
|
+
'</td>' +
|
|
896
|
+
'<td class="td-task">' +
|
|
897
|
+
escapeHtml(s.task) +
|
|
898
|
+
'</td>' +
|
|
899
|
+
'<td><span class="outcome-badge outcome-badge--' +
|
|
900
|
+
s.outcome +
|
|
901
|
+
'">' +
|
|
902
|
+
s.outcome +
|
|
903
|
+
'</span></td>' +
|
|
904
|
+
'<td class="td-num">' +
|
|
905
|
+
(s.duration_min != null ? s.duration_min + 'm' : '\u2014') +
|
|
906
|
+
'</td>' +
|
|
907
|
+
'<td class="td-num">' +
|
|
908
|
+
(s.files_changed != null ? s.files_changed : '\u2014') +
|
|
909
|
+
'</td>' +
|
|
910
|
+
'<td class="td-num">' +
|
|
911
|
+
(s.retries != null ? s.retries : '\u2014') +
|
|
912
|
+
'</td>' +
|
|
913
|
+
'<td class="td-issue">' +
|
|
914
|
+
(s.linear_issue ? escapeHtml(s.linear_issue) : '\u2014') +
|
|
915
|
+
'</td>' +
|
|
916
|
+
'</tr>'
|
|
917
|
+
)
|
|
918
|
+
.join('') +
|
|
919
|
+
'</tbody></table>';
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// ── Main ──────────────────────────────────────────────────
|
|
923
|
+
|
|
924
|
+
async function main() {
|
|
925
|
+
const [sessions, delegations, panels] = await Promise.all([
|
|
926
|
+
loadNdjson(base + 'data/sessions.ndjson'),
|
|
927
|
+
loadNdjson(base + 'data/delegations.ndjson'),
|
|
928
|
+
loadNdjson(base + 'data/panels.ndjson'),
|
|
929
|
+
]);
|
|
930
|
+
|
|
931
|
+
// Show/hide welcome banner
|
|
932
|
+
const allEmpty = sessions.length === 0 && delegations.length === 0 && panels.length === 0;
|
|
933
|
+
if (allEmpty) {
|
|
934
|
+
renderWelcomeBanner();
|
|
935
|
+
} else {
|
|
936
|
+
removeWelcomeBanner();
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
renderKpis(sessions, delegations);
|
|
940
|
+
renderPipeline(delegations);
|
|
941
|
+
renderAgentChart(sessions);
|
|
942
|
+
renderTierChart(delegations);
|
|
943
|
+
renderMechanismChart(delegations);
|
|
944
|
+
renderDelegationOutcomeChart(delegations);
|
|
945
|
+
renderTimelineChart(sessions, delegations);
|
|
946
|
+
renderModelChart(sessions);
|
|
947
|
+
renderExecutionLog(sessions);
|
|
948
|
+
renderPanelChart(panels);
|
|
949
|
+
renderSessionsTable(sessions);
|
|
950
|
+
|
|
951
|
+
// ── Sidebar Navigation ────────────────────────────────
|
|
952
|
+
initSidebarNav();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function initSidebarNav() {
|
|
956
|
+
const links = document.querySelectorAll('.dash-sidebar__link');
|
|
957
|
+
const sections = document.querySelectorAll('[data-nav-section]');
|
|
958
|
+
|
|
959
|
+
// Intersection observer for active state
|
|
960
|
+
const observer = new IntersectionObserver(
|
|
961
|
+
(entries) => {
|
|
962
|
+
entries.forEach((entry) => {
|
|
963
|
+
if (entry.isIntersecting) {
|
|
964
|
+
const id = entry.target.id;
|
|
965
|
+
links.forEach((link) => {
|
|
966
|
+
link.classList.toggle(
|
|
967
|
+
'dash-sidebar__link--active',
|
|
968
|
+
link.dataset.section === id
|
|
969
|
+
);
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
},
|
|
974
|
+
{ rootMargin: '-20% 0px -70% 0px', threshold: 0 }
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
sections.forEach((s) => observer.observe(s));
|
|
978
|
+
|
|
979
|
+
// Smooth scroll on click
|
|
980
|
+
links.forEach((link) => {
|
|
981
|
+
link.addEventListener('click', (e) => {
|
|
982
|
+
e.preventDefault();
|
|
983
|
+
const target = document.getElementById(link.dataset.section);
|
|
984
|
+
if (target) {
|
|
985
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
main();
|
|
992
|
+
})();</script>
|