opencastle 0.28.0 → 0.30.0

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.
Files changed (94) hide show
  1. package/README.md +12 -3
  2. package/dist/cli/convoy/engine.d.ts.map +1 -1
  3. package/dist/cli/convoy/engine.js +1 -10
  4. package/dist/cli/convoy/engine.js.map +1 -1
  5. package/dist/cli/convoy/engine.test.js +1 -0
  6. package/dist/cli/convoy/engine.test.js.map +1 -1
  7. package/dist/cli/convoy/export.d.ts +1 -3
  8. package/dist/cli/convoy/export.d.ts.map +1 -1
  9. package/dist/cli/convoy/export.js +9 -88
  10. package/dist/cli/convoy/export.js.map +1 -1
  11. package/dist/cli/convoy/export.test.js +7 -186
  12. package/dist/cli/convoy/export.test.js.map +1 -1
  13. package/dist/cli/convoy/pipeline.d.ts.map +1 -1
  14. package/dist/cli/convoy/pipeline.js +0 -21
  15. package/dist/cli/convoy/pipeline.js.map +1 -1
  16. package/dist/cli/convoy/pipeline.test.js +0 -21
  17. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  18. package/dist/cli/dashboard.d.ts.map +1 -1
  19. package/dist/cli/dashboard.js +32 -8
  20. package/dist/cli/dashboard.js.map +1 -1
  21. package/dist/cli/destroy.d.ts.map +1 -1
  22. package/dist/cli/destroy.js +13 -0
  23. package/dist/cli/destroy.js.map +1 -1
  24. package/dist/cli/dispute.d.ts +3 -0
  25. package/dist/cli/dispute.d.ts.map +1 -0
  26. package/dist/cli/dispute.js +25 -0
  27. package/dist/cli/dispute.js.map +1 -0
  28. package/dist/cli/doctor.d.ts +1 -1
  29. package/dist/cli/doctor.d.ts.map +1 -1
  30. package/dist/cli/doctor.js +14 -1
  31. package/dist/cli/doctor.js.map +1 -1
  32. package/dist/cli/eject.d.ts.map +1 -1
  33. package/dist/cli/eject.js +14 -0
  34. package/dist/cli/eject.js.map +1 -1
  35. package/dist/cli/init.d.ts.map +1 -1
  36. package/dist/cli/init.js +14 -0
  37. package/dist/cli/init.js.map +1 -1
  38. package/dist/cli/log.d.ts +0 -11
  39. package/dist/cli/log.d.ts.map +1 -1
  40. package/dist/cli/log.js +2 -114
  41. package/dist/cli/log.js.map +1 -1
  42. package/dist/cli/pipeline.d.ts +17 -0
  43. package/dist/cli/pipeline.d.ts.map +1 -1
  44. package/dist/cli/pipeline.js +259 -24
  45. package/dist/cli/pipeline.js.map +1 -1
  46. package/dist/cli/pipeline.test.d.ts +2 -0
  47. package/dist/cli/pipeline.test.d.ts.map +1 -0
  48. package/dist/cli/pipeline.test.js +178 -0
  49. package/dist/cli/pipeline.test.js.map +1 -0
  50. package/dist/cli/run.js +2 -2
  51. package/dist/cli/run.js.map +1 -1
  52. package/dist/cli/update.d.ts.map +1 -1
  53. package/dist/cli/update.js +16 -0
  54. package/dist/cli/update.js.map +1 -1
  55. package/dist/cli/watch.d.ts.map +1 -1
  56. package/dist/cli/watch.js +1 -3
  57. package/dist/cli/watch.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/cli/convoy/engine.test.ts +1 -0
  60. package/src/cli/convoy/engine.ts +1 -4
  61. package/src/cli/convoy/export.test.ts +7 -224
  62. package/src/cli/convoy/export.ts +10 -106
  63. package/src/cli/convoy/pipeline.test.ts +0 -25
  64. package/src/cli/convoy/pipeline.ts +0 -19
  65. package/src/cli/dashboard.ts +33 -8
  66. package/src/cli/destroy.ts +15 -0
  67. package/src/cli/dispute.ts +28 -0
  68. package/src/cli/doctor.ts +16 -1
  69. package/src/cli/eject.ts +16 -0
  70. package/src/cli/init.ts +16 -0
  71. package/src/cli/log.ts +2 -120
  72. package/src/cli/pipeline.test.ts +191 -0
  73. package/src/cli/pipeline.ts +326 -26
  74. package/src/cli/run.ts +2 -2
  75. package/src/cli/update.ts +18 -0
  76. package/src/cli/watch.ts +1 -3
  77. package/src/dashboard/dist/_astro/index.Je1YjU_y.css +1 -0
  78. package/src/dashboard/dist/index.html +537 -1394
  79. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  80. package/src/dashboard/scripts/etl.test.ts +4 -62
  81. package/src/dashboard/scripts/etl.ts +13 -33
  82. package/src/dashboard/src/pages/index.astro +684 -1624
  83. package/src/dashboard/src/styles/dashboard.css +473 -7
  84. package/src/orchestrator/agents/team-lead.agent.md +13 -0
  85. package/src/orchestrator/prompts/brainstorm.prompt.md +1 -0
  86. package/src/orchestrator/prompts/fix-prd.prompt.md +58 -0
  87. package/src/orchestrator/prompts/generate-convoy.prompt.md +30 -0
  88. package/src/orchestrator/prompts/generate-prd.prompt.md +38 -0
  89. package/dist/cli/convoy/log-merge.test.d.ts +0 -2
  90. package/dist/cli/convoy/log-merge.test.d.ts.map +0 -1
  91. package/dist/cli/convoy/log-merge.test.js +0 -147
  92. package/dist/cli/convoy/log-merge.test.js.map +0 -1
  93. package/src/cli/convoy/log-merge.test.ts +0 -179
  94. package/src/dashboard/dist/_astro/index.6L3_HsPT.css +0 -1
@@ -25,12 +25,6 @@ try {
25
25
  <h1 class="dash-header__title">Observability Dashboard</h1>
26
26
  </div>
27
27
  <div class="dash-header__actions">
28
- <div class="convoy-selector">
29
- <label class="convoy-selector__label" for="convoy-select">Convoy</label>
30
- <select class="convoy-selector__select" id="convoy-select" aria-label="Select a convoy to view its details">
31
- <option value="">Select convoy…</option>
32
- </select>
33
- </div>
34
28
  <button class="dash-btn dash-btn--ghost" id="export-btn" type="button" title="Export data as JSON" aria-label="Export dashboard data as JSON">
35
29
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
36
30
  Export
@@ -43,18 +37,31 @@ try {
43
37
  <!-- Sidebar Navigation -->
44
38
  <nav class="dash-sidebar" id="dash-sidebar">
45
39
  <ul class="dash-sidebar__list">
46
- <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#overall-section" data-section="overall-section" aria-label="Overview section">Overview</a></li>
47
- <li><a class="dash-sidebar__link" href="#tasks-section" data-section="tasks-section" aria-label="Tasks section">Tasks</a></li>
48
- <li><a class="dash-sidebar__link" href="#quality-section" data-section="quality-section" aria-label="Quality section">Quality</a></li>
49
- <li><a class="dash-sidebar__link" href="#reliability-section" data-section="reliability-section" aria-label="Reliability section">Reliability</a></li>
50
- <li><a class="dash-sidebar__link" href="#drift-section" data-section="drift-section" aria-label="Drift section">Drift</a></li>
51
- <li><a class="dash-sidebar__link" href="#outputs-section" data-section="outputs-section" aria-label="Outputs section">Outputs</a></li>
52
- <li><a class="dash-sidebar__link" href="#event-timeline-section" data-section="event-timeline-section" aria-label="Event Log section">Event Log</a></li>
53
- <li><a class="dash-sidebar__link" href="#convoy-section" data-section="convoy-section" aria-label="Convoy section">Convoy</a></li>
40
+ <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#overall-section" data-section="overall-section" data-view="home" aria-label="Overview section">Overview</a></li>
41
+ <li><a class="dash-sidebar__link" href="#convoy-list-section" data-section="convoy-list-section" data-view="home" aria-label="Convoys section">Convoys</a></li>
42
+ <li><a class="dash-sidebar__link" href="#tasks-section" data-section="tasks-section" data-view="detail" aria-label="Tasks section">Tasks</a></li>
43
+ <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section" data-view="detail" aria-label="Pipeline section">Pipeline</a></li>
44
+ <li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section" data-view="detail" aria-label="Agents section">Agents</a></li>
45
+ <li><a class="dash-sidebar__link" href="#tier-section" data-section="tier-section" data-view="detail" aria-label="Tiers section">Tiers</a></li>
46
+ <li><a class="dash-sidebar__link" href="#model-section" data-section="model-section" data-view="detail" aria-label="Models section">Models</a></li>
47
+ <li><a class="dash-sidebar__link" href="#quality-section" data-section="quality-section" data-view="detail" aria-label="Quality section">Quality</a></li>
48
+ <li><a class="dash-sidebar__link" href="#reliability-section" data-section="reliability-section" data-view="detail" aria-label="Reliability section">Reliability</a></li>
49
+ <li><a class="dash-sidebar__link" href="#drift-section" data-section="drift-section" data-view="detail" aria-label="Drift section">Drift</a></li>
50
+ <li><a class="dash-sidebar__link" href="#outputs-section" data-section="outputs-section" data-view="detail" aria-label="Outputs section">Outputs</a></li>
51
+ <li><a class="dash-sidebar__link" href="#execution-section" data-section="execution-section" data-view="detail" aria-label="Execution Log section">Execution Log</a></li>
52
+ <li><a class="dash-sidebar__link" href="#event-timeline-section" data-section="event-timeline-section" data-view="detail" aria-label="Event Log section">Event Log</a></li>
53
+ <li><a class="dash-sidebar__link" href="#panel-section" data-section="panel-section" data-view="detail" aria-label="Panel Reviews section">Panel Reviews</a></li>
54
+ <li><a class="dash-sidebar__link" href="#reviews-section" data-section="reviews-section" data-view="detail" aria-label="Fast Reviews section">Fast Reviews</a></li>
54
55
  </ul>
55
56
  </nav>
56
57
 
57
58
  <main class="dash-main">
59
+ <nav class="breadcrumbs" id="breadcrumbs" data-view-hidden>
60
+ <a class="breadcrumbs__link" href="#" id="breadcrumbs-home">Observability</a>
61
+ <span class="breadcrumbs__separator">/</span>
62
+ <span class="breadcrumbs__current" id="breadcrumbs-convoy"></span>
63
+ </nav>
64
+ <div class="view-home" id="view-home">
58
65
  <!-- Overall Stats Section -->
59
66
  <section class="overall-stats" id="overall-section" data-nav-section>
60
67
  <div class="overall-stats__header">
@@ -89,21 +96,57 @@ try {
89
96
  </div>
90
97
  </section>
91
98
 
92
- <!-- Selected Convoy Header -->
93
- <section class="convoy-detail-header" id="convoy-detail-header">
94
- <div class="convoy-detail-header__top">
95
- <h2 class="convoy-detail-header__name" id="selected-convoy-name">No convoy selected</h2>
96
- <span class="status-badge" id="selected-convoy-status" role="status"></span>
97
- <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: A convoy is a set of AI tasks working together on one goal." data-tooltip="A convoy is a set of AI tasks working together on one goal."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
99
+ <!-- Convoy List Section -->
100
+ <section class="convoy-list-section" id="convoy-list-section">
101
+ <div class="convoy-list-section__header">
102
+ <h2>Convoys</h2>
103
+ <p class="convoy-list-section__desc">All convoy runs across your project</p>
98
104
  </div>
99
- <p class="convoy-status-explanation" id="convoy-status-explanation" role="status" aria-live="polite"></p>
100
- <div class="convoy-detail-header__meta" id="selected-convoy-meta">
101
- <!-- Populated by JS: branch, timestamps, duration, tokens, cost -->
105
+ <div class="convoy-list-filters" id="convoy-list-filters">
106
+ <div class="convoy-list-filters__group">
107
+ <label for="cl-filter-search">Search</label>
108
+ <input class="convoy-list-filters__input" type="text" id="cl-filter-search" placeholder="Convoy name…" />
109
+ </div>
110
+ <div class="convoy-list-filters__group">
111
+ <label for="cl-filter-status">Status</label>
112
+ <select class="convoy-list-filters__select" id="cl-filter-status">
113
+ <option value="">All</option>
114
+ <option value="done">Done</option>
115
+ <option value="running">Running</option>
116
+ <option value="failed">Failed</option>
117
+ <option value="gate-failed">Gate Failed</option>
118
+ <option value="pending">Pending</option>
119
+ </select>
120
+ </div>
121
+ <div class="convoy-list-filters__group">
122
+ <label for="cl-filter-from">From</label>
123
+ <input class="convoy-list-filters__date" type="date" id="cl-filter-from" />
124
+ </div>
125
+ <div class="convoy-list-filters__group">
126
+ <label for="cl-filter-to">To</label>
127
+ <input class="convoy-list-filters__date" type="date" id="cl-filter-to" />
128
+ </div>
129
+ <button class="convoy-list-filters__reset" type="button" id="cl-filter-reset">Reset</button>
130
+ </div>
131
+ <div id="convoy-list-table-wrap"></div>
132
+ <div class="convoy-list-empty" id="convoy-list-empty" style="display:none">
133
+ <div class="convoy-list-empty__icon">
134
+ <svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="6" y="6" width="28" height="28" rx="4"/><line x1="14" y1="20" x2="26" y2="20" opacity="0.5"/></svg>
135
+ </div>
136
+ <p class="convoy-list-empty__text">No convoys match your filters</p>
102
137
  </div>
103
138
  </section>
139
+ </div><!-- .view-home -->
140
+
141
+ <div class="view-convoy-detail" id="view-convoy-detail" data-view-hidden>
142
+ <section class="convoy-detail-hero" id="convoy-detail-hero">
143
+ <div class="convoy-detail-hero__title" id="detail-hero-title"></div>
144
+ <span class="convoy-detail-hero__status" id="detail-hero-status"></span>
145
+ <div class="convoy-detail-hero__meta" id="detail-hero-meta"></div>
146
+ </section>
104
147
 
105
148
  <!-- Tasks Section -->
106
- <section class="chart-card" id="tasks-section" data-nav-section style="display:none">
149
+ <section class="chart-card" id="tasks-section" data-nav-section>
107
150
  <div class="chart-card__header">
108
151
  <h2 class="chart-card__title">Tasks</h2>
109
152
  <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Individual units of work in this convoy." data-tooltip="Individual units of work in this convoy."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
@@ -116,8 +159,63 @@ try {
116
159
  </div>
117
160
  </section>
118
161
 
162
+ <!-- Pipeline View -->
163
+ <section class="chart-card" id="pipeline-section" data-nav-section>
164
+ <div class="chart-card__header">
165
+ <h2 class="chart-card__title">Task Pipeline</h2>
166
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Task flow across execution phases." data-tooltip="Task flow across execution phases."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
167
+ <p class="chart-card__desc">Task flow across execution phases</p>
168
+ </div>
169
+ <div class="chart-card__body" id="pipeline-view"></div>
170
+ </section>
171
+
172
+ <!-- Charts Row: Agent + Outcomes -->
173
+ <div class="charts-row" id="agent-section" data-nav-section>
174
+ <section class="chart-card">
175
+ <div class="chart-card__header">
176
+ <h2 class="chart-card__title">Sessions by Agent</h2>
177
+ <p class="chart-card__desc">Task count per agent, stacked by outcome</p>
178
+ </div>
179
+ <div class="chart-card__body" id="agent-chart"></div>
180
+ </section>
181
+ <section class="chart-card">
182
+ <div class="chart-card__header">
183
+ <h2 class="chart-card__title">Delegation Outcomes</h2>
184
+ <p class="chart-card__desc">Task outcome distribution</p>
185
+ </div>
186
+ <div class="chart-card__body" id="delegation-outcome-chart"></div>
187
+ </section>
188
+ </div>
189
+
190
+ <!-- Charts Row: Tiers + Mechanism -->
191
+ <div class="charts-row" id="tier-section" data-nav-section>
192
+ <section class="chart-card">
193
+ <div class="chart-card__header">
194
+ <h2 class="chart-card__title">Tier Distribution</h2>
195
+ <p class="chart-card__desc">Model tier breakdown</p>
196
+ </div>
197
+ <div class="chart-card__body" id="tier-chart"></div>
198
+ </section>
199
+ <section class="chart-card">
200
+ <div class="chart-card__header">
201
+ <h2 class="chart-card__title">Delegation Mechanism</h2>
202
+ <p class="chart-card__desc">Sub-agent vs background split</p>
203
+ </div>
204
+ <div class="chart-card__body" id="mechanism-chart"></div>
205
+ </section>
206
+ </div>
207
+
208
+ <!-- Model Usage -->
209
+ <section class="chart-card" id="model-section" data-nav-section>
210
+ <div class="chart-card__header">
211
+ <h2 class="chart-card__title">Model Usage</h2>
212
+ <p class="chart-card__desc">Tasks by model</p>
213
+ </div>
214
+ <div class="chart-card__body" id="model-chart"></div>
215
+ </section>
216
+
119
217
  <!-- Quality Section -->
120
- <section class="chart-card" id="quality-section" data-nav-section style="display:none">
218
+ <section class="chart-card" id="quality-section" data-nav-section>
121
219
  <div class="chart-card__header">
122
220
  <h2 class="chart-card__title">Quality / Review</h2>
123
221
  <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Code review results and dispute resolution." data-tooltip="Code review results and dispute resolution."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
@@ -130,7 +228,7 @@ try {
130
228
  </section>
131
229
 
132
230
  <!-- Reliability Section -->
133
- <section class="chart-card" id="reliability-section" data-nav-section style="display:none">
231
+ <section class="chart-card" id="reliability-section" data-nav-section>
134
232
  <div class="chart-card__header">
135
233
  <h2 class="chart-card__title">Reliability</h2>
136
234
  <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Errors, retry attempts, and safety mechanisms." data-tooltip="Errors, retry attempts, and safety mechanisms."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
@@ -147,7 +245,7 @@ try {
147
245
  </section>
148
246
 
149
247
  <!-- Drift Section -->
150
- <section class="chart-card" id="drift-section" data-nav-section style="display:none">
248
+ <section class="chart-card" id="drift-section" data-nav-section>
151
249
  <div class="chart-card__header">
152
250
  <h2 class="chart-card__title">Drift</h2>
153
251
  <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: How far the actual work deviated from the original plan." data-tooltip="How far the actual work deviated from the original plan."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
@@ -160,7 +258,7 @@ try {
160
258
  </section>
161
259
 
162
260
  <!-- Outputs Section -->
163
- <section class="chart-card" id="outputs-section" data-nav-section style="display:none">
261
+ <section class="chart-card" id="outputs-section" data-nav-section>
164
262
  <div class="chart-card__header">
165
263
  <h2 class="chart-card__title">Outputs &amp; Artifacts</h2>
166
264
  <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Files, data, and summaries produced by this convoy." data-tooltip="Files, data, and summaries produced by this convoy."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
@@ -173,7 +271,7 @@ try {
173
271
  </section>
174
272
 
175
273
  <!-- Event Timeline Section -->
176
- <section class="chart-card" id="event-timeline-section" data-nav-section style="display:none">
274
+ <section class="chart-card" id="event-timeline-section" data-nav-section>
177
275
  <div class="chart-card__header">
178
276
  <h2 class="chart-card__title">Event Timeline</h2>
179
277
  <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Chronological log of everything that happened during this run." data-tooltip="Chronological log of everything that happened during this run."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
@@ -188,220 +286,39 @@ try {
188
286
  </div>
189
287
  </section>
190
288
 
191
- <!-- Filter Bar -->
192
- <div class="filter-bar" id="filter-bar">
193
- <div class="filter-group">
194
- <label class="filter-label" for="filter-date-from">From</label>
195
- <input class="filter-input" type="date" id="filter-date-from" />
196
- </div>
197
- <div class="filter-group">
198
- <label class="filter-label" for="filter-date-to">To</label>
199
- <input class="filter-input" type="date" id="filter-date-to" />
200
- </div>
201
- <div class="filter-group">
202
- <label class="filter-label" for="filter-agent">Agent</label>
203
- <select class="filter-select" id="filter-agent">
204
- <option value="">All agents</option>
205
- </select>
206
- </div>
207
- <div class="filter-group">
208
- <label class="filter-label" for="filter-outcome">Outcome</label>
209
- <select class="filter-select" id="filter-outcome">
210
- <option value="">All outcomes</option>
211
- <option value="success">Success</option>
212
- <option value="partial">Partial</option>
213
- <option value="failed">Failed</option>
214
- </select>
215
- </div>
216
- <div class="filter-group">
217
- <label class="filter-label" for="filter-convoy">Convoy</label>
218
- <select class="filter-select" id="filter-convoy">
219
- <option value="">All convoys</option>
220
- </select>
221
- </div>
222
- <div class="filter-group">
223
- <label class="filter-label" for="filter-pipeline">Pipeline</label>
224
- <select class="filter-select" id="filter-pipeline">
225
- <option value="">All</option>
226
- </select>
227
- </div>
228
- <button class="dash-btn dash-btn--ghost filter-reset" id="filter-reset" type="button">Reset</button>
229
- </div>
230
-
231
- <!-- Convoy Status Section -->
232
- <section class="chart-card convoy-status" id="convoy-section" data-nav-section style="display:none">
233
- <div class="chart-card__header">
234
- <h2 class="chart-card__title">Convoy Status</h2>
235
- <p class="chart-card__desc" id="convoy-desc">Select a convoy to view details</p>
236
- </div>
237
- <div class="chart-card__body" id="convoy-body">
238
- </div>
239
- </section>
240
-
241
- <!-- Convoy Pipeline (Chaining) Section -->
242
- <section class="chart-card" id="convoy-pipeline-section" data-nav-section style="display:none">
243
- <div class="chart-card__header">
244
- <h2 class="chart-card__title">Convoy Pipeline</h2>
245
- <p class="chart-card__desc" id="convoy-pipeline-desc">Pipeline convoy chain progress</p>
246
- </div>
247
- <div class="chart-card__body" id="convoy-pipeline-body">
248
- </div>
249
- </section>
250
-
251
- <!-- KPI Row -->
252
- <section class="kpi-row" id="kpi-row" data-nav-section>
253
- <div class="kpi-card" id="kpi-sessions">
254
- <span class="kpi-card__label">Total Sessions</span>
255
- <span class="kpi-card__value">&mdash;</span>
256
- <span class="kpi-card__sub"></span>
257
- </div>
258
- <div class="kpi-card" id="kpi-success">
259
- <span class="kpi-card__label">Success Rate</span>
260
- <span class="kpi-card__value">&mdash;</span>
261
- <span class="kpi-card__sub"></span>
262
- </div>
263
- <div class="kpi-card" id="kpi-delegations">
264
- <span class="kpi-card__label">Total Delegations</span>
265
- <span class="kpi-card__value">&mdash;</span>
266
- <span class="kpi-card__sub"></span>
267
- </div>
268
- <div class="kpi-card" id="kpi-duration">
269
- <span class="kpi-card__label">Avg Duration</span>
270
- <span class="kpi-card__value">&mdash;</span>
271
- <span class="kpi-card__sub"></span>
272
- </div>
273
- <div class="kpi-card" id="kpi-retries">
274
- <span class="kpi-card__label">Total Retries</span>
275
- <span class="kpi-card__value">&mdash;</span>
276
- <span class="kpi-card__sub"></span>
277
- </div>
278
- <div class="kpi-card" id="kpi-lessons">
279
- <span class="kpi-card__label">Lessons Added</span>
280
- <span class="kpi-card__value">&mdash;</span>
281
- <span class="kpi-card__sub"></span>
282
- </div>
283
- </section>
284
-
285
- <!-- Pipeline View (Steroids-inspired) -->
286
- <section class="chart-card" id="pipeline-section" data-nav-section>
287
- <div class="chart-card__header">
288
- <h2 class="chart-card__title">Task Pipeline</h2>
289
- <p class="chart-card__desc">Delegation flow across execution phases</p>
290
- </div>
291
- <div class="chart-card__body" id="pipeline-view">
292
- <div class="loading-skeleton"></div>
293
- </div>
294
- </section>
295
-
296
- <!-- Charts Row 1 -->
297
- <div class="charts-row" id="agent-section" data-nav-section>
298
- <section class="chart-card">
299
- <div class="chart-card__header">
300
- <h2 class="chart-card__title">Sessions by Agent</h2>
301
- <p class="chart-card__desc">Stacked by outcome</p>
302
- </div>
303
- <div class="chart-card__body" id="agent-chart">
304
- <div class="loading-skeleton"></div>
305
- </div>
306
- </section>
307
- <section class="chart-card" id="tier-section" data-nav-section>
308
- <div class="chart-card__header">
309
- <h2 class="chart-card__title">Tier Distribution</h2>
310
- <p class="chart-card__desc">Delegation model tiers</p>
311
- </div>
312
- <div class="chart-card__body" id="tier-chart">
313
- <div class="loading-skeleton"></div>
314
- </div>
315
- </section>
316
- </div>
317
-
318
- <!-- Charts Row: Delegation Insights -->
319
- <div class="charts-row" id="delegation-section" data-nav-section>
320
- <section class="chart-card">
321
- <div class="chart-card__header">
322
- <h2 class="chart-card__title">Delegation Mechanism</h2>
323
- <p class="chart-card__desc">Sub-agent vs background split</p>
324
- </div>
325
- <div class="chart-card__body" id="mechanism-chart">
326
- <div class="loading-skeleton"></div>
327
- </div>
328
- </section>
329
- <section class="chart-card">
330
- <div class="chart-card__header">
331
- <h2 class="chart-card__title">Delegation Outcomes</h2>
332
- <p class="chart-card__desc">Success rate by delegation</p>
333
- </div>
334
- <div class="chart-card__body" id="delegation-outcome-chart">
335
- <div class="loading-skeleton"></div>
336
- </div>
337
- </section>
338
- </div>
339
-
340
- <!-- Charts Row 2 -->
341
- <div class="charts-row" id="timeline-section" data-nav-section>
342
- <section class="chart-card">
343
- <div class="chart-card__header">
344
- <h2 class="chart-card__title">Timeline</h2>
345
- <p class="chart-card__desc">Sessions and delegations over time</p>
346
- </div>
347
- <div class="chart-card__body" id="timeline-chart">
348
- <div class="loading-skeleton"></div>
349
- </div>
350
- </section>
351
- <section class="chart-card" id="model-section" data-nav-section>
352
- <div class="chart-card__header">
353
- <h2 class="chart-card__title">Model Usage</h2>
354
- <p class="chart-card__desc">Sessions by model</p>
355
- </div>
356
- <div class="chart-card__body" id="model-chart">
357
- <div class="loading-skeleton"></div>
358
- </div>
359
- </section>
360
- </div>
361
-
362
- <!-- Execution Log (Duvo-inspired) -->
289
+ <!-- Execution Log -->
363
290
  <section class="chart-card" id="execution-section" data-nav-section>
364
291
  <div class="chart-card__header">
365
292
  <h2 class="chart-card__title">Execution Log</h2>
293
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Step-by-step trace of recent task activity." data-tooltip="Step-by-step trace of recent task activity."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
366
294
  <p class="chart-card__desc">Recent agent activity, step by step</p>
367
295
  </div>
368
- <div class="chart-card__body" id="execution-log">
369
- <div class="loading-skeleton"></div>
370
- </div>
296
+ <div class="chart-card__body" id="execution-log"></div>
371
297
  </section>
372
298
 
373
- <!-- Panel Results -->
299
+ <!-- Panel Reviews -->
374
300
  <section class="chart-card" id="panel-section" data-nav-section>
375
301
  <div class="chart-card__header">
376
302
  <h2 class="chart-card__title">Panel Reviews</h2>
303
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Quality gate verdicts from majority-vote panels." data-tooltip="Quality gate verdicts from majority-vote panels."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
377
304
  <p class="chart-card__desc">Quality gate verdicts and fix items</p>
378
305
  </div>
379
- <div class="chart-card__body" id="panel-chart">
380
- <div class="loading-skeleton"></div>
381
- </div>
306
+ <div class="chart-card__body" id="panel-chart"></div>
382
307
  </section>
383
308
 
384
309
  <!-- Fast Reviews -->
385
310
  <section class="chart-card" id="reviews-section" data-nav-section>
386
311
  <div class="chart-card__header">
387
312
  <h2 class="chart-card__title">Fast Reviews</h2>
313
+ <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Single-reviewer quality gate results." data-tooltip="Single-reviewer quality gate results."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
388
314
  <p class="chart-card__desc">Single-reviewer quality gate results</p>
389
315
  </div>
390
- <div class="chart-card__body chart-card__body--table" id="reviews-table">
391
- <div class="loading-skeleton"></div>
392
- </div>
316
+ <div class="chart-card__body chart-card__body--table" id="reviews-table"></div>
393
317
  </section>
394
318
 
395
- <!-- Sessions Table -->
396
- <section class="chart-card" id="sessions-section" data-nav-section>
397
- <div class="chart-card__header">
398
- <h2 class="chart-card__title">Recent Sessions</h2>
399
- <p class="chart-card__desc">Last 15 sessions by timestamp</p>
400
- </div>
401
- <div class="chart-card__body chart-card__body--table" id="sessions-table">
402
- <div class="loading-skeleton"></div>
403
- </div>
404
- </section>
319
+ </div><!-- .view-convoy-detail -->
320
+
321
+
405
322
  </main>
406
323
  </div>
407
324
  </Layout>
@@ -564,1270 +481,108 @@ try {
564
481
  '<strong>Watch insights flow in</strong>' +
565
482
  '<span>Charts, metrics, and trends populate in real time</span>' +
566
483
  '</div>' +
567
- '</div>' +
568
- '</div>' +
569
- '</div>';
570
-
571
- const main = document.querySelector('.dash-main');
572
- if (main) main.prepend(banner);
573
- }
574
-
575
- function removeWelcomeBanner() {
576
- const existing = document.getElementById('welcome-banner');
577
- if (existing) existing.remove();
578
- }
579
-
580
- // ── KPI Rendering ────────────────────────────────────────
581
-
582
- function renderKpis(sessions, delegations, reviews) {
583
- const total = sessions.length;
584
- const isEmpty = total === 0;
585
- const successCount = sessions.filter((s) => s.outcome === 'success').length;
586
- const rate = total > 0 ? Math.round((successCount / total) * 100) : 0;
587
- const durSessions = sessions.filter((s) => s.duration_min != null);
588
- const avgDur =
589
- durSessions.length > 0
590
- ? Math.round(
591
- durSessions.reduce((sum, s) => sum + (s.duration_min || 0), 0) /
592
- durSessions.length
593
- )
594
- : 0;
595
- const uniqueAgents = new Set(delegations.map((d) => d.agent)).size;
596
-
597
- // Toggle ghost class on KPI row
598
- const kpiRow = document.querySelector('.kpi-row');
599
- if (kpiRow) kpiRow.classList.toggle('kpi-row--empty', isEmpty);
600
-
601
- const kpiSessions = document.getElementById('kpi-sessions');
602
- const kpiSuccess = document.getElementById('kpi-success');
603
- const kpiDelegations = document.getElementById('kpi-delegations');
604
- const kpiDuration = document.getElementById('kpi-duration');
605
-
606
- if (kpiSessions) {
607
- kpiSessions.querySelector('.kpi-card__value').textContent = isEmpty ? '0' : total;
608
- kpiSessions.querySelector('.kpi-card__sub').innerHTML = isEmpty
609
- ? '<span class="kpi-card__hint">No sessions yet</span>'
610
- : '<span class="kpi-trend kpi-trend--up">\u2191</span> ' + successCount + ' successful';
611
- }
612
- if (kpiSuccess) {
613
- if (isEmpty) {
614
- kpiSuccess.querySelector('.kpi-card__value').textContent = '\u2014';
615
- kpiSuccess.querySelector('.kpi-card__sub').innerHTML =
616
- '<span class="kpi-card__hint">No sessions yet</span>';
617
- } else {
618
- const trendClass =
619
- rate >= 80 ? 'up' : rate >= 60 ? 'neutral' : 'down';
620
- kpiSuccess.querySelector('.kpi-card__value').textContent = rate + '%';
621
- kpiSuccess.querySelector('.kpi-card__sub').innerHTML =
622
- '<span class="kpi-trend kpi-trend--' +
623
- trendClass +
624
- '">' +
625
- (trendClass === 'up' ? '\u2191' : trendClass === 'down' ? '\u2193' : '\u2192') +
626
- '</span> across all sessions';
627
- }
628
- }
629
- if (kpiDelegations) {
630
- kpiDelegations.querySelector('.kpi-card__value').textContent =
631
- delegations.length === 0 ? '0' : delegations.length;
632
- kpiDelegations.querySelector('.kpi-card__sub').innerHTML = isEmpty
633
- ? '<span class="kpi-card__hint">No delegations yet</span>'
634
- : uniqueAgents + ' unique agents';
635
- }
636
- if (kpiDuration) {
637
- kpiDuration.querySelector('.kpi-card__value').textContent = isEmpty ? '\u2014' : avgDur + 'm';
638
- kpiDuration.querySelector('.kpi-card__sub').innerHTML = isEmpty
639
- ? '<span class="kpi-card__hint">No duration yet</span>'
640
- : '<span class="kpi-trend kpi-trend--neutral">\u2192</span> per session';
641
- }
642
-
643
- // Retries KPI
644
- const totalRetries = sessions.reduce((sum, s) => sum + (s.retries || 0), 0);
645
- const kpiRetries = document.getElementById('kpi-retries');
646
- if (kpiRetries) {
647
- kpiRetries.querySelector('.kpi-card__value').textContent = isEmpty ? '0' : totalRetries;
648
- const retriedSessions = sessions.filter((s) => (s.retries || 0) > 0).length;
649
- kpiRetries.querySelector('.kpi-card__sub').innerHTML = isEmpty
650
- ? '<span class="kpi-card__hint">No retries yet</span>'
651
- : retriedSessions + ' sessions with retries';
652
- }
653
-
654
- // Lessons KPI
655
- const totalLessons = sessions.reduce(
656
- (sum, s) => sum + (s.lessons_added ? s.lessons_added.length : 0),
657
- 0
658
- );
659
- const kpiLessons = document.getElementById('kpi-lessons');
660
- if (kpiLessons) {
661
- kpiLessons.querySelector('.kpi-card__value').textContent = isEmpty ? '0' : totalLessons;
662
- const discoveryCount = sessions.reduce(
663
- (sum, s) => sum + (s.discoveries ? s.discoveries.length : 0),
664
- 0
665
- );
666
- kpiLessons.querySelector('.kpi-card__sub').innerHTML = isEmpty
667
- ? '<span class="kpi-card__hint">No lessons yet</span>'
668
- : discoveryCount + ' issues discovered';
669
- }
670
- }
671
-
672
- // ── Pipeline View ─────────────────────────────────────────
673
-
674
- function renderPipeline(delegations) {
675
- const el = document.getElementById('pipeline-view');
676
- if (!el) return;
677
-
678
- if (delegations.length === 0) {
679
- el.innerHTML = emptyStateHtml('pipeline', 'No pipeline activity yet', 'Delegation phases appear here as tasks flow through Foundation, Integration, Validation, and QA stages.');
680
- return;
681
- }
682
-
683
- const phases = { 1: 0, 2: 0, 3: 0, 4: 0 };
684
- delegations.forEach((d) => {
685
- const p = d.phase || 1;
686
- if (phases[p] !== undefined) phases[p]++;
687
- });
688
-
689
- const stageConfig = [
690
- {
691
- label: 'Foundation',
692
- phase: 1,
693
- iconClass: 'pending',
694
- 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>',
695
- },
696
- {
697
- label: 'Integration',
698
- phase: 2,
699
- iconClass: 'active',
700
- 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>',
701
- },
702
- {
703
- label: 'Validation',
704
- phase: 3,
705
- iconClass: 'review',
706
- 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>',
707
- },
708
- {
709
- label: 'QA Gate',
710
- phase: 4,
711
- iconClass: 'done',
712
- 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>',
713
- },
714
- ];
715
-
716
- el.innerHTML =
717
- '<div class="pipeline">' +
718
- stageConfig
719
- .map(
720
- (stage, i) =>
721
- (i > 0 ? '<div class="pipeline-arrow">\u2192</div>' : '') +
722
- '<div class="pipeline-stage">' +
723
- '<div class="pipeline-stage__icon pipeline-stage__icon--' +
724
- stage.iconClass +
725
- '">' +
726
- stage.icon +
727
- '</div>' +
728
- '<span class="pipeline-stage__count">' +
729
- (phases[stage.phase] || 0) +
730
- '</span>' +
731
- '<span class="pipeline-stage__label">' +
732
- stage.label +
733
- '</span>' +
734
- '</div>'
735
- )
736
- .join('') +
737
- '</div>';
738
- }
739
-
740
- // ── Agent Chart ───────────────────────────────────────────
741
-
742
- function renderAgentChart(sessions) {
743
- const el = document.getElementById('agent-chart');
744
- if (!el) return;
745
-
746
- if (sessions.length === 0) {
747
- el.innerHTML = emptyStateHtml('agents', 'No agent sessions yet', 'A breakdown of sessions per agent will appear here — stacked by outcome (success, partial, failed).');
748
- return;
749
- }
750
-
751
- const agentMap = {};
752
- sessions.forEach((s) => {
753
- if (!agentMap[s.agent])
754
- agentMap[s.agent] = { success: 0, partial: 0, failed: 0, total: 0 };
755
- agentMap[s.agent][s.outcome] = (agentMap[s.agent][s.outcome] || 0) + 1;
756
- agentMap[s.agent].total++;
757
- });
758
-
759
- const agents = Object.entries(agentMap).sort(
760
- (a, b) => b[1].total - a[1].total
761
- );
762
- const maxTotal = Math.max(...agents.map(([, d]) => d.total));
763
-
764
- el.innerHTML = agents
765
- .map(
766
- ([name, data]) =>
767
- '<div class="bar-row">' +
768
- '<span class="bar-label">' +
769
- escapeHtml(name) +
770
- '</span>' +
771
- '<div class="bar-track">' +
772
- (data.success > 0
773
- ? '<div class="bar-segment bar--success" style="width: ' +
774
- ((data.success / maxTotal) * 100).toFixed(1) + '%"></div>'
775
- : '') +
776
- (data.partial > 0
777
- ? '<div class="bar-segment bar--partial" style="width: ' +
778
- ((data.partial / maxTotal) * 100).toFixed(1) + '%"></div>'
779
- : '') +
780
- (data.failed > 0
781
- ? '<div class="bar-segment bar--failed" style="width: ' +
782
- ((data.failed / maxTotal) * 100).toFixed(1) + '%"></div>'
783
- : '') +
784
- '</div>' +
785
- '<span class="bar-value">' +
786
- data.total +
787
- '</span>' +
788
- '</div>'
789
- )
790
- .join('');
791
- }
792
-
793
- // ── Tier Donut Chart ──────────────────────────────────────
794
-
795
- function renderTierChart(delegations) {
796
- const el = document.getElementById('tier-chart');
797
- if (!el) return;
798
-
799
- if (delegations.length === 0) {
800
- el.innerHTML = emptyStateHtml('tiers', 'No tier data yet', 'Model tier distribution (Premium, Standard, Utility, Economy) will be visualized as a donut chart.');
801
- return;
802
- }
803
-
804
- const tierCounts = {};
805
- delegations.forEach((d) => {
806
- tierCounts[d.tier] = (tierCounts[d.tier] || 0) + 1;
807
- });
808
-
809
- const order = ['premium', 'standard', 'utility', 'economy'];
810
- const tiers = order
811
- .filter((t) => tierCounts[t])
812
- .map((t) => ({ name: t, count: tierCounts[t] }));
813
-
814
- const total = delegations.length;
815
- const r = 70;
816
- const circumference = 2 * Math.PI * r;
817
- let cumOffset = 0;
818
-
819
- const circles = tiers.map((t) => {
820
- const pct = t.count / total;
821
- const dashLen = pct * circumference;
822
- // Skip round linecap for single-segment donuts to avoid overlap artifact
823
- const linecap = tiers.length === 1 ? 'butt' : 'round';
824
- const segment =
825
- '<circle cx="90" cy="90" r="' +
826
- r +
827
- '" fill="none" ' +
828
- 'stroke="' +
829
- (TIER_COLORS[t.name] || '#64748b') +
830
- '" stroke-width="18" ' +
831
- 'stroke-dasharray="' +
832
- dashLen.toFixed(2) +
833
- ' ' +
834
- (circumference - dashLen).toFixed(2) +
835
- '" ' +
836
- 'stroke-dashoffset="' +
837
- (-cumOffset).toFixed(2) +
838
- '" ' +
839
- 'transform="rotate(-90 90 90)" ' +
840
- 'stroke-linecap="' + linecap + '"/>';
841
- cumOffset += dashLen;
842
- return segment;
843
- });
844
-
845
- const legend = tiers
846
- .map(
847
- (t) =>
848
- '<div class="legend-item">' +
849
- '<span class="legend-dot" style="background: ' +
850
- (TIER_COLORS[t.name] || '#64748b') +
851
- '"></span>' +
852
- '<span class="legend-name">' +
853
- t.name +
854
- '</span>' +
855
- '<span class="legend-count">' +
856
- t.count +
857
- ' (' +
858
- Math.round((t.count / total) * 100) +
859
- '%)</span>' +
860
- '</div>'
861
- )
862
- .join('');
863
-
864
- el.innerHTML =
865
- '<div class="donut-container">' +
866
- '<div class="donut-wrap">' +
867
- '<svg viewBox="0 0 180 180" class="donut-svg">' +
868
- circles.join('') +
869
- '</svg>' +
870
- '<div class="donut-center">' +
871
- '<span class="donut-total">' +
872
- total +
873
- '</span>' +
874
- '<span class="donut-total-label">total</span>' +
875
- '</div>' +
876
- '</div>' +
877
- '<div class="donut-legend">' +
878
- legend +
879
- '</div>' +
880
- '</div>';
881
- }
882
-
883
- // ── Mechanism Donut Chart ─────────────────────────────────
884
-
885
- function renderMechanismChart(delegations) {
886
- const el = document.getElementById('mechanism-chart');
887
- if (!el) return;
888
-
889
- if (delegations.length === 0) {
890
- el.innerHTML = emptyStateHtml('mechanism', 'No delegation data yet', 'The split between sub-agent (inline) and background (worktree) delegations will be shown here.');
891
- return;
892
- }
893
-
894
- const mechCounts = {};
895
- delegations.forEach((d) => {
896
- const mech = d.mechanism || 'unknown';
897
- mechCounts[mech] = (mechCounts[mech] || 0) + 1;
898
- });
899
-
900
- var MECH_COLORS = {
901
- 'sub-agent': '#3b82f6',
902
- 'background': '#a78bfa',
903
- 'unknown': '#64748b',
904
- };
905
-
906
- var MECH_LABELS = {
907
- 'sub-agent': 'Sub-agent (inline)',
908
- 'background': 'Background (worktree)',
909
- 'unknown': 'Unknown',
910
- };
911
-
912
- var mechOrder = ['sub-agent', 'background', 'unknown'];
913
- var mechs = mechOrder
914
- .filter(function (m) { return mechCounts[m]; })
915
- .map(function (m) { return { name: m, count: mechCounts[m] }; });
916
-
917
- var total = delegations.length;
918
- var r = 70;
919
- var circumference = 2 * Math.PI * r;
920
- var cumOffset = 0;
921
-
922
- var circles = mechs.map(function (m) {
923
- var pct = m.count / total;
924
- var dashLen = pct * circumference;
925
- // Skip round linecap for single-segment donuts to avoid overlap artifact
926
- var linecap = mechs.length === 1 ? 'butt' : 'round';
927
- var segment =
928
- '<circle cx="90" cy="90" r="' + r + '" fill="none" ' +
929
- 'stroke="' + (MECH_COLORS[m.name] || '#64748b') + '" stroke-width="18" ' +
930
- 'stroke-dasharray="' + dashLen.toFixed(2) + ' ' + (circumference - dashLen).toFixed(2) + '" ' +
931
- 'stroke-dashoffset="' + (-cumOffset).toFixed(2) + '" ' +
932
- 'transform="rotate(-90 90 90)" stroke-linecap="' + linecap + '"/>';
933
- cumOffset += dashLen;
934
- return segment;
935
- });
936
-
937
- var legend = mechs
938
- .map(function (m) {
939
- return '<div class="legend-item">' +
940
- '<span class="legend-dot" style="background: ' + (MECH_COLORS[m.name] || '#64748b') + '"></span>' +
941
- '<span class="legend-name">' + (MECH_LABELS[m.name] || m.name) + '</span>' +
942
- '<span class="legend-count">' + m.count + ' (' + Math.round((m.count / total) * 100) + '%)</span>' +
943
- '</div>';
944
- })
945
- .join('');
946
-
947
- el.innerHTML =
948
- '<div class="donut-container">' +
949
- '<div class="donut-wrap">' +
950
- '<svg viewBox="0 0 180 180" class="donut-svg">' +
951
- circles.join('') +
952
- '</svg>' +
953
- '<div class="donut-center">' +
954
- '<span class="donut-total">' + total + '</span>' +
955
- '<span class="donut-total-label">total</span>' +
956
- '</div>' +
957
- '</div>' +
958
- '<div class="donut-legend">' +
959
- legend +
960
- '</div>' +
961
- '</div>';
962
- }
963
-
964
- // ── Delegation Outcome Chart ──────────────────────────────
965
-
966
- function renderDelegationOutcomeChart(delegations) {
967
- var el = document.getElementById('delegation-outcome-chart');
968
- if (!el) return;
969
-
970
- if (delegations.length === 0) {
971
- el.innerHTML = emptyStateHtml('outcomes', 'No outcome data yet', 'Delegation results — success, partial, failed, redirected — will be tracked and compared here.');
972
- return;
973
- }
974
-
975
- var OUTCOME_COLORS = {
976
- success: '#22c55e',
977
- partial: '#f59e0b',
978
- failed: '#ef4444',
979
- redirected: '#64748b',
980
- };
981
-
982
- var outcomeCounts = {};
983
- delegations.forEach(function (d) {
984
- var outcome = d.outcome || 'unknown';
985
- outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
986
- });
987
-
988
- var outcomes = Object.entries(outcomeCounts).sort(function (a, b) { return b[1] - a[1]; });
989
- var maxCount = Math.max.apply(null, outcomes.map(function (o) { return o[1]; }));
990
-
991
- el.innerHTML = outcomes
992
- .map(function (entry) {
993
- var name = entry[0];
994
- var count = entry[1];
995
- return '<div class="bar-row">' +
996
- '<span class="bar-label">' + escapeHtml(name) + '</span>' +
997
- '<div class="bar-track">' +
998
- '<div class="bar-segment" style="width: ' + ((count / maxCount) * 100).toFixed(1) + '%; background: ' + (OUTCOME_COLORS[name] || '#64748b') + '"></div>' +
999
- '</div>' +
1000
- '<span class="bar-value">' + count + '</span>' +
1001
- '</div>';
1002
- })
1003
- .join('');
1004
- }
1005
-
1006
- // ── Timeline Chart ────────────────────────────────────────
1007
-
1008
- function renderTimelineChart(sessions, delegations) {
1009
- const el = document.getElementById('timeline-chart');
1010
- if (!el) return;
1011
-
1012
- const dateMap = {};
1013
- sessions.forEach((s) => {
1014
- const key = s.timestamp.slice(0, 10);
1015
- if (!dateMap[key]) dateMap[key] = { sessions: 0, delegations: 0 };
1016
- dateMap[key].sessions++;
1017
- });
1018
- delegations.forEach((d) => {
1019
- const key = d.timestamp.slice(0, 10);
1020
- if (!dateMap[key]) dateMap[key] = { sessions: 0, delegations: 0 };
1021
- dateMap[key].delegations++;
1022
- });
1023
-
1024
- const dates = Object.entries(dateMap).sort(([a], [b]) =>
1025
- a.localeCompare(b)
1026
- );
1027
-
1028
- if (dates.length === 0) {
1029
- el.innerHTML = emptyStateHtml('timeline', 'No timeline data yet', 'A daily activity chart will build here as sessions and delegations accumulate over time.');
1030
- return;
1031
- }
1032
-
1033
- const maxVal = Math.max(
1034
- ...dates.map(([, d]) => Math.max(d.sessions, d.delegations))
1035
- );
1036
- const w = 500;
1037
- const h = 180;
1038
- const pad = { top: 10, right: 10, bottom: 28, left: 10 };
1039
- const plotW = w - pad.left - pad.right;
1040
- const plotH = h - pad.top - pad.bottom;
1041
- // Prevent sparse layout when there are very few dates
1042
- const groupWidth = dates.length <= 3
1043
- ? Math.min(100, plotW / dates.length)
1044
- : plotW / dates.length;
1045
- const barWidth = Math.min(dates.length <= 3 ? 24 : 16, groupWidth * 0.35);
1046
- // Center the bars when there are few dates
1047
- const timelineStartX = dates.length <= 3
1048
- ? pad.left + (plotW - dates.length * groupWidth) / 2
1049
- : pad.left;
1050
-
1051
- let rects = '';
1052
- let labels = '';
1053
-
1054
- dates.forEach(([date, data], i) => {
1055
- const x = timelineStartX + i * groupWidth + groupWidth / 2;
1056
- const sH = maxVal > 0 ? (data.sessions / maxVal) * plotH : 0;
1057
- const dH = maxVal > 0 ? (data.delegations / maxVal) * plotH : 0;
1058
-
1059
- rects +=
1060
- '<rect x="' +
1061
- (x - barWidth - 1).toFixed(1) +
1062
- '" y="' +
1063
- (pad.top + plotH - sH).toFixed(1) +
1064
- '" width="' +
1065
- barWidth.toFixed(1) +
1066
- '" height="' +
1067
- sH.toFixed(1) +
1068
- '" fill="#3b82f6" rx="3" opacity="0.85"/>';
1069
- rects +=
1070
- '<rect x="' +
1071
- (x + 1).toFixed(1) +
1072
- '" y="' +
1073
- (pad.top + plotH - dH).toFixed(1) +
1074
- '" width="' +
1075
- barWidth.toFixed(1) +
1076
- '" height="' +
1077
- dH.toFixed(1) +
1078
- '" fill="#a78bfa" rx="3" opacity="0.65"/>';
1079
- labels +=
1080
- '<text x="' +
1081
- x.toFixed(1) +
1082
- '" y="' +
1083
- (h - 6) +
1084
- '" text-anchor="middle" fill="#5a5a6e" font-size="10">' +
1085
- formatShortDate(date) +
1086
- '</text>';
1087
- });
1088
-
1089
- el.innerHTML =
1090
- '<svg viewBox="0 0 ' +
1091
- w +
1092
- ' ' +
1093
- h +
1094
- '" class="timeline-svg" preserveAspectRatio="xMidYMid meet">' +
1095
- rects +
1096
- labels +
1097
- '</svg>' +
1098
- '<div class="timeline-legend">' +
1099
- '<div class="timeline-legend__item">' +
1100
- '<span class="timeline-legend__dot" style="background: #3b82f6"></span>' +
1101
- 'Sessions</div>' +
1102
- '<div class="timeline-legend__item">' +
1103
- '<span class="timeline-legend__dot" style="background: #a78bfa"></span>' +
1104
- 'Delegations</div>' +
1105
- '</div>';
1106
- }
1107
-
1108
- // ── Model Chart ───────────────────────────────────────────
1109
-
1110
- function renderModelChart(sessions) {
1111
- const el = document.getElementById('model-chart');
1112
- if (!el) return;
1113
-
1114
- if (sessions.length === 0) {
1115
- el.innerHTML = emptyStateHtml('models', 'No model data yet', 'Model utilization across sessions — Claude Opus, GPT-5, Gemini, etc. — will be compared here.');
1116
- return;
1117
- }
1118
-
1119
- const modelCounts = {};
1120
- sessions.forEach((s) => {
1121
- modelCounts[s.model] = (modelCounts[s.model] || 0) + 1;
1122
- });
1123
-
1124
- const models = Object.entries(modelCounts).sort((a, b) => b[1] - a[1]);
1125
- const maxCount = Math.max(...models.map(([, c]) => c));
1126
-
1127
- el.innerHTML = models
1128
- .map(
1129
- ([name, count]) =>
1130
- '<div class="bar-row">' +
1131
- '<span class="bar-label">' +
1132
- escapeHtml(name) +
1133
- '</span>' +
1134
- '<div class="bar-track">' +
1135
- '<div class="bar-segment" style="width: ' +
1136
- ((count / maxCount) * 100).toFixed(1) +
1137
- '%; background: ' +
1138
- (MODEL_COLORS[name] || '#64748b') +
1139
- '"></div>' +
1140
- '</div>' +
1141
- '<span class="bar-value">' +
1142
- count +
1143
- '</span>' +
1144
- '</div>'
1145
- )
1146
- .join('');
1147
- }
1148
-
1149
- // ── Execution Log ─────────────────────────────────────────
1150
-
1151
- function renderExecutionLog(sessions) {
1152
- const el = document.getElementById('execution-log');
1153
- if (!el) return;
1154
-
1155
- const sorted = sessions
1156
- .slice()
1157
- .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
1158
- .slice(0, 10);
1159
-
1160
- if (sorted.length === 0) {
1161
- el.innerHTML = emptyStateHtml('execLog', 'No execution history yet', 'A step-by-step trace of agent activity — with outcomes, durations, and metadata — will appear here.');
1162
- return;
1163
- }
1164
-
1165
- el.innerHTML =
1166
- '<div class="exec-log">' +
1167
- sorted
1168
- .map(
1169
- (s, i) =>
1170
- '<div class="exec-step">' +
1171
- '<div class="exec-step__indicator">' +
1172
- '<div class="exec-step__dot exec-step__dot--' +
1173
- s.outcome +
1174
- '">' +
1175
- (OUTCOME_ICONS[s.outcome] || '') +
1176
- '</div>' +
1177
- (i < sorted.length - 1
1178
- ? '<div class="exec-step__line"></div>'
1179
- : '') +
1180
- '</div>' +
1181
- '<div class="exec-step__content">' +
1182
- '<div class="exec-step__header">' +
1183
- '<span class="exec-step__agent">' +
1184
- escapeHtml(s.agent) +
1185
- '</span>' +
1186
- '<span class="exec-step__badge exec-step__badge--' +
1187
- s.outcome +
1188
- '">' +
1189
- s.outcome +
1190
- '</span>' +
1191
- '</div>' +
1192
- '<div class="exec-step__task">' +
1193
- escapeHtml(s.task) +
1194
- '</div>' +
1195
- '<div class="exec-step__meta">' +
1196
- '<span class="exec-step__meta-item">\uD83D\uDD52 ' +
1197
- formatTime(s.timestamp) +
1198
- '</span>' +
1199
- (s.duration_min != null
1200
- ? '<span class="exec-step__meta-item">\u23F1 ' +
1201
- s.duration_min +
1202
- 'm</span>'
1203
- : '') +
1204
- (s.files_changed != null
1205
- ? '<span class="exec-step__meta-item">\uD83D\uDCC1 ' +
1206
- s.files_changed +
1207
- ' files</span>'
1208
- : '') +
1209
- (s.model
1210
- ? '<span class="exec-step__meta-item">\uD83E\uDD16 ' +
1211
- escapeHtml(s.model) +
1212
- '</span>'
1213
- : '') +
1214
- (s.retries > 0
1215
- ? '<span class="exec-step__meta-item">\uD83D\uDD04 ' +
1216
- s.retries +
1217
- ' retries</span>'
1218
- : '') +
1219
- (s.lessons_added && s.lessons_added.length > 0
1220
- ? '<span class="exec-step__meta-item">\uD83D\uDCA1 ' +
1221
- s.lessons_added.length +
1222
- ' lessons</span>'
1223
- : '') +
1224
- (s.discoveries && s.discoveries.length > 0
1225
- ? '<span class="exec-step__meta-item">\uD83D\uDD0D ' +
1226
- s.discoveries.length +
1227
- ' discoveries</span>'
1228
- : '') +
1229
- '</div>' +
1230
- '</div>' +
1231
- '</div>'
1232
- )
1233
- .join('') +
1234
- '</div>';
1235
- }
1236
-
1237
- // ── Panel Chart ───────────────────────────────────────────
1238
-
1239
- function renderPanelChart(panels) {
1240
- const el = document.getElementById('panel-chart');
1241
- if (!el) return;
1242
-
1243
- if (panels.length === 0) {
1244
- 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.');
1245
- return;
1246
- }
1247
-
1248
- el.innerHTML =
1249
- '<div class="panel-grid">' +
1250
- panels
1251
- .map(
1252
- (p) =>
1253
- '<div class="panel-item">' +
1254
- '<div class="panel-item__header">' +
1255
- '<span class="panel-item__key">' +
1256
- escapeHtml(p.panel_key) +
1257
- '</span>' +
1258
- '<span class="panel-item__verdict panel-item__verdict--' +
1259
- p.verdict +
1260
- '">' +
1261
- p.verdict +
1262
- '</span>' +
1263
- '</div>' +
1264
- '<div class="panel-item__votes">' +
1265
- Array.from({ length: p.pass_count })
1266
- .map(
1267
- () =>
1268
- '<div class="panel-item__vote panel-item__vote--pass">\u2713</div>'
1269
- )
1270
- .join('') +
1271
- Array.from({ length: p.block_count })
1272
- .map(
1273
- () =>
1274
- '<div class="panel-item__vote panel-item__vote--block">\u2717</div>'
1275
- )
1276
- .join('') +
1277
- '</div>' +
1278
- '<div class="panel-item__fixes">' +
1279
- (p.must_fix > 0
1280
- ? '<strong>' + p.must_fix + ' must-fix</strong>'
1281
- : '') +
1282
- (p.must_fix > 0 && p.should_fix > 0 ? ' \u00B7 ' : '') +
1283
- (p.should_fix > 0 ? p.should_fix + ' should-fix' : '') +
1284
- (p.must_fix === 0 && p.should_fix === 0 ? 'Clean' : '') +
1285
- '</div>' +
1286
- '<div class="panel-item__meta">' +
1287
- '<span class="panel-item__meta-item">\uD83E\uDD16 ' + escapeHtml(p.reviewer_model || 'unknown') + '</span>' +
1288
- (p.attempt > 1 ? '<span class="panel-item__meta-item">\uD83D\uDD04 attempt ' + p.attempt + '</span>' : '') +
1289
- (p.artifacts_count ? '<span class="panel-item__meta-item">\uD83D\uDCC4 ' + p.artifacts_count + ' artifacts</span>' : '') +
1290
- '</div>' +
1291
- '</div>'
1292
- )
1293
- .join('') +
1294
- '</div>';
1295
- }
1296
-
1297
- // ── Sessions Table ────────────────────────────────────────
1298
-
1299
- function renderSessionsTable(sessions) {
1300
- const el = document.getElementById('sessions-table');
1301
- if (!el) return;
1302
-
1303
- const sorted = sessions
1304
- .slice()
1305
- .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
1306
- .slice(0, 15);
1307
-
1308
- if (sorted.length === 0) {
1309
- 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.');
1310
- return;
1311
- }
1312
-
1313
- el.innerHTML =
1314
- '<table class="sessions-table">' +
1315
- '<thead><tr>' +
1316
- '<th>Timestamp</th>' +
1317
- '<th>Agent</th>' +
1318
- '<th>Task</th>' +
1319
- '<th>Outcome</th>' +
1320
- '<th>Duration</th>' +
1321
- '<th>Files</th>' +
1322
- '<th>Retries</th>' +
1323
- '<th>Issue</th>' +
1324
- '</tr></thead>' +
1325
- '<tbody>' +
1326
- sorted
1327
- .map(
1328
- (s) =>
1329
- '<tr>' +
1330
- '<td>' +
1331
- formatTime(s.timestamp) +
1332
- '</td>' +
1333
- '<td class="td-agent">' +
1334
- escapeHtml(s.agent) +
1335
- '</td>' +
1336
- '<td class="td-task">' +
1337
- escapeHtml(s.task) +
1338
- '</td>' +
1339
- '<td><span class="outcome-badge outcome-badge--' +
1340
- s.outcome +
1341
- '">' +
1342
- s.outcome +
1343
- '</span></td>' +
1344
- '<td class="td-num">' +
1345
- (s.duration_min != null ? s.duration_min + 'm' : '\u2014') +
1346
- '</td>' +
1347
- '<td class="td-num">' +
1348
- (s.files_changed != null ? s.files_changed : '\u2014') +
1349
- '</td>' +
1350
- '<td class="td-num">' +
1351
- (s.retries != null ? s.retries : '\u2014') +
1352
- '</td>' +
1353
- '<td class="td-issue">' +
1354
- (s.tracker_issue ? escapeHtml(s.tracker_issue) : '\u2014') +
1355
- '</td>' +
1356
- '</tr>'
1357
- )
1358
- .join('') +
1359
- '</tbody></table>';
1360
- }
1361
-
1362
- // ── Main ──────────────────────────────────────────────────
1363
-
1364
- // Store raw data globally for filtering/export
1365
- let rawSessions = [];
1366
- let rawDelegations = [];
1367
- let rawPanels = [];
1368
- let rawReviews = [];
1369
- let rawConvoys = [];
1370
- let rawPipelines = [];
1371
-
1372
- function applyFilters() {
1373
- const dateFrom = document.getElementById('filter-date-from').value;
1374
- const dateTo = document.getElementById('filter-date-to').value;
1375
- const agentFilter = document.getElementById('filter-agent').value;
1376
- const outcomeFilter = document.getElementById('filter-outcome').value;
1377
- const convoyFilter = document.getElementById('filter-convoy').value;
1378
- const pipelineFilter = document.getElementById('filter-pipeline')?.value || '';
1379
-
1380
- function matchDate(ts) {
1381
- const date = ts.slice(0, 10);
1382
- if (dateFrom && date < dateFrom) return false;
1383
- if (dateTo && date > dateTo) return false;
1384
- return true;
1385
- }
1386
-
1387
- let sessions = rawSessions.filter((s) => {
1388
- if (!matchDate(s.timestamp)) return false;
1389
- if (agentFilter && s.agent !== agentFilter) return false;
1390
- if (outcomeFilter && s.outcome !== outcomeFilter) return false;
1391
- return true;
1392
- });
1393
-
1394
- let delegations = rawDelegations.filter((d) => {
1395
- if (!matchDate(d.timestamp)) return false;
1396
- if (agentFilter && d.agent !== agentFilter) return false;
1397
- if (outcomeFilter && d.outcome !== outcomeFilter) return false;
1398
- return true;
1399
- });
1400
-
1401
- let panels = rawPanels.filter((p) => matchDate(p.timestamp));
1402
- let reviews = rawReviews.filter((r) => {
1403
- if (!matchDate(r.timestamp)) return false;
1404
- if (agentFilter && r.agent !== agentFilter) return false;
1405
- return true;
1406
- });
1407
-
1408
- // Pipeline filter: restrict events to convoy_ids within the selected pipeline
1409
- if (pipelineFilter) {
1410
- const activePipeline = rawPipelines.find((p) => p.id === pipelineFilter);
1411
- const pipelineConvoyIds = new Set((activePipeline && activePipeline.convoy_ids) || []);
1412
- if (pipelineConvoyIds.size > 0) {
1413
- sessions = sessions.filter((s) => !s.convoy_id || pipelineConvoyIds.has(s.convoy_id));
1414
- delegations = delegations.filter((d) => !d.convoy_id || pipelineConvoyIds.has(d.convoy_id));
1415
- panels = panels.filter((p2) => !p2.convoy_id || pipelineConvoyIds.has(p2.convoy_id));
1416
- reviews = reviews.filter((r) => !r.convoy_id || pipelineConvoyIds.has(r.convoy_id));
1417
- }
1418
- }
1419
-
1420
- if (convoyFilter) {
1421
- sessions = sessions.filter((s) => s.convoy_id === convoyFilter);
1422
- delegations = delegations.filter((d) => d.convoy_id === convoyFilter);
1423
- panels = panels.filter((p) => p.convoy_id === convoyFilter);
1424
- reviews = reviews.filter((r) => r.convoy_id === convoyFilter);
1425
- }
1426
-
1427
- const convoySection = document.getElementById('convoy-section');
1428
- if (convoySection) {
1429
- convoySection.style.display = convoyFilter ? '' : 'none';
1430
- if (convoyFilter) {
1431
- const convoy = rawConvoys.find((c) => c.id === convoyFilter);
1432
- renderConvoyStatus(convoy);
1433
- }
1434
- }
1435
-
1436
- // Show/hide convoy pipeline section based on pipeline filter
1437
- const pipelineSectionEl = document.getElementById('convoy-pipeline-section');
1438
- if (pipelineSectionEl) {
1439
- if (pipelineFilter) {
1440
- const activePipeline = rawPipelines.find((p) => p.id === pipelineFilter);
1441
- renderConvoyPipeline(activePipeline, rawConvoys);
1442
- } else {
1443
- pipelineSectionEl.style.display = 'none';
1444
- }
1445
- }
1446
-
1447
- renderAll(sessions, delegations, panels, reviews);
1448
- }
1449
-
1450
- function populateAgentFilter(sessions, delegations, reviews) {
1451
- const agents = new Set();
1452
- sessions.forEach((s) => agents.add(s.agent));
1453
- delegations.forEach((d) => agents.add(d.agent));
1454
- reviews.forEach((r) => agents.add(r.agent));
1455
- const select = document.getElementById('filter-agent');
1456
- if (!select) return;
1457
- // Keep the "All agents" option, remove old dynamic options
1458
- while (select.options.length > 1) select.remove(1);
1459
- Array.from(agents).sort().forEach((a) => {
1460
- const opt = document.createElement('option');
1461
- opt.value = a;
1462
- opt.textContent = a;
1463
- select.appendChild(opt);
1464
- });
1465
- }
1466
-
1467
- function renderAll(sessions, delegations, panels, reviews) {
1468
- const allEmpty = sessions.length === 0 && delegations.length === 0 && panels.length === 0 && reviews.length === 0;
1469
- if (allEmpty) {
1470
- renderWelcomeBanner();
1471
- } else {
1472
- removeWelcomeBanner();
1473
- }
1474
-
1475
- renderKpis(sessions, delegations, reviews);
1476
- renderPipeline(delegations);
1477
- renderAgentChart(sessions);
1478
- renderTierChart(delegations);
1479
- renderMechanismChart(delegations);
1480
- renderDelegationOutcomeChart(delegations);
1481
- renderTimelineChart(sessions, delegations);
1482
- renderModelChart(sessions);
1483
- renderExecutionLog(sessions);
1484
- renderPanelChart(panels);
1485
- renderReviewsTable(reviews);
1486
- renderSessionsTable(sessions);
1487
- }
1488
-
1489
- // ── Reviews Table ─────────────────────────────────────────
1490
-
1491
- function renderReviewsTable(reviews) {
1492
- const el = document.getElementById('reviews-table');
1493
- if (!el) return;
1494
-
1495
- const sorted = reviews
1496
- .slice()
1497
- .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
1498
- .slice(0, 20);
1499
-
1500
- if (sorted.length === 0) {
1501
- el.innerHTML = emptyStateHtml('panels', 'No fast reviews yet', 'Single-reviewer quality gate results — with verdicts, issue counts, and escalation status — will be listed here.');
1502
- return;
1503
- }
1504
-
1505
- el.innerHTML =
1506
- '<table class="sessions-table">' +
1507
- '<thead><tr>' +
1508
- '<th>Timestamp</th>' +
1509
- '<th>Agent</th>' +
1510
- '<th>Verdict</th>' +
1511
- '<th>Critical</th>' +
1512
- '<th>Major</th>' +
1513
- '<th>Minor</th>' +
1514
- '<th>Confidence</th>' +
1515
- '<th>Attempt</th>' +
1516
- '<th>Escalated</th>' +
1517
- '<th>Issue</th>' +
1518
- '</tr></thead>' +
1519
- '<tbody>' +
1520
- sorted
1521
- .map(
1522
- (r) =>
1523
- '<tr>' +
1524
- '<td>' + formatTime(r.timestamp) + '</td>' +
1525
- '<td class="td-agent">' + escapeHtml(r.agent || '') + '</td>' +
1526
- '<td><span class="outcome-badge outcome-badge--' + (r.verdict === 'pass' ? 'success' : 'failed') + '">' + r.verdict + '</span></td>' +
1527
- '<td class="td-num">' + (r.issues_critical ?? 0) + '</td>' +
1528
- '<td class="td-num">' + (r.issues_major ?? 0) + '</td>' +
1529
- '<td class="td-num">' + (r.issues_minor ?? 0) + '</td>' +
1530
- '<td class="td-num">' + (r.confidence || '\u2014') + '</td>' +
1531
- '<td class="td-num">' + (r.attempt ?? 1) + '</td>' +
1532
- '<td class="td-num">' + (r.escalated ? '\u26A0' : '\u2014') + '</td>' +
1533
- '<td class="td-issue">' + (r.tracker_issue ? escapeHtml(r.tracker_issue) : '\u2014') + '</td>' +
1534
- '</tr>'
1535
- )
1536
- .join('') +
1537
- '</tbody></table>';
1538
- }
1539
-
1540
- // ── Export ─────────────────────────────────────────────────
1541
-
1542
- function exportData() {
1543
- const events = [
1544
- ...rawSessions,
1545
- ...rawDelegations,
1546
- ...rawPanels,
1547
- ...rawReviews,
1548
- ].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
1549
- const blob = new Blob([events.map((e) => JSON.stringify(e)).join('\n') + '\n'], { type: 'application/x-ndjson' });
1550
- const url = URL.createObjectURL(blob);
1551
- const a = document.createElement('a');
1552
- a.href = url;
1553
- a.download = 'opencastle-events-' + new Date().toISOString().slice(0, 10) + '.ndjson';
1554
- a.click();
1555
- URL.revokeObjectURL(url);
1556
- }
1557
-
1558
- function populateConvoyFilter(convoys) {
1559
- const select = document.getElementById('filter-convoy');
1560
- if (!select) return;
1561
- while (select.options.length > 1) select.remove(1);
1562
- const sorted = convoys.slice().sort((a, b) => b.created_at.localeCompare(a.created_at));
1563
- sorted.forEach((c) => {
1564
- const opt = document.createElement('option');
1565
- opt.value = c.id;
1566
- opt.textContent = c.name + ' (' + c.status + ')';
1567
- select.appendChild(opt);
1568
- });
1569
- }
1570
-
1571
- function renderConvoyStatus(convoy) {
1572
- const descEl = document.getElementById('convoy-desc');
1573
- const bodyEl = document.getElementById('convoy-body');
1574
- if (!descEl || !bodyEl) return;
1575
-
1576
- if (!convoy) {
1577
- bodyEl.innerHTML = emptyStateHtml('pipeline', 'Convoy not found', 'No matching convoy data available.');
1578
- return;
1579
- }
1580
-
1581
- descEl.textContent = convoy.name + ' — ' + (convoy.branch || 'no branch');
1582
-
1583
- const s = convoy.summary || {};
1584
- const total = s.total || (convoy.tasks ? convoy.tasks.length : 0);
1585
- const done = s.done || 0;
1586
- const pct = total > 0 ? Math.round((done / total) * 100) : 0;
1587
-
1588
- const statusClass = convoy.status === 'done' ? 'success'
1589
- : (convoy.status === 'failed' || convoy.status === 'gate-failed') ? 'failed'
1590
- : convoy.status === 'running' ? 'partial' : '';
1591
-
1592
- let html = '';
1593
- html += '<div class="convoy-overview">';
1594
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Status</span><span class="outcome-badge outcome-badge--' + statusClass + '">' + escapeHtml(convoy.status) + '</span></div>';
1595
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Branch</span><span class="convoy-stat__value">' + escapeHtml(convoy.branch || '\u2014') + '</span></div>';
1596
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Tasks</span><span class="convoy-stat__value">' + done + '/' + total + '</span></div>';
1597
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Events</span><span class="convoy-stat__value">' + (convoy.events_count || 0) + '</span></div>';
1598
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Started</span><span class="convoy-stat__value">' + (convoy.started_at ? formatTime(convoy.started_at) : '\u2014') + '</span></div>';
1599
- if (convoy.total_tokens != null) {
1600
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Tokens</span><span class="convoy-stat__value">' + formatTokens(convoy.total_tokens) + '</span></div>';
1601
- }
1602
- if (convoy.finished_at) {
1603
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Finished</span><span class="convoy-stat__value">' + formatTime(convoy.finished_at) + '</span></div>';
1604
- }
1605
- const convoyDur = formatDuration(convoy.started_at, convoy.finished_at);
1606
- if (convoyDur) {
1607
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Duration</span><span class="convoy-stat__value">' + convoyDur + '</span></div>';
1608
- }
1609
- if (convoy.total_cost_usd != null && convoy.total_cost_usd > 0) {
1610
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Cost</span><span class="convoy-stat__value">$' + convoy.total_cost_usd.toFixed(2) + '</span></div>';
1611
- }
1612
- const failedCount = (s.failed || 0) + (s.timedOut || 0);
1613
- if (failedCount > 0) {
1614
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Failed</span><span class="convoy-stat__value convoy-stat__value--error">' + failedCount + '</span></div>';
1615
- }
1616
- html += '</div>';
1617
-
1618
- html += '<div class="convoy-progress">';
1619
- html += '<div class="convoy-progress__bar"><div class="convoy-progress__fill" style="width:' + pct + '%"></div></div>';
1620
- html += '<span class="convoy-progress__label">' + pct + '% complete</span>';
1621
- html += '</div>';
1622
-
1623
- if (convoy.tasks && convoy.tasks.length > 0) {
1624
- html += '<table class="sessions-table convoy-tasks">';
1625
- html += '<thead><tr><th>Task</th><th>Phase</th><th>Agent</th><th>Adapter</th><th>Status</th><th>Retries</th><th>Tokens</th><th>Duration</th></tr></thead>';
1626
- html += '<tbody>';
1627
- convoy.tasks.forEach(function(t) {
1628
- const tStatus = t.status === 'done' ? 'success'
1629
- : (t.status === 'failed' || t.status === 'timed-out') ? 'failed'
1630
- : t.status === 'running' ? 'partial' : '';
1631
- html += '<tr>';
1632
- html += '<td>' + escapeHtml(t.id) + '</td>';
1633
- html += '<td class="td-num">' + t.phase + '</td>';
1634
- html += '<td class="td-agent">' + escapeHtml(t.agent) + '</td>';
1635
- html += '<td>' + escapeHtml(t.adapter || '\u2014') + '</td>';
1636
- html += '<td><span class="outcome-badge outcome-badge--' + tStatus + '">' + escapeHtml(t.status) + '</span></td>';
1637
- html += '<td class="td-num">' + (t.retries || 0) + '</td>';
1638
- html += '<td class="td-num">' + (t.total_tokens != null ? formatTokens(t.total_tokens) : '\u2014') + '</td>';
1639
- html += '<td class="td-num">' + (formatDuration(t.started_at, t.finished_at) || '\u2014') + '</td>';
1640
- html += '</tr>';
1641
- });
1642
- html += '</tbody></table>';
1643
- }
484
+ '</div>' +
485
+ '</div>' +
486
+ '</div>';
1644
487
 
1645
- bodyEl.innerHTML = html;
488
+ const main = document.querySelector('.dash-main');
489
+ if (main) main.prepend(banner);
1646
490
  }
1647
491
 
1648
- // ── Pipeline Filter Population ───────────────────────────
492
+ function removeWelcomeBanner() {
493
+ const existing = document.getElementById('welcome-banner');
494
+ if (existing) existing.remove();
495
+ }
1649
496
 
1650
- function populatePipelineFilter(pipelines) {
1651
- const select = document.getElementById('filter-pipeline');
1652
- if (!select) return;
1653
- while (select.options.length > 1) select.remove(1);
1654
- const sorted = pipelines.slice().sort((a, b) =>
1655
- (b.created_at || '').localeCompare(a.created_at || '')
1656
- );
1657
- sorted.forEach((p) => {
1658
- const opt = document.createElement('option');
1659
- opt.value = p.id;
1660
- opt.textContent = (p.name || p.id) + ' (' + (p.status || 'unknown') + ')';
1661
- select.appendChild(opt);
1662
- });
497
+ // ── View Management ─────────────────────────────────────
498
+ function showHomeView() {
499
+ const home = document.getElementById('view-home');
500
+ const detail = document.getElementById('view-convoy-detail');
501
+ const breadcrumbs = document.getElementById('breadcrumbs');
502
+ if (home) delete home.dataset.viewHidden;
503
+ if (detail) detail.dataset.viewHidden = '';
504
+ if (breadcrumbs) breadcrumbs.dataset.viewHidden = '';
505
+ document.querySelectorAll('.dash-sidebar__link[data-view="detail"]').forEach(el => el.closest('li').style.display = 'none');
506
+ document.querySelectorAll('.dash-sidebar__link[data-view="home"]').forEach(el => el.closest('li').style.display = '');
507
+ const url = new URL(window.location);
508
+ url.searchParams.delete('convoy');
509
+ history.pushState({}, '', url);
1663
510
  }
1664
511
 
1665
- // ── Convoy Pipeline (Chaining) Render ────────────────────
512
+ function showConvoyDetailView(convoyId, convoyName) {
513
+ const home = document.getElementById('view-home');
514
+ const detail = document.getElementById('view-convoy-detail');
515
+ const breadcrumbs = document.getElementById('breadcrumbs');
516
+ if (home) home.dataset.viewHidden = '';
517
+ if (detail) delete detail.dataset.viewHidden;
518
+ if (breadcrumbs) delete breadcrumbs.dataset.viewHidden;
519
+ document.querySelectorAll('.dash-sidebar__link[data-view="home"]').forEach(el => el.closest('li').style.display = 'none');
520
+ document.querySelectorAll('.dash-sidebar__link[data-view="detail"]').forEach(el => el.closest('li').style.display = '');
521
+ const crumbText = document.getElementById('breadcrumbs-convoy');
522
+ if (crumbText) crumbText.textContent = convoyName || convoyId;
523
+ const url = new URL(window.location);
524
+ url.searchParams.set('convoy', convoyId);
525
+ history.pushState({}, '', url);
526
+ loadConvoyDetail(convoyId);
527
+ window.scrollTo({ top: 0, behavior: 'smooth' });
528
+ }
1666
529
 
1667
- function renderConvoyPipeline(pipeline, convoys) {
1668
- const sectionEl = document.getElementById('convoy-pipeline-section');
1669
- const descEl = document.getElementById('convoy-pipeline-desc');
1670
- const bodyEl = document.getElementById('convoy-pipeline-body');
1671
- if (!sectionEl || !bodyEl) return;
530
+ // ── Convoy List ─────────────────────────────────────────
531
+ function renderConvoyList() {
532
+ const data = window.__DASHBOARD_DATA__;
533
+ const list = data?.convoyList ?? [];
534
+ const wrap = document.getElementById('convoy-list-table-wrap');
535
+ const emptyEl = document.getElementById('convoy-list-empty');
536
+ if (!wrap) return;
537
+
538
+ const search = (document.getElementById('cl-filter-search')?.value || '').toLowerCase();
539
+ const status = document.getElementById('cl-filter-status')?.value || '';
540
+ const fromDate = document.getElementById('cl-filter-from')?.value || '';
541
+ const toDate = document.getElementById('cl-filter-to')?.value || '';
542
+
543
+ let filtered = list;
544
+ if (search) filtered = filtered.filter(c => (c.name || c.id || '').toLowerCase().includes(search));
545
+ if (status) filtered = filtered.filter(c => c.status === status);
546
+ if (fromDate) filtered = filtered.filter(c => c.created_at && c.created_at >= fromDate);
547
+ if (toDate) filtered = filtered.filter(c => c.created_at && c.created_at.slice(0, 10) <= toDate);
1672
548
 
1673
- if (!pipeline) {
1674
- sectionEl.style.display = 'none';
549
+ if (filtered.length === 0) {
550
+ wrap.innerHTML = '';
551
+ if (emptyEl) emptyEl.style.display = '';
1675
552
  return;
1676
553
  }
554
+ if (emptyEl) emptyEl.style.display = 'none';
1677
555
 
1678
- sectionEl.style.display = '';
1679
- if (descEl) {
1680
- descEl.textContent =
1681
- (pipeline.name || pipeline.id) + ' \u2014 ' + (pipeline.branch || 'no branch');
1682
- }
556
+ const statusBadgeClass = (s) => {
557
+ const map = { done: '--done', running: '--running', failed: '--failed', 'gate-failed': '--gate-failed', pending: '--pending' };
558
+ return 'status-badge ' + (map[s] || '');
559
+ };
1683
560
 
1684
- const convoyIds = pipeline.convoy_ids || [];
1685
- const pipelineConvoys = convoyIds
1686
- .map((id) => convoys.find((c) => c.id === id))
1687
- .filter(Boolean);
1688
-
1689
- const total = pipelineConvoys.length;
1690
- const done = pipelineConvoys.filter((c) => c.status === 'done').length;
1691
- const failed = pipelineConvoys.filter(
1692
- (c) => c.status === 'failed' || c.status === 'gate-failed'
1693
- ).length;
1694
- const totalTasks = pipelineConvoys.reduce((sum, c) => {
1695
- const s = c.summary || {};
1696
- return sum + (s.total || (c.tasks ? c.tasks.length : 0));
1697
- }, 0);
1698
- const doneTasks = pipelineConvoys.reduce((sum, c) => {
1699
- const s = c.summary || {};
1700
- return sum + (s.done || 0);
1701
- }, 0);
1702
- const totalTokens = pipelineConvoys.reduce((sum, c) => sum + (c.total_tokens || 0), 0);
1703
-
1704
- const pct =
1705
- totalTasks > 0
1706
- ? Math.round((doneTasks / totalTasks) * 100)
1707
- : total > 0
1708
- ? Math.round((done / total) * 100)
1709
- : 0;
1710
-
1711
- const statusClass =
1712
- pipeline.status === 'done'
1713
- ? 'success'
1714
- : pipeline.status === 'failed' || pipeline.status === 'gate-failed'
1715
- ? 'failed'
1716
- : pipeline.status === 'running'
1717
- ? 'partial'
1718
- : '';
1719
-
1720
- let html = '<div class="convoy-overview">';
1721
- html +=
1722
- '<div class="convoy-stat"><span class="convoy-stat__label">Status</span>' +
1723
- '<span class="outcome-badge outcome-badge--' + statusClass + '">' +
1724
- escapeHtml(pipeline.status || 'unknown') + '</span></div>';
1725
- html +=
1726
- '<div class="convoy-stat"><span class="convoy-stat__label">Branch</span>' +
1727
- '<span class="convoy-stat__value">' + escapeHtml(pipeline.branch || '\u2014') + '</span></div>';
1728
- html +=
1729
- '<div class="convoy-stat"><span class="convoy-stat__label">Convoys</span>' +
1730
- '<span class="convoy-stat__value">' + done + '/' + total + '</span></div>';
1731
- if (totalTasks > 0) {
1732
- html +=
1733
- '<div class="convoy-stat"><span class="convoy-stat__label">Tasks</span>' +
1734
- '<span class="convoy-stat__value">' + doneTasks + '/' + totalTasks + '</span></div>';
1735
- }
1736
- if (totalTokens > 0) {
1737
- html +=
1738
- '<div class="convoy-stat"><span class="convoy-stat__label">Tokens</span>' +
1739
- '<span class="convoy-stat__value">' + formatTokens(totalTokens) + '</span></div>';
1740
- }
1741
- html +=
1742
- '<div class="convoy-stat"><span class="convoy-stat__label">Started</span>' +
1743
- '<span class="convoy-stat__value">' +
1744
- (pipeline.started_at ? formatTime(pipeline.started_at) : '\u2014') + '</span></div>';
1745
- if (pipeline.finished_at) {
1746
- html +=
1747
- '<div class="convoy-stat"><span class="convoy-stat__label">Finished</span>' +
1748
- '<span class="convoy-stat__value">' + formatTime(pipeline.finished_at) + '</span></div>';
1749
- }
1750
- const pipelineDur = formatDuration(pipeline.started_at, pipeline.finished_at);
1751
- if (pipelineDur) {
1752
- html +=
1753
- '<div class="convoy-stat"><span class="convoy-stat__label">Duration</span>' +
1754
- '<span class="convoy-stat__value">' + pipelineDur + '</span></div>';
1755
- }
1756
- if (pipeline.total_cost_usd != null && pipeline.total_cost_usd > 0) {
1757
- html +=
1758
- '<div class="convoy-stat"><span class="convoy-stat__label">Cost</span>' +
1759
- '<span class="convoy-stat__value">$' + pipeline.total_cost_usd.toFixed(2) + '</span></div>';
561
+ let html = '<table class="convoy-list-table"><thead><tr>' +
562
+ '<th>Name</th><th>Status</th><th>Tasks</th><th>Created</th><th>Duration</th>' +
563
+ '</tr></thead><tbody>';
564
+
565
+ for (const c of filtered) {
566
+ const name = escapeHtml(c.name || c.id);
567
+ const dateStr = c.created_at ? formatTime(c.created_at) : '\u2014';
568
+ const duration = c.started_at && c.finished_at ? formatDuration(c.started_at, c.finished_at) : (c.status === 'running' ? 'In progress' : '\u2014');
569
+ const taskCount = c.task_count ?? c.taskCount ?? '\u2014';
570
+ html += '<tr data-convoy-id="' + escapeHtml(c.id) + '" class="task-row--clickable">' +
571
+ '<td><strong>' + name + '</strong></td>' +
572
+ '<td><span class="' + statusBadgeClass(c.status) + '">' + escapeHtml(c.status || 'unknown') + '</span></td>' +
573
+ '<td>' + taskCount + '</td>' +
574
+ '<td>' + dateStr + '</td>' +
575
+ '<td>' + (duration || '\u2014') + '</td>' +
576
+ '</tr>';
1760
577
  }
1761
- html += '</div>';
1762
-
1763
- // Convoy chain visualization
1764
- html += '<div class="convoy-chain">';
1765
- pipelineConvoys.forEach((convoy, i) => {
1766
- const cs = convoy.summary || {};
1767
- const cDone = cs.done || 0;
1768
- const cTotal = cs.total || (convoy.tasks ? convoy.tasks.length : 0);
1769
- const cTokens = convoy.total_tokens || 0;
1770
- const isActive =
1771
- (pipeline.current_convoy_id && pipeline.current_convoy_id === convoy.id) ||
1772
- convoy.status === 'running';
1773
- const nodeStatusClass =
1774
- convoy.status === 'done'
1775
- ? 'done'
1776
- : convoy.status === 'failed' || convoy.status === 'gate-failed'
1777
- ? 'failed'
1778
- : isActive
1779
- ? 'active'
1780
- : 'pending';
1781
- const badgeClass =
1782
- convoy.status === 'done'
1783
- ? 'success'
1784
- : convoy.status === 'failed' || convoy.status === 'gate-failed'
1785
- ? 'failed'
1786
- : convoy.status === 'running'
1787
- ? 'partial'
1788
- : '';
1789
-
1790
- if (i > 0) {
1791
- html += '<div class="convoy-chain__connector">\u2192</div>';
1792
- }
1793
- html +=
1794
- '<div class="convoy-chain__node convoy-chain__node--' + nodeStatusClass +
1795
- '" data-convoy-id="' + escapeHtml(convoy.id) + '" title="Click to filter to this convoy">';
1796
- html += '<div class="convoy-chain__node-name">' + escapeHtml(convoy.name || convoy.id) + '</div>';
1797
- html +=
1798
- '<span class="outcome-badge outcome-badge--' + badgeClass + '">' +
1799
- escapeHtml(convoy.status) + '</span>';
1800
- if (cTotal > 0) {
1801
- html += '<div class="convoy-chain__node-meta">' + cDone + '/' + cTotal + ' tasks</div>';
1802
- }
1803
- if (cTokens > 0) {
1804
- html += '<div class="convoy-chain__node-meta">' + formatTokens(cTokens) + ' tokens</div>';
1805
- }
1806
- html += '</div>';
1807
- });
1808
- html += '</div>';
1809
-
1810
- // Progress bar
1811
- html += '<div class="convoy-progress">';
1812
- html +=
1813
- '<div class="convoy-progress__bar">' +
1814
- '<div class="convoy-progress__fill" style="width:' + pct + '%"></div></div>';
1815
- html +=
1816
- '<span class="convoy-progress__label">' + pct + '% complete' +
1817
- (failed > 0 ? ' \u00B7 ' + failed + ' failed' : '') + '</span>';
1818
- html += '</div>';
1819
-
1820
- bodyEl.innerHTML = html;
1821
-
1822
- // Click handlers for convoy drill-down
1823
- bodyEl.querySelectorAll('.convoy-chain__node').forEach((node) => {
1824
- node.addEventListener('click', () => {
1825
- const convoyId = node.dataset.convoyId;
1826
- const sel = document.getElementById('filter-convoy');
1827
- if (sel && convoyId) {
1828
- sel.value = convoyId;
1829
- applyFilters();
1830
- }
578
+ html += '</tbody></table>';
579
+ wrap.innerHTML = html;
580
+
581
+ wrap.querySelectorAll('tr[data-convoy-id]').forEach(row => {
582
+ row.addEventListener('click', () => {
583
+ const id = row.dataset.convoyId;
584
+ const nameCell = row.querySelector('td strong');
585
+ showConvoyDetailView(id, nameCell ? nameCell.textContent : id);
1831
586
  });
1832
587
  });
1833
588
  }
@@ -1874,34 +629,10 @@ try {
1874
629
  return hr + 'h ' + remMin + 'm';
1875
630
  }
1876
631
 
1877
- // ── Convoy Selector ──────────────────────────────────────
1878
-
1879
- function populateConvoySelector() {
1880
- const data = window.__DASHBOARD_DATA__;
1881
- const select = document.getElementById('convoy-select');
1882
- if (!select || !data || !data.convoyList) return;
1883
-
1884
- select.innerHTML = '<option value="">Select convoy\u2026</option>';
1885
- const list = data.convoyList;
1886
- for (const c of list) {
1887
- const opt = document.createElement('option');
1888
- opt.value = c.id;
1889
- const dateStr = c.created_at ? c.created_at.slice(0, 10) : '';
1890
- opt.textContent = (c.name || c.id) + ' \u2014 ' + c.status + ' (' + dateStr + ')';
1891
- select.appendChild(opt);
1892
- }
1893
-
1894
- // Default: select latest (first in list)
1895
- if (list.length > 0) {
1896
- select.value = list[0].id;
1897
- loadConvoyDetail(list[0].id);
1898
- }
1899
- }
1900
-
1901
632
  async function loadConvoyDetail(convoyId) {
1902
633
  if (!convoyId) {
1903
634
  renderConvoyDetailHeader(null);
1904
- ['quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'].forEach(function(id) {
635
+ ['pipeline-section', 'agent-section', 'tier-section', 'model-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'execution-section', 'event-timeline-section', 'panel-section', 'reviews-section'].forEach(function(id) {
1905
636
  var el = document.getElementById(id);
1906
637
  if (el) el.style.display = 'none';
1907
638
  });
@@ -1918,15 +649,24 @@ try {
1918
649
  renderPhaseBreakdown(detail.tasks || []);
1919
650
  const tasksSection = document.getElementById('tasks-section');
1920
651
  if (tasksSection) tasksSection.style.display = '';
652
+ renderDetailPipeline(detail.tasks || []);
653
+ renderDetailAgentChart(detail.tasks || []);
654
+ renderDetailOutcomeChart(detail.tasks || []);
655
+ renderDetailTierChart(detail.tasks || []);
656
+ renderDetailMechanismChart(detail.events || []);
657
+ renderDetailModelChart(detail.tasks || []);
1921
658
  renderQualitySection(detail);
1922
659
  renderReliabilitySection(detail);
1923
660
  renderDriftSection(detail);
1924
661
  renderOutputsSection(detail);
662
+ renderDetailExecutionLog(detail.tasks || []);
1925
663
  renderEventTimeline(detail);
664
+ renderDetailPanelChart(detail.tasks || []);
665
+ renderDetailReviewsTable(detail.tasks || []);
1926
666
  } catch (e) {
1927
667
  console.error('Failed to load convoy detail:', e);
1928
668
  renderConvoyDetailHeader(null);
1929
- ['quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'].forEach(function(id) {
669
+ ['pipeline-section', 'agent-section', 'tier-section', 'model-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'execution-section', 'event-timeline-section', 'panel-section', 'reviews-section'].forEach(function(id) {
1930
670
  var el = document.getElementById(id);
1931
671
  if (el) el.style.display = 'none';
1932
672
  });
@@ -1934,15 +674,15 @@ try {
1934
674
  }
1935
675
 
1936
676
  function renderConvoyDetailHeader(detail) {
1937
- const nameEl = document.getElementById('selected-convoy-name');
1938
- const statusEl = document.getElementById('selected-convoy-status');
1939
- const metaEl = document.getElementById('selected-convoy-meta');
677
+ const nameEl = document.getElementById('detail-hero-title');
678
+ const statusEl = document.getElementById('detail-hero-status');
679
+ const metaEl = document.getElementById('detail-hero-meta');
1940
680
 
1941
681
  if (!detail || !detail.convoy) {
1942
682
  if (nameEl) nameEl.textContent = 'No convoy selected';
1943
683
  if (statusEl) { statusEl.textContent = ''; statusEl.className = 'status-badge'; }
1944
684
  if (metaEl) metaEl.innerHTML = '';
1945
- ['tasks-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'].forEach(function(id) {
685
+ ['tasks-section', 'pipeline-section', 'agent-section', 'tier-section', 'model-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'execution-section', 'event-timeline-section', 'panel-section', 'reviews-section'].forEach(function(id) {
1946
686
  var el = document.getElementById(id);
1947
687
  if (el) el.style.display = 'none';
1948
688
  });
@@ -1963,11 +703,6 @@ try {
1963
703
  'gate-failed': 'This run stopped because a quality check failed.',
1964
704
  'hook-failed': 'This run stopped because a lifecycle script failed.',
1965
705
  };
1966
- const explanationEl = document.getElementById('convoy-status-explanation');
1967
- if (explanationEl) {
1968
- explanationEl.textContent = statusExplanations[c.status] || '';
1969
- explanationEl.style.display = statusExplanations[c.status] ? '' : 'none';
1970
- }
1971
706
  if (metaEl) {
1972
707
  let html = '';
1973
708
  if (c.branch) html += '<span class="convoy-meta__item">🌿 ' + escapeHtml(c.branch) + '</span>';
@@ -2145,6 +880,390 @@ try {
2145
880
  el.innerHTML = html;
2146
881
  }
2147
882
 
883
+ // ── Convoy Detail: Derive Tier from Model ────────────────
884
+
885
+ function deriveTier(model) {
886
+ if (!model) return 'unknown';
887
+ var m = model.toLowerCase();
888
+ if (m.includes('opus')) return 'premium';
889
+ if (m.includes('sonnet') || m.includes('pro')) return 'standard';
890
+ if (m.includes('haiku') || m.includes('flash') || m.includes('mini')) return 'economy';
891
+ return 'utility';
892
+ }
893
+
894
+ // ── Convoy Detail: Pipeline View ─────────────────────────
895
+
896
+ function renderDetailPipeline(tasks) {
897
+ var el = document.getElementById('pipeline-view');
898
+ if (!el) return;
899
+
900
+ if (!tasks || tasks.length === 0) {
901
+ el.innerHTML = emptyStateHtml('pipeline', 'No pipeline activity yet', 'Tasks will flow through execution phases here.');
902
+ return;
903
+ }
904
+
905
+ var phases = {};
906
+ tasks.forEach(function(t) {
907
+ var p = t.phase != null ? t.phase : 1;
908
+ if (!phases[p]) phases[p] = 0;
909
+ phases[p]++;
910
+ });
911
+
912
+ var stageConfig = [
913
+ { label: 'Foundation', phase: 1, iconClass: 'pending', 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>' },
914
+ { label: 'Integration', phase: 2, iconClass: 'active', 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>' },
915
+ { label: 'Validation', phase: 3, iconClass: 'review', 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>' },
916
+ { label: 'QA Gate', phase: 4, iconClass: 'done', 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>' },
917
+ ];
918
+
919
+ el.innerHTML =
920
+ '<div class="pipeline">' +
921
+ stageConfig.map(function(stage, i) {
922
+ return (i > 0 ? '<div class="pipeline-arrow">\u2192</div>' : '') +
923
+ '<div class="pipeline-stage">' +
924
+ '<div class="pipeline-stage__icon pipeline-stage__icon--' + stage.iconClass + '">' + stage.icon + '</div>' +
925
+ '<span class="pipeline-stage__count">' + (phases[stage.phase] || 0) + '</span>' +
926
+ '<span class="pipeline-stage__label">' + stage.label + '</span>' +
927
+ '</div>';
928
+ }).join('') +
929
+ '</div>';
930
+ }
931
+
932
+ // ── Convoy Detail: Agent Chart ───────────────────────────
933
+
934
+ function renderDetailAgentChart(tasks) {
935
+ var el = document.getElementById('agent-chart');
936
+ if (!el) return;
937
+
938
+ if (!tasks || tasks.length === 0) {
939
+ el.innerHTML = emptyStateHtml('agents', 'No agent data yet', 'A breakdown of tasks per agent will appear here.');
940
+ return;
941
+ }
942
+
943
+ var agentMap = {};
944
+ tasks.forEach(function(t) {
945
+ var agent = t.agent || 'unknown';
946
+ if (!agentMap[agent]) agentMap[agent] = { done: 0, failed: 0, running: 0, other: 0, total: 0 };
947
+ if (t.status === 'done') agentMap[agent].done++;
948
+ else if (t.status === 'running') agentMap[agent].running++;
949
+ else if (['failed', 'gate-failed', 'timed-out', 'hook-failed'].includes(t.status)) agentMap[agent].failed++;
950
+ else agentMap[agent].other++;
951
+ agentMap[agent].total++;
952
+ });
953
+
954
+ var agents = Object.entries(agentMap).sort(function(a, b) { return b[1].total - a[1].total; });
955
+ var maxTotal = Math.max.apply(null, agents.map(function(a) { return a[1].total; }));
956
+
957
+ el.innerHTML = agents.map(function(entry) {
958
+ var name = entry[0];
959
+ var data = entry[1];
960
+ return '<div class="bar-row">' +
961
+ '<span class="bar-label">' + escapeHtml(name) + '</span>' +
962
+ '<div class="bar-track">' +
963
+ (data.done > 0 ? '<div class="bar-segment bar--success" style="width: ' + ((data.done / maxTotal) * 100).toFixed(1) + '%"></div>' : '') +
964
+ (data.running > 0 ? '<div class="bar-segment" style="width: ' + ((data.running / maxTotal) * 100).toFixed(1) + '%; background: #3b82f6"></div>' : '') +
965
+ (data.failed > 0 ? '<div class="bar-segment bar--failed" style="width: ' + ((data.failed / maxTotal) * 100).toFixed(1) + '%"></div>' : '') +
966
+ (data.other > 0 ? '<div class="bar-segment" style="width: ' + ((data.other / maxTotal) * 100).toFixed(1) + '%; background: #64748b"></div>' : '') +
967
+ '</div>' +
968
+ '<span class="bar-value">' + data.total + '</span>' +
969
+ '</div>';
970
+ }).join('');
971
+ }
972
+
973
+ // ── Convoy Detail: Tier Donut Chart ──────────────────────
974
+
975
+ function renderDetailTierChart(tasks) {
976
+ var el = document.getElementById('tier-chart');
977
+ if (!el) return;
978
+
979
+ if (!tasks || tasks.length === 0) {
980
+ el.innerHTML = emptyStateHtml('tiers', 'No tier data yet', 'Model tier distribution will be shown as a donut chart.');
981
+ return;
982
+ }
983
+
984
+ var tierCounts = {};
985
+ tasks.forEach(function(t) {
986
+ var tier = deriveTier(t.model);
987
+ tierCounts[tier] = (tierCounts[tier] || 0) + 1;
988
+ });
989
+
990
+ var order = ['premium', 'standard', 'utility', 'economy', 'unknown'];
991
+ var tiers = order.filter(function(t) { return tierCounts[t]; }).map(function(t) { return { name: t, count: tierCounts[t] }; });
992
+ var total = tasks.length;
993
+ var r = 70;
994
+ var circumference = 2 * Math.PI * r;
995
+ var cumOffset = 0;
996
+
997
+ var circles = tiers.map(function(t) {
998
+ var pct = t.count / total;
999
+ var dashLen = pct * circumference;
1000
+ var linecap = tiers.length === 1 ? 'butt' : 'round';
1001
+ var segment = '<circle cx="90" cy="90" r="' + r + '" fill="none" stroke="' + (TIER_COLORS[t.name] || '#64748b') + '" stroke-width="18" stroke-dasharray="' + dashLen.toFixed(2) + ' ' + (circumference - dashLen).toFixed(2) + '" stroke-dashoffset="' + (-cumOffset).toFixed(2) + '" transform="rotate(-90 90 90)" stroke-linecap="' + linecap + '"/>';
1002
+ cumOffset += dashLen;
1003
+ return segment;
1004
+ });
1005
+
1006
+ var legend = tiers.map(function(t) {
1007
+ return '<div class="legend-item"><span class="legend-dot" style="background: ' + (TIER_COLORS[t.name] || '#64748b') + '"></span><span class="legend-name">' + t.name + '</span><span class="legend-count">' + t.count + ' (' + Math.round((t.count / total) * 100) + '%)</span></div>';
1008
+ }).join('');
1009
+
1010
+ el.innerHTML =
1011
+ '<div class="donut-container"><div class="donut-wrap"><svg viewBox="0 0 180 180" class="donut-svg">' + circles.join('') + '</svg><div class="donut-center"><span class="donut-total">' + total + '</span><span class="donut-total-label">total</span></div></div><div class="donut-legend">' + legend + '</div></div>';
1012
+ }
1013
+
1014
+ // ── Convoy Detail: Mechanism Donut Chart ─────────────────
1015
+
1016
+ function renderDetailMechanismChart(events) {
1017
+ var el = document.getElementById('mechanism-chart');
1018
+ if (!el) return;
1019
+
1020
+ var MECH_COLORS = { 'sub-agent': '#3b82f6', 'background': '#a78bfa', 'unknown': '#64748b' };
1021
+ var MECH_LABELS = { 'sub-agent': 'Sub-agent (inline)', 'background': 'Background (worktree)', 'unknown': 'Unknown' };
1022
+
1023
+ if (!events || events.length === 0) {
1024
+ el.innerHTML = emptyStateHtml('mechanism', 'No delegation data yet', 'The split between sub-agent and background delegations will be shown here.');
1025
+ return;
1026
+ }
1027
+
1028
+ var mechCounts = {};
1029
+ events.forEach(function(e) {
1030
+ if (e.type === 'task_assigned' || e.type === 'task_started') {
1031
+ var mech = (e.data && e.data.mechanism) || 'unknown';
1032
+ mechCounts[mech] = (mechCounts[mech] || 0) + 1;
1033
+ }
1034
+ });
1035
+
1036
+ var mechOrder = ['sub-agent', 'background', 'unknown'];
1037
+ var mechs = mechOrder.filter(function(m) { return mechCounts[m]; }).map(function(m) { return { name: m, count: mechCounts[m] }; });
1038
+
1039
+ if (mechs.length === 0) {
1040
+ el.innerHTML = emptyStateHtml('mechanism', 'No delegation data yet', 'The split between sub-agent and background delegations will be shown here.');
1041
+ return;
1042
+ }
1043
+
1044
+ var total = mechs.reduce(function(s, m) { return s + m.count; }, 0);
1045
+ var r = 70;
1046
+ var circumference = 2 * Math.PI * r;
1047
+ var cumOffset = 0;
1048
+
1049
+ var circles = mechs.map(function(m) {
1050
+ var pct = m.count / total;
1051
+ var dashLen = pct * circumference;
1052
+ var linecap = mechs.length === 1 ? 'butt' : 'round';
1053
+ var segment = '<circle cx="90" cy="90" r="' + r + '" fill="none" stroke="' + (MECH_COLORS[m.name] || '#64748b') + '" stroke-width="18" stroke-dasharray="' + dashLen.toFixed(2) + ' ' + (circumference - dashLen).toFixed(2) + '" stroke-dashoffset="' + (-cumOffset).toFixed(2) + '" transform="rotate(-90 90 90)" stroke-linecap="' + linecap + '"/>';
1054
+ cumOffset += dashLen;
1055
+ return segment;
1056
+ });
1057
+
1058
+ var legend = mechs.map(function(m) {
1059
+ return '<div class="legend-item"><span class="legend-dot" style="background: ' + (MECH_COLORS[m.name] || '#64748b') + '"></span><span class="legend-name">' + (MECH_LABELS[m.name] || m.name) + '</span><span class="legend-count">' + m.count + ' (' + Math.round((m.count / total) * 100) + '%)</span></div>';
1060
+ }).join('');
1061
+
1062
+ el.innerHTML =
1063
+ '<div class="donut-container"><div class="donut-wrap"><svg viewBox="0 0 180 180" class="donut-svg">' + circles.join('') + '</svg><div class="donut-center"><span class="donut-total">' + total + '</span><span class="donut-total-label">total</span></div></div><div class="donut-legend">' + legend + '</div></div>';
1064
+ }
1065
+
1066
+ // ── Convoy Detail: Delegation Outcome Chart ──────────────
1067
+
1068
+ function renderDetailOutcomeChart(tasks) {
1069
+ var el = document.getElementById('delegation-outcome-chart');
1070
+ if (!el) return;
1071
+
1072
+ if (!tasks || tasks.length === 0) {
1073
+ el.innerHTML = emptyStateHtml('outcomes', 'No outcome data yet', 'Task outcome distribution will be shown here.');
1074
+ return;
1075
+ }
1076
+
1077
+ var OUTCOME_COLORS = { done: '#22c55e', running: '#3b82f6', pending: '#64748b', assigned: '#64748b', failed: '#ef4444', 'gate-failed': '#f59e0b', 'timed-out': '#a78bfa', 'hook-failed': '#64748b', 'review-blocked': '#f59e0b', skipped: '#94a3b8' };
1078
+
1079
+ var outcomeCounts = {};
1080
+ tasks.forEach(function(t) {
1081
+ var outcome = t.status || 'unknown';
1082
+ outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
1083
+ });
1084
+
1085
+ var outcomes = Object.entries(outcomeCounts).sort(function(a, b) { return b[1] - a[1]; });
1086
+ var maxCount = Math.max.apply(null, outcomes.map(function(o) { return o[1]; }));
1087
+
1088
+ el.innerHTML = outcomes.map(function(entry) {
1089
+ var name = entry[0];
1090
+ var count = entry[1];
1091
+ return '<div class="bar-row">' +
1092
+ '<span class="bar-label">' + escapeHtml(name) + '</span>' +
1093
+ '<div class="bar-track"><div class="bar-segment" style="width: ' + ((count / maxCount) * 100).toFixed(1) + '%; background: ' + (OUTCOME_COLORS[name] || '#64748b') + '"></div></div>' +
1094
+ '<span class="bar-value">' + count + '</span>' +
1095
+ '</div>';
1096
+ }).join('');
1097
+ }
1098
+
1099
+ // ── Convoy Detail: Model Chart ───────────────────────────
1100
+
1101
+ function renderDetailModelChart(tasks) {
1102
+ var el = document.getElementById('model-chart');
1103
+ if (!el) return;
1104
+
1105
+ if (!tasks || tasks.length === 0) {
1106
+ el.innerHTML = emptyStateHtml('models', 'No model data yet', 'Model utilization across tasks will be shown here.');
1107
+ return;
1108
+ }
1109
+
1110
+ var modelCounts = {};
1111
+ tasks.forEach(function(t) {
1112
+ if (t.model) modelCounts[t.model] = (modelCounts[t.model] || 0) + 1;
1113
+ });
1114
+
1115
+ var models = Object.entries(modelCounts).sort(function(a, b) { return b[1] - a[1]; });
1116
+
1117
+ if (models.length === 0) {
1118
+ el.innerHTML = emptyStateHtml('models', 'No model data yet', 'Model utilization across tasks will be shown here.');
1119
+ return;
1120
+ }
1121
+
1122
+ var maxCount = Math.max.apply(null, models.map(function(m) { return m[1]; }));
1123
+
1124
+ el.innerHTML = models.map(function(entry) {
1125
+ var name = entry[0];
1126
+ var count = entry[1];
1127
+ return '<div class="bar-row">' +
1128
+ '<span class="bar-label">' + escapeHtml(name) + '</span>' +
1129
+ '<div class="bar-track"><div class="bar-segment" style="width: ' + ((count / maxCount) * 100).toFixed(1) + '%; background: ' + (MODEL_COLORS[name] || '#64748b') + '"></div></div>' +
1130
+ '<span class="bar-value">' + count + '</span>' +
1131
+ '</div>';
1132
+ }).join('');
1133
+ }
1134
+
1135
+ // ── Convoy Detail: Execution Log ─────────────────────────
1136
+
1137
+ function renderDetailExecutionLog(tasks) {
1138
+ var el = document.getElementById('execution-log');
1139
+ if (!el) return;
1140
+
1141
+ if (!tasks || tasks.length === 0) {
1142
+ el.innerHTML = emptyStateHtml('execLog', 'No execution history yet', 'A step-by-step trace of task activity will appear here.');
1143
+ return;
1144
+ }
1145
+
1146
+ var sorted = tasks.slice().filter(function(t) { return t.started_at; }).sort(function(a, b) { return new Date(b.started_at) - new Date(a.started_at); }).slice(0, 10);
1147
+
1148
+ if (sorted.length === 0) {
1149
+ el.innerHTML = emptyStateHtml('execLog', 'No execution history yet', 'A step-by-step trace of task activity will appear here.');
1150
+ return;
1151
+ }
1152
+
1153
+ var statusToOutcome = function(s) {
1154
+ if (s === 'done') return 'success';
1155
+ if (['failed', 'gate-failed', 'timed-out', 'hook-failed'].includes(s)) return 'failed';
1156
+ return 'partial';
1157
+ };
1158
+
1159
+ el.innerHTML =
1160
+ '<div class="exec-log">' +
1161
+ sorted.map(function(t, i) {
1162
+ var outcome = statusToOutcome(t.status);
1163
+ var dur = formatDuration(t.started_at, t.finished_at);
1164
+ var fileCount = t.files ? t.files.length : 0;
1165
+ return '<div class="exec-step">' +
1166
+ '<div class="exec-step__indicator">' +
1167
+ '<div class="exec-step__dot exec-step__dot--' + outcome + '">' + (OUTCOME_ICONS[outcome] || '') + '</div>' +
1168
+ (i < sorted.length - 1 ? '<div class="exec-step__line"></div>' : '') +
1169
+ '</div>' +
1170
+ '<div class="exec-step__content">' +
1171
+ '<div class="exec-step__header">' +
1172
+ '<span class="exec-step__agent">' + escapeHtml(t.agent || 'unknown') + '</span>' +
1173
+ '<span class="exec-step__badge exec-step__badge--' + outcome + '">' + escapeHtml(t.status) + '</span>' +
1174
+ '</div>' +
1175
+ '<div class="exec-step__task">' + escapeHtml(t.id) + '</div>' +
1176
+ '<div class="exec-step__meta">' +
1177
+ '<span class="exec-step__meta-item">\uD83D\uDD52 ' + formatTime(t.started_at) + '</span>' +
1178
+ (dur ? '<span class="exec-step__meta-item">\u23F1 ' + dur + '</span>' : '') +
1179
+ (fileCount > 0 ? '<span class="exec-step__meta-item">\uD83D\uDCC1 ' + fileCount + ' files</span>' : '') +
1180
+ (t.model ? '<span class="exec-step__meta-item">\uD83E\uDD16 ' + escapeHtml(t.model) + '</span>' : '') +
1181
+ (t.retries > 0 ? '<span class="exec-step__meta-item">\uD83D\uDD04 ' + t.retries + ' retries</span>' : '') +
1182
+ (t.total_tokens != null ? '<span class="exec-step__meta-item">\uD83D\uDD24 ' + formatTokens(t.total_tokens) + '</span>' : '') +
1183
+ '</div>' +
1184
+ '</div>' +
1185
+ '</div>';
1186
+ }).join('') +
1187
+ '</div>';
1188
+ }
1189
+
1190
+ // ── Convoy Detail: Panel Reviews ─────────────────────────
1191
+
1192
+ function renderDetailPanelChart(tasks) {
1193
+ var el = document.getElementById('panel-chart');
1194
+ if (!el) return;
1195
+
1196
+ var panelTasks = (tasks || []).filter(function(t) { return t.panel_attempts > 0; });
1197
+
1198
+ if (panelTasks.length === 0) {
1199
+ el.innerHTML = emptyStateHtml('panels', 'No panel reviews yet', 'Quality gate verdicts from majority-vote panels will be shown here.');
1200
+ return;
1201
+ }
1202
+
1203
+ el.innerHTML =
1204
+ '<div class="panel-grid">' +
1205
+ panelTasks.map(function(t) {
1206
+ var verdictUpper = t.review_verdict ? t.review_verdict.toUpperCase() : 'PENDING';
1207
+ var verdictClass = verdictUpper === 'PASS' ? 'PASS' : 'BLOCK';
1208
+ return '<div class="panel-item">' +
1209
+ '<div class="panel-item__header">' +
1210
+ '<span class="panel-item__key">' + escapeHtml(t.id) + '</span>' +
1211
+ '<span class="panel-item__verdict panel-item__verdict--' + verdictClass + '">' + verdictUpper + '</span>' +
1212
+ '</div>' +
1213
+ '<div class="panel-item__votes">' +
1214
+ (verdictUpper === 'PASS' ? '<div class="panel-item__vote panel-item__vote--pass">\u2713</div>' : '<div class="panel-item__vote panel-item__vote--block">\u2717</div>') +
1215
+ '</div>' +
1216
+ '<div class="panel-item__meta">' +
1217
+ '<span class="panel-item__meta-item">\uD83E\uDD16 ' + escapeHtml(t.review_model || 'unknown') + '</span>' +
1218
+ (t.panel_attempts > 1 ? '<span class="panel-item__meta-item">\uD83D\uDD04 ' + t.panel_attempts + ' attempts</span>' : '') +
1219
+ (t.review_tokens != null ? '<span class="panel-item__meta-item">\uD83D\uDD24 ' + formatTokens(t.review_tokens) + '</span>' : '') +
1220
+ '</div>' +
1221
+ '</div>';
1222
+ }).join('') +
1223
+ '</div>';
1224
+ }
1225
+
1226
+ // ── Convoy Detail: Fast Reviews ──────────────────────────
1227
+
1228
+ function renderDetailReviewsTable(tasks) {
1229
+ var el = document.getElementById('reviews-table');
1230
+ if (!el) return;
1231
+
1232
+ var reviewedTasks = (tasks || []).filter(function(t) { return t.review_level != null; });
1233
+
1234
+ if (reviewedTasks.length === 0) {
1235
+ el.innerHTML = emptyStateHtml('panels', 'No fast reviews yet', 'Single-reviewer quality gate results will be listed here.');
1236
+ return;
1237
+ }
1238
+
1239
+ el.innerHTML =
1240
+ '<table class="sessions-table">' +
1241
+ '<thead><tr>' +
1242
+ '<th scope="col">Task ID</th>' +
1243
+ '<th scope="col">Agent</th>' +
1244
+ '<th scope="col">Review Level</th>' +
1245
+ '<th scope="col">Verdict</th>' +
1246
+ '<th scope="col">Model</th>' +
1247
+ '<th scope="col">Tokens</th>' +
1248
+ '</tr></thead>' +
1249
+ '<tbody>' +
1250
+ reviewedTasks.map(function(t) {
1251
+ var verdictUpper = t.review_verdict ? t.review_verdict.toUpperCase() : '';
1252
+ var verdictBadge = t.review_verdict
1253
+ ? '<span class="status-badge status-badge--' + (verdictUpper === 'PASS' ? 'done' : 'failed') + '">' + escapeHtml(t.review_verdict) + '</span>'
1254
+ : '<span style="opacity:0.4">\u2014</span>';
1255
+ return '<tr>' +
1256
+ '<td class="td-task">' + escapeHtml(t.id || '\u2014') + '</td>' +
1257
+ '<td class="td-agent">' + escapeHtml(t.agent || '\u2014') + '</td>' +
1258
+ '<td>' + escapeHtml(t.review_level || '\u2014') + '</td>' +
1259
+ '<td>' + verdictBadge + '</td>' +
1260
+ '<td>' + escapeHtml(t.review_model || '\u2014') + '</td>' +
1261
+ '<td class="td-num">' + (t.review_tokens != null ? formatTokens(t.review_tokens) : '\u2014') + '</td>' +
1262
+ '</tr>';
1263
+ }).join('') +
1264
+ '</tbody></table>';
1265
+ }
1266
+
2148
1267
  // ── Quality Section ──────────────────────────────────────
2149
1268
 
2150
1269
  function renderQualitySection(detail) {
@@ -2509,57 +1628,22 @@ try {
2509
1628
  }
2510
1629
 
2511
1630
  async function main() {
2512
- const events = await loadNdjson(base + 'data/events.ndjson');
2513
- const pipelines = await loadNdjson(base + 'data/pipelines.ndjson');
2514
1631
  const convoys = window.__DASHBOARD_DATA__?.convoyList ?? [];
2515
1632
 
2516
- const sessions = events.filter((e) => e.type === 'session');
2517
- const delegations = events.filter((e) => e.type === 'delegation');
2518
- const panels = events.filter((e) => e.type === 'panel');
2519
- const reviews = events.filter((e) => e.type === 'review');
2520
-
2521
- rawSessions = sessions;
2522
- rawDelegations = delegations;
2523
- rawPanels = panels;
2524
- rawReviews = reviews;
2525
- rawConvoys = convoys;
2526
- rawPipelines = pipelines;
2527
-
2528
- populateAgentFilter(sessions, delegations, reviews);
2529
- populateConvoyFilter(convoys);
2530
- populatePipelineFilter(pipelines);
2531
-
2532
- // ── Read URL params ───────────────────────────────────
2533
1633
  const urlParams = new URLSearchParams(window.location.search);
2534
1634
  const convoyParam = urlParams.get('convoy');
1635
+
2535
1636
  if (convoyParam === 'active') {
2536
- const running = rawConvoys.find((c) => c.status === 'running');
2537
- const latest = rawConvoys.slice().sort((a, b) => b.created_at.localeCompare(a.created_at))[0];
1637
+ const running = convoys.find((c) => c.status === 'running');
1638
+ const latest = convoys.slice().sort((a, b) => b.created_at.localeCompare(a.created_at))[0];
2538
1639
  const target = running || latest;
2539
- if (target) {
2540
- const sel = document.getElementById('filter-convoy');
2541
- if (sel) sel.value = target.id;
2542
- }
1640
+ if (target) showConvoyDetailView(target.id, target.name || target.id);
2543
1641
  } else if (convoyParam) {
2544
- const sel = document.getElementById('filter-convoy');
2545
- if (sel) sel.value = convoyParam;
1642
+ showConvoyDetailView(convoyParam, convoyParam);
2546
1643
  }
2547
1644
 
2548
- renderAll(sessions, delegations, panels, reviews);
2549
-
2550
- // Apply convoy param after initial render (shows convoy section if needed)
2551
- if (convoyParam) applyFilters();
2552
-
2553
- // ── Overall stats + convoy selector ──────────────────
2554
1645
  renderOverallStats();
2555
- populateConvoySelector();
2556
-
2557
- const convoySelectEl = document.getElementById('convoy-select');
2558
- if (convoySelectEl) {
2559
- convoySelectEl.addEventListener('change', function() {
2560
- loadConvoyDetail(this.value);
2561
- });
2562
- }
1646
+ renderConvoyList();
2563
1647
 
2564
1648
  var loadMoreBtn = document.getElementById('event-timeline-more-btn');
2565
1649
  if (loadMoreBtn) {
@@ -2569,66 +1653,47 @@ try {
2569
1653
  });
2570
1654
  }
2571
1655
 
2572
- // ── Filter event listeners ────────────────────────────
2573
- document.getElementById('filter-date-from')?.addEventListener('change', applyFilters);
2574
- document.getElementById('filter-date-to')?.addEventListener('change', applyFilters);
2575
- document.getElementById('filter-agent')?.addEventListener('change', applyFilters);
2576
- document.getElementById('filter-outcome')?.addEventListener('change', applyFilters);
2577
- document.getElementById('filter-convoy')?.addEventListener('change', applyFilters);
2578
- document.getElementById('filter-pipeline')?.addEventListener('change', applyFilters);
2579
- document.getElementById('filter-reset')?.addEventListener('click', () => {
2580
- document.getElementById('filter-date-from').value = '';
2581
- document.getElementById('filter-date-to').value = '';
2582
- document.getElementById('filter-agent').value = '';
2583
- document.getElementById('filter-outcome').value = '';
2584
- document.getElementById('filter-convoy').value = '';
2585
- document.getElementById('filter-pipeline').value = '';
2586
- applyFilters();
1656
+ document.getElementById('breadcrumbs-home')?.addEventListener('click', (e) => {
1657
+ e.preventDefault();
1658
+ showHomeView();
2587
1659
  });
2588
1660
 
2589
- // ── Auto-refresh for live convoy monitoring ───────────
2590
- let refreshInterval = null;
2591
- function startAutoRefresh() {
2592
- if (refreshInterval) return;
2593
- refreshInterval = setInterval(async () => {
2594
- const freshEvents = await loadNdjson(base + 'data/events.ndjson');
2595
- const freshConvoys = await loadJson(base + 'data/convoy-list.json');
2596
- const freshPipelines = await loadNdjson(base + 'data/pipelines.ndjson');
2597
- rawSessions = freshEvents.filter((e) => e.type === 'session');
2598
- rawDelegations = freshEvents.filter((e) => e.type === 'delegation');
2599
- rawPanels = freshEvents.filter((e) => e.type === 'panel');
2600
- rawReviews = freshEvents.filter((e) => e.type === 'review');
2601
- rawConvoys = freshConvoys;
2602
- rawPipelines = freshPipelines;
2603
- const currentValue = document.getElementById('filter-convoy')?.value;
2604
- const currentPipelineValue = document.getElementById('filter-pipeline')?.value;
2605
- populateConvoyFilter(freshConvoys);
2606
- populatePipelineFilter(freshPipelines);
2607
- const sel = document.getElementById('filter-convoy');
2608
- if (sel && currentValue) sel.value = currentValue;
2609
- const pSel = document.getElementById('filter-pipeline');
2610
- if (pSel && currentPipelineValue) pSel.value = currentPipelineValue;
2611
- applyFilters();
2612
- }, 5000);
2613
- }
2614
-
2615
- const selectedConvoy = rawConvoys.find((c) => c.id === document.getElementById('filter-convoy')?.value);
2616
- if (convoyParam === 'active' || (selectedConvoy && selectedConvoy.status === 'running')) {
2617
- startAutoRefresh();
2618
- }
1661
+ document.getElementById('cl-filter-search')?.addEventListener('input', renderConvoyList);
1662
+ document.getElementById('cl-filter-status')?.addEventListener('change', renderConvoyList);
1663
+ document.getElementById('cl-filter-from')?.addEventListener('change', renderConvoyList);
1664
+ document.getElementById('cl-filter-to')?.addEventListener('change', renderConvoyList);
1665
+ document.getElementById('cl-filter-reset')?.addEventListener('click', () => {
1666
+ const s = document.getElementById('cl-filter-search'); if (s) s.value = '';
1667
+ const st = document.getElementById('cl-filter-status'); if (st) st.value = '';
1668
+ const f = document.getElementById('cl-filter-from'); if (f) f.value = '';
1669
+ const t = document.getElementById('cl-filter-to'); if (t) t.value = '';
1670
+ renderConvoyList();
1671
+ });
2619
1672
 
2620
- // ── Export button ─────────────────────────────────────
2621
- document.getElementById('export-btn')?.addEventListener('click', exportData);
1673
+ window.addEventListener('popstate', () => {
1674
+ const params = new URLSearchParams(window.location.search);
1675
+ const c = params.get('convoy');
1676
+ if (c) {
1677
+ loadConvoyDetail(c);
1678
+ const home = document.getElementById('view-home');
1679
+ const detail = document.getElementById('view-convoy-detail');
1680
+ const breadcrumbs = document.getElementById('breadcrumbs');
1681
+ if (home) home.dataset.viewHidden = '';
1682
+ if (detail) delete detail.dataset.viewHidden;
1683
+ if (breadcrumbs) delete breadcrumbs.dataset.viewHidden;
1684
+ document.querySelectorAll('.dash-sidebar__link[data-view="home"]').forEach(el => el.closest('li').style.display = 'none');
1685
+ document.querySelectorAll('.dash-sidebar__link[data-view="detail"]').forEach(el => el.closest('li').style.display = '');
1686
+ } else {
1687
+ showHomeView();
1688
+ }
1689
+ });
2622
1690
 
2623
- // ── Sidebar Navigation ────────────────────────────────
2624
1691
  initSidebarNav();
2625
1692
  }
2626
-
2627
1693
  function initSidebarNav() {
2628
- const links = document.querySelectorAll('.dash-sidebar__link');
2629
- const sections = document.querySelectorAll('[data-nav-section]');
1694
+ const links = document.querySelectorAll(".dash-sidebar__link");
1695
+ const sections = document.querySelectorAll("[data-nav-section]");
2630
1696
 
2631
- // Intersection observer for active state
2632
1697
  const observer = new IntersectionObserver(
2633
1698
  (entries) => {
2634
1699
  entries.forEach((entry) => {
@@ -2636,55 +1701,50 @@ try {
2636
1701
  const id = entry.target.id;
2637
1702
  links.forEach((link) => {
2638
1703
  link.classList.toggle(
2639
- 'dash-sidebar__link--active',
1704
+ "dash-sidebar__link--active",
2640
1705
  link.dataset.section === id
2641
1706
  );
2642
1707
  });
2643
1708
  }
2644
1709
  });
2645
1710
  },
2646
- { rootMargin: '-20% 0px -70% 0px', threshold: 0 }
1711
+ { rootMargin: "-20% 0px -70% 0px", threshold: 0 }
2647
1712
  );
2648
1713
 
2649
1714
  sections.forEach((s) => observer.observe(s));
2650
1715
 
2651
- // Convoy-detail sections that start hidden and need a convoy selected first
2652
- const CONVOY_DETAIL_SECTIONS = ['tasks-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'];
1716
+ const CONVOY_DETAIL_SECTIONS = ["tasks-section", "pipeline-section", "agent-section", "tier-section", "model-section", "quality-section", "reliability-section", "drift-section", "outputs-section", "execution-section", "event-timeline-section", "panel-section", "reviews-section"];
2653
1717
 
2654
- // Smooth scroll on click
2655
1718
  links.forEach((link) => {
2656
- link.addEventListener('click', async (e) => {
1719
+ link.addEventListener("click", async (e) => {
2657
1720
  e.preventDefault();
2658
1721
  const sectionId = link.dataset.section;
2659
1722
 
2660
1723
  if (CONVOY_DETAIL_SECTIONS.includes(sectionId)) {
2661
- // Auto-select and load convoy detail if none selected yet
2662
- const convoySelectEl = document.getElementById('convoy-select');
2663
- if (convoySelectEl && !convoySelectEl.value && convoySelectEl.options.length > 1) {
2664
- convoySelectEl.value = convoySelectEl.options[1].value;
2665
- await loadConvoyDetail(convoySelectEl.value);
2666
- }
2667
- } else if (sectionId === 'convoy-section') {
2668
- const convoyFilterEl = document.getElementById('filter-convoy');
2669
- if (convoyFilterEl && !convoyFilterEl.value && convoyFilterEl.options.length > 1) {
2670
- convoyFilterEl.value = convoyFilterEl.options[1].value;
2671
- applyFilters();
2672
- }
2673
- } else if (sectionId === 'convoy-pipeline-section') {
2674
- const pipelineSelect = document.getElementById('filter-pipeline');
2675
- if (pipelineSelect && !pipelineSelect.value && pipelineSelect.options.length > 1) {
2676
- pipelineSelect.value = pipelineSelect.options[1].value;
2677
- applyFilters();
1724
+ const convoyList = window.__DASHBOARD_DATA__?.convoyList ?? [];
1725
+ if (!window.__SELECTED_CONVOY__ && convoyList.length > 0) {
1726
+ showConvoyDetailView(convoyList[0].id, convoyList[0].name || convoyList[0].id);
2678
1727
  }
2679
1728
  }
2680
1729
 
2681
1730
  const target = document.getElementById(sectionId);
2682
1731
  if (target) {
2683
- target.scrollIntoView({ behavior: 'smooth', block: 'start' });
1732
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
2684
1733
  }
2685
1734
  });
2686
1735
  });
1736
+
1737
+ // Set initial nav visibility based on current view
1738
+ const initialParams = new URLSearchParams(window.location.search);
1739
+ if (initialParams.get('convoy')) {
1740
+ document.querySelectorAll('.dash-sidebar__link[data-view="home"]').forEach(el => el.closest('li').style.display = 'none');
1741
+ document.querySelectorAll('.dash-sidebar__link[data-view="detail"]').forEach(el => el.closest('li').style.display = '');
1742
+ } else {
1743
+ document.querySelectorAll('.dash-sidebar__link[data-view="detail"]').forEach(el => el.closest('li').style.display = 'none');
1744
+ document.querySelectorAll('.dash-sidebar__link[data-view="home"]').forEach(el => el.closest('li').style.display = '');
1745
+ }
2687
1746
  }
2688
1747
 
1748
+
2689
1749
  main();
2690
1750
  </script>