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