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