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