opencastle 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +12 -3
  2. package/dist/cli/convoy/engine.d.ts.map +1 -1
  3. package/dist/cli/convoy/engine.js +1 -10
  4. package/dist/cli/convoy/engine.js.map +1 -1
  5. package/dist/cli/convoy/engine.test.js +1 -0
  6. package/dist/cli/convoy/engine.test.js.map +1 -1
  7. package/dist/cli/convoy/export.d.ts +1 -3
  8. package/dist/cli/convoy/export.d.ts.map +1 -1
  9. package/dist/cli/convoy/export.js +9 -88
  10. package/dist/cli/convoy/export.js.map +1 -1
  11. package/dist/cli/convoy/export.test.js +7 -186
  12. package/dist/cli/convoy/export.test.js.map +1 -1
  13. package/dist/cli/convoy/pipeline.d.ts.map +1 -1
  14. package/dist/cli/convoy/pipeline.js +0 -21
  15. package/dist/cli/convoy/pipeline.js.map +1 -1
  16. package/dist/cli/convoy/pipeline.test.js +0 -21
  17. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  18. package/dist/cli/dashboard.d.ts.map +1 -1
  19. package/dist/cli/dashboard.js +32 -8
  20. package/dist/cli/dashboard.js.map +1 -1
  21. package/dist/cli/destroy.d.ts.map +1 -1
  22. package/dist/cli/destroy.js +13 -0
  23. package/dist/cli/destroy.js.map +1 -1
  24. package/dist/cli/dispute.d.ts +3 -0
  25. package/dist/cli/dispute.d.ts.map +1 -0
  26. package/dist/cli/dispute.js +25 -0
  27. package/dist/cli/dispute.js.map +1 -0
  28. package/dist/cli/doctor.d.ts +1 -1
  29. package/dist/cli/doctor.d.ts.map +1 -1
  30. package/dist/cli/doctor.js +14 -1
  31. package/dist/cli/doctor.js.map +1 -1
  32. package/dist/cli/eject.d.ts.map +1 -1
  33. package/dist/cli/eject.js +14 -0
  34. package/dist/cli/eject.js.map +1 -1
  35. package/dist/cli/init.d.ts.map +1 -1
  36. package/dist/cli/init.js +14 -0
  37. package/dist/cli/init.js.map +1 -1
  38. package/dist/cli/log.d.ts +0 -11
  39. package/dist/cli/log.d.ts.map +1 -1
  40. package/dist/cli/log.js +2 -114
  41. package/dist/cli/log.js.map +1 -1
  42. package/dist/cli/pipeline.d.ts +17 -0
  43. package/dist/cli/pipeline.d.ts.map +1 -1
  44. package/dist/cli/pipeline.js +259 -24
  45. package/dist/cli/pipeline.js.map +1 -1
  46. package/dist/cli/pipeline.test.d.ts +2 -0
  47. package/dist/cli/pipeline.test.d.ts.map +1 -0
  48. package/dist/cli/pipeline.test.js +178 -0
  49. package/dist/cli/pipeline.test.js.map +1 -0
  50. package/dist/cli/run.js +2 -2
  51. package/dist/cli/run.js.map +1 -1
  52. package/dist/cli/update.d.ts.map +1 -1
  53. package/dist/cli/update.js +16 -0
  54. package/dist/cli/update.js.map +1 -1
  55. package/dist/cli/watch.d.ts.map +1 -1
  56. package/dist/cli/watch.js +1 -3
  57. package/dist/cli/watch.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/cli/convoy/engine.test.ts +1 -0
  60. package/src/cli/convoy/engine.ts +1 -4
  61. package/src/cli/convoy/export.test.ts +7 -224
  62. package/src/cli/convoy/export.ts +10 -106
  63. package/src/cli/convoy/pipeline.test.ts +0 -25
  64. package/src/cli/convoy/pipeline.ts +0 -19
  65. package/src/cli/dashboard.ts +33 -8
  66. package/src/cli/destroy.ts +15 -0
  67. package/src/cli/dispute.ts +28 -0
  68. package/src/cli/doctor.ts +16 -1
  69. package/src/cli/eject.ts +16 -0
  70. package/src/cli/init.ts +16 -0
  71. package/src/cli/log.ts +2 -120
  72. package/src/cli/pipeline.test.ts +191 -0
  73. package/src/cli/pipeline.ts +326 -26
  74. package/src/cli/run.ts +2 -2
  75. package/src/cli/update.ts +18 -0
  76. package/src/cli/watch.ts +1 -3
  77. package/src/dashboard/dist/_astro/index.Je1YjU_y.css +1 -0
  78. package/src/dashboard/dist/index.html +537 -1394
  79. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  80. package/src/dashboard/scripts/etl.test.ts +4 -62
  81. package/src/dashboard/scripts/etl.ts +13 -33
  82. package/src/dashboard/src/pages/index.astro +684 -1624
  83. package/src/dashboard/src/styles/dashboard.css +473 -7
  84. package/src/orchestrator/agents/team-lead.agent.md +13 -0
  85. package/src/orchestrator/prompts/brainstorm.prompt.md +1 -0
  86. package/src/orchestrator/prompts/fix-prd.prompt.md +58 -0
  87. package/src/orchestrator/prompts/generate-convoy.prompt.md +30 -0
  88. package/src/orchestrator/prompts/generate-prd.prompt.md +38 -0
  89. package/dist/cli/convoy/log-merge.test.d.ts +0 -2
  90. package/dist/cli/convoy/log-merge.test.d.ts.map +0 -1
  91. package/dist/cli/convoy/log-merge.test.js +0 -147
  92. package/dist/cli/convoy/log-merge.test.js.map +0 -1
  93. package/src/cli/convoy/log-merge.test.ts +0 -179
  94. package/src/dashboard/dist/_astro/index.6L3_HsPT.css +0 -1
@@ -1,10 +1,10 @@
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.6L3_HsPT.css"></head> <body> <script>(function(){const overallStats = {"convoyCounts":{"total":7,"running":1,"done":5,"failed":0,"gate_failed":0},"durationStats":{"avg_sec":2950.0000052154064,"p95_sec":3720.000019669533,"max_sec":5879.9999713897705},"tokenCostTotals":{"total_tokens":226750,"total_cost_usd":22.67},"topAgents":[{"agent":"Developer","task_count":6,"total_tokens":70100},{"agent":"Reviewer","task_count":5,"total_tokens":16250},{"agent":"UI/UX Expert","task_count":3,"total_tokens":32300},{"agent":"Testing Expert","task_count":3,"total_tokens":28800},{"agent":"DevOps Expert","task_count":3,"total_tokens":6400}],"topModels":[{"model":"claude-sonnet-4-6","task_count":21,"total_tokens":177650},{"model":"claude-haiku-3-5","task_count":5,"total_tokens":17700},{"model":"claude-opus-4-6","task_count":4,"total_tokens":40700}],"dlqSummary":{"count":3,"top_failure_types":[{"type":"timeout","count":1},{"type":"review_blocked","count":1},{"type":"gate_failed","count":1}]}};
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.Je1YjU_y.css"></head> <body> <script>(function(){const overallStats = {"convoyCounts":{"total":7,"running":1,"done":5,"failed":0,"gate_failed":0},"durationStats":{"avg_sec":2950.0000052154064,"p95_sec":3720.000019669533,"max_sec":5879.9999713897705},"tokenCostTotals":{"total_tokens":226750,"total_cost_usd":22.67},"topAgents":[{"agent":"Developer","task_count":6,"total_tokens":70100},{"agent":"Reviewer","task_count":5,"total_tokens":16250},{"agent":"UI/UX Expert","task_count":3,"total_tokens":32300},{"agent":"Testing Expert","task_count":3,"total_tokens":28800},{"agent":"DevOps Expert","task_count":3,"total_tokens":6400}],"topModels":[{"model":"claude-sonnet-4-6","task_count":21,"total_tokens":177650},{"model":"claude-haiku-3-5","task_count":5,"total_tokens":17700},{"model":"claude-opus-4-6","task_count":4,"total_tokens":40700}],"dlqSummary":{"count":3,"top_failure_types":[{"type":"timeout","count":1},{"type":"review_blocked","count":1},{"type":"gate_failed","count":1}]}};
2
2
  const convoyList = [{"id":"demo-deploy-ci","name":"CI/CD Pipeline Setup","status":"running","created_at":"2026-03-11T08:00:00.000Z","finished_at":null,"total_tokens":null,"total_cost_usd":null},{"id":"demo-docs-update","name":"Documentation Refresh","status":"done","created_at":"2026-02-28T15:00:00.000Z","finished_at":"2026-02-28T15:22:00.000Z","total_tokens":14800,"total_cost_usd":1.48},{"id":"demo-data-pipeline","name":"Analytics ETL Pipeline","status":"done","created_at":"2026-02-22T13:00:00.000Z","finished_at":"2026-02-22T13:38:00.000Z","total_tokens":28900,"total_cost_usd":2.89},{"id":"demo-perf-opt","name":"Frontend Performance Boost","status":"done","created_at":"2026-02-17T10:00:00.000Z","finished_at":"2026-02-17T11:02:00.000Z","total_tokens":37200,"total_cost_usd":3.72},{"id":"demo-api-v2","name":"REST API v2 Migration","status":"gate_failed","created_at":"2026-02-12T16:00:00.000Z","finished_at":"2026-02-12T16:28:00.000Z","total_tokens":24600,"total_cost_usd":2.46},{"id":"demo-dashboard-ui","name":"Observability Dashboard UI","status":"done","created_at":"2026-02-07T14:00:00.000Z","finished_at":"2026-02-07T15:38:00.000Z","total_tokens":78400,"total_cost_usd":7.84},{"id":"demo-auth-revamp","name":"Auth System Revamp","status":"done","created_at":"2026-02-03T09:00:00.000Z","finished_at":"2026-02-03T09:47:00.000Z","total_tokens":42850,"total_cost_usd":4.28}];
3
3
 
4
4
  window.__DASHBOARD_DATA__ = { overallStats, convoyList }
5
- })();</script> <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"> <div class="convoy-selector"> <label class="convoy-selector__label" for="convoy-select">Convoy</label> <select class="convoy-selector__select" id="convoy-select" aria-label="Select a convoy to view its details"> <option value="">Select convoy…</option> </select> </div> <button class="dash-btn dash-btn--ghost" id="export-btn" type="button" title="Export data as JSON" aria-label="Export dashboard 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>
5
+ })();</script> <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" aria-label="Export dashboard 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>
6
6
  Export
7
- </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="#overall-section" data-section="overall-section" aria-label="Overview section">Overview</a></li> <li><a class="dash-sidebar__link" href="#tasks-section" data-section="tasks-section" aria-label="Tasks section">Tasks</a></li> <li><a class="dash-sidebar__link" href="#quality-section" data-section="quality-section" aria-label="Quality section">Quality</a></li> <li><a class="dash-sidebar__link" href="#reliability-section" data-section="reliability-section" aria-label="Reliability section">Reliability</a></li> <li><a class="dash-sidebar__link" href="#drift-section" data-section="drift-section" aria-label="Drift section">Drift</a></li> <li><a class="dash-sidebar__link" href="#outputs-section" data-section="outputs-section" aria-label="Outputs section">Outputs</a></li> <li><a class="dash-sidebar__link" href="#event-timeline-section" data-section="event-timeline-section" aria-label="Event Log section">Event Log</a></li> <li><a class="dash-sidebar__link" href="#convoy-section" data-section="convoy-section" aria-label="Convoy section">Convoy</a></li> </ul> </nav> <main class="dash-main"> <!-- Overall Stats Section --> <section class="overall-stats" id="overall-section" data-nav-section> <div class="overall-stats__header"> <h2 class="overall-stats__title">Overall Stats</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> </div> <div class="overall-stats__grid"> <div class="overall-kpi" id="overall-total-runs"> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span></span> <span class="overall-kpi__value">&mdash;</span> </div> <div class="overall-kpi" id="overall-running"> <span class="overall-kpi__label">Running Now</span> <span class="overall-kpi__value">&mdash;</span> </div> <div class="overall-kpi" id="overall-success-rate"> <span class="overall-kpi__label">Success Rate</span> <span class="overall-kpi__value">&mdash;</span> </div> <div class="overall-kpi" id="overall-avg-duration"> <span class="overall-kpi__label">Avg Duration</span> <span class="overall-kpi__value">&mdash;</span> </div> <div class="overall-kpi" id="overall-total-tokens"> <span class="overall-kpi__label">Total Tokens</span> <span class="overall-kpi__value">&mdash;</span> </div> <div class="overall-kpi" id="overall-total-cost"> <span class="overall-kpi__label">Total Cost</span> <span class="overall-kpi__value">&mdash;</span> </div> </div> </section> <!-- Selected Convoy Header --> <section class="convoy-detail-header" id="convoy-detail-header"> <div class="convoy-detail-header__top"> <h2 class="convoy-detail-header__name" id="selected-convoy-name">No convoy selected</h2> <span class="status-badge" id="selected-convoy-status" role="status"></span> <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: A convoy is a set of AI tasks working together on one goal." data-tooltip="A convoy is a set of AI tasks working together on one goal."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> </div> <p class="convoy-status-explanation" id="convoy-status-explanation" role="status" aria-live="polite"></p> <div class="convoy-detail-header__meta" id="selected-convoy-meta"> <!-- Populated by JS: branch, timestamps, duration, tokens, cost --> </div> </section> <!-- Tasks Section --> <section class="chart-card" id="tasks-section" data-nav-section style="display:none"> <div class="chart-card__header"> <h2 class="chart-card__title">Tasks</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc" id="tasks-section-desc">Task breakdown for the selected convoy</p> </div> <div class="chart-card__body" id="tasks-section-body"> <div id="task-summary-cards" class="task-summary-cards"></div> <div id="phase-breakdown" class="phase-breakdown"></div> <div id="task-table-wrap" class="task-table-wrap"></div> </div> </section> <!-- Quality Section --> <section class="chart-card" id="quality-section" data-nav-section style="display:none"> <div class="chart-card__header"> <h2 class="chart-card__title">Quality / Review</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Code review results and quality gate outcomes for the selected convoy</p> </div> <div class="chart-card__body"> <div id="quality-cards" class="task-summary-cards"></div> <div id="quality-review-table"></div> </div> </section> <!-- Reliability Section --> <section class="chart-card" id="reliability-section" data-nav-section style="display:none"> <div class="chart-card__header"> <h2 class="chart-card__title">Reliability</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Retry queue and error overview for the selected convoy</p> </div> <div class="chart-card__body"> <div id="reliability-dlq-card"></div> <div id="reliability-dlq-table"></div> <div style="margin-top:24px"> <h3 style="font-size:1rem;font-weight:600;color:var(--text-secondary);margin:0 0 12px">Error Overview</h3> <div id="reliability-error-overview"></div> </div> </div> </section> <!-- Drift Section --> <section class="chart-card" id="drift-section" data-nav-section style="display:none"> <div class="chart-card__header"> <h2 class="chart-card__title">Drift</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Plan adherence and deviation metrics for the selected convoy</p> </div> <div class="chart-card__body"> <div id="drift-cards" class="task-summary-cards"></div> <div id="drift-secret-banner" style="display:none"></div> </div> </section> <!-- Outputs Section --> <section class="chart-card" id="outputs-section" data-nav-section style="display:none"> <div class="chart-card__header"> <h2 class="chart-card__title">Outputs &amp; Artifacts</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Files, summaries, and structured data produced by tasks in this convoy</p> </div> <div class="chart-card__body"> <div id="outputs-cards" class="task-summary-cards"></div> <div id="artifact-table-wrap"></div> </div> </section> <!-- Event Timeline Section --> <section class="chart-card" id="event-timeline-section" data-nav-section style="display:none"> <div class="chart-card__header"> <h2 class="chart-card__title">Event Timeline</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Convoy events in reverse chronological order</p> </div> <div class="chart-card__body"> <div id="event-timeline-filters" class="timeline-filters"></div> <div id="event-timeline-list"></div> <div id="event-timeline-load-more" style="display:none;text-align:center;padding:16px"> <button class="dash-btn dash-btn--ghost" id="event-timeline-more-btn" type="button">Load more</button> </div> </div> </section> <!-- 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> <div class="filter-group"> <label class="filter-label" for="filter-pipeline">Pipeline</label> <select class="filter-select" id="filter-pipeline"> <option value="">All</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> <!-- Convoy Pipeline (Chaining) Section --> <section class="chart-card" id="convoy-pipeline-section" data-nav-section style="display:none"> <div class="chart-card__header"> <h2 class="chart-card__title">Convoy Pipeline</h2> <p class="chart-card__desc" id="convoy-pipeline-desc">Pipeline convoy chain progress</p> </div> <div class="chart-card__body" id="convoy-pipeline-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 = "/";
7
+ </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="#overall-section" data-section="overall-section" data-view="home" aria-label="Overview section">Overview</a></li> <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> <li><a class="dash-sidebar__link" href="#tasks-section" data-section="tasks-section" data-view="detail" aria-label="Tasks section">Tasks</a></li> <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section" data-view="detail" aria-label="Pipeline section">Pipeline</a></li> <li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section" data-view="detail" aria-label="Agents section">Agents</a></li> <li><a class="dash-sidebar__link" href="#tier-section" data-section="tier-section" data-view="detail" aria-label="Tiers section">Tiers</a></li> <li><a class="dash-sidebar__link" href="#model-section" data-section="model-section" data-view="detail" aria-label="Models section">Models</a></li> <li><a class="dash-sidebar__link" href="#quality-section" data-section="quality-section" data-view="detail" aria-label="Quality section">Quality</a></li> <li><a class="dash-sidebar__link" href="#reliability-section" data-section="reliability-section" data-view="detail" aria-label="Reliability section">Reliability</a></li> <li><a class="dash-sidebar__link" href="#drift-section" data-section="drift-section" data-view="detail" aria-label="Drift section">Drift</a></li> <li><a class="dash-sidebar__link" href="#outputs-section" data-section="outputs-section" data-view="detail" aria-label="Outputs section">Outputs</a></li> <li><a class="dash-sidebar__link" href="#execution-section" data-section="execution-section" data-view="detail" aria-label="Execution Log section">Execution Log</a></li> <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> <li><a class="dash-sidebar__link" href="#panel-section" data-section="panel-section" data-view="detail" aria-label="Panel Reviews section">Panel Reviews</a></li> <li><a class="dash-sidebar__link" href="#reviews-section" data-section="reviews-section" data-view="detail" aria-label="Fast Reviews section">Fast Reviews</a></li> </ul> </nav> <main class="dash-main"> <nav class="breadcrumbs" id="breadcrumbs" data-view-hidden> <a class="breadcrumbs__link" href="#" id="breadcrumbs-home">Observability</a> <span class="breadcrumbs__separator">/</span> <span class="breadcrumbs__current" id="breadcrumbs-convoy"></span> </nav> <div class="view-home" id="view-home"> <!-- Overall Stats Section --> <section class="overall-stats" id="overall-section" data-nav-section> <div class="overall-stats__header"> <h2 class="overall-stats__title">Overall Stats</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> </div> <div class="overall-stats__grid"> <div class="overall-kpi" id="overall-total-runs"> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span></span> <span class="overall-kpi__value">&mdash;</span> </div> <div class="overall-kpi" id="overall-running"> <span class="overall-kpi__label">Running Now</span> <span class="overall-kpi__value">&mdash;</span> </div> <div class="overall-kpi" id="overall-success-rate"> <span class="overall-kpi__label">Success Rate</span> <span class="overall-kpi__value">&mdash;</span> </div> <div class="overall-kpi" id="overall-avg-duration"> <span class="overall-kpi__label">Avg Duration</span> <span class="overall-kpi__value">&mdash;</span> </div> <div class="overall-kpi" id="overall-total-tokens"> <span class="overall-kpi__label">Total Tokens</span> <span class="overall-kpi__value">&mdash;</span> </div> <div class="overall-kpi" id="overall-total-cost"> <span class="overall-kpi__label">Total Cost</span> <span class="overall-kpi__value">&mdash;</span> </div> </div> </section> <!-- Convoy List Section --> <section class="convoy-list-section" id="convoy-list-section"> <div class="convoy-list-section__header"> <h2>Convoys</h2> <p class="convoy-list-section__desc">All convoy runs across your project</p> </div> <div class="convoy-list-filters" id="convoy-list-filters"> <div class="convoy-list-filters__group"> <label for="cl-filter-search">Search</label> <input class="convoy-list-filters__input" type="text" id="cl-filter-search" placeholder="Convoy name…"> </div> <div class="convoy-list-filters__group"> <label for="cl-filter-status">Status</label> <select class="convoy-list-filters__select" id="cl-filter-status"> <option value="">All</option> <option value="done">Done</option> <option value="running">Running</option> <option value="failed">Failed</option> <option value="gate-failed">Gate Failed</option> <option value="pending">Pending</option> </select> </div> <div class="convoy-list-filters__group"> <label for="cl-filter-from">From</label> <input class="convoy-list-filters__date" type="date" id="cl-filter-from"> </div> <div class="convoy-list-filters__group"> <label for="cl-filter-to">To</label> <input class="convoy-list-filters__date" type="date" id="cl-filter-to"> </div> <button class="convoy-list-filters__reset" type="button" id="cl-filter-reset">Reset</button> </div> <div id="convoy-list-table-wrap"></div> <div class="convoy-list-empty" id="convoy-list-empty" style="display:none"> <div class="convoy-list-empty__icon"> <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"></rect><line x1="14" y1="20" x2="26" y2="20" opacity="0.5"></line></svg> </div> <p class="convoy-list-empty__text">No convoys match your filters</p> </div> </section> </div><!-- .view-home --> <div class="view-convoy-detail" id="view-convoy-detail" data-view-hidden> <section class="convoy-detail-hero" id="convoy-detail-hero"> <div class="convoy-detail-hero__title" id="detail-hero-title"></div> <span class="convoy-detail-hero__status" id="detail-hero-status"></span> <div class="convoy-detail-hero__meta" id="detail-hero-meta"></div> </section> <!-- Tasks Section --> <section class="chart-card" id="tasks-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Tasks</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc" id="tasks-section-desc">Task breakdown for the selected convoy</p> </div> <div class="chart-card__body" id="tasks-section-body"> <div id="task-summary-cards" class="task-summary-cards"></div> <div id="phase-breakdown" class="phase-breakdown"></div> <div id="task-table-wrap" class="task-table-wrap"></div> </div> </section> <!-- Pipeline View --> <section class="chart-card" id="pipeline-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Task Pipeline</h2> <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Task flow across execution phases." data-tooltip="Task flow across execution phases."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Task flow across execution phases</p> </div> <div class="chart-card__body" id="pipeline-view"></div> </section> <!-- Charts Row: Agent + Outcomes --> <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">Task count per agent, stacked by outcome</p> </div> <div class="chart-card__body" id="agent-chart"></div> </section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Delegation Outcomes</h2> <p class="chart-card__desc">Task outcome distribution</p> </div> <div class="chart-card__body" id="delegation-outcome-chart"></div> </section> </div> <!-- Charts Row: Tiers + Mechanism --> <div class="charts-row" id="tier-section" data-nav-section> <section class="chart-card"> <div class="chart-card__header"> <h2 class="chart-card__title">Tier Distribution</h2> <p class="chart-card__desc">Model tier breakdown</p> </div> <div class="chart-card__body" id="tier-chart"></div> </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> </section> </div> <!-- Model Usage --> <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">Tasks by model</p> </div> <div class="chart-card__body" id="model-chart"></div> </section> <!-- Quality Section --> <section class="chart-card" id="quality-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Quality / Review</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Code review results and quality gate outcomes for the selected convoy</p> </div> <div class="chart-card__body"> <div id="quality-cards" class="task-summary-cards"></div> <div id="quality-review-table"></div> </div> </section> <!-- Reliability Section --> <section class="chart-card" id="reliability-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Reliability</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Retry queue and error overview for the selected convoy</p> </div> <div class="chart-card__body"> <div id="reliability-dlq-card"></div> <div id="reliability-dlq-table"></div> <div style="margin-top:24px"> <h3 style="font-size:1rem;font-weight:600;color:var(--text-secondary);margin:0 0 12px">Error Overview</h3> <div id="reliability-error-overview"></div> </div> </div> </section> <!-- Drift Section --> <section class="chart-card" id="drift-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Drift</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Plan adherence and deviation metrics for the selected convoy</p> </div> <div class="chart-card__body"> <div id="drift-cards" class="task-summary-cards"></div> <div id="drift-secret-banner" style="display:none"></div> </div> </section> <!-- Outputs Section --> <section class="chart-card" id="outputs-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Outputs &amp; Artifacts</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Files, summaries, and structured data produced by tasks in this convoy</p> </div> <div class="chart-card__body"> <div id="outputs-cards" class="task-summary-cards"></div> <div id="artifact-table-wrap"></div> </div> </section> <!-- Event Timeline Section --> <section class="chart-card" id="event-timeline-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Event Timeline</h2> <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"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Convoy events in reverse chronological order</p> </div> <div class="chart-card__body"> <div id="event-timeline-filters" class="timeline-filters"></div> <div id="event-timeline-list"></div> <div id="event-timeline-load-more" style="display:none;text-align:center;padding:16px"> <button class="dash-btn dash-btn--ghost" id="event-timeline-more-btn" type="button">Load more</button> </div> </div> </section> <!-- Execution Log --> <section class="chart-card" id="execution-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Execution Log</h2> <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Step-by-step trace of recent task activity." data-tooltip="Step-by-step trace of recent task activity."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Recent agent activity, step by step</p> </div> <div class="chart-card__body" id="execution-log"></div> </section> <!-- Panel Reviews --> <section class="chart-card" id="panel-section" data-nav-section> <div class="chart-card__header"> <h2 class="chart-card__title">Panel Reviews</h2> <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Quality gate verdicts from majority-vote panels." data-tooltip="Quality gate verdicts from majority-vote panels."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <p class="chart-card__desc">Quality gate verdicts and fix items</p> </div> <div class="chart-card__body" id="panel-chart"></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> <span class="tooltip-trigger" tabindex="0" role="button" aria-label="Info: Single-reviewer quality gate results." data-tooltip="Single-reviewer quality gate results."><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span> <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> </section> </div><!-- .view-convoy-detail --> </main> </div> </body></html> <script>(function(){const base = "/";
8
8
 
9
9
  // ── Data Loading ──────────────────────────────────────────
10
10
 
@@ -176,1257 +176,95 @@ Export
176
176
  if (existing) existing.remove();
177
177
  }
178
178
 
179
- // ── KPI Rendering ────────────────────────────────────────
180
-
181
- function renderKpis(sessions, delegations, reviews) {
182
- const total = sessions.length;
183
- const isEmpty = total === 0;
184
- const successCount = sessions.filter((s) => s.outcome === 'success').length;
185
- const rate = total > 0 ? Math.round((successCount / total) * 100) : 0;
186
- const durSessions = sessions.filter((s) => s.duration_min != null);
187
- const avgDur =
188
- durSessions.length > 0
189
- ? Math.round(
190
- durSessions.reduce((sum, s) => sum + (s.duration_min || 0), 0) /
191
- durSessions.length
192
- )
193
- : 0;
194
- const uniqueAgents = new Set(delegations.map((d) => d.agent)).size;
195
-
196
- // Toggle ghost class on KPI row
197
- const kpiRow = document.querySelector('.kpi-row');
198
- if (kpiRow) kpiRow.classList.toggle('kpi-row--empty', isEmpty);
199
-
200
- const kpiSessions = document.getElementById('kpi-sessions');
201
- const kpiSuccess = document.getElementById('kpi-success');
202
- const kpiDelegations = document.getElementById('kpi-delegations');
203
- const kpiDuration = document.getElementById('kpi-duration');
204
-
205
- if (kpiSessions) {
206
- kpiSessions.querySelector('.kpi-card__value').textContent = isEmpty ? '0' : total;
207
- kpiSessions.querySelector('.kpi-card__sub').innerHTML = isEmpty
208
- ? '<span class="kpi-card__hint">No sessions yet</span>'
209
- : '<span class="kpi-trend kpi-trend--up">\u2191</span> ' + successCount + ' successful';
210
- }
211
- if (kpiSuccess) {
212
- if (isEmpty) {
213
- kpiSuccess.querySelector('.kpi-card__value').textContent = '\u2014';
214
- kpiSuccess.querySelector('.kpi-card__sub').innerHTML =
215
- '<span class="kpi-card__hint">No sessions yet</span>';
216
- } else {
217
- const trendClass =
218
- rate >= 80 ? 'up' : rate >= 60 ? 'neutral' : 'down';
219
- kpiSuccess.querySelector('.kpi-card__value').textContent = rate + '%';
220
- kpiSuccess.querySelector('.kpi-card__sub').innerHTML =
221
- '<span class="kpi-trend kpi-trend--' +
222
- trendClass +
223
- '">' +
224
- (trendClass === 'up' ? '\u2191' : trendClass === 'down' ? '\u2193' : '\u2192') +
225
- '</span> across all sessions';
226
- }
227
- }
228
- if (kpiDelegations) {
229
- kpiDelegations.querySelector('.kpi-card__value').textContent =
230
- delegations.length === 0 ? '0' : delegations.length;
231
- kpiDelegations.querySelector('.kpi-card__sub').innerHTML = isEmpty
232
- ? '<span class="kpi-card__hint">No delegations yet</span>'
233
- : uniqueAgents + ' unique agents';
234
- }
235
- if (kpiDuration) {
236
- kpiDuration.querySelector('.kpi-card__value').textContent = isEmpty ? '\u2014' : avgDur + 'm';
237
- kpiDuration.querySelector('.kpi-card__sub').innerHTML = isEmpty
238
- ? '<span class="kpi-card__hint">No duration yet</span>'
239
- : '<span class="kpi-trend kpi-trend--neutral">\u2192</span> per session';
240
- }
241
-
242
- // Retries KPI
243
- const totalRetries = sessions.reduce((sum, s) => sum + (s.retries || 0), 0);
244
- const kpiRetries = document.getElementById('kpi-retries');
245
- if (kpiRetries) {
246
- kpiRetries.querySelector('.kpi-card__value').textContent = isEmpty ? '0' : totalRetries;
247
- const retriedSessions = sessions.filter((s) => (s.retries || 0) > 0).length;
248
- kpiRetries.querySelector('.kpi-card__sub').innerHTML = isEmpty
249
- ? '<span class="kpi-card__hint">No retries yet</span>'
250
- : retriedSessions + ' sessions with retries';
251
- }
252
-
253
- // Lessons KPI
254
- const totalLessons = sessions.reduce(
255
- (sum, s) => sum + (s.lessons_added ? s.lessons_added.length : 0),
256
- 0
257
- );
258
- const kpiLessons = document.getElementById('kpi-lessons');
259
- if (kpiLessons) {
260
- kpiLessons.querySelector('.kpi-card__value').textContent = isEmpty ? '0' : totalLessons;
261
- const discoveryCount = sessions.reduce(
262
- (sum, s) => sum + (s.discoveries ? s.discoveries.length : 0),
263
- 0
264
- );
265
- kpiLessons.querySelector('.kpi-card__sub').innerHTML = isEmpty
266
- ? '<span class="kpi-card__hint">No lessons yet</span>'
267
- : discoveryCount + ' issues discovered';
268
- }
269
- }
270
-
271
- // ── Pipeline View ─────────────────────────────────────────
272
-
273
- function renderPipeline(delegations) {
274
- const el = document.getElementById('pipeline-view');
275
- if (!el) return;
276
-
277
- if (delegations.length === 0) {
278
- el.innerHTML = emptyStateHtml('pipeline', 'No pipeline activity yet', 'Delegation phases appear here as tasks flow through Foundation, Integration, Validation, and QA stages.');
279
- return;
280
- }
281
-
282
- const phases = { 1: 0, 2: 0, 3: 0, 4: 0 };
283
- delegations.forEach((d) => {
284
- const p = d.phase || 1;
285
- if (phases[p] !== undefined) phases[p]++;
286
- });
287
-
288
- const stageConfig = [
289
- {
290
- label: 'Foundation',
291
- phase: 1,
292
- iconClass: 'pending',
293
- 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>',
294
- },
295
- {
296
- label: 'Integration',
297
- phase: 2,
298
- iconClass: 'active',
299
- 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>',
300
- },
301
- {
302
- label: 'Validation',
303
- phase: 3,
304
- iconClass: 'review',
305
- 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>',
306
- },
307
- {
308
- label: 'QA Gate',
309
- phase: 4,
310
- iconClass: 'done',
311
- 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>',
312
- },
313
- ];
314
-
315
- el.innerHTML =
316
- '<div class="pipeline">' +
317
- stageConfig
318
- .map(
319
- (stage, i) =>
320
- (i > 0 ? '<div class="pipeline-arrow">\u2192</div>' : '') +
321
- '<div class="pipeline-stage">' +
322
- '<div class="pipeline-stage__icon pipeline-stage__icon--' +
323
- stage.iconClass +
324
- '">' +
325
- stage.icon +
326
- '</div>' +
327
- '<span class="pipeline-stage__count">' +
328
- (phases[stage.phase] || 0) +
329
- '</span>' +
330
- '<span class="pipeline-stage__label">' +
331
- stage.label +
332
- '</span>' +
333
- '</div>'
334
- )
335
- .join('') +
336
- '</div>';
337
- }
338
-
339
- // ── Agent Chart ───────────────────────────────────────────
340
-
341
- function renderAgentChart(sessions) {
342
- const el = document.getElementById('agent-chart');
343
- if (!el) return;
344
-
345
- if (sessions.length === 0) {
346
- el.innerHTML = emptyStateHtml('agents', 'No agent sessions yet', 'A breakdown of sessions per agent will appear here — stacked by outcome (success, partial, failed).');
347
- return;
348
- }
349
-
350
- const agentMap = {};
351
- sessions.forEach((s) => {
352
- if (!agentMap[s.agent])
353
- agentMap[s.agent] = { success: 0, partial: 0, failed: 0, total: 0 };
354
- agentMap[s.agent][s.outcome] = (agentMap[s.agent][s.outcome] || 0) + 1;
355
- agentMap[s.agent].total++;
356
- });
357
-
358
- const agents = Object.entries(agentMap).sort(
359
- (a, b) => b[1].total - a[1].total
360
- );
361
- const maxTotal = Math.max(...agents.map(([, d]) => d.total));
362
-
363
- el.innerHTML = agents
364
- .map(
365
- ([name, data]) =>
366
- '<div class="bar-row">' +
367
- '<span class="bar-label">' +
368
- escapeHtml(name) +
369
- '</span>' +
370
- '<div class="bar-track">' +
371
- (data.success > 0
372
- ? '<div class="bar-segment bar--success" style="width: ' +
373
- ((data.success / maxTotal) * 100).toFixed(1) + '%"></div>'
374
- : '') +
375
- (data.partial > 0
376
- ? '<div class="bar-segment bar--partial" style="width: ' +
377
- ((data.partial / maxTotal) * 100).toFixed(1) + '%"></div>'
378
- : '') +
379
- (data.failed > 0
380
- ? '<div class="bar-segment bar--failed" style="width: ' +
381
- ((data.failed / maxTotal) * 100).toFixed(1) + '%"></div>'
382
- : '') +
383
- '</div>' +
384
- '<span class="bar-value">' +
385
- data.total +
386
- '</span>' +
387
- '</div>'
388
- )
389
- .join('');
390
- }
391
-
392
- // ── Tier Donut Chart ──────────────────────────────────────
393
-
394
- function renderTierChart(delegations) {
395
- const el = document.getElementById('tier-chart');
396
- if (!el) return;
397
-
398
- if (delegations.length === 0) {
399
- el.innerHTML = emptyStateHtml('tiers', 'No tier data yet', 'Model tier distribution (Premium, Standard, Utility, Economy) will be visualized as a donut chart.');
400
- return;
401
- }
402
-
403
- const tierCounts = {};
404
- delegations.forEach((d) => {
405
- tierCounts[d.tier] = (tierCounts[d.tier] || 0) + 1;
406
- });
407
-
408
- const order = ['premium', 'standard', 'utility', 'economy'];
409
- const tiers = order
410
- .filter((t) => tierCounts[t])
411
- .map((t) => ({ name: t, count: tierCounts[t] }));
412
-
413
- const total = delegations.length;
414
- const r = 70;
415
- const circumference = 2 * Math.PI * r;
416
- let cumOffset = 0;
417
-
418
- const circles = tiers.map((t) => {
419
- const pct = t.count / total;
420
- const dashLen = pct * circumference;
421
- // Skip round linecap for single-segment donuts to avoid overlap artifact
422
- const linecap = tiers.length === 1 ? 'butt' : 'round';
423
- const segment =
424
- '<circle cx="90" cy="90" r="' +
425
- r +
426
- '" fill="none" ' +
427
- 'stroke="' +
428
- (TIER_COLORS[t.name] || '#64748b') +
429
- '" stroke-width="18" ' +
430
- 'stroke-dasharray="' +
431
- dashLen.toFixed(2) +
432
- ' ' +
433
- (circumference - dashLen).toFixed(2) +
434
- '" ' +
435
- 'stroke-dashoffset="' +
436
- (-cumOffset).toFixed(2) +
437
- '" ' +
438
- 'transform="rotate(-90 90 90)" ' +
439
- 'stroke-linecap="' + linecap + '"/>';
440
- cumOffset += dashLen;
441
- return segment;
442
- });
443
-
444
- const legend = tiers
445
- .map(
446
- (t) =>
447
- '<div class="legend-item">' +
448
- '<span class="legend-dot" style="background: ' +
449
- (TIER_COLORS[t.name] || '#64748b') +
450
- '"></span>' +
451
- '<span class="legend-name">' +
452
- t.name +
453
- '</span>' +
454
- '<span class="legend-count">' +
455
- t.count +
456
- ' (' +
457
- Math.round((t.count / total) * 100) +
458
- '%)</span>' +
459
- '</div>'
460
- )
461
- .join('');
462
-
463
- el.innerHTML =
464
- '<div class="donut-container">' +
465
- '<div class="donut-wrap">' +
466
- '<svg viewBox="0 0 180 180" class="donut-svg">' +
467
- circles.join('') +
468
- '</svg>' +
469
- '<div class="donut-center">' +
470
- '<span class="donut-total">' +
471
- total +
472
- '</span>' +
473
- '<span class="donut-total-label">total</span>' +
474
- '</div>' +
475
- '</div>' +
476
- '<div class="donut-legend">' +
477
- legend +
478
- '</div>' +
479
- '</div>';
179
+ // ── View Management ─────────────────────────────────────
180
+ function showHomeView() {
181
+ const home = document.getElementById('view-home');
182
+ const detail = document.getElementById('view-convoy-detail');
183
+ const breadcrumbs = document.getElementById('breadcrumbs');
184
+ if (home) delete home.dataset.viewHidden;
185
+ if (detail) detail.dataset.viewHidden = '';
186
+ if (breadcrumbs) breadcrumbs.dataset.viewHidden = '';
187
+ document.querySelectorAll('.dash-sidebar__link[data-view="detail"]').forEach(el => el.closest('li').style.display = 'none');
188
+ document.querySelectorAll('.dash-sidebar__link[data-view="home"]').forEach(el => el.closest('li').style.display = '');
189
+ const url = new URL(window.location);
190
+ url.searchParams.delete('convoy');
191
+ history.pushState({}, '', url);
480
192
  }
481
193
 
482
- // ── Mechanism Donut Chart ─────────────────────────────────
483
-
484
- function renderMechanismChart(delegations) {
485
- const el = document.getElementById('mechanism-chart');
486
- if (!el) return;
487
-
488
- if (delegations.length === 0) {
489
- el.innerHTML = emptyStateHtml('mechanism', 'No delegation data yet', 'The split between sub-agent (inline) and background (worktree) delegations will be shown here.');
490
- return;
491
- }
492
-
493
- const mechCounts = {};
494
- delegations.forEach((d) => {
495
- const mech = d.mechanism || 'unknown';
496
- mechCounts[mech] = (mechCounts[mech] || 0) + 1;
497
- });
498
-
499
- var MECH_COLORS = {
500
- 'sub-agent': '#3b82f6',
501
- 'background': '#a78bfa',
502
- 'unknown': '#64748b',
503
- };
504
-
505
- var MECH_LABELS = {
506
- 'sub-agent': 'Sub-agent (inline)',
507
- 'background': 'Background (worktree)',
508
- 'unknown': 'Unknown',
509
- };
510
-
511
- var mechOrder = ['sub-agent', 'background', 'unknown'];
512
- var mechs = mechOrder
513
- .filter(function (m) { return mechCounts[m]; })
514
- .map(function (m) { return { name: m, count: mechCounts[m] }; });
515
-
516
- var total = delegations.length;
517
- var r = 70;
518
- var circumference = 2 * Math.PI * r;
519
- var cumOffset = 0;
520
-
521
- var circles = mechs.map(function (m) {
522
- var pct = m.count / total;
523
- var dashLen = pct * circumference;
524
- // Skip round linecap for single-segment donuts to avoid overlap artifact
525
- var linecap = mechs.length === 1 ? 'butt' : 'round';
526
- var segment =
527
- '<circle cx="90" cy="90" r="' + r + '" fill="none" ' +
528
- 'stroke="' + (MECH_COLORS[m.name] || '#64748b') + '" stroke-width="18" ' +
529
- 'stroke-dasharray="' + dashLen.toFixed(2) + ' ' + (circumference - dashLen).toFixed(2) + '" ' +
530
- 'stroke-dashoffset="' + (-cumOffset).toFixed(2) + '" ' +
531
- 'transform="rotate(-90 90 90)" stroke-linecap="' + linecap + '"/>';
532
- cumOffset += dashLen;
533
- return segment;
534
- });
535
-
536
- var legend = mechs
537
- .map(function (m) {
538
- return '<div class="legend-item">' +
539
- '<span class="legend-dot" style="background: ' + (MECH_COLORS[m.name] || '#64748b') + '"></span>' +
540
- '<span class="legend-name">' + (MECH_LABELS[m.name] || m.name) + '</span>' +
541
- '<span class="legend-count">' + m.count + ' (' + Math.round((m.count / total) * 100) + '%)</span>' +
542
- '</div>';
543
- })
544
- .join('');
545
-
546
- el.innerHTML =
547
- '<div class="donut-container">' +
548
- '<div class="donut-wrap">' +
549
- '<svg viewBox="0 0 180 180" class="donut-svg">' +
550
- circles.join('') +
551
- '</svg>' +
552
- '<div class="donut-center">' +
553
- '<span class="donut-total">' + total + '</span>' +
554
- '<span class="donut-total-label">total</span>' +
555
- '</div>' +
556
- '</div>' +
557
- '<div class="donut-legend">' +
558
- legend +
559
- '</div>' +
560
- '</div>';
194
+ function showConvoyDetailView(convoyId, convoyName) {
195
+ const home = document.getElementById('view-home');
196
+ const detail = document.getElementById('view-convoy-detail');
197
+ const breadcrumbs = document.getElementById('breadcrumbs');
198
+ if (home) home.dataset.viewHidden = '';
199
+ if (detail) delete detail.dataset.viewHidden;
200
+ if (breadcrumbs) delete breadcrumbs.dataset.viewHidden;
201
+ document.querySelectorAll('.dash-sidebar__link[data-view="home"]').forEach(el => el.closest('li').style.display = 'none');
202
+ document.querySelectorAll('.dash-sidebar__link[data-view="detail"]').forEach(el => el.closest('li').style.display = '');
203
+ const crumbText = document.getElementById('breadcrumbs-convoy');
204
+ if (crumbText) crumbText.textContent = convoyName || convoyId;
205
+ const url = new URL(window.location);
206
+ url.searchParams.set('convoy', convoyId);
207
+ history.pushState({}, '', url);
208
+ loadConvoyDetail(convoyId);
209
+ window.scrollTo({ top: 0, behavior: 'smooth' });
561
210
  }
562
211
 
563
- // ── Delegation Outcome Chart ──────────────────────────────
564
-
565
- function renderDelegationOutcomeChart(delegations) {
566
- var el = document.getElementById('delegation-outcome-chart');
567
- if (!el) return;
212
+ // ── Convoy List ─────────────────────────────────────────
213
+ function renderConvoyList() {
214
+ const data = window.__DASHBOARD_DATA__;
215
+ const list = data?.convoyList ?? [];
216
+ const wrap = document.getElementById('convoy-list-table-wrap');
217
+ const emptyEl = document.getElementById('convoy-list-empty');
218
+ if (!wrap) return;
219
+
220
+ const search = (document.getElementById('cl-filter-search')?.value || '').toLowerCase();
221
+ const status = document.getElementById('cl-filter-status')?.value || '';
222
+ const fromDate = document.getElementById('cl-filter-from')?.value || '';
223
+ const toDate = document.getElementById('cl-filter-to')?.value || '';
224
+
225
+ let filtered = list;
226
+ if (search) filtered = filtered.filter(c => (c.name || c.id || '').toLowerCase().includes(search));
227
+ if (status) filtered = filtered.filter(c => c.status === status);
228
+ if (fromDate) filtered = filtered.filter(c => c.created_at && c.created_at >= fromDate);
229
+ if (toDate) filtered = filtered.filter(c => c.created_at && c.created_at.slice(0, 10) <= toDate);
568
230
 
569
- if (delegations.length === 0) {
570
- el.innerHTML = emptyStateHtml('outcomes', 'No outcome data yet', 'Delegation results — success, partial, failed, redirected — will be tracked and compared here.');
231
+ if (filtered.length === 0) {
232
+ wrap.innerHTML = '';
233
+ if (emptyEl) emptyEl.style.display = '';
571
234
  return;
572
235
  }
236
+ if (emptyEl) emptyEl.style.display = 'none';
573
237
 
574
- var OUTCOME_COLORS = {
575
- success: '#22c55e',
576
- partial: '#f59e0b',
577
- failed: '#ef4444',
578
- redirected: '#64748b',
238
+ const statusBadgeClass = (s) => {
239
+ const map = { done: '--done', running: '--running', failed: '--failed', 'gate-failed': '--gate-failed', pending: '--pending' };
240
+ return 'status-badge ' + (map[s] || '');
579
241
  };
580
242
 
581
- var outcomeCounts = {};
582
- delegations.forEach(function (d) {
583
- var outcome = d.outcome || 'unknown';
584
- outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
585
- });
586
-
587
- var outcomes = Object.entries(outcomeCounts).sort(function (a, b) { return b[1] - a[1]; });
588
- var maxCount = Math.max.apply(null, outcomes.map(function (o) { return o[1]; }));
589
-
590
- el.innerHTML = outcomes
591
- .map(function (entry) {
592
- var name = entry[0];
593
- var count = entry[1];
594
- return '<div class="bar-row">' +
595
- '<span class="bar-label">' + escapeHtml(name) + '</span>' +
596
- '<div class="bar-track">' +
597
- '<div class="bar-segment" style="width: ' + ((count / maxCount) * 100).toFixed(1) + '%; background: ' + (OUTCOME_COLORS[name] || '#64748b') + '"></div>' +
598
- '</div>' +
599
- '<span class="bar-value">' + count + '</span>' +
600
- '</div>';
601
- })
602
- .join('');
603
- }
604
-
605
- // ── Timeline Chart ────────────────────────────────────────
606
-
607
- function renderTimelineChart(sessions, delegations) {
608
- const el = document.getElementById('timeline-chart');
609
- if (!el) return;
610
-
611
- const dateMap = {};
612
- sessions.forEach((s) => {
613
- const key = s.timestamp.slice(0, 10);
614
- if (!dateMap[key]) dateMap[key] = { sessions: 0, delegations: 0 };
615
- dateMap[key].sessions++;
616
- });
617
- delegations.forEach((d) => {
618
- const key = d.timestamp.slice(0, 10);
619
- if (!dateMap[key]) dateMap[key] = { sessions: 0, delegations: 0 };
620
- dateMap[key].delegations++;
621
- });
622
-
623
- const dates = Object.entries(dateMap).sort(([a], [b]) =>
624
- a.localeCompare(b)
625
- );
626
-
627
- if (dates.length === 0) {
628
- el.innerHTML = emptyStateHtml('timeline', 'No timeline data yet', 'A daily activity chart will build here as sessions and delegations accumulate over time.');
629
- return;
630
- }
631
-
632
- const maxVal = Math.max(
633
- ...dates.map(([, d]) => Math.max(d.sessions, d.delegations))
634
- );
635
- const w = 500;
636
- const h = 180;
637
- const pad = { top: 10, right: 10, bottom: 28, left: 10 };
638
- const plotW = w - pad.left - pad.right;
639
- const plotH = h - pad.top - pad.bottom;
640
- // Prevent sparse layout when there are very few dates
641
- const groupWidth = dates.length <= 3
642
- ? Math.min(100, plotW / dates.length)
643
- : plotW / dates.length;
644
- const barWidth = Math.min(dates.length <= 3 ? 24 : 16, groupWidth * 0.35);
645
- // Center the bars when there are few dates
646
- const timelineStartX = dates.length <= 3
647
- ? pad.left + (plotW - dates.length * groupWidth) / 2
648
- : pad.left;
649
-
650
- let rects = '';
651
- let labels = '';
652
-
653
- dates.forEach(([date, data], i) => {
654
- const x = timelineStartX + i * groupWidth + groupWidth / 2;
655
- const sH = maxVal > 0 ? (data.sessions / maxVal) * plotH : 0;
656
- const dH = maxVal > 0 ? (data.delegations / maxVal) * plotH : 0;
657
-
658
- rects +=
659
- '<rect x="' +
660
- (x - barWidth - 1).toFixed(1) +
661
- '" y="' +
662
- (pad.top + plotH - sH).toFixed(1) +
663
- '" width="' +
664
- barWidth.toFixed(1) +
665
- '" height="' +
666
- sH.toFixed(1) +
667
- '" fill="#3b82f6" rx="3" opacity="0.85"/>';
668
- rects +=
669
- '<rect x="' +
670
- (x + 1).toFixed(1) +
671
- '" y="' +
672
- (pad.top + plotH - dH).toFixed(1) +
673
- '" width="' +
674
- barWidth.toFixed(1) +
675
- '" height="' +
676
- dH.toFixed(1) +
677
- '" fill="#a78bfa" rx="3" opacity="0.65"/>';
678
- labels +=
679
- '<text x="' +
680
- x.toFixed(1) +
681
- '" y="' +
682
- (h - 6) +
683
- '" text-anchor="middle" fill="#5a5a6e" font-size="10">' +
684
- formatShortDate(date) +
685
- '</text>';
686
- });
687
-
688
- el.innerHTML =
689
- '<svg viewBox="0 0 ' +
690
- w +
691
- ' ' +
692
- h +
693
- '" class="timeline-svg" preserveAspectRatio="xMidYMid meet">' +
694
- rects +
695
- labels +
696
- '</svg>' +
697
- '<div class="timeline-legend">' +
698
- '<div class="timeline-legend__item">' +
699
- '<span class="timeline-legend__dot" style="background: #3b82f6"></span>' +
700
- 'Sessions</div>' +
701
- '<div class="timeline-legend__item">' +
702
- '<span class="timeline-legend__dot" style="background: #a78bfa"></span>' +
703
- 'Delegations</div>' +
704
- '</div>';
705
- }
706
-
707
- // ── Model Chart ───────────────────────────────────────────
708
-
709
- function renderModelChart(sessions) {
710
- const el = document.getElementById('model-chart');
711
- if (!el) return;
712
-
713
- if (sessions.length === 0) {
714
- el.innerHTML = emptyStateHtml('models', 'No model data yet', 'Model utilization across sessions — Claude Opus, GPT-5, Gemini, etc. — will be compared here.');
715
- return;
716
- }
717
-
718
- const modelCounts = {};
719
- sessions.forEach((s) => {
720
- modelCounts[s.model] = (modelCounts[s.model] || 0) + 1;
721
- });
722
-
723
- const models = Object.entries(modelCounts).sort((a, b) => b[1] - a[1]);
724
- const maxCount = Math.max(...models.map(([, c]) => c));
725
-
726
- el.innerHTML = models
727
- .map(
728
- ([name, count]) =>
729
- '<div class="bar-row">' +
730
- '<span class="bar-label">' +
731
- escapeHtml(name) +
732
- '</span>' +
733
- '<div class="bar-track">' +
734
- '<div class="bar-segment" style="width: ' +
735
- ((count / maxCount) * 100).toFixed(1) +
736
- '%; background: ' +
737
- (MODEL_COLORS[name] || '#64748b') +
738
- '"></div>' +
739
- '</div>' +
740
- '<span class="bar-value">' +
741
- count +
742
- '</span>' +
743
- '</div>'
744
- )
745
- .join('');
746
- }
747
-
748
- // ── Execution Log ─────────────────────────────────────────
749
-
750
- function renderExecutionLog(sessions) {
751
- const el = document.getElementById('execution-log');
752
- if (!el) return;
753
-
754
- const sorted = sessions
755
- .slice()
756
- .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
757
- .slice(0, 10);
758
-
759
- if (sorted.length === 0) {
760
- el.innerHTML = emptyStateHtml('execLog', 'No execution history yet', 'A step-by-step trace of agent activity — with outcomes, durations, and metadata — will appear here.');
761
- return;
762
- }
763
-
764
- el.innerHTML =
765
- '<div class="exec-log">' +
766
- sorted
767
- .map(
768
- (s, i) =>
769
- '<div class="exec-step">' +
770
- '<div class="exec-step__indicator">' +
771
- '<div class="exec-step__dot exec-step__dot--' +
772
- s.outcome +
773
- '">' +
774
- (OUTCOME_ICONS[s.outcome] || '') +
775
- '</div>' +
776
- (i < sorted.length - 1
777
- ? '<div class="exec-step__line"></div>'
778
- : '') +
779
- '</div>' +
780
- '<div class="exec-step__content">' +
781
- '<div class="exec-step__header">' +
782
- '<span class="exec-step__agent">' +
783
- escapeHtml(s.agent) +
784
- '</span>' +
785
- '<span class="exec-step__badge exec-step__badge--' +
786
- s.outcome +
787
- '">' +
788
- s.outcome +
789
- '</span>' +
790
- '</div>' +
791
- '<div class="exec-step__task">' +
792
- escapeHtml(s.task) +
793
- '</div>' +
794
- '<div class="exec-step__meta">' +
795
- '<span class="exec-step__meta-item">\uD83D\uDD52 ' +
796
- formatTime(s.timestamp) +
797
- '</span>' +
798
- (s.duration_min != null
799
- ? '<span class="exec-step__meta-item">\u23F1 ' +
800
- s.duration_min +
801
- 'm</span>'
802
- : '') +
803
- (s.files_changed != null
804
- ? '<span class="exec-step__meta-item">\uD83D\uDCC1 ' +
805
- s.files_changed +
806
- ' files</span>'
807
- : '') +
808
- (s.model
809
- ? '<span class="exec-step__meta-item">\uD83E\uDD16 ' +
810
- escapeHtml(s.model) +
811
- '</span>'
812
- : '') +
813
- (s.retries > 0
814
- ? '<span class="exec-step__meta-item">\uD83D\uDD04 ' +
815
- s.retries +
816
- ' retries</span>'
817
- : '') +
818
- (s.lessons_added && s.lessons_added.length > 0
819
- ? '<span class="exec-step__meta-item">\uD83D\uDCA1 ' +
820
- s.lessons_added.length +
821
- ' lessons</span>'
822
- : '') +
823
- (s.discoveries && s.discoveries.length > 0
824
- ? '<span class="exec-step__meta-item">\uD83D\uDD0D ' +
825
- s.discoveries.length +
826
- ' discoveries</span>'
827
- : '') +
828
- '</div>' +
829
- '</div>' +
830
- '</div>'
831
- )
832
- .join('') +
833
- '</div>';
834
- }
835
-
836
- // ── Panel Chart ───────────────────────────────────────────
837
-
838
- function renderPanelChart(panels) {
839
- const el = document.getElementById('panel-chart');
840
- if (!el) return;
841
-
842
- if (panels.length === 0) {
843
- 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.');
844
- return;
845
- }
846
-
847
- el.innerHTML =
848
- '<div class="panel-grid">' +
849
- panels
850
- .map(
851
- (p) =>
852
- '<div class="panel-item">' +
853
- '<div class="panel-item__header">' +
854
- '<span class="panel-item__key">' +
855
- escapeHtml(p.panel_key) +
856
- '</span>' +
857
- '<span class="panel-item__verdict panel-item__verdict--' +
858
- p.verdict +
859
- '">' +
860
- p.verdict +
861
- '</span>' +
862
- '</div>' +
863
- '<div class="panel-item__votes">' +
864
- Array.from({ length: p.pass_count })
865
- .map(
866
- () =>
867
- '<div class="panel-item__vote panel-item__vote--pass">\u2713</div>'
868
- )
869
- .join('') +
870
- Array.from({ length: p.block_count })
871
- .map(
872
- () =>
873
- '<div class="panel-item__vote panel-item__vote--block">\u2717</div>'
874
- )
875
- .join('') +
876
- '</div>' +
877
- '<div class="panel-item__fixes">' +
878
- (p.must_fix > 0
879
- ? '<strong>' + p.must_fix + ' must-fix</strong>'
880
- : '') +
881
- (p.must_fix > 0 && p.should_fix > 0 ? ' \u00B7 ' : '') +
882
- (p.should_fix > 0 ? p.should_fix + ' should-fix' : '') +
883
- (p.must_fix === 0 && p.should_fix === 0 ? 'Clean' : '') +
884
- '</div>' +
885
- '<div class="panel-item__meta">' +
886
- '<span class="panel-item__meta-item">\uD83E\uDD16 ' + escapeHtml(p.reviewer_model || 'unknown') + '</span>' +
887
- (p.attempt > 1 ? '<span class="panel-item__meta-item">\uD83D\uDD04 attempt ' + p.attempt + '</span>' : '') +
888
- (p.artifacts_count ? '<span class="panel-item__meta-item">\uD83D\uDCC4 ' + p.artifacts_count + ' artifacts</span>' : '') +
889
- '</div>' +
890
- '</div>'
891
- )
892
- .join('') +
893
- '</div>';
894
- }
895
-
896
- // ── Sessions Table ────────────────────────────────────────
897
-
898
- function renderSessionsTable(sessions) {
899
- const el = document.getElementById('sessions-table');
900
- if (!el) return;
901
-
902
- const sorted = sessions
903
- .slice()
904
- .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
905
- .slice(0, 15);
906
-
907
- if (sorted.length === 0) {
908
- 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.');
909
- return;
910
- }
911
-
912
- el.innerHTML =
913
- '<table class="sessions-table">' +
914
- '<thead><tr>' +
915
- '<th>Timestamp</th>' +
916
- '<th>Agent</th>' +
917
- '<th>Task</th>' +
918
- '<th>Outcome</th>' +
919
- '<th>Duration</th>' +
920
- '<th>Files</th>' +
921
- '<th>Retries</th>' +
922
- '<th>Issue</th>' +
923
- '</tr></thead>' +
924
- '<tbody>' +
925
- sorted
926
- .map(
927
- (s) =>
928
- '<tr>' +
929
- '<td>' +
930
- formatTime(s.timestamp) +
931
- '</td>' +
932
- '<td class="td-agent">' +
933
- escapeHtml(s.agent) +
934
- '</td>' +
935
- '<td class="td-task">' +
936
- escapeHtml(s.task) +
937
- '</td>' +
938
- '<td><span class="outcome-badge outcome-badge--' +
939
- s.outcome +
940
- '">' +
941
- s.outcome +
942
- '</span></td>' +
943
- '<td class="td-num">' +
944
- (s.duration_min != null ? s.duration_min + 'm' : '\u2014') +
945
- '</td>' +
946
- '<td class="td-num">' +
947
- (s.files_changed != null ? s.files_changed : '\u2014') +
948
- '</td>' +
949
- '<td class="td-num">' +
950
- (s.retries != null ? s.retries : '\u2014') +
951
- '</td>' +
952
- '<td class="td-issue">' +
953
- (s.tracker_issue ? escapeHtml(s.tracker_issue) : '\u2014') +
954
- '</td>' +
955
- '</tr>'
956
- )
957
- .join('') +
958
- '</tbody></table>';
959
- }
960
-
961
- // ── Main ──────────────────────────────────────────────────
962
-
963
- // Store raw data globally for filtering/export
964
- let rawSessions = [];
965
- let rawDelegations = [];
966
- let rawPanels = [];
967
- let rawReviews = [];
968
- let rawConvoys = [];
969
- let rawPipelines = [];
970
-
971
- function applyFilters() {
972
- const dateFrom = document.getElementById('filter-date-from').value;
973
- const dateTo = document.getElementById('filter-date-to').value;
974
- const agentFilter = document.getElementById('filter-agent').value;
975
- const outcomeFilter = document.getElementById('filter-outcome').value;
976
- const convoyFilter = document.getElementById('filter-convoy').value;
977
- const pipelineFilter = document.getElementById('filter-pipeline')?.value || '';
978
-
979
- function matchDate(ts) {
980
- const date = ts.slice(0, 10);
981
- if (dateFrom && date < dateFrom) return false;
982
- if (dateTo && date > dateTo) return false;
983
- return true;
984
- }
985
-
986
- let sessions = rawSessions.filter((s) => {
987
- if (!matchDate(s.timestamp)) return false;
988
- if (agentFilter && s.agent !== agentFilter) return false;
989
- if (outcomeFilter && s.outcome !== outcomeFilter) return false;
990
- return true;
991
- });
992
-
993
- let delegations = rawDelegations.filter((d) => {
994
- if (!matchDate(d.timestamp)) return false;
995
- if (agentFilter && d.agent !== agentFilter) return false;
996
- if (outcomeFilter && d.outcome !== outcomeFilter) return false;
997
- return true;
998
- });
999
-
1000
- let panels = rawPanels.filter((p) => matchDate(p.timestamp));
1001
- let reviews = rawReviews.filter((r) => {
1002
- if (!matchDate(r.timestamp)) return false;
1003
- if (agentFilter && r.agent !== agentFilter) return false;
1004
- return true;
1005
- });
1006
-
1007
- // Pipeline filter: restrict events to convoy_ids within the selected pipeline
1008
- if (pipelineFilter) {
1009
- const activePipeline = rawPipelines.find((p) => p.id === pipelineFilter);
1010
- const pipelineConvoyIds = new Set((activePipeline && activePipeline.convoy_ids) || []);
1011
- if (pipelineConvoyIds.size > 0) {
1012
- sessions = sessions.filter((s) => !s.convoy_id || pipelineConvoyIds.has(s.convoy_id));
1013
- delegations = delegations.filter((d) => !d.convoy_id || pipelineConvoyIds.has(d.convoy_id));
1014
- panels = panels.filter((p2) => !p2.convoy_id || pipelineConvoyIds.has(p2.convoy_id));
1015
- reviews = reviews.filter((r) => !r.convoy_id || pipelineConvoyIds.has(r.convoy_id));
1016
- }
1017
- }
1018
-
1019
- if (convoyFilter) {
1020
- sessions = sessions.filter((s) => s.convoy_id === convoyFilter);
1021
- delegations = delegations.filter((d) => d.convoy_id === convoyFilter);
1022
- panels = panels.filter((p) => p.convoy_id === convoyFilter);
1023
- reviews = reviews.filter((r) => r.convoy_id === convoyFilter);
1024
- }
1025
-
1026
- const convoySection = document.getElementById('convoy-section');
1027
- if (convoySection) {
1028
- convoySection.style.display = convoyFilter ? '' : 'none';
1029
- if (convoyFilter) {
1030
- const convoy = rawConvoys.find((c) => c.id === convoyFilter);
1031
- renderConvoyStatus(convoy);
1032
- }
1033
- }
1034
-
1035
- // Show/hide convoy pipeline section based on pipeline filter
1036
- const pipelineSectionEl = document.getElementById('convoy-pipeline-section');
1037
- if (pipelineSectionEl) {
1038
- if (pipelineFilter) {
1039
- const activePipeline = rawPipelines.find((p) => p.id === pipelineFilter);
1040
- renderConvoyPipeline(activePipeline, rawConvoys);
1041
- } else {
1042
- pipelineSectionEl.style.display = 'none';
1043
- }
1044
- }
1045
-
1046
- renderAll(sessions, delegations, panels, reviews);
1047
- }
1048
-
1049
- function populateAgentFilter(sessions, delegations, reviews) {
1050
- const agents = new Set();
1051
- sessions.forEach((s) => agents.add(s.agent));
1052
- delegations.forEach((d) => agents.add(d.agent));
1053
- reviews.forEach((r) => agents.add(r.agent));
1054
- const select = document.getElementById('filter-agent');
1055
- if (!select) return;
1056
- // Keep the "All agents" option, remove old dynamic options
1057
- while (select.options.length > 1) select.remove(1);
1058
- Array.from(agents).sort().forEach((a) => {
1059
- const opt = document.createElement('option');
1060
- opt.value = a;
1061
- opt.textContent = a;
1062
- select.appendChild(opt);
1063
- });
1064
- }
1065
-
1066
- function renderAll(sessions, delegations, panels, reviews) {
1067
- const allEmpty = sessions.length === 0 && delegations.length === 0 && panels.length === 0 && reviews.length === 0;
1068
- if (allEmpty) {
1069
- renderWelcomeBanner();
1070
- } else {
1071
- removeWelcomeBanner();
1072
- }
1073
-
1074
- renderKpis(sessions, delegations, reviews);
1075
- renderPipeline(delegations);
1076
- renderAgentChart(sessions);
1077
- renderTierChart(delegations);
1078
- renderMechanismChart(delegations);
1079
- renderDelegationOutcomeChart(delegations);
1080
- renderTimelineChart(sessions, delegations);
1081
- renderModelChart(sessions);
1082
- renderExecutionLog(sessions);
1083
- renderPanelChart(panels);
1084
- renderReviewsTable(reviews);
1085
- renderSessionsTable(sessions);
1086
- }
1087
-
1088
- // ── Reviews Table ─────────────────────────────────────────
1089
-
1090
- function renderReviewsTable(reviews) {
1091
- const el = document.getElementById('reviews-table');
1092
- if (!el) return;
1093
-
1094
- const sorted = reviews
1095
- .slice()
1096
- .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
1097
- .slice(0, 20);
1098
-
1099
- if (sorted.length === 0) {
1100
- el.innerHTML = emptyStateHtml('panels', 'No fast reviews yet', 'Single-reviewer quality gate results — with verdicts, issue counts, and escalation status — will be listed here.');
1101
- return;
1102
- }
1103
-
1104
- el.innerHTML =
1105
- '<table class="sessions-table">' +
1106
- '<thead><tr>' +
1107
- '<th>Timestamp</th>' +
1108
- '<th>Agent</th>' +
1109
- '<th>Verdict</th>' +
1110
- '<th>Critical</th>' +
1111
- '<th>Major</th>' +
1112
- '<th>Minor</th>' +
1113
- '<th>Confidence</th>' +
1114
- '<th>Attempt</th>' +
1115
- '<th>Escalated</th>' +
1116
- '<th>Issue</th>' +
1117
- '</tr></thead>' +
1118
- '<tbody>' +
1119
- sorted
1120
- .map(
1121
- (r) =>
1122
- '<tr>' +
1123
- '<td>' + formatTime(r.timestamp) + '</td>' +
1124
- '<td class="td-agent">' + escapeHtml(r.agent || '') + '</td>' +
1125
- '<td><span class="outcome-badge outcome-badge--' + (r.verdict === 'pass' ? 'success' : 'failed') + '">' + r.verdict + '</span></td>' +
1126
- '<td class="td-num">' + (r.issues_critical ?? 0) + '</td>' +
1127
- '<td class="td-num">' + (r.issues_major ?? 0) + '</td>' +
1128
- '<td class="td-num">' + (r.issues_minor ?? 0) + '</td>' +
1129
- '<td class="td-num">' + (r.confidence || '\u2014') + '</td>' +
1130
- '<td class="td-num">' + (r.attempt ?? 1) + '</td>' +
1131
- '<td class="td-num">' + (r.escalated ? '\u26A0' : '\u2014') + '</td>' +
1132
- '<td class="td-issue">' + (r.tracker_issue ? escapeHtml(r.tracker_issue) : '\u2014') + '</td>' +
1133
- '</tr>'
1134
- )
1135
- .join('') +
1136
- '</tbody></table>';
1137
- }
1138
-
1139
- // ── Export ─────────────────────────────────────────────────
1140
-
1141
- function exportData() {
1142
- const events = [
1143
- ...rawSessions,
1144
- ...rawDelegations,
1145
- ...rawPanels,
1146
- ...rawReviews,
1147
- ].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
1148
- const blob = new Blob([events.map((e) => JSON.stringify(e)).join('\n') + '\n'], { type: 'application/x-ndjson' });
1149
- const url = URL.createObjectURL(blob);
1150
- const a = document.createElement('a');
1151
- a.href = url;
1152
- a.download = 'opencastle-events-' + new Date().toISOString().slice(0, 10) + '.ndjson';
1153
- a.click();
1154
- URL.revokeObjectURL(url);
1155
- }
1156
-
1157
- function populateConvoyFilter(convoys) {
1158
- const select = document.getElementById('filter-convoy');
1159
- if (!select) return;
1160
- while (select.options.length > 1) select.remove(1);
1161
- const sorted = convoys.slice().sort((a, b) => b.created_at.localeCompare(a.created_at));
1162
- sorted.forEach((c) => {
1163
- const opt = document.createElement('option');
1164
- opt.value = c.id;
1165
- opt.textContent = c.name + ' (' + c.status + ')';
1166
- select.appendChild(opt);
1167
- });
1168
- }
1169
-
1170
- function renderConvoyStatus(convoy) {
1171
- const descEl = document.getElementById('convoy-desc');
1172
- const bodyEl = document.getElementById('convoy-body');
1173
- if (!descEl || !bodyEl) return;
1174
-
1175
- if (!convoy) {
1176
- bodyEl.innerHTML = emptyStateHtml('pipeline', 'Convoy not found', 'No matching convoy data available.');
1177
- return;
1178
- }
1179
-
1180
- descEl.textContent = convoy.name + ' — ' + (convoy.branch || 'no branch');
1181
-
1182
- const s = convoy.summary || {};
1183
- const total = s.total || (convoy.tasks ? convoy.tasks.length : 0);
1184
- const done = s.done || 0;
1185
- const pct = total > 0 ? Math.round((done / total) * 100) : 0;
1186
-
1187
- const statusClass = convoy.status === 'done' ? 'success'
1188
- : (convoy.status === 'failed' || convoy.status === 'gate-failed') ? 'failed'
1189
- : convoy.status === 'running' ? 'partial' : '';
1190
-
1191
- let html = '';
1192
- html += '<div class="convoy-overview">';
1193
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Status</span><span class="outcome-badge outcome-badge--' + statusClass + '">' + escapeHtml(convoy.status) + '</span></div>';
1194
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Branch</span><span class="convoy-stat__value">' + escapeHtml(convoy.branch || '\u2014') + '</span></div>';
1195
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Tasks</span><span class="convoy-stat__value">' + done + '/' + total + '</span></div>';
1196
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Events</span><span class="convoy-stat__value">' + (convoy.events_count || 0) + '</span></div>';
1197
- 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>';
1198
- if (convoy.total_tokens != null) {
1199
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Tokens</span><span class="convoy-stat__value">' + formatTokens(convoy.total_tokens) + '</span></div>';
1200
- }
1201
- if (convoy.finished_at) {
1202
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Finished</span><span class="convoy-stat__value">' + formatTime(convoy.finished_at) + '</span></div>';
1203
- }
1204
- const convoyDur = formatDuration(convoy.started_at, convoy.finished_at);
1205
- if (convoyDur) {
1206
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Duration</span><span class="convoy-stat__value">' + convoyDur + '</span></div>';
1207
- }
1208
- if (convoy.total_cost_usd != null && convoy.total_cost_usd > 0) {
1209
- 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>';
1210
- }
1211
- const failedCount = (s.failed || 0) + (s.timedOut || 0);
1212
- if (failedCount > 0) {
1213
- html += '<div class="convoy-stat"><span class="convoy-stat__label">Failed</span><span class="convoy-stat__value convoy-stat__value--error">' + failedCount + '</span></div>';
1214
- }
1215
- html += '</div>';
1216
-
1217
- html += '<div class="convoy-progress">';
1218
- html += '<div class="convoy-progress__bar"><div class="convoy-progress__fill" style="width:' + pct + '%"></div></div>';
1219
- html += '<span class="convoy-progress__label">' + pct + '% complete</span>';
1220
- html += '</div>';
1221
-
1222
- if (convoy.tasks && convoy.tasks.length > 0) {
1223
- html += '<table class="sessions-table convoy-tasks">';
1224
- 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>';
1225
- html += '<tbody>';
1226
- convoy.tasks.forEach(function(t) {
1227
- const tStatus = t.status === 'done' ? 'success'
1228
- : (t.status === 'failed' || t.status === 'timed-out') ? 'failed'
1229
- : t.status === 'running' ? 'partial' : '';
1230
- html += '<tr>';
1231
- html += '<td>' + escapeHtml(t.id) + '</td>';
1232
- html += '<td class="td-num">' + t.phase + '</td>';
1233
- html += '<td class="td-agent">' + escapeHtml(t.agent) + '</td>';
1234
- html += '<td>' + escapeHtml(t.adapter || '\u2014') + '</td>';
1235
- html += '<td><span class="outcome-badge outcome-badge--' + tStatus + '">' + escapeHtml(t.status) + '</span></td>';
1236
- html += '<td class="td-num">' + (t.retries || 0) + '</td>';
1237
- html += '<td class="td-num">' + (t.total_tokens != null ? formatTokens(t.total_tokens) : '\u2014') + '</td>';
1238
- html += '<td class="td-num">' + (formatDuration(t.started_at, t.finished_at) || '\u2014') + '</td>';
1239
- html += '</tr>';
1240
- });
1241
- html += '</tbody></table>';
1242
- }
1243
-
1244
- bodyEl.innerHTML = html;
1245
- }
1246
-
1247
- // ── Pipeline Filter Population ───────────────────────────
1248
-
1249
- function populatePipelineFilter(pipelines) {
1250
- const select = document.getElementById('filter-pipeline');
1251
- if (!select) return;
1252
- while (select.options.length > 1) select.remove(1);
1253
- const sorted = pipelines.slice().sort((a, b) =>
1254
- (b.created_at || '').localeCompare(a.created_at || '')
1255
- );
1256
- sorted.forEach((p) => {
1257
- const opt = document.createElement('option');
1258
- opt.value = p.id;
1259
- opt.textContent = (p.name || p.id) + ' (' + (p.status || 'unknown') + ')';
1260
- select.appendChild(opt);
1261
- });
1262
- }
1263
-
1264
- // ── Convoy Pipeline (Chaining) Render ────────────────────
1265
-
1266
- function renderConvoyPipeline(pipeline, convoys) {
1267
- const sectionEl = document.getElementById('convoy-pipeline-section');
1268
- const descEl = document.getElementById('convoy-pipeline-desc');
1269
- const bodyEl = document.getElementById('convoy-pipeline-body');
1270
- if (!sectionEl || !bodyEl) return;
1271
-
1272
- if (!pipeline) {
1273
- sectionEl.style.display = 'none';
1274
- return;
1275
- }
1276
-
1277
- sectionEl.style.display = '';
1278
- if (descEl) {
1279
- descEl.textContent =
1280
- (pipeline.name || pipeline.id) + ' \u2014 ' + (pipeline.branch || 'no branch');
243
+ let html = '<table class="convoy-list-table"><thead><tr>' +
244
+ '<th>Name</th><th>Status</th><th>Tasks</th><th>Created</th><th>Duration</th>' +
245
+ '</tr></thead><tbody>';
246
+
247
+ for (const c of filtered) {
248
+ const name = escapeHtml(c.name || c.id);
249
+ const dateStr = c.created_at ? formatTime(c.created_at) : '\u2014';
250
+ const duration = c.started_at && c.finished_at ? formatDuration(c.started_at, c.finished_at) : (c.status === 'running' ? 'In progress' : '\u2014');
251
+ const taskCount = c.task_count ?? c.taskCount ?? '\u2014';
252
+ html += '<tr data-convoy-id="' + escapeHtml(c.id) + '" class="task-row--clickable">' +
253
+ '<td><strong>' + name + '</strong></td>' +
254
+ '<td><span class="' + statusBadgeClass(c.status) + '">' + escapeHtml(c.status || 'unknown') + '</span></td>' +
255
+ '<td>' + taskCount + '</td>' +
256
+ '<td>' + dateStr + '</td>' +
257
+ '<td>' + (duration || '\u2014') + '</td>' +
258
+ '</tr>';
1281
259
  }
260
+ html += '</tbody></table>';
261
+ wrap.innerHTML = html;
1282
262
 
1283
- const convoyIds = pipeline.convoy_ids || [];
1284
- const pipelineConvoys = convoyIds
1285
- .map((id) => convoys.find((c) => c.id === id))
1286
- .filter(Boolean);
1287
-
1288
- const total = pipelineConvoys.length;
1289
- const done = pipelineConvoys.filter((c) => c.status === 'done').length;
1290
- const failed = pipelineConvoys.filter(
1291
- (c) => c.status === 'failed' || c.status === 'gate-failed'
1292
- ).length;
1293
- const totalTasks = pipelineConvoys.reduce((sum, c) => {
1294
- const s = c.summary || {};
1295
- return sum + (s.total || (c.tasks ? c.tasks.length : 0));
1296
- }, 0);
1297
- const doneTasks = pipelineConvoys.reduce((sum, c) => {
1298
- const s = c.summary || {};
1299
- return sum + (s.done || 0);
1300
- }, 0);
1301
- const totalTokens = pipelineConvoys.reduce((sum, c) => sum + (c.total_tokens || 0), 0);
1302
-
1303
- const pct =
1304
- totalTasks > 0
1305
- ? Math.round((doneTasks / totalTasks) * 100)
1306
- : total > 0
1307
- ? Math.round((done / total) * 100)
1308
- : 0;
1309
-
1310
- const statusClass =
1311
- pipeline.status === 'done'
1312
- ? 'success'
1313
- : pipeline.status === 'failed' || pipeline.status === 'gate-failed'
1314
- ? 'failed'
1315
- : pipeline.status === 'running'
1316
- ? 'partial'
1317
- : '';
1318
-
1319
- let html = '<div class="convoy-overview">';
1320
- html +=
1321
- '<div class="convoy-stat"><span class="convoy-stat__label">Status</span>' +
1322
- '<span class="outcome-badge outcome-badge--' + statusClass + '">' +
1323
- escapeHtml(pipeline.status || 'unknown') + '</span></div>';
1324
- html +=
1325
- '<div class="convoy-stat"><span class="convoy-stat__label">Branch</span>' +
1326
- '<span class="convoy-stat__value">' + escapeHtml(pipeline.branch || '\u2014') + '</span></div>';
1327
- html +=
1328
- '<div class="convoy-stat"><span class="convoy-stat__label">Convoys</span>' +
1329
- '<span class="convoy-stat__value">' + done + '/' + total + '</span></div>';
1330
- if (totalTasks > 0) {
1331
- html +=
1332
- '<div class="convoy-stat"><span class="convoy-stat__label">Tasks</span>' +
1333
- '<span class="convoy-stat__value">' + doneTasks + '/' + totalTasks + '</span></div>';
1334
- }
1335
- if (totalTokens > 0) {
1336
- html +=
1337
- '<div class="convoy-stat"><span class="convoy-stat__label">Tokens</span>' +
1338
- '<span class="convoy-stat__value">' + formatTokens(totalTokens) + '</span></div>';
1339
- }
1340
- html +=
1341
- '<div class="convoy-stat"><span class="convoy-stat__label">Started</span>' +
1342
- '<span class="convoy-stat__value">' +
1343
- (pipeline.started_at ? formatTime(pipeline.started_at) : '\u2014') + '</span></div>';
1344
- if (pipeline.finished_at) {
1345
- html +=
1346
- '<div class="convoy-stat"><span class="convoy-stat__label">Finished</span>' +
1347
- '<span class="convoy-stat__value">' + formatTime(pipeline.finished_at) + '</span></div>';
1348
- }
1349
- const pipelineDur = formatDuration(pipeline.started_at, pipeline.finished_at);
1350
- if (pipelineDur) {
1351
- html +=
1352
- '<div class="convoy-stat"><span class="convoy-stat__label">Duration</span>' +
1353
- '<span class="convoy-stat__value">' + pipelineDur + '</span></div>';
1354
- }
1355
- if (pipeline.total_cost_usd != null && pipeline.total_cost_usd > 0) {
1356
- html +=
1357
- '<div class="convoy-stat"><span class="convoy-stat__label">Cost</span>' +
1358
- '<span class="convoy-stat__value">$' + pipeline.total_cost_usd.toFixed(2) + '</span></div>';
1359
- }
1360
- html += '</div>';
1361
-
1362
- // Convoy chain visualization
1363
- html += '<div class="convoy-chain">';
1364
- pipelineConvoys.forEach((convoy, i) => {
1365
- const cs = convoy.summary || {};
1366
- const cDone = cs.done || 0;
1367
- const cTotal = cs.total || (convoy.tasks ? convoy.tasks.length : 0);
1368
- const cTokens = convoy.total_tokens || 0;
1369
- const isActive =
1370
- (pipeline.current_convoy_id && pipeline.current_convoy_id === convoy.id) ||
1371
- convoy.status === 'running';
1372
- const nodeStatusClass =
1373
- convoy.status === 'done'
1374
- ? 'done'
1375
- : convoy.status === 'failed' || convoy.status === 'gate-failed'
1376
- ? 'failed'
1377
- : isActive
1378
- ? 'active'
1379
- : 'pending';
1380
- const badgeClass =
1381
- convoy.status === 'done'
1382
- ? 'success'
1383
- : convoy.status === 'failed' || convoy.status === 'gate-failed'
1384
- ? 'failed'
1385
- : convoy.status === 'running'
1386
- ? 'partial'
1387
- : '';
1388
-
1389
- if (i > 0) {
1390
- html += '<div class="convoy-chain__connector">\u2192</div>';
1391
- }
1392
- html +=
1393
- '<div class="convoy-chain__node convoy-chain__node--' + nodeStatusClass +
1394
- '" data-convoy-id="' + escapeHtml(convoy.id) + '" title="Click to filter to this convoy">';
1395
- html += '<div class="convoy-chain__node-name">' + escapeHtml(convoy.name || convoy.id) + '</div>';
1396
- html +=
1397
- '<span class="outcome-badge outcome-badge--' + badgeClass + '">' +
1398
- escapeHtml(convoy.status) + '</span>';
1399
- if (cTotal > 0) {
1400
- html += '<div class="convoy-chain__node-meta">' + cDone + '/' + cTotal + ' tasks</div>';
1401
- }
1402
- if (cTokens > 0) {
1403
- html += '<div class="convoy-chain__node-meta">' + formatTokens(cTokens) + ' tokens</div>';
1404
- }
1405
- html += '</div>';
1406
- });
1407
- html += '</div>';
1408
-
1409
- // Progress bar
1410
- html += '<div class="convoy-progress">';
1411
- html +=
1412
- '<div class="convoy-progress__bar">' +
1413
- '<div class="convoy-progress__fill" style="width:' + pct + '%"></div></div>';
1414
- html +=
1415
- '<span class="convoy-progress__label">' + pct + '% complete' +
1416
- (failed > 0 ? ' \u00B7 ' + failed + ' failed' : '') + '</span>';
1417
- html += '</div>';
1418
-
1419
- bodyEl.innerHTML = html;
1420
-
1421
- // Click handlers for convoy drill-down
1422
- bodyEl.querySelectorAll('.convoy-chain__node').forEach((node) => {
1423
- node.addEventListener('click', () => {
1424
- const convoyId = node.dataset.convoyId;
1425
- const sel = document.getElementById('filter-convoy');
1426
- if (sel && convoyId) {
1427
- sel.value = convoyId;
1428
- applyFilters();
1429
- }
263
+ wrap.querySelectorAll('tr[data-convoy-id]').forEach(row => {
264
+ row.addEventListener('click', () => {
265
+ const id = row.dataset.convoyId;
266
+ const nameCell = row.querySelector('td strong');
267
+ showConvoyDetailView(id, nameCell ? nameCell.textContent : id);
1430
268
  });
1431
269
  });
1432
270
  }
@@ -1473,34 +311,10 @@ Export
1473
311
  return hr + 'h ' + remMin + 'm';
1474
312
  }
1475
313
 
1476
- // ── Convoy Selector ──────────────────────────────────────
1477
-
1478
- function populateConvoySelector() {
1479
- const data = window.__DASHBOARD_DATA__;
1480
- const select = document.getElementById('convoy-select');
1481
- if (!select || !data || !data.convoyList) return;
1482
-
1483
- select.innerHTML = '<option value="">Select convoy\u2026</option>';
1484
- const list = data.convoyList;
1485
- for (const c of list) {
1486
- const opt = document.createElement('option');
1487
- opt.value = c.id;
1488
- const dateStr = c.created_at ? c.created_at.slice(0, 10) : '';
1489
- opt.textContent = (c.name || c.id) + ' \u2014 ' + c.status + ' (' + dateStr + ')';
1490
- select.appendChild(opt);
1491
- }
1492
-
1493
- // Default: select latest (first in list)
1494
- if (list.length > 0) {
1495
- select.value = list[0].id;
1496
- loadConvoyDetail(list[0].id);
1497
- }
1498
- }
1499
-
1500
314
  async function loadConvoyDetail(convoyId) {
1501
315
  if (!convoyId) {
1502
316
  renderConvoyDetailHeader(null);
1503
- ['quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'].forEach(function(id) {
317
+ ['pipeline-section', 'agent-section', 'tier-section', 'model-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'execution-section', 'event-timeline-section', 'panel-section', 'reviews-section'].forEach(function(id) {
1504
318
  var el = document.getElementById(id);
1505
319
  if (el) el.style.display = 'none';
1506
320
  });
@@ -1517,15 +331,24 @@ Export
1517
331
  renderPhaseBreakdown(detail.tasks || []);
1518
332
  const tasksSection = document.getElementById('tasks-section');
1519
333
  if (tasksSection) tasksSection.style.display = '';
334
+ renderDetailPipeline(detail.tasks || []);
335
+ renderDetailAgentChart(detail.tasks || []);
336
+ renderDetailOutcomeChart(detail.tasks || []);
337
+ renderDetailTierChart(detail.tasks || []);
338
+ renderDetailMechanismChart(detail.events || []);
339
+ renderDetailModelChart(detail.tasks || []);
1520
340
  renderQualitySection(detail);
1521
341
  renderReliabilitySection(detail);
1522
342
  renderDriftSection(detail);
1523
343
  renderOutputsSection(detail);
344
+ renderDetailExecutionLog(detail.tasks || []);
1524
345
  renderEventTimeline(detail);
346
+ renderDetailPanelChart(detail.tasks || []);
347
+ renderDetailReviewsTable(detail.tasks || []);
1525
348
  } catch (e) {
1526
349
  console.error('Failed to load convoy detail:', e);
1527
350
  renderConvoyDetailHeader(null);
1528
- ['quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'].forEach(function(id) {
351
+ ['pipeline-section', 'agent-section', 'tier-section', 'model-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'execution-section', 'event-timeline-section', 'panel-section', 'reviews-section'].forEach(function(id) {
1529
352
  var el = document.getElementById(id);
1530
353
  if (el) el.style.display = 'none';
1531
354
  });
@@ -1533,15 +356,15 @@ Export
1533
356
  }
1534
357
 
1535
358
  function renderConvoyDetailHeader(detail) {
1536
- const nameEl = document.getElementById('selected-convoy-name');
1537
- const statusEl = document.getElementById('selected-convoy-status');
1538
- const metaEl = document.getElementById('selected-convoy-meta');
359
+ const nameEl = document.getElementById('detail-hero-title');
360
+ const statusEl = document.getElementById('detail-hero-status');
361
+ const metaEl = document.getElementById('detail-hero-meta');
1539
362
 
1540
363
  if (!detail || !detail.convoy) {
1541
364
  if (nameEl) nameEl.textContent = 'No convoy selected';
1542
365
  if (statusEl) { statusEl.textContent = ''; statusEl.className = 'status-badge'; }
1543
366
  if (metaEl) metaEl.innerHTML = '';
1544
- ['tasks-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'].forEach(function(id) {
367
+ ['tasks-section', 'pipeline-section', 'agent-section', 'tier-section', 'model-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'execution-section', 'event-timeline-section', 'panel-section', 'reviews-section'].forEach(function(id) {
1545
368
  var el = document.getElementById(id);
1546
369
  if (el) el.style.display = 'none';
1547
370
  });
@@ -1562,11 +385,6 @@ Export
1562
385
  'gate-failed': 'This run stopped because a quality check failed.',
1563
386
  'hook-failed': 'This run stopped because a lifecycle script failed.',
1564
387
  };
1565
- const explanationEl = document.getElementById('convoy-status-explanation');
1566
- if (explanationEl) {
1567
- explanationEl.textContent = statusExplanations[c.status] || '';
1568
- explanationEl.style.display = statusExplanations[c.status] ? '' : 'none';
1569
- }
1570
388
  if (metaEl) {
1571
389
  let html = '';
1572
390
  if (c.branch) html += '<span class="convoy-meta__item">🌿 ' + escapeHtml(c.branch) + '</span>';
@@ -1744,6 +562,390 @@ Export
1744
562
  el.innerHTML = html;
1745
563
  }
1746
564
 
565
+ // ── Convoy Detail: Derive Tier from Model ────────────────
566
+
567
+ function deriveTier(model) {
568
+ if (!model) return 'unknown';
569
+ var m = model.toLowerCase();
570
+ if (m.includes('opus')) return 'premium';
571
+ if (m.includes('sonnet') || m.includes('pro')) return 'standard';
572
+ if (m.includes('haiku') || m.includes('flash') || m.includes('mini')) return 'economy';
573
+ return 'utility';
574
+ }
575
+
576
+ // ── Convoy Detail: Pipeline View ─────────────────────────
577
+
578
+ function renderDetailPipeline(tasks) {
579
+ var el = document.getElementById('pipeline-view');
580
+ if (!el) return;
581
+
582
+ if (!tasks || tasks.length === 0) {
583
+ el.innerHTML = emptyStateHtml('pipeline', 'No pipeline activity yet', 'Tasks will flow through execution phases here.');
584
+ return;
585
+ }
586
+
587
+ var phases = {};
588
+ tasks.forEach(function(t) {
589
+ var p = t.phase != null ? t.phase : 1;
590
+ if (!phases[p]) phases[p] = 0;
591
+ phases[p]++;
592
+ });
593
+
594
+ var stageConfig = [
595
+ { label: 'Foundation', phase: 1, iconClass: 'pending', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="13" y2="14"/></svg>' },
596
+ { label: 'Integration', phase: 2, iconClass: 'active', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>' },
597
+ { label: 'Validation', phase: 3, iconClass: 'review', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' },
598
+ { label: 'QA Gate', phase: 4, iconClass: 'done', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>' },
599
+ ];
600
+
601
+ el.innerHTML =
602
+ '<div class="pipeline">' +
603
+ stageConfig.map(function(stage, i) {
604
+ return (i > 0 ? '<div class="pipeline-arrow">\u2192</div>' : '') +
605
+ '<div class="pipeline-stage">' +
606
+ '<div class="pipeline-stage__icon pipeline-stage__icon--' + stage.iconClass + '">' + stage.icon + '</div>' +
607
+ '<span class="pipeline-stage__count">' + (phases[stage.phase] || 0) + '</span>' +
608
+ '<span class="pipeline-stage__label">' + stage.label + '</span>' +
609
+ '</div>';
610
+ }).join('') +
611
+ '</div>';
612
+ }
613
+
614
+ // ── Convoy Detail: Agent Chart ───────────────────────────
615
+
616
+ function renderDetailAgentChart(tasks) {
617
+ var el = document.getElementById('agent-chart');
618
+ if (!el) return;
619
+
620
+ if (!tasks || tasks.length === 0) {
621
+ el.innerHTML = emptyStateHtml('agents', 'No agent data yet', 'A breakdown of tasks per agent will appear here.');
622
+ return;
623
+ }
624
+
625
+ var agentMap = {};
626
+ tasks.forEach(function(t) {
627
+ var agent = t.agent || 'unknown';
628
+ if (!agentMap[agent]) agentMap[agent] = { done: 0, failed: 0, running: 0, other: 0, total: 0 };
629
+ if (t.status === 'done') agentMap[agent].done++;
630
+ else if (t.status === 'running') agentMap[agent].running++;
631
+ else if (['failed', 'gate-failed', 'timed-out', 'hook-failed'].includes(t.status)) agentMap[agent].failed++;
632
+ else agentMap[agent].other++;
633
+ agentMap[agent].total++;
634
+ });
635
+
636
+ var agents = Object.entries(agentMap).sort(function(a, b) { return b[1].total - a[1].total; });
637
+ var maxTotal = Math.max.apply(null, agents.map(function(a) { return a[1].total; }));
638
+
639
+ el.innerHTML = agents.map(function(entry) {
640
+ var name = entry[0];
641
+ var data = entry[1];
642
+ return '<div class="bar-row">' +
643
+ '<span class="bar-label">' + escapeHtml(name) + '</span>' +
644
+ '<div class="bar-track">' +
645
+ (data.done > 0 ? '<div class="bar-segment bar--success" style="width: ' + ((data.done / maxTotal) * 100).toFixed(1) + '%"></div>' : '') +
646
+ (data.running > 0 ? '<div class="bar-segment" style="width: ' + ((data.running / maxTotal) * 100).toFixed(1) + '%; background: #3b82f6"></div>' : '') +
647
+ (data.failed > 0 ? '<div class="bar-segment bar--failed" style="width: ' + ((data.failed / maxTotal) * 100).toFixed(1) + '%"></div>' : '') +
648
+ (data.other > 0 ? '<div class="bar-segment" style="width: ' + ((data.other / maxTotal) * 100).toFixed(1) + '%; background: #64748b"></div>' : '') +
649
+ '</div>' +
650
+ '<span class="bar-value">' + data.total + '</span>' +
651
+ '</div>';
652
+ }).join('');
653
+ }
654
+
655
+ // ── Convoy Detail: Tier Donut Chart ──────────────────────
656
+
657
+ function renderDetailTierChart(tasks) {
658
+ var el = document.getElementById('tier-chart');
659
+ if (!el) return;
660
+
661
+ if (!tasks || tasks.length === 0) {
662
+ el.innerHTML = emptyStateHtml('tiers', 'No tier data yet', 'Model tier distribution will be shown as a donut chart.');
663
+ return;
664
+ }
665
+
666
+ var tierCounts = {};
667
+ tasks.forEach(function(t) {
668
+ var tier = deriveTier(t.model);
669
+ tierCounts[tier] = (tierCounts[tier] || 0) + 1;
670
+ });
671
+
672
+ var order = ['premium', 'standard', 'utility', 'economy', 'unknown'];
673
+ var tiers = order.filter(function(t) { return tierCounts[t]; }).map(function(t) { return { name: t, count: tierCounts[t] }; });
674
+ var total = tasks.length;
675
+ var r = 70;
676
+ var circumference = 2 * Math.PI * r;
677
+ var cumOffset = 0;
678
+
679
+ var circles = tiers.map(function(t) {
680
+ var pct = t.count / total;
681
+ var dashLen = pct * circumference;
682
+ var linecap = tiers.length === 1 ? 'butt' : 'round';
683
+ var segment = '<circle cx="90" cy="90" r="' + r + '" fill="none" stroke="' + (TIER_COLORS[t.name] || '#64748b') + '" stroke-width="18" stroke-dasharray="' + dashLen.toFixed(2) + ' ' + (circumference - dashLen).toFixed(2) + '" stroke-dashoffset="' + (-cumOffset).toFixed(2) + '" transform="rotate(-90 90 90)" stroke-linecap="' + linecap + '"/>';
684
+ cumOffset += dashLen;
685
+ return segment;
686
+ });
687
+
688
+ var legend = tiers.map(function(t) {
689
+ return '<div class="legend-item"><span class="legend-dot" style="background: ' + (TIER_COLORS[t.name] || '#64748b') + '"></span><span class="legend-name">' + t.name + '</span><span class="legend-count">' + t.count + ' (' + Math.round((t.count / total) * 100) + '%)</span></div>';
690
+ }).join('');
691
+
692
+ el.innerHTML =
693
+ '<div class="donut-container"><div class="donut-wrap"><svg viewBox="0 0 180 180" class="donut-svg">' + circles.join('') + '</svg><div class="donut-center"><span class="donut-total">' + total + '</span><span class="donut-total-label">total</span></div></div><div class="donut-legend">' + legend + '</div></div>';
694
+ }
695
+
696
+ // ── Convoy Detail: Mechanism Donut Chart ─────────────────
697
+
698
+ function renderDetailMechanismChart(events) {
699
+ var el = document.getElementById('mechanism-chart');
700
+ if (!el) return;
701
+
702
+ var MECH_COLORS = { 'sub-agent': '#3b82f6', 'background': '#a78bfa', 'unknown': '#64748b' };
703
+ var MECH_LABELS = { 'sub-agent': 'Sub-agent (inline)', 'background': 'Background (worktree)', 'unknown': 'Unknown' };
704
+
705
+ if (!events || events.length === 0) {
706
+ el.innerHTML = emptyStateHtml('mechanism', 'No delegation data yet', 'The split between sub-agent and background delegations will be shown here.');
707
+ return;
708
+ }
709
+
710
+ var mechCounts = {};
711
+ events.forEach(function(e) {
712
+ if (e.type === 'task_assigned' || e.type === 'task_started') {
713
+ var mech = (e.data && e.data.mechanism) || 'unknown';
714
+ mechCounts[mech] = (mechCounts[mech] || 0) + 1;
715
+ }
716
+ });
717
+
718
+ var mechOrder = ['sub-agent', 'background', 'unknown'];
719
+ var mechs = mechOrder.filter(function(m) { return mechCounts[m]; }).map(function(m) { return { name: m, count: mechCounts[m] }; });
720
+
721
+ if (mechs.length === 0) {
722
+ el.innerHTML = emptyStateHtml('mechanism', 'No delegation data yet', 'The split between sub-agent and background delegations will be shown here.');
723
+ return;
724
+ }
725
+
726
+ var total = mechs.reduce(function(s, m) { return s + m.count; }, 0);
727
+ var r = 70;
728
+ var circumference = 2 * Math.PI * r;
729
+ var cumOffset = 0;
730
+
731
+ var circles = mechs.map(function(m) {
732
+ var pct = m.count / total;
733
+ var dashLen = pct * circumference;
734
+ var linecap = mechs.length === 1 ? 'butt' : 'round';
735
+ var segment = '<circle cx="90" cy="90" r="' + r + '" fill="none" stroke="' + (MECH_COLORS[m.name] || '#64748b') + '" stroke-width="18" stroke-dasharray="' + dashLen.toFixed(2) + ' ' + (circumference - dashLen).toFixed(2) + '" stroke-dashoffset="' + (-cumOffset).toFixed(2) + '" transform="rotate(-90 90 90)" stroke-linecap="' + linecap + '"/>';
736
+ cumOffset += dashLen;
737
+ return segment;
738
+ });
739
+
740
+ var legend = mechs.map(function(m) {
741
+ return '<div class="legend-item"><span class="legend-dot" style="background: ' + (MECH_COLORS[m.name] || '#64748b') + '"></span><span class="legend-name">' + (MECH_LABELS[m.name] || m.name) + '</span><span class="legend-count">' + m.count + ' (' + Math.round((m.count / total) * 100) + '%)</span></div>';
742
+ }).join('');
743
+
744
+ el.innerHTML =
745
+ '<div class="donut-container"><div class="donut-wrap"><svg viewBox="0 0 180 180" class="donut-svg">' + circles.join('') + '</svg><div class="donut-center"><span class="donut-total">' + total + '</span><span class="donut-total-label">total</span></div></div><div class="donut-legend">' + legend + '</div></div>';
746
+ }
747
+
748
+ // ── Convoy Detail: Delegation Outcome Chart ──────────────
749
+
750
+ function renderDetailOutcomeChart(tasks) {
751
+ var el = document.getElementById('delegation-outcome-chart');
752
+ if (!el) return;
753
+
754
+ if (!tasks || tasks.length === 0) {
755
+ el.innerHTML = emptyStateHtml('outcomes', 'No outcome data yet', 'Task outcome distribution will be shown here.');
756
+ return;
757
+ }
758
+
759
+ var OUTCOME_COLORS = { done: '#22c55e', running: '#3b82f6', pending: '#64748b', assigned: '#64748b', failed: '#ef4444', 'gate-failed': '#f59e0b', 'timed-out': '#a78bfa', 'hook-failed': '#64748b', 'review-blocked': '#f59e0b', skipped: '#94a3b8' };
760
+
761
+ var outcomeCounts = {};
762
+ tasks.forEach(function(t) {
763
+ var outcome = t.status || 'unknown';
764
+ outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
765
+ });
766
+
767
+ var outcomes = Object.entries(outcomeCounts).sort(function(a, b) { return b[1] - a[1]; });
768
+ var maxCount = Math.max.apply(null, outcomes.map(function(o) { return o[1]; }));
769
+
770
+ el.innerHTML = outcomes.map(function(entry) {
771
+ var name = entry[0];
772
+ var count = entry[1];
773
+ return '<div class="bar-row">' +
774
+ '<span class="bar-label">' + escapeHtml(name) + '</span>' +
775
+ '<div class="bar-track"><div class="bar-segment" style="width: ' + ((count / maxCount) * 100).toFixed(1) + '%; background: ' + (OUTCOME_COLORS[name] || '#64748b') + '"></div></div>' +
776
+ '<span class="bar-value">' + count + '</span>' +
777
+ '</div>';
778
+ }).join('');
779
+ }
780
+
781
+ // ── Convoy Detail: Model Chart ───────────────────────────
782
+
783
+ function renderDetailModelChart(tasks) {
784
+ var el = document.getElementById('model-chart');
785
+ if (!el) return;
786
+
787
+ if (!tasks || tasks.length === 0) {
788
+ el.innerHTML = emptyStateHtml('models', 'No model data yet', 'Model utilization across tasks will be shown here.');
789
+ return;
790
+ }
791
+
792
+ var modelCounts = {};
793
+ tasks.forEach(function(t) {
794
+ if (t.model) modelCounts[t.model] = (modelCounts[t.model] || 0) + 1;
795
+ });
796
+
797
+ var models = Object.entries(modelCounts).sort(function(a, b) { return b[1] - a[1]; });
798
+
799
+ if (models.length === 0) {
800
+ el.innerHTML = emptyStateHtml('models', 'No model data yet', 'Model utilization across tasks will be shown here.');
801
+ return;
802
+ }
803
+
804
+ var maxCount = Math.max.apply(null, models.map(function(m) { return m[1]; }));
805
+
806
+ el.innerHTML = models.map(function(entry) {
807
+ var name = entry[0];
808
+ var count = entry[1];
809
+ return '<div class="bar-row">' +
810
+ '<span class="bar-label">' + escapeHtml(name) + '</span>' +
811
+ '<div class="bar-track"><div class="bar-segment" style="width: ' + ((count / maxCount) * 100).toFixed(1) + '%; background: ' + (MODEL_COLORS[name] || '#64748b') + '"></div></div>' +
812
+ '<span class="bar-value">' + count + '</span>' +
813
+ '</div>';
814
+ }).join('');
815
+ }
816
+
817
+ // ── Convoy Detail: Execution Log ─────────────────────────
818
+
819
+ function renderDetailExecutionLog(tasks) {
820
+ var el = document.getElementById('execution-log');
821
+ if (!el) return;
822
+
823
+ if (!tasks || tasks.length === 0) {
824
+ el.innerHTML = emptyStateHtml('execLog', 'No execution history yet', 'A step-by-step trace of task activity will appear here.');
825
+ return;
826
+ }
827
+
828
+ var sorted = tasks.slice().filter(function(t) { return t.started_at; }).sort(function(a, b) { return new Date(b.started_at) - new Date(a.started_at); }).slice(0, 10);
829
+
830
+ if (sorted.length === 0) {
831
+ el.innerHTML = emptyStateHtml('execLog', 'No execution history yet', 'A step-by-step trace of task activity will appear here.');
832
+ return;
833
+ }
834
+
835
+ var statusToOutcome = function(s) {
836
+ if (s === 'done') return 'success';
837
+ if (['failed', 'gate-failed', 'timed-out', 'hook-failed'].includes(s)) return 'failed';
838
+ return 'partial';
839
+ };
840
+
841
+ el.innerHTML =
842
+ '<div class="exec-log">' +
843
+ sorted.map(function(t, i) {
844
+ var outcome = statusToOutcome(t.status);
845
+ var dur = formatDuration(t.started_at, t.finished_at);
846
+ var fileCount = t.files ? t.files.length : 0;
847
+ return '<div class="exec-step">' +
848
+ '<div class="exec-step__indicator">' +
849
+ '<div class="exec-step__dot exec-step__dot--' + outcome + '">' + (OUTCOME_ICONS[outcome] || '') + '</div>' +
850
+ (i < sorted.length - 1 ? '<div class="exec-step__line"></div>' : '') +
851
+ '</div>' +
852
+ '<div class="exec-step__content">' +
853
+ '<div class="exec-step__header">' +
854
+ '<span class="exec-step__agent">' + escapeHtml(t.agent || 'unknown') + '</span>' +
855
+ '<span class="exec-step__badge exec-step__badge--' + outcome + '">' + escapeHtml(t.status) + '</span>' +
856
+ '</div>' +
857
+ '<div class="exec-step__task">' + escapeHtml(t.id) + '</div>' +
858
+ '<div class="exec-step__meta">' +
859
+ '<span class="exec-step__meta-item">\uD83D\uDD52 ' + formatTime(t.started_at) + '</span>' +
860
+ (dur ? '<span class="exec-step__meta-item">\u23F1 ' + dur + '</span>' : '') +
861
+ (fileCount > 0 ? '<span class="exec-step__meta-item">\uD83D\uDCC1 ' + fileCount + ' files</span>' : '') +
862
+ (t.model ? '<span class="exec-step__meta-item">\uD83E\uDD16 ' + escapeHtml(t.model) + '</span>' : '') +
863
+ (t.retries > 0 ? '<span class="exec-step__meta-item">\uD83D\uDD04 ' + t.retries + ' retries</span>' : '') +
864
+ (t.total_tokens != null ? '<span class="exec-step__meta-item">\uD83D\uDD24 ' + formatTokens(t.total_tokens) + '</span>' : '') +
865
+ '</div>' +
866
+ '</div>' +
867
+ '</div>';
868
+ }).join('') +
869
+ '</div>';
870
+ }
871
+
872
+ // ── Convoy Detail: Panel Reviews ─────────────────────────
873
+
874
+ function renderDetailPanelChart(tasks) {
875
+ var el = document.getElementById('panel-chart');
876
+ if (!el) return;
877
+
878
+ var panelTasks = (tasks || []).filter(function(t) { return t.panel_attempts > 0; });
879
+
880
+ if (panelTasks.length === 0) {
881
+ el.innerHTML = emptyStateHtml('panels', 'No panel reviews yet', 'Quality gate verdicts from majority-vote panels will be shown here.');
882
+ return;
883
+ }
884
+
885
+ el.innerHTML =
886
+ '<div class="panel-grid">' +
887
+ panelTasks.map(function(t) {
888
+ var verdictUpper = t.review_verdict ? t.review_verdict.toUpperCase() : 'PENDING';
889
+ var verdictClass = verdictUpper === 'PASS' ? 'PASS' : 'BLOCK';
890
+ return '<div class="panel-item">' +
891
+ '<div class="panel-item__header">' +
892
+ '<span class="panel-item__key">' + escapeHtml(t.id) + '</span>' +
893
+ '<span class="panel-item__verdict panel-item__verdict--' + verdictClass + '">' + verdictUpper + '</span>' +
894
+ '</div>' +
895
+ '<div class="panel-item__votes">' +
896
+ (verdictUpper === 'PASS' ? '<div class="panel-item__vote panel-item__vote--pass">\u2713</div>' : '<div class="panel-item__vote panel-item__vote--block">\u2717</div>') +
897
+ '</div>' +
898
+ '<div class="panel-item__meta">' +
899
+ '<span class="panel-item__meta-item">\uD83E\uDD16 ' + escapeHtml(t.review_model || 'unknown') + '</span>' +
900
+ (t.panel_attempts > 1 ? '<span class="panel-item__meta-item">\uD83D\uDD04 ' + t.panel_attempts + ' attempts</span>' : '') +
901
+ (t.review_tokens != null ? '<span class="panel-item__meta-item">\uD83D\uDD24 ' + formatTokens(t.review_tokens) + '</span>' : '') +
902
+ '</div>' +
903
+ '</div>';
904
+ }).join('') +
905
+ '</div>';
906
+ }
907
+
908
+ // ── Convoy Detail: Fast Reviews ──────────────────────────
909
+
910
+ function renderDetailReviewsTable(tasks) {
911
+ var el = document.getElementById('reviews-table');
912
+ if (!el) return;
913
+
914
+ var reviewedTasks = (tasks || []).filter(function(t) { return t.review_level != null; });
915
+
916
+ if (reviewedTasks.length === 0) {
917
+ el.innerHTML = emptyStateHtml('panels', 'No fast reviews yet', 'Single-reviewer quality gate results will be listed here.');
918
+ return;
919
+ }
920
+
921
+ el.innerHTML =
922
+ '<table class="sessions-table">' +
923
+ '<thead><tr>' +
924
+ '<th scope="col">Task ID</th>' +
925
+ '<th scope="col">Agent</th>' +
926
+ '<th scope="col">Review Level</th>' +
927
+ '<th scope="col">Verdict</th>' +
928
+ '<th scope="col">Model</th>' +
929
+ '<th scope="col">Tokens</th>' +
930
+ '</tr></thead>' +
931
+ '<tbody>' +
932
+ reviewedTasks.map(function(t) {
933
+ var verdictUpper = t.review_verdict ? t.review_verdict.toUpperCase() : '';
934
+ var verdictBadge = t.review_verdict
935
+ ? '<span class="status-badge status-badge--' + (verdictUpper === 'PASS' ? 'done' : 'failed') + '">' + escapeHtml(t.review_verdict) + '</span>'
936
+ : '<span style="opacity:0.4">\u2014</span>';
937
+ return '<tr>' +
938
+ '<td class="td-task">' + escapeHtml(t.id || '\u2014') + '</td>' +
939
+ '<td class="td-agent">' + escapeHtml(t.agent || '\u2014') + '</td>' +
940
+ '<td>' + escapeHtml(t.review_level || '\u2014') + '</td>' +
941
+ '<td>' + verdictBadge + '</td>' +
942
+ '<td>' + escapeHtml(t.review_model || '\u2014') + '</td>' +
943
+ '<td class="td-num">' + (t.review_tokens != null ? formatTokens(t.review_tokens) : '\u2014') + '</td>' +
944
+ '</tr>';
945
+ }).join('') +
946
+ '</tbody></table>';
947
+ }
948
+
1747
949
  // ── Quality Section ──────────────────────────────────────
1748
950
 
1749
951
  function renderQualitySection(detail) {
@@ -2108,57 +1310,22 @@ Export
2108
1310
  }
2109
1311
 
2110
1312
  async function main() {
2111
- const events = await loadNdjson(base + 'data/events.ndjson');
2112
- const pipelines = await loadNdjson(base + 'data/pipelines.ndjson');
2113
1313
  const convoys = window.__DASHBOARD_DATA__?.convoyList ?? [];
2114
1314
 
2115
- const sessions = events.filter((e) => e.type === 'session');
2116
- const delegations = events.filter((e) => e.type === 'delegation');
2117
- const panels = events.filter((e) => e.type === 'panel');
2118
- const reviews = events.filter((e) => e.type === 'review');
2119
-
2120
- rawSessions = sessions;
2121
- rawDelegations = delegations;
2122
- rawPanels = panels;
2123
- rawReviews = reviews;
2124
- rawConvoys = convoys;
2125
- rawPipelines = pipelines;
2126
-
2127
- populateAgentFilter(sessions, delegations, reviews);
2128
- populateConvoyFilter(convoys);
2129
- populatePipelineFilter(pipelines);
2130
-
2131
- // ── Read URL params ───────────────────────────────────
2132
1315
  const urlParams = new URLSearchParams(window.location.search);
2133
1316
  const convoyParam = urlParams.get('convoy');
1317
+
2134
1318
  if (convoyParam === 'active') {
2135
- const running = rawConvoys.find((c) => c.status === 'running');
2136
- const latest = rawConvoys.slice().sort((a, b) => b.created_at.localeCompare(a.created_at))[0];
1319
+ const running = convoys.find((c) => c.status === 'running');
1320
+ const latest = convoys.slice().sort((a, b) => b.created_at.localeCompare(a.created_at))[0];
2137
1321
  const target = running || latest;
2138
- if (target) {
2139
- const sel = document.getElementById('filter-convoy');
2140
- if (sel) sel.value = target.id;
2141
- }
1322
+ if (target) showConvoyDetailView(target.id, target.name || target.id);
2142
1323
  } else if (convoyParam) {
2143
- const sel = document.getElementById('filter-convoy');
2144
- if (sel) sel.value = convoyParam;
1324
+ showConvoyDetailView(convoyParam, convoyParam);
2145
1325
  }
2146
1326
 
2147
- renderAll(sessions, delegations, panels, reviews);
2148
-
2149
- // Apply convoy param after initial render (shows convoy section if needed)
2150
- if (convoyParam) applyFilters();
2151
-
2152
- // ── Overall stats + convoy selector ──────────────────
2153
1327
  renderOverallStats();
2154
- populateConvoySelector();
2155
-
2156
- const convoySelectEl = document.getElementById('convoy-select');
2157
- if (convoySelectEl) {
2158
- convoySelectEl.addEventListener('change', function() {
2159
- loadConvoyDetail(this.value);
2160
- });
2161
- }
1328
+ renderConvoyList();
2162
1329
 
2163
1330
  var loadMoreBtn = document.getElementById('event-timeline-more-btn');
2164
1331
  if (loadMoreBtn) {
@@ -2168,66 +1335,47 @@ Export
2168
1335
  });
2169
1336
  }
2170
1337
 
2171
- // ── Filter event listeners ────────────────────────────
2172
- document.getElementById('filter-date-from')?.addEventListener('change', applyFilters);
2173
- document.getElementById('filter-date-to')?.addEventListener('change', applyFilters);
2174
- document.getElementById('filter-agent')?.addEventListener('change', applyFilters);
2175
- document.getElementById('filter-outcome')?.addEventListener('change', applyFilters);
2176
- document.getElementById('filter-convoy')?.addEventListener('change', applyFilters);
2177
- document.getElementById('filter-pipeline')?.addEventListener('change', applyFilters);
2178
- document.getElementById('filter-reset')?.addEventListener('click', () => {
2179
- document.getElementById('filter-date-from').value = '';
2180
- document.getElementById('filter-date-to').value = '';
2181
- document.getElementById('filter-agent').value = '';
2182
- document.getElementById('filter-outcome').value = '';
2183
- document.getElementById('filter-convoy').value = '';
2184
- document.getElementById('filter-pipeline').value = '';
2185
- applyFilters();
1338
+ document.getElementById('breadcrumbs-home')?.addEventListener('click', (e) => {
1339
+ e.preventDefault();
1340
+ showHomeView();
2186
1341
  });
2187
1342
 
2188
- // ── Auto-refresh for live convoy monitoring ───────────
2189
- let refreshInterval = null;
2190
- function startAutoRefresh() {
2191
- if (refreshInterval) return;
2192
- refreshInterval = setInterval(async () => {
2193
- const freshEvents = await loadNdjson(base + 'data/events.ndjson');
2194
- const freshConvoys = await loadJson(base + 'data/convoy-list.json');
2195
- const freshPipelines = await loadNdjson(base + 'data/pipelines.ndjson');
2196
- rawSessions = freshEvents.filter((e) => e.type === 'session');
2197
- rawDelegations = freshEvents.filter((e) => e.type === 'delegation');
2198
- rawPanels = freshEvents.filter((e) => e.type === 'panel');
2199
- rawReviews = freshEvents.filter((e) => e.type === 'review');
2200
- rawConvoys = freshConvoys;
2201
- rawPipelines = freshPipelines;
2202
- const currentValue = document.getElementById('filter-convoy')?.value;
2203
- const currentPipelineValue = document.getElementById('filter-pipeline')?.value;
2204
- populateConvoyFilter(freshConvoys);
2205
- populatePipelineFilter(freshPipelines);
2206
- const sel = document.getElementById('filter-convoy');
2207
- if (sel && currentValue) sel.value = currentValue;
2208
- const pSel = document.getElementById('filter-pipeline');
2209
- if (pSel && currentPipelineValue) pSel.value = currentPipelineValue;
2210
- applyFilters();
2211
- }, 5000);
2212
- }
2213
-
2214
- const selectedConvoy = rawConvoys.find((c) => c.id === document.getElementById('filter-convoy')?.value);
2215
- if (convoyParam === 'active' || (selectedConvoy && selectedConvoy.status === 'running')) {
2216
- startAutoRefresh();
2217
- }
1343
+ document.getElementById('cl-filter-search')?.addEventListener('input', renderConvoyList);
1344
+ document.getElementById('cl-filter-status')?.addEventListener('change', renderConvoyList);
1345
+ document.getElementById('cl-filter-from')?.addEventListener('change', renderConvoyList);
1346
+ document.getElementById('cl-filter-to')?.addEventListener('change', renderConvoyList);
1347
+ document.getElementById('cl-filter-reset')?.addEventListener('click', () => {
1348
+ const s = document.getElementById('cl-filter-search'); if (s) s.value = '';
1349
+ const st = document.getElementById('cl-filter-status'); if (st) st.value = '';
1350
+ const f = document.getElementById('cl-filter-from'); if (f) f.value = '';
1351
+ const t = document.getElementById('cl-filter-to'); if (t) t.value = '';
1352
+ renderConvoyList();
1353
+ });
2218
1354
 
2219
- // ── Export button ─────────────────────────────────────
2220
- document.getElementById('export-btn')?.addEventListener('click', exportData);
1355
+ window.addEventListener('popstate', () => {
1356
+ const params = new URLSearchParams(window.location.search);
1357
+ const c = params.get('convoy');
1358
+ if (c) {
1359
+ loadConvoyDetail(c);
1360
+ const home = document.getElementById('view-home');
1361
+ const detail = document.getElementById('view-convoy-detail');
1362
+ const breadcrumbs = document.getElementById('breadcrumbs');
1363
+ if (home) home.dataset.viewHidden = '';
1364
+ if (detail) delete detail.dataset.viewHidden;
1365
+ if (breadcrumbs) delete breadcrumbs.dataset.viewHidden;
1366
+ document.querySelectorAll('.dash-sidebar__link[data-view="home"]').forEach(el => el.closest('li').style.display = 'none');
1367
+ document.querySelectorAll('.dash-sidebar__link[data-view="detail"]').forEach(el => el.closest('li').style.display = '');
1368
+ } else {
1369
+ showHomeView();
1370
+ }
1371
+ });
2221
1372
 
2222
- // ── Sidebar Navigation ────────────────────────────────
2223
1373
  initSidebarNav();
2224
1374
  }
2225
-
2226
1375
  function initSidebarNav() {
2227
- const links = document.querySelectorAll('.dash-sidebar__link');
2228
- const sections = document.querySelectorAll('[data-nav-section]');
1376
+ const links = document.querySelectorAll(".dash-sidebar__link");
1377
+ const sections = document.querySelectorAll("[data-nav-section]");
2229
1378
 
2230
- // Intersection observer for active state
2231
1379
  const observer = new IntersectionObserver(
2232
1380
  (entries) => {
2233
1381
  entries.forEach((entry) => {
@@ -2235,55 +1383,50 @@ Export
2235
1383
  const id = entry.target.id;
2236
1384
  links.forEach((link) => {
2237
1385
  link.classList.toggle(
2238
- 'dash-sidebar__link--active',
1386
+ "dash-sidebar__link--active",
2239
1387
  link.dataset.section === id
2240
1388
  );
2241
1389
  });
2242
1390
  }
2243
1391
  });
2244
1392
  },
2245
- { rootMargin: '-20% 0px -70% 0px', threshold: 0 }
1393
+ { rootMargin: "-20% 0px -70% 0px", threshold: 0 }
2246
1394
  );
2247
1395
 
2248
1396
  sections.forEach((s) => observer.observe(s));
2249
1397
 
2250
- // Convoy-detail sections that start hidden and need a convoy selected first
2251
- const CONVOY_DETAIL_SECTIONS = ['tasks-section', 'quality-section', 'reliability-section', 'drift-section', 'outputs-section', 'event-timeline-section'];
1398
+ const CONVOY_DETAIL_SECTIONS = ["tasks-section", "pipeline-section", "agent-section", "tier-section", "model-section", "quality-section", "reliability-section", "drift-section", "outputs-section", "execution-section", "event-timeline-section", "panel-section", "reviews-section"];
2252
1399
 
2253
- // Smooth scroll on click
2254
1400
  links.forEach((link) => {
2255
- link.addEventListener('click', async (e) => {
1401
+ link.addEventListener("click", async (e) => {
2256
1402
  e.preventDefault();
2257
1403
  const sectionId = link.dataset.section;
2258
1404
 
2259
1405
  if (CONVOY_DETAIL_SECTIONS.includes(sectionId)) {
2260
- // Auto-select and load convoy detail if none selected yet
2261
- const convoySelectEl = document.getElementById('convoy-select');
2262
- if (convoySelectEl && !convoySelectEl.value && convoySelectEl.options.length > 1) {
2263
- convoySelectEl.value = convoySelectEl.options[1].value;
2264
- await loadConvoyDetail(convoySelectEl.value);
2265
- }
2266
- } else if (sectionId === 'convoy-section') {
2267
- const convoyFilterEl = document.getElementById('filter-convoy');
2268
- if (convoyFilterEl && !convoyFilterEl.value && convoyFilterEl.options.length > 1) {
2269
- convoyFilterEl.value = convoyFilterEl.options[1].value;
2270
- applyFilters();
2271
- }
2272
- } else if (sectionId === 'convoy-pipeline-section') {
2273
- const pipelineSelect = document.getElementById('filter-pipeline');
2274
- if (pipelineSelect && !pipelineSelect.value && pipelineSelect.options.length > 1) {
2275
- pipelineSelect.value = pipelineSelect.options[1].value;
2276
- applyFilters();
1406
+ const convoyList = window.__DASHBOARD_DATA__?.convoyList ?? [];
1407
+ if (!window.__SELECTED_CONVOY__ && convoyList.length > 0) {
1408
+ showConvoyDetailView(convoyList[0].id, convoyList[0].name || convoyList[0].id);
2277
1409
  }
2278
1410
  }
2279
1411
 
2280
1412
  const target = document.getElementById(sectionId);
2281
1413
  if (target) {
2282
- target.scrollIntoView({ behavior: 'smooth', block: 'start' });
1414
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
2283
1415
  }
2284
1416
  });
2285
1417
  });
1418
+
1419
+ // Set initial nav visibility based on current view
1420
+ const initialParams = new URLSearchParams(window.location.search);
1421
+ if (initialParams.get('convoy')) {
1422
+ document.querySelectorAll('.dash-sidebar__link[data-view="home"]').forEach(el => el.closest('li').style.display = 'none');
1423
+ document.querySelectorAll('.dash-sidebar__link[data-view="detail"]').forEach(el => el.closest('li').style.display = '');
1424
+ } else {
1425
+ document.querySelectorAll('.dash-sidebar__link[data-view="detail"]').forEach(el => el.closest('li').style.display = 'none');
1426
+ document.querySelectorAll('.dash-sidebar__link[data-view="home"]').forEach(el => el.closest('li').style.display = '');
1427
+ }
2286
1428
  }
2287
1429
 
1430
+
2288
1431
  main();
2289
1432
  })();</script>