opencastle 0.16.0 → 0.18.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 (65) hide show
  1. package/dist/cli/convoy/engine.d.ts.map +1 -1
  2. package/dist/cli/convoy/engine.js +47 -11
  3. package/dist/cli/convoy/engine.js.map +1 -1
  4. package/dist/cli/convoy/engine.test.js +104 -1
  5. package/dist/cli/convoy/engine.test.js.map +1 -1
  6. package/dist/cli/convoy/export.d.ts +3 -0
  7. package/dist/cli/convoy/export.d.ts.map +1 -0
  8. package/dist/cli/convoy/export.js +46 -0
  9. package/dist/cli/convoy/export.js.map +1 -0
  10. package/dist/cli/convoy/export.test.d.ts +2 -0
  11. package/dist/cli/convoy/export.test.d.ts.map +1 -0
  12. package/dist/cli/convoy/export.test.js +157 -0
  13. package/dist/cli/convoy/export.test.js.map +1 -0
  14. package/dist/cli/convoy/health.test.js +1 -0
  15. package/dist/cli/convoy/health.test.js.map +1 -1
  16. package/dist/cli/convoy/store.d.ts.map +1 -1
  17. package/dist/cli/convoy/store.js +8 -3
  18. package/dist/cli/convoy/store.js.map +1 -1
  19. package/dist/cli/convoy/store.test.js +83 -3
  20. package/dist/cli/convoy/store.test.js.map +1 -1
  21. package/dist/cli/convoy/types.d.ts +1 -0
  22. package/dist/cli/convoy/types.d.ts.map +1 -1
  23. package/dist/cli/dashboard.d.ts +14 -0
  24. package/dist/cli/dashboard.d.ts.map +1 -1
  25. package/dist/cli/dashboard.js +73 -36
  26. package/dist/cli/dashboard.js.map +1 -1
  27. package/dist/cli/run/adapters/index.d.ts.map +1 -1
  28. package/dist/cli/run/adapters/index.js +2 -1
  29. package/dist/cli/run/adapters/index.js.map +1 -1
  30. package/dist/cli/run/adapters/opencode.d.ts +16 -0
  31. package/dist/cli/run/adapters/opencode.d.ts.map +1 -0
  32. package/dist/cli/run/adapters/opencode.js +75 -0
  33. package/dist/cli/run/adapters/opencode.js.map +1 -0
  34. package/dist/cli/run/schema.d.ts.map +1 -1
  35. package/dist/cli/run/schema.js +11 -0
  36. package/dist/cli/run/schema.js.map +1 -1
  37. package/dist/cli/run/schema.test.js +44 -0
  38. package/dist/cli/run/schema.test.js.map +1 -1
  39. package/dist/cli/run.d.ts +1 -1
  40. package/dist/cli/run.d.ts.map +1 -1
  41. package/dist/cli/run.js +18 -1
  42. package/dist/cli/run.js.map +1 -1
  43. package/dist/cli/types.d.ts +3 -0
  44. package/dist/cli/types.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/cli/convoy/engine.test.ts +126 -1
  47. package/src/cli/convoy/engine.ts +39 -9
  48. package/src/cli/convoy/export.test.ts +190 -0
  49. package/src/cli/convoy/export.ts +52 -0
  50. package/src/cli/convoy/health.test.ts +1 -0
  51. package/src/cli/convoy/store.test.ts +89 -3
  52. package/src/cli/convoy/store.ts +8 -3
  53. package/src/cli/convoy/types.ts +1 -0
  54. package/src/cli/dashboard.ts +94 -42
  55. package/src/cli/run/adapters/index.ts +2 -1
  56. package/src/cli/run/adapters/opencode.ts +88 -0
  57. package/src/cli/run/schema.test.ts +50 -0
  58. package/src/cli/run/schema.ts +13 -0
  59. package/src/cli/run.ts +19 -1
  60. package/src/cli/types.ts +3 -0
  61. package/src/dashboard/dist/_astro/{index.Bnq19_1M.css → index.DyyaCW8L.css} +1 -1
  62. package/src/dashboard/dist/index.html +145 -6
  63. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  64. package/src/dashboard/src/pages/index.astro +160 -4
  65. package/src/dashboard/src/styles/dashboard.css +60 -0
@@ -1,6 +1,6 @@
1
- <!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Observability Dashboard — OpenCastle</title><meta name="description" content="Real-time observability for OpenCastle multi-agent orchestration — sessions, delegations, model tiers, and quality gates."><meta name="theme-color" content="#0a0a0f"><link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png"><link rel="stylesheet" href="/_astro/index.Bnq19_1M.css"></head> <body> <header class="dash-header"> <div class="dash-header__inner"> <div class="dash-header__brand"> <img class="dash-header__icon" src="/icon-192.png" alt="OpenCastle" width="32" height="32"> <h1 class="dash-header__title">Observability Dashboard</h1> </div> <div class="dash-header__actions"> <button class="dash-btn dash-btn--ghost" id="export-btn" type="button" title="Export data as JSON"> <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"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
1
+ <!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Observability Dashboard — OpenCastle</title><meta name="description" content="Real-time observability for OpenCastle multi-agent orchestration — sessions, delegations, model tiers, and quality gates."><meta name="theme-color" content="#0a0a0f"><link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png"><link rel="stylesheet" href="/_astro/index.DyyaCW8L.css"></head> <body> <header class="dash-header"> <div class="dash-header__inner"> <div class="dash-header__brand"> <img class="dash-header__icon" src="/icon-192.png" alt="OpenCastle" width="32" height="32"> <h1 class="dash-header__title">Observability Dashboard</h1> </div> <div class="dash-header__actions"> <button class="dash-btn dash-btn--ghost" id="export-btn" type="button" title="Export data as JSON"> <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"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
2
2
  Export
3
- </button> </div> </div> </header> <div class="dash-layout"> <!-- Sidebar Navigation --> <nav class="dash-sidebar" id="dash-sidebar"> <ul class="dash-sidebar__list"> <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#kpi-row" data-section="kpi-row">Overview</a></li> <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section">Pipeline</a></li> <li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section">Agents</a></li> <li><a class="dash-sidebar__link" href="#tier-section" data-section="tier-section">Tiers</a></li> <li><a class="dash-sidebar__link" href="#delegation-section" data-section="delegation-section">Delegations</a></li> <li><a class="dash-sidebar__link" href="#timeline-section" data-section="timeline-section">Timeline</a></li> <li><a class="dash-sidebar__link" href="#model-section" data-section="model-section">Models</a></li> <li><a class="dash-sidebar__link" href="#execution-section" data-section="execution-section">Exec Log</a></li> <li><a class="dash-sidebar__link" href="#panel-section" data-section="panel-section">Panels</a></li> <li><a class="dash-sidebar__link" href="#reviews-section" data-section="reviews-section">Reviews</a></li> <li><a class="dash-sidebar__link" href="#sessions-section" data-section="sessions-section">Sessions</a></li> </ul> </nav> <main class="dash-main"> <!-- Filter Bar --> <div class="filter-bar" id="filter-bar"> <div class="filter-group"> <label class="filter-label" for="filter-date-from">From</label> <input class="filter-input" type="date" id="filter-date-from"> </div> <div class="filter-group"> <label class="filter-label" for="filter-date-to">To</label> <input class="filter-input" type="date" id="filter-date-to"> </div> <div class="filter-group"> <label class="filter-label" for="filter-agent">Agent</label> <select class="filter-select" id="filter-agent"> <option value="">All agents</option> </select> </div> <div class="filter-group"> <label class="filter-label" for="filter-outcome">Outcome</label> <select class="filter-select" id="filter-outcome"> <option value="">All outcomes</option> <option value="success">Success</option> <option value="partial">Partial</option> <option value="failed">Failed</option> </select> </div> <button class="dash-btn dash-btn--ghost filter-reset" id="filter-reset" type="button">Reset</button> </div> <!-- KPI Row --> <section class="kpi-row" id="kpi-row" data-nav-section> <div class="kpi-card" id="kpi-sessions"> <span class="kpi-card__label">Total Sessions</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-success"> <span class="kpi-card__label">Success Rate</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-delegations"> <span class="kpi-card__label">Total Delegations</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-duration"> <span class="kpi-card__label">Avg Duration</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-retries"> <span class="kpi-card__label">Total Retries</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-lessons"> <span class="kpi-card__label">Lessons Added</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> </section> <!-- Pipeline View (Steroids-inspired) --> <section class="chart-card" id="pipeline-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Task Pipeline</h2> <p class="chart-card__desc">Delegation flow across execution phases</p> </div> <div class="chart-card__body" id="pipeline-view"> <div class="loading-skeleton"></div> </div> </section> <!-- Charts Row 1 --> <div class="charts-row" id="agent-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Sessions by Agent</h2> <p class="chart-card__desc">Stacked by outcome</p> </div> <div class="chart-card__body" id="agent-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card" id="tier-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Tier Distribution</h2> <p class="chart-card__desc">Delegation model tiers</p> </div> <div class="chart-card__body" id="tier-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Charts Row: Delegation Insights --> <div class="charts-row" id="delegation-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Delegation Mechanism</h2> <p class="chart-card__desc">Sub-agent vs background split</p> </div> <div class="chart-card__body" id="mechanism-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Delegation Outcomes</h2> <p class="chart-card__desc">Success rate by delegation</p> </div> <div class="chart-card__body" id="delegation-outcome-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Charts Row 2 --> <div class="charts-row" id="timeline-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Timeline</h2> <p class="chart-card__desc">Sessions and delegations over time</p> </div> <div class="chart-card__body" id="timeline-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card" id="model-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Model Usage</h2> <p class="chart-card__desc">Sessions by model</p> </div> <div class="chart-card__body" id="model-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Execution Log (Duvo-inspired) --> <section class="chart-card" id="execution-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Execution Log</h2> <p class="chart-card__desc">Recent agent activity, step by step</p> </div> <div class="chart-card__body" id="execution-log"> <div class="loading-skeleton"></div> </div> </section> <!-- Panel Results --> <section class="chart-card" id="panel-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Panel Reviews</h2> <p class="chart-card__desc">Quality gate verdicts and fix items</p> </div> <div class="chart-card__body" id="panel-chart"> <div class="loading-skeleton"></div> </div> </section> <!-- Fast Reviews --> <section class="chart-card" id="reviews-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Fast Reviews</h2> <p class="chart-card__desc">Single-reviewer quality gate results</p> </div> <div class="chart-card__body chart-card__body--table" id="reviews-table"> <div class="loading-skeleton"></div> </div> </section> <!-- Sessions Table --> <section class="chart-card" id="sessions-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Recent Sessions</h2> <p class="chart-card__desc">Last 15 sessions by timestamp</p> </div> <div class="chart-card__body chart-card__body--table" id="sessions-table"> <div class="loading-skeleton"></div> </div> </section> </main> </div> </body></html> <script>(function(){const base = "/";
3
+ </button> </div> </div> </header> <div class="dash-layout"> <!-- Sidebar Navigation --> <nav class="dash-sidebar" id="dash-sidebar"> <ul class="dash-sidebar__list"> <li><a class="dash-sidebar__link" href="#convoy-section" data-section="convoy-section">Convoy</a></li> <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#kpi-row" data-section="kpi-row">Overview</a></li> <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section">Pipeline</a></li> <li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section">Agents</a></li> <li><a class="dash-sidebar__link" href="#tier-section" data-section="tier-section">Tiers</a></li> <li><a class="dash-sidebar__link" href="#delegation-section" data-section="delegation-section">Delegations</a></li> <li><a class="dash-sidebar__link" href="#timeline-section" data-section="timeline-section">Timeline</a></li> <li><a class="dash-sidebar__link" href="#model-section" data-section="model-section">Models</a></li> <li><a class="dash-sidebar__link" href="#execution-section" data-section="execution-section">Exec Log</a></li> <li><a class="dash-sidebar__link" href="#panel-section" data-section="panel-section">Panels</a></li> <li><a class="dash-sidebar__link" href="#reviews-section" data-section="reviews-section">Reviews</a></li> <li><a class="dash-sidebar__link" href="#sessions-section" data-section="sessions-section">Sessions</a></li> </ul> </nav> <main class="dash-main"> <!-- Filter Bar --> <div class="filter-bar" id="filter-bar"> <div class="filter-group"> <label class="filter-label" for="filter-date-from">From</label> <input class="filter-input" type="date" id="filter-date-from"> </div> <div class="filter-group"> <label class="filter-label" for="filter-date-to">To</label> <input class="filter-input" type="date" id="filter-date-to"> </div> <div class="filter-group"> <label class="filter-label" for="filter-agent">Agent</label> <select class="filter-select" id="filter-agent"> <option value="">All agents</option> </select> </div> <div class="filter-group"> <label class="filter-label" for="filter-outcome">Outcome</label> <select class="filter-select" id="filter-outcome"> <option value="">All outcomes</option> <option value="success">Success</option> <option value="partial">Partial</option> <option value="failed">Failed</option> </select> </div> <div class="filter-group"> <label class="filter-label" for="filter-convoy">Convoy</label> <select class="filter-select" id="filter-convoy"> <option value="">All convoys</option> </select> </div> <button class="dash-btn dash-btn--ghost filter-reset" id="filter-reset" type="button">Reset</button> </div> <!-- Convoy Status Section --> <section class="chart-card convoy-status" id="convoy-section" data-nav-section style="display:none"> <div class="chart-card__header"> <h2 class="chart-card__title">Convoy Status</h2> <p class="chart-card__desc" id="convoy-desc">Select a convoy to view details</p> </div> <div class="chart-card__body" id="convoy-body"></div> </section> <!-- KPI Row --> <section class="kpi-row" id="kpi-row" data-nav-section> <div class="kpi-card" id="kpi-sessions"> <span class="kpi-card__label">Total Sessions</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-success"> <span class="kpi-card__label">Success Rate</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-delegations"> <span class="kpi-card__label">Total Delegations</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-duration"> <span class="kpi-card__label">Avg Duration</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-retries"> <span class="kpi-card__label">Total Retries</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> <div class="kpi-card" id="kpi-lessons"> <span class="kpi-card__label">Lessons Added</span> <span class="kpi-card__value">&mdash;</span> <span class="kpi-card__sub"></span> </div> </section> <!-- Pipeline View (Steroids-inspired) --> <section class="chart-card" id="pipeline-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Task Pipeline</h2> <p class="chart-card__desc">Delegation flow across execution phases</p> </div> <div class="chart-card__body" id="pipeline-view"> <div class="loading-skeleton"></div> </div> </section> <!-- Charts Row 1 --> <div class="charts-row" id="agent-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Sessions by Agent</h2> <p class="chart-card__desc">Stacked by outcome</p> </div> <div class="chart-card__body" id="agent-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card" id="tier-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Tier Distribution</h2> <p class="chart-card__desc">Delegation model tiers</p> </div> <div class="chart-card__body" id="tier-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Charts Row: Delegation Insights --> <div class="charts-row" id="delegation-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Delegation Mechanism</h2> <p class="chart-card__desc">Sub-agent vs background split</p> </div> <div class="chart-card__body" id="mechanism-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Delegation Outcomes</h2> <p class="chart-card__desc">Success rate by delegation</p> </div> <div class="chart-card__body" id="delegation-outcome-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Charts Row 2 --> <div class="charts-row" id="timeline-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Timeline</h2> <p class="chart-card__desc">Sessions and delegations over time</p> </div> <div class="chart-card__body" id="timeline-chart"> <div class="loading-skeleton"></div> </div> </section> <section class="chart-card" id="model-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Model Usage</h2> <p class="chart-card__desc">Sessions by model</p> </div> <div class="chart-card__body" id="model-chart"> <div class="loading-skeleton"></div> </div> </section> </div> <!-- Execution Log (Duvo-inspired) --> <section class="chart-card" id="execution-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Execution Log</h2> <p class="chart-card__desc">Recent agent activity, step by step</p> </div> <div class="chart-card__body" id="execution-log"> <div class="loading-skeleton"></div> </div> </section> <!-- Panel Results --> <section class="chart-card" id="panel-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Panel Reviews</h2> <p class="chart-card__desc">Quality gate verdicts and fix items</p> </div> <div class="chart-card__body" id="panel-chart"> <div class="loading-skeleton"></div> </div> </section> <!-- Fast Reviews --> <section class="chart-card" id="reviews-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Fast Reviews</h2> <p class="chart-card__desc">Single-reviewer quality gate results</p> </div> <div class="chart-card__body chart-card__body--table" id="reviews-table"> <div class="loading-skeleton"></div> </div> </section> <!-- Sessions Table --> <section class="chart-card" id="sessions-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Recent Sessions</h2> <p class="chart-card__desc">Last 15 sessions by timestamp</p> </div> <div class="chart-card__body chart-card__body--table" id="sessions-table"> <div class="loading-skeleton"></div> </div> </section> </main> </div> </body></html> <script>(function(){const base = "/";
4
4
 
5
5
  // ── Data Loading ──────────────────────────────────────────
6
6
 
@@ -928,12 +928,14 @@ Export
928
928
  let rawDelegations = [];
929
929
  let rawPanels = [];
930
930
  let rawReviews = [];
931
+ let rawConvoys = [];
931
932
 
932
933
  function applyFilters() {
933
934
  const dateFrom = document.getElementById('filter-date-from').value;
934
935
  const dateTo = document.getElementById('filter-date-to').value;
935
936
  const agentFilter = document.getElementById('filter-agent').value;
936
937
  const outcomeFilter = document.getElementById('filter-outcome').value;
938
+ const convoyFilter = document.getElementById('filter-convoy').value;
937
939
 
938
940
  function matchDate(ts) {
939
941
  const date = ts.slice(0, 10);
@@ -942,27 +944,43 @@ Export
942
944
  return true;
943
945
  }
944
946
 
945
- const sessions = rawSessions.filter((s) => {
947
+ let sessions = rawSessions.filter((s) => {
946
948
  if (!matchDate(s.timestamp)) return false;
947
949
  if (agentFilter && s.agent !== agentFilter) return false;
948
950
  if (outcomeFilter && s.outcome !== outcomeFilter) return false;
949
951
  return true;
950
952
  });
951
953
 
952
- const delegations = rawDelegations.filter((d) => {
954
+ let delegations = rawDelegations.filter((d) => {
953
955
  if (!matchDate(d.timestamp)) return false;
954
956
  if (agentFilter && d.agent !== agentFilter) return false;
955
957
  if (outcomeFilter && d.outcome !== outcomeFilter) return false;
956
958
  return true;
957
959
  });
958
960
 
959
- const panels = rawPanels.filter((p) => matchDate(p.timestamp));
960
- const reviews = rawReviews.filter((r) => {
961
+ let panels = rawPanels.filter((p) => matchDate(p.timestamp));
962
+ let reviews = rawReviews.filter((r) => {
961
963
  if (!matchDate(r.timestamp)) return false;
962
964
  if (agentFilter && r.agent !== agentFilter) return false;
963
965
  return true;
964
966
  });
965
967
 
968
+ if (convoyFilter) {
969
+ sessions = sessions.filter((s) => s.convoy_id === convoyFilter);
970
+ delegations = delegations.filter((d) => d.convoy_id === convoyFilter);
971
+ panels = panels.filter((p) => p.convoy_id === convoyFilter);
972
+ reviews = reviews.filter((r) => r.convoy_id === convoyFilter);
973
+ }
974
+
975
+ const convoySection = document.getElementById('convoy-section');
976
+ if (convoySection) {
977
+ convoySection.style.display = convoyFilter ? '' : 'none';
978
+ if (convoyFilter) {
979
+ const convoy = rawConvoys.find((c) => c.id === convoyFilter);
980
+ renderConvoyStatus(convoy);
981
+ }
982
+ }
983
+
966
984
  renderAll(sessions, delegations, panels, reviews);
967
985
  }
968
986
 
@@ -1074,8 +1092,80 @@ Export
1074
1092
  URL.revokeObjectURL(url);
1075
1093
  }
1076
1094
 
1095
+ function populateConvoyFilter(convoys) {
1096
+ const select = document.getElementById('filter-convoy');
1097
+ if (!select) return;
1098
+ while (select.options.length > 1) select.remove(1);
1099
+ const sorted = convoys.slice().sort((a, b) => b.created_at.localeCompare(a.created_at));
1100
+ sorted.forEach((c) => {
1101
+ const opt = document.createElement('option');
1102
+ opt.value = c.id;
1103
+ opt.textContent = c.name + ' (' + c.status + ')';
1104
+ select.appendChild(opt);
1105
+ });
1106
+ }
1107
+
1108
+ function renderConvoyStatus(convoy) {
1109
+ const descEl = document.getElementById('convoy-desc');
1110
+ const bodyEl = document.getElementById('convoy-body');
1111
+ if (!descEl || !bodyEl) return;
1112
+
1113
+ if (!convoy) {
1114
+ bodyEl.innerHTML = emptyStateHtml('pipeline', 'Convoy not found', 'No matching convoy data available.');
1115
+ return;
1116
+ }
1117
+
1118
+ descEl.textContent = convoy.name + ' — ' + (convoy.branch || 'no branch');
1119
+
1120
+ const s = convoy.summary || {};
1121
+ const total = s.total || (convoy.tasks ? convoy.tasks.length : 0);
1122
+ const done = s.done || 0;
1123
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0;
1124
+
1125
+ const statusClass = convoy.status === 'done' ? 'success'
1126
+ : (convoy.status === 'failed' || convoy.status === 'gate-failed') ? 'failed'
1127
+ : convoy.status === 'running' ? 'partial' : '';
1128
+
1129
+ let html = '';
1130
+ html += '<div class="convoy-overview">';
1131
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Status</span><span class="outcome-badge outcome-badge--' + statusClass + '">' + escapeHtml(convoy.status) + '</span></div>';
1132
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Branch</span><span class="convoy-stat__value">' + escapeHtml(convoy.branch || '\u2014') + '</span></div>';
1133
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Tasks</span><span class="convoy-stat__value">' + done + '/' + total + '</span></div>';
1134
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Events</span><span class="convoy-stat__value">' + (convoy.events_count || 0) + '</span></div>';
1135
+ 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>';
1136
+ html += '</div>';
1137
+
1138
+ html += '<div class="convoy-progress">';
1139
+ html += '<div class="convoy-progress__bar"><div class="convoy-progress__fill" style="width:' + pct + '%"></div></div>';
1140
+ html += '<span class="convoy-progress__label">' + pct + '% complete</span>';
1141
+ html += '</div>';
1142
+
1143
+ if (convoy.tasks && convoy.tasks.length > 0) {
1144
+ html += '<table class="sessions-table convoy-tasks">';
1145
+ html += '<thead><tr><th>Task</th><th>Phase</th><th>Agent</th><th>Adapter</th><th>Status</th><th>Retries</th></tr></thead>';
1146
+ html += '<tbody>';
1147
+ convoy.tasks.forEach(function(t) {
1148
+ const tStatus = t.status === 'done' ? 'success'
1149
+ : (t.status === 'failed' || t.status === 'timed-out') ? 'failed'
1150
+ : t.status === 'running' ? 'partial' : '';
1151
+ html += '<tr>';
1152
+ html += '<td>' + escapeHtml(t.id) + '</td>';
1153
+ html += '<td class="td-num">' + t.phase + '</td>';
1154
+ html += '<td class="td-agent">' + escapeHtml(t.agent) + '</td>';
1155
+ html += '<td>' + escapeHtml(t.adapter || '\u2014') + '</td>';
1156
+ html += '<td><span class="outcome-badge outcome-badge--' + tStatus + '">' + escapeHtml(t.status) + '</span></td>';
1157
+ html += '<td class="td-num">' + (t.retries || 0) + '</td>';
1158
+ html += '</tr>';
1159
+ });
1160
+ html += '</tbody></table>';
1161
+ }
1162
+
1163
+ bodyEl.innerHTML = html;
1164
+ }
1165
+
1077
1166
  async function main() {
1078
1167
  const events = await loadNdjson(base + 'data/events.ndjson');
1168
+ const convoys = await loadNdjson(base + 'data/convoys.ndjson');
1079
1169
 
1080
1170
  const sessions = events.filter((e) => e.type === 'session');
1081
1171
  const delegations = events.filter((e) => e.type === 'delegation');
@@ -1086,23 +1176,72 @@ Export
1086
1176
  rawDelegations = delegations;
1087
1177
  rawPanels = panels;
1088
1178
  rawReviews = reviews;
1179
+ rawConvoys = convoys;
1089
1180
 
1090
1181
  populateAgentFilter(sessions, delegations, reviews);
1182
+ populateConvoyFilter(convoys);
1183
+
1184
+ // ── Read URL params ───────────────────────────────────
1185
+ const urlParams = new URLSearchParams(window.location.search);
1186
+ const convoyParam = urlParams.get('convoy');
1187
+ if (convoyParam === 'active') {
1188
+ const running = rawConvoys.find((c) => c.status === 'running');
1189
+ const latest = rawConvoys.slice().sort((a, b) => b.created_at.localeCompare(a.created_at))[0];
1190
+ const target = running || latest;
1191
+ if (target) {
1192
+ const sel = document.getElementById('filter-convoy');
1193
+ if (sel) sel.value = target.id;
1194
+ }
1195
+ } else if (convoyParam) {
1196
+ const sel = document.getElementById('filter-convoy');
1197
+ if (sel) sel.value = convoyParam;
1198
+ }
1199
+
1091
1200
  renderAll(sessions, delegations, panels, reviews);
1092
1201
 
1202
+ // Apply convoy param after initial render (shows convoy section if needed)
1203
+ if (convoyParam) applyFilters();
1204
+
1093
1205
  // ── Filter event listeners ────────────────────────────
1094
1206
  document.getElementById('filter-date-from')?.addEventListener('change', applyFilters);
1095
1207
  document.getElementById('filter-date-to')?.addEventListener('change', applyFilters);
1096
1208
  document.getElementById('filter-agent')?.addEventListener('change', applyFilters);
1097
1209
  document.getElementById('filter-outcome')?.addEventListener('change', applyFilters);
1210
+ document.getElementById('filter-convoy')?.addEventListener('change', applyFilters);
1098
1211
  document.getElementById('filter-reset')?.addEventListener('click', () => {
1099
1212
  document.getElementById('filter-date-from').value = '';
1100
1213
  document.getElementById('filter-date-to').value = '';
1101
1214
  document.getElementById('filter-agent').value = '';
1102
1215
  document.getElementById('filter-outcome').value = '';
1216
+ document.getElementById('filter-convoy').value = '';
1103
1217
  applyFilters();
1104
1218
  });
1105
1219
 
1220
+ // ── Auto-refresh for live convoy monitoring ───────────
1221
+ let refreshInterval = null;
1222
+ function startAutoRefresh() {
1223
+ if (refreshInterval) return;
1224
+ refreshInterval = setInterval(async () => {
1225
+ const freshEvents = await loadNdjson(base + 'data/events.ndjson');
1226
+ const freshConvoys = await loadNdjson(base + 'data/convoys.ndjson');
1227
+ rawSessions = freshEvents.filter((e) => e.type === 'session');
1228
+ rawDelegations = freshEvents.filter((e) => e.type === 'delegation');
1229
+ rawPanels = freshEvents.filter((e) => e.type === 'panel');
1230
+ rawReviews = freshEvents.filter((e) => e.type === 'review');
1231
+ rawConvoys = freshConvoys;
1232
+ const currentValue = document.getElementById('filter-convoy')?.value;
1233
+ populateConvoyFilter(freshConvoys);
1234
+ const sel = document.getElementById('filter-convoy');
1235
+ if (sel && currentValue) sel.value = currentValue;
1236
+ applyFilters();
1237
+ }, 5000);
1238
+ }
1239
+
1240
+ const selectedConvoy = rawConvoys.find((c) => c.id === document.getElementById('filter-convoy')?.value);
1241
+ if (convoyParam === 'active' || (selectedConvoy && selectedConvoy.status === 'running')) {
1242
+ startAutoRefresh();
1243
+ }
1244
+
1106
1245
  // ── Export button ─────────────────────────────────────
1107
1246
  document.getElementById('export-btn')?.addEventListener('click', exportData);
1108
1247
 
@@ -1,25 +1,25 @@
1
1
  {
2
- "hash": "f5f05037",
2
+ "hash": "2da8e910",
3
3
  "configHash": "30f8ea04",
4
- "lockfileHash": "99d70434",
5
- "browserHash": "d43a2a07",
4
+ "lockfileHash": "d05fc548",
5
+ "browserHash": "f317e71b",
6
6
  "optimized": {
7
7
  "astro > cssesc": {
8
8
  "src": "../../../../../node_modules/cssesc/cssesc.js",
9
9
  "file": "astro___cssesc.js",
10
- "fileHash": "29d5277e",
10
+ "fileHash": "28561a14",
11
11
  "needsInterop": true
12
12
  },
13
13
  "astro > aria-query": {
14
14
  "src": "../../../../../node_modules/aria-query/lib/index.js",
15
15
  "file": "astro___aria-query.js",
16
- "fileHash": "4ca620a4",
16
+ "fileHash": "8f623d19",
17
17
  "needsInterop": true
18
18
  },
19
19
  "astro > axobject-query": {
20
20
  "src": "../../../../../node_modules/axobject-query/lib/index.js",
21
21
  "file": "astro___axobject-query.js",
22
- "fileHash": "4072a265",
22
+ "fileHash": "31fe5731",
23
23
  "needsInterop": true
24
24
  }
25
25
  },
@@ -26,6 +26,7 @@ const base = import.meta.env.BASE_URL;
26
26
  <!-- Sidebar Navigation -->
27
27
  <nav class="dash-sidebar" id="dash-sidebar">
28
28
  <ul class="dash-sidebar__list">
29
+ <li><a class="dash-sidebar__link" href="#convoy-section" data-section="convoy-section">Convoy</a></li>
29
30
  <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#kpi-row" data-section="kpi-row">Overview</a></li>
30
31
  <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section">Pipeline</a></li>
31
32
  <li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section">Agents</a></li>
@@ -66,9 +67,25 @@ const base = import.meta.env.BASE_URL;
66
67
  <option value="failed">Failed</option>
67
68
  </select>
68
69
  </div>
70
+ <div class="filter-group">
71
+ <label class="filter-label" for="filter-convoy">Convoy</label>
72
+ <select class="filter-select" id="filter-convoy">
73
+ <option value="">All convoys</option>
74
+ </select>
75
+ </div>
69
76
  <button class="dash-btn dash-btn--ghost filter-reset" id="filter-reset" type="button">Reset</button>
70
77
  </div>
71
78
 
79
+ <!-- Convoy Status Section -->
80
+ <section class="chart-card convoy-status" id="convoy-section" data-nav-section style="display:none">
81
+ <div class="chart-card__header">
82
+ <h2 class="chart-card__title">Convoy Status</h2>
83
+ <p class="chart-card__desc" id="convoy-desc">Select a convoy to view details</p>
84
+ </div>
85
+ <div class="chart-card__body" id="convoy-body">
86
+ </div>
87
+ </section>
88
+
72
89
  <!-- KPI Row -->
73
90
  <section class="kpi-row" id="kpi-row" data-nav-section>
74
91
  <div class="kpi-card" id="kpi-sessions">
@@ -1154,12 +1171,14 @@ const base = import.meta.env.BASE_URL;
1154
1171
  let rawDelegations = [];
1155
1172
  let rawPanels = [];
1156
1173
  let rawReviews = [];
1174
+ let rawConvoys = [];
1157
1175
 
1158
1176
  function applyFilters() {
1159
1177
  const dateFrom = document.getElementById('filter-date-from').value;
1160
1178
  const dateTo = document.getElementById('filter-date-to').value;
1161
1179
  const agentFilter = document.getElementById('filter-agent').value;
1162
1180
  const outcomeFilter = document.getElementById('filter-outcome').value;
1181
+ const convoyFilter = document.getElementById('filter-convoy').value;
1163
1182
 
1164
1183
  function matchDate(ts) {
1165
1184
  const date = ts.slice(0, 10);
@@ -1168,27 +1187,43 @@ const base = import.meta.env.BASE_URL;
1168
1187
  return true;
1169
1188
  }
1170
1189
 
1171
- const sessions = rawSessions.filter((s) => {
1190
+ let sessions = rawSessions.filter((s) => {
1172
1191
  if (!matchDate(s.timestamp)) return false;
1173
1192
  if (agentFilter && s.agent !== agentFilter) return false;
1174
1193
  if (outcomeFilter && s.outcome !== outcomeFilter) return false;
1175
1194
  return true;
1176
1195
  });
1177
1196
 
1178
- const delegations = rawDelegations.filter((d) => {
1197
+ let delegations = rawDelegations.filter((d) => {
1179
1198
  if (!matchDate(d.timestamp)) return false;
1180
1199
  if (agentFilter && d.agent !== agentFilter) return false;
1181
1200
  if (outcomeFilter && d.outcome !== outcomeFilter) return false;
1182
1201
  return true;
1183
1202
  });
1184
1203
 
1185
- const panels = rawPanels.filter((p) => matchDate(p.timestamp));
1186
- const reviews = rawReviews.filter((r) => {
1204
+ let panels = rawPanels.filter((p) => matchDate(p.timestamp));
1205
+ let reviews = rawReviews.filter((r) => {
1187
1206
  if (!matchDate(r.timestamp)) return false;
1188
1207
  if (agentFilter && r.agent !== agentFilter) return false;
1189
1208
  return true;
1190
1209
  });
1191
1210
 
1211
+ if (convoyFilter) {
1212
+ sessions = sessions.filter((s) => s.convoy_id === convoyFilter);
1213
+ delegations = delegations.filter((d) => d.convoy_id === convoyFilter);
1214
+ panels = panels.filter((p) => p.convoy_id === convoyFilter);
1215
+ reviews = reviews.filter((r) => r.convoy_id === convoyFilter);
1216
+ }
1217
+
1218
+ const convoySection = document.getElementById('convoy-section');
1219
+ if (convoySection) {
1220
+ convoySection.style.display = convoyFilter ? '' : 'none';
1221
+ if (convoyFilter) {
1222
+ const convoy = rawConvoys.find((c) => c.id === convoyFilter);
1223
+ renderConvoyStatus(convoy);
1224
+ }
1225
+ }
1226
+
1192
1227
  renderAll(sessions, delegations, panels, reviews);
1193
1228
  }
1194
1229
 
@@ -1300,8 +1335,80 @@ const base = import.meta.env.BASE_URL;
1300
1335
  URL.revokeObjectURL(url);
1301
1336
  }
1302
1337
 
1338
+ function populateConvoyFilter(convoys) {
1339
+ const select = document.getElementById('filter-convoy');
1340
+ if (!select) return;
1341
+ while (select.options.length > 1) select.remove(1);
1342
+ const sorted = convoys.slice().sort((a, b) => b.created_at.localeCompare(a.created_at));
1343
+ sorted.forEach((c) => {
1344
+ const opt = document.createElement('option');
1345
+ opt.value = c.id;
1346
+ opt.textContent = c.name + ' (' + c.status + ')';
1347
+ select.appendChild(opt);
1348
+ });
1349
+ }
1350
+
1351
+ function renderConvoyStatus(convoy) {
1352
+ const descEl = document.getElementById('convoy-desc');
1353
+ const bodyEl = document.getElementById('convoy-body');
1354
+ if (!descEl || !bodyEl) return;
1355
+
1356
+ if (!convoy) {
1357
+ bodyEl.innerHTML = emptyStateHtml('pipeline', 'Convoy not found', 'No matching convoy data available.');
1358
+ return;
1359
+ }
1360
+
1361
+ descEl.textContent = convoy.name + ' — ' + (convoy.branch || 'no branch');
1362
+
1363
+ const s = convoy.summary || {};
1364
+ const total = s.total || (convoy.tasks ? convoy.tasks.length : 0);
1365
+ const done = s.done || 0;
1366
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0;
1367
+
1368
+ const statusClass = convoy.status === 'done' ? 'success'
1369
+ : (convoy.status === 'failed' || convoy.status === 'gate-failed') ? 'failed'
1370
+ : convoy.status === 'running' ? 'partial' : '';
1371
+
1372
+ let html = '';
1373
+ html += '<div class="convoy-overview">';
1374
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Status</span><span class="outcome-badge outcome-badge--' + statusClass + '">' + escapeHtml(convoy.status) + '</span></div>';
1375
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Branch</span><span class="convoy-stat__value">' + escapeHtml(convoy.branch || '\u2014') + '</span></div>';
1376
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Tasks</span><span class="convoy-stat__value">' + done + '/' + total + '</span></div>';
1377
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Events</span><span class="convoy-stat__value">' + (convoy.events_count || 0) + '</span></div>';
1378
+ 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>';
1379
+ html += '</div>';
1380
+
1381
+ html += '<div class="convoy-progress">';
1382
+ html += '<div class="convoy-progress__bar"><div class="convoy-progress__fill" style="width:' + pct + '%"></div></div>';
1383
+ html += '<span class="convoy-progress__label">' + pct + '% complete</span>';
1384
+ html += '</div>';
1385
+
1386
+ if (convoy.tasks && convoy.tasks.length > 0) {
1387
+ html += '<table class="sessions-table convoy-tasks">';
1388
+ html += '<thead><tr><th>Task</th><th>Phase</th><th>Agent</th><th>Adapter</th><th>Status</th><th>Retries</th></tr></thead>';
1389
+ html += '<tbody>';
1390
+ convoy.tasks.forEach(function(t) {
1391
+ const tStatus = t.status === 'done' ? 'success'
1392
+ : (t.status === 'failed' || t.status === 'timed-out') ? 'failed'
1393
+ : t.status === 'running' ? 'partial' : '';
1394
+ html += '<tr>';
1395
+ html += '<td>' + escapeHtml(t.id) + '</td>';
1396
+ html += '<td class="td-num">' + t.phase + '</td>';
1397
+ html += '<td class="td-agent">' + escapeHtml(t.agent) + '</td>';
1398
+ html += '<td>' + escapeHtml(t.adapter || '\u2014') + '</td>';
1399
+ html += '<td><span class="outcome-badge outcome-badge--' + tStatus + '">' + escapeHtml(t.status) + '</span></td>';
1400
+ html += '<td class="td-num">' + (t.retries || 0) + '</td>';
1401
+ html += '</tr>';
1402
+ });
1403
+ html += '</tbody></table>';
1404
+ }
1405
+
1406
+ bodyEl.innerHTML = html;
1407
+ }
1408
+
1303
1409
  async function main() {
1304
1410
  const events = await loadNdjson(base + 'data/events.ndjson');
1411
+ const convoys = await loadNdjson(base + 'data/convoys.ndjson');
1305
1412
 
1306
1413
  const sessions = events.filter((e) => e.type === 'session');
1307
1414
  const delegations = events.filter((e) => e.type === 'delegation');
@@ -1312,23 +1419,72 @@ const base = import.meta.env.BASE_URL;
1312
1419
  rawDelegations = delegations;
1313
1420
  rawPanels = panels;
1314
1421
  rawReviews = reviews;
1422
+ rawConvoys = convoys;
1315
1423
 
1316
1424
  populateAgentFilter(sessions, delegations, reviews);
1425
+ populateConvoyFilter(convoys);
1426
+
1427
+ // ── Read URL params ───────────────────────────────────
1428
+ const urlParams = new URLSearchParams(window.location.search);
1429
+ const convoyParam = urlParams.get('convoy');
1430
+ if (convoyParam === 'active') {
1431
+ const running = rawConvoys.find((c) => c.status === 'running');
1432
+ const latest = rawConvoys.slice().sort((a, b) => b.created_at.localeCompare(a.created_at))[0];
1433
+ const target = running || latest;
1434
+ if (target) {
1435
+ const sel = document.getElementById('filter-convoy');
1436
+ if (sel) sel.value = target.id;
1437
+ }
1438
+ } else if (convoyParam) {
1439
+ const sel = document.getElementById('filter-convoy');
1440
+ if (sel) sel.value = convoyParam;
1441
+ }
1442
+
1317
1443
  renderAll(sessions, delegations, panels, reviews);
1318
1444
 
1445
+ // Apply convoy param after initial render (shows convoy section if needed)
1446
+ if (convoyParam) applyFilters();
1447
+
1319
1448
  // ── Filter event listeners ────────────────────────────
1320
1449
  document.getElementById('filter-date-from')?.addEventListener('change', applyFilters);
1321
1450
  document.getElementById('filter-date-to')?.addEventListener('change', applyFilters);
1322
1451
  document.getElementById('filter-agent')?.addEventListener('change', applyFilters);
1323
1452
  document.getElementById('filter-outcome')?.addEventListener('change', applyFilters);
1453
+ document.getElementById('filter-convoy')?.addEventListener('change', applyFilters);
1324
1454
  document.getElementById('filter-reset')?.addEventListener('click', () => {
1325
1455
  document.getElementById('filter-date-from').value = '';
1326
1456
  document.getElementById('filter-date-to').value = '';
1327
1457
  document.getElementById('filter-agent').value = '';
1328
1458
  document.getElementById('filter-outcome').value = '';
1459
+ document.getElementById('filter-convoy').value = '';
1329
1460
  applyFilters();
1330
1461
  });
1331
1462
 
1463
+ // ── Auto-refresh for live convoy monitoring ───────────
1464
+ let refreshInterval = null;
1465
+ function startAutoRefresh() {
1466
+ if (refreshInterval) return;
1467
+ refreshInterval = setInterval(async () => {
1468
+ const freshEvents = await loadNdjson(base + 'data/events.ndjson');
1469
+ const freshConvoys = await loadNdjson(base + 'data/convoys.ndjson');
1470
+ rawSessions = freshEvents.filter((e) => e.type === 'session');
1471
+ rawDelegations = freshEvents.filter((e) => e.type === 'delegation');
1472
+ rawPanels = freshEvents.filter((e) => e.type === 'panel');
1473
+ rawReviews = freshEvents.filter((e) => e.type === 'review');
1474
+ rawConvoys = freshConvoys;
1475
+ const currentValue = document.getElementById('filter-convoy')?.value;
1476
+ populateConvoyFilter(freshConvoys);
1477
+ const sel = document.getElementById('filter-convoy');
1478
+ if (sel && currentValue) sel.value = currentValue;
1479
+ applyFilters();
1480
+ }, 5000);
1481
+ }
1482
+
1483
+ const selectedConvoy = rawConvoys.find((c) => c.id === document.getElementById('filter-convoy')?.value);
1484
+ if (convoyParam === 'active' || (selectedConvoy && selectedConvoy.status === 'running')) {
1485
+ startAutoRefresh();
1486
+ }
1487
+
1332
1488
  // ── Export button ─────────────────────────────────────
1333
1489
  document.getElementById('export-btn')?.addEventListener('click', exportData);
1334
1490
 
@@ -1533,3 +1533,63 @@ body {
1533
1533
  padding: 8px 6px;
1534
1534
  }
1535
1535
  }
1536
+
1537
+ /* ---------- Convoy Section ---------- */
1538
+ .convoy-overview {
1539
+ display: flex;
1540
+ flex-wrap: wrap;
1541
+ gap: 24px;
1542
+ margin-bottom: 20px;
1543
+ }
1544
+
1545
+ .convoy-stat {
1546
+ display: flex;
1547
+ flex-direction: column;
1548
+ gap: 4px;
1549
+ }
1550
+
1551
+ .convoy-stat__label {
1552
+ font-size: 0.75rem;
1553
+ color: var(--text-tertiary);
1554
+ text-transform: uppercase;
1555
+ letter-spacing: 0.05em;
1556
+ }
1557
+
1558
+ .convoy-stat__value {
1559
+ font-size: 0.95rem;
1560
+ color: var(--text-primary);
1561
+ }
1562
+
1563
+ /* Progress bar */
1564
+ .convoy-progress {
1565
+ display: flex;
1566
+ align-items: center;
1567
+ gap: 12px;
1568
+ margin-bottom: 20px;
1569
+ }
1570
+
1571
+ .convoy-progress__bar {
1572
+ flex: 1;
1573
+ height: 8px;
1574
+ background: var(--bg-tertiary);
1575
+ border-radius: 4px;
1576
+ overflow: hidden;
1577
+ }
1578
+
1579
+ .convoy-progress__fill {
1580
+ height: 100%;
1581
+ background: var(--gradient-accent);
1582
+ border-radius: 4px;
1583
+ transition: width var(--transition-base);
1584
+ }
1585
+
1586
+ .convoy-progress__label {
1587
+ font-size: 0.8rem;
1588
+ color: var(--text-secondary);
1589
+ white-space: nowrap;
1590
+ }
1591
+
1592
+ /* Task table inherits .sessions-table styles */
1593
+ .convoy-tasks {
1594
+ margin-top: 8px;
1595
+ }