opencastle 0.17.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.
@@ -26,6 +26,7 @@ const base = import.meta.env.BASE_URL;
26
26
  <!-- Sidebar Navigation -->
27
27
  <nav class="dash-sidebar" id="dash-sidebar">
28
28
  <ul class="dash-sidebar__list">
29
+ <li><a class="dash-sidebar__link" href="#convoy-section" data-section="convoy-section">Convoy</a></li>
29
30
  <li><a class="dash-sidebar__link dash-sidebar__link--active" href="#kpi-row" data-section="kpi-row">Overview</a></li>
30
31
  <li><a class="dash-sidebar__link" href="#pipeline-section" data-section="pipeline-section">Pipeline</a></li>
31
32
  <li><a class="dash-sidebar__link" href="#agent-section" data-section="agent-section">Agents</a></li>
@@ -66,9 +67,25 @@ const base = import.meta.env.BASE_URL;
66
67
  <option value="failed">Failed</option>
67
68
  </select>
68
69
  </div>
70
+ <div class="filter-group">
71
+ <label class="filter-label" for="filter-convoy">Convoy</label>
72
+ <select class="filter-select" id="filter-convoy">
73
+ <option value="">All convoys</option>
74
+ </select>
75
+ </div>
69
76
  <button class="dash-btn dash-btn--ghost filter-reset" id="filter-reset" type="button">Reset</button>
70
77
  </div>
71
78
 
79
+ <!-- Convoy Status Section -->
80
+ <section class="chart-card convoy-status" id="convoy-section" data-nav-section style="display:none">
81
+ <div class="chart-card__header">
82
+ <h2 class="chart-card__title">Convoy Status</h2>
83
+ <p class="chart-card__desc" id="convoy-desc">Select a convoy to view details</p>
84
+ </div>
85
+ <div class="chart-card__body" id="convoy-body">
86
+ </div>
87
+ </section>
88
+
72
89
  <!-- KPI Row -->
73
90
  <section class="kpi-row" id="kpi-row" data-nav-section>
74
91
  <div class="kpi-card" id="kpi-sessions">
@@ -1154,12 +1171,14 @@ const base = import.meta.env.BASE_URL;
1154
1171
  let rawDelegations = [];
1155
1172
  let rawPanels = [];
1156
1173
  let rawReviews = [];
1174
+ let rawConvoys = [];
1157
1175
 
1158
1176
  function applyFilters() {
1159
1177
  const dateFrom = document.getElementById('filter-date-from').value;
1160
1178
  const dateTo = document.getElementById('filter-date-to').value;
1161
1179
  const agentFilter = document.getElementById('filter-agent').value;
1162
1180
  const outcomeFilter = document.getElementById('filter-outcome').value;
1181
+ const convoyFilter = document.getElementById('filter-convoy').value;
1163
1182
 
1164
1183
  function matchDate(ts) {
1165
1184
  const date = ts.slice(0, 10);
@@ -1168,27 +1187,43 @@ const base = import.meta.env.BASE_URL;
1168
1187
  return true;
1169
1188
  }
1170
1189
 
1171
- const sessions = rawSessions.filter((s) => {
1190
+ let sessions = rawSessions.filter((s) => {
1172
1191
  if (!matchDate(s.timestamp)) return false;
1173
1192
  if (agentFilter && s.agent !== agentFilter) return false;
1174
1193
  if (outcomeFilter && s.outcome !== outcomeFilter) return false;
1175
1194
  return true;
1176
1195
  });
1177
1196
 
1178
- const delegations = rawDelegations.filter((d) => {
1197
+ let delegations = rawDelegations.filter((d) => {
1179
1198
  if (!matchDate(d.timestamp)) return false;
1180
1199
  if (agentFilter && d.agent !== agentFilter) return false;
1181
1200
  if (outcomeFilter && d.outcome !== outcomeFilter) return false;
1182
1201
  return true;
1183
1202
  });
1184
1203
 
1185
- const panels = rawPanels.filter((p) => matchDate(p.timestamp));
1186
- const reviews = rawReviews.filter((r) => {
1204
+ let panels = rawPanels.filter((p) => matchDate(p.timestamp));
1205
+ let reviews = rawReviews.filter((r) => {
1187
1206
  if (!matchDate(r.timestamp)) return false;
1188
1207
  if (agentFilter && r.agent !== agentFilter) return false;
1189
1208
  return true;
1190
1209
  });
1191
1210
 
1211
+ if (convoyFilter) {
1212
+ sessions = sessions.filter((s) => s.convoy_id === convoyFilter);
1213
+ delegations = delegations.filter((d) => d.convoy_id === convoyFilter);
1214
+ panels = panels.filter((p) => p.convoy_id === convoyFilter);
1215
+ reviews = reviews.filter((r) => r.convoy_id === convoyFilter);
1216
+ }
1217
+
1218
+ const convoySection = document.getElementById('convoy-section');
1219
+ if (convoySection) {
1220
+ convoySection.style.display = convoyFilter ? '' : 'none';
1221
+ if (convoyFilter) {
1222
+ const convoy = rawConvoys.find((c) => c.id === convoyFilter);
1223
+ renderConvoyStatus(convoy);
1224
+ }
1225
+ }
1226
+
1192
1227
  renderAll(sessions, delegations, panels, reviews);
1193
1228
  }
1194
1229
 
@@ -1300,8 +1335,80 @@ const base = import.meta.env.BASE_URL;
1300
1335
  URL.revokeObjectURL(url);
1301
1336
  }
1302
1337
 
1338
+ function populateConvoyFilter(convoys) {
1339
+ const select = document.getElementById('filter-convoy');
1340
+ if (!select) return;
1341
+ while (select.options.length > 1) select.remove(1);
1342
+ const sorted = convoys.slice().sort((a, b) => b.created_at.localeCompare(a.created_at));
1343
+ sorted.forEach((c) => {
1344
+ const opt = document.createElement('option');
1345
+ opt.value = c.id;
1346
+ opt.textContent = c.name + ' (' + c.status + ')';
1347
+ select.appendChild(opt);
1348
+ });
1349
+ }
1350
+
1351
+ function renderConvoyStatus(convoy) {
1352
+ const descEl = document.getElementById('convoy-desc');
1353
+ const bodyEl = document.getElementById('convoy-body');
1354
+ if (!descEl || !bodyEl) return;
1355
+
1356
+ if (!convoy) {
1357
+ bodyEl.innerHTML = emptyStateHtml('pipeline', 'Convoy not found', 'No matching convoy data available.');
1358
+ return;
1359
+ }
1360
+
1361
+ descEl.textContent = convoy.name + ' — ' + (convoy.branch || 'no branch');
1362
+
1363
+ const s = convoy.summary || {};
1364
+ const total = s.total || (convoy.tasks ? convoy.tasks.length : 0);
1365
+ const done = s.done || 0;
1366
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0;
1367
+
1368
+ const statusClass = convoy.status === 'done' ? 'success'
1369
+ : (convoy.status === 'failed' || convoy.status === 'gate-failed') ? 'failed'
1370
+ : convoy.status === 'running' ? 'partial' : '';
1371
+
1372
+ let html = '';
1373
+ html += '<div class="convoy-overview">';
1374
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Status</span><span class="outcome-badge outcome-badge--' + statusClass + '">' + escapeHtml(convoy.status) + '</span></div>';
1375
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Branch</span><span class="convoy-stat__value">' + escapeHtml(convoy.branch || '\u2014') + '</span></div>';
1376
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Tasks</span><span class="convoy-stat__value">' + done + '/' + total + '</span></div>';
1377
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Events</span><span class="convoy-stat__value">' + (convoy.events_count || 0) + '</span></div>';
1378
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Started</span><span class="convoy-stat__value">' + (convoy.started_at ? formatTime(convoy.started_at) : '\u2014') + '</span></div>';
1379
+ html += '</div>';
1380
+
1381
+ html += '<div class="convoy-progress">';
1382
+ html += '<div class="convoy-progress__bar"><div class="convoy-progress__fill" style="width:' + pct + '%"></div></div>';
1383
+ html += '<span class="convoy-progress__label">' + pct + '% complete</span>';
1384
+ html += '</div>';
1385
+
1386
+ if (convoy.tasks && convoy.tasks.length > 0) {
1387
+ html += '<table class="sessions-table convoy-tasks">';
1388
+ html += '<thead><tr><th>Task</th><th>Phase</th><th>Agent</th><th>Adapter</th><th>Status</th><th>Retries</th></tr></thead>';
1389
+ html += '<tbody>';
1390
+ convoy.tasks.forEach(function(t) {
1391
+ const tStatus = t.status === 'done' ? 'success'
1392
+ : (t.status === 'failed' || t.status === 'timed-out') ? 'failed'
1393
+ : t.status === 'running' ? 'partial' : '';
1394
+ html += '<tr>';
1395
+ html += '<td>' + escapeHtml(t.id) + '</td>';
1396
+ html += '<td class="td-num">' + t.phase + '</td>';
1397
+ html += '<td class="td-agent">' + escapeHtml(t.agent) + '</td>';
1398
+ html += '<td>' + escapeHtml(t.adapter || '\u2014') + '</td>';
1399
+ html += '<td><span class="outcome-badge outcome-badge--' + tStatus + '">' + escapeHtml(t.status) + '</span></td>';
1400
+ html += '<td class="td-num">' + (t.retries || 0) + '</td>';
1401
+ html += '</tr>';
1402
+ });
1403
+ html += '</tbody></table>';
1404
+ }
1405
+
1406
+ bodyEl.innerHTML = html;
1407
+ }
1408
+
1303
1409
  async function main() {
1304
1410
  const events = await loadNdjson(base + 'data/events.ndjson');
1411
+ const convoys = await loadNdjson(base + 'data/convoys.ndjson');
1305
1412
 
1306
1413
  const sessions = events.filter((e) => e.type === 'session');
1307
1414
  const delegations = events.filter((e) => e.type === 'delegation');
@@ -1312,23 +1419,72 @@ const base = import.meta.env.BASE_URL;
1312
1419
  rawDelegations = delegations;
1313
1420
  rawPanels = panels;
1314
1421
  rawReviews = reviews;
1422
+ rawConvoys = convoys;
1315
1423
 
1316
1424
  populateAgentFilter(sessions, delegations, reviews);
1425
+ populateConvoyFilter(convoys);
1426
+
1427
+ // ── Read URL params ───────────────────────────────────
1428
+ const urlParams = new URLSearchParams(window.location.search);
1429
+ const convoyParam = urlParams.get('convoy');
1430
+ if (convoyParam === 'active') {
1431
+ const running = rawConvoys.find((c) => c.status === 'running');
1432
+ const latest = rawConvoys.slice().sort((a, b) => b.created_at.localeCompare(a.created_at))[0];
1433
+ const target = running || latest;
1434
+ if (target) {
1435
+ const sel = document.getElementById('filter-convoy');
1436
+ if (sel) sel.value = target.id;
1437
+ }
1438
+ } else if (convoyParam) {
1439
+ const sel = document.getElementById('filter-convoy');
1440
+ if (sel) sel.value = convoyParam;
1441
+ }
1442
+
1317
1443
  renderAll(sessions, delegations, panels, reviews);
1318
1444
 
1445
+ // Apply convoy param after initial render (shows convoy section if needed)
1446
+ if (convoyParam) applyFilters();
1447
+
1319
1448
  // ── Filter event listeners ────────────────────────────
1320
1449
  document.getElementById('filter-date-from')?.addEventListener('change', applyFilters);
1321
1450
  document.getElementById('filter-date-to')?.addEventListener('change', applyFilters);
1322
1451
  document.getElementById('filter-agent')?.addEventListener('change', applyFilters);
1323
1452
  document.getElementById('filter-outcome')?.addEventListener('change', applyFilters);
1453
+ document.getElementById('filter-convoy')?.addEventListener('change', applyFilters);
1324
1454
  document.getElementById('filter-reset')?.addEventListener('click', () => {
1325
1455
  document.getElementById('filter-date-from').value = '';
1326
1456
  document.getElementById('filter-date-to').value = '';
1327
1457
  document.getElementById('filter-agent').value = '';
1328
1458
  document.getElementById('filter-outcome').value = '';
1459
+ document.getElementById('filter-convoy').value = '';
1329
1460
  applyFilters();
1330
1461
  });
1331
1462
 
1463
+ // ── Auto-refresh for live convoy monitoring ───────────
1464
+ let refreshInterval = null;
1465
+ function startAutoRefresh() {
1466
+ if (refreshInterval) return;
1467
+ refreshInterval = setInterval(async () => {
1468
+ const freshEvents = await loadNdjson(base + 'data/events.ndjson');
1469
+ const freshConvoys = await loadNdjson(base + 'data/convoys.ndjson');
1470
+ rawSessions = freshEvents.filter((e) => e.type === 'session');
1471
+ rawDelegations = freshEvents.filter((e) => e.type === 'delegation');
1472
+ rawPanels = freshEvents.filter((e) => e.type === 'panel');
1473
+ rawReviews = freshEvents.filter((e) => e.type === 'review');
1474
+ rawConvoys = freshConvoys;
1475
+ const currentValue = document.getElementById('filter-convoy')?.value;
1476
+ populateConvoyFilter(freshConvoys);
1477
+ const sel = document.getElementById('filter-convoy');
1478
+ if (sel && currentValue) sel.value = currentValue;
1479
+ applyFilters();
1480
+ }, 5000);
1481
+ }
1482
+
1483
+ const selectedConvoy = rawConvoys.find((c) => c.id === document.getElementById('filter-convoy')?.value);
1484
+ if (convoyParam === 'active' || (selectedConvoy && selectedConvoy.status === 'running')) {
1485
+ startAutoRefresh();
1486
+ }
1487
+
1332
1488
  // ── Export button ─────────────────────────────────────
1333
1489
  document.getElementById('export-btn')?.addEventListener('click', exportData);
1334
1490
 
@@ -1533,3 +1533,63 @@ body {
1533
1533
  padding: 8px 6px;
1534
1534
  }
1535
1535
  }
1536
+
1537
+ /* ---------- Convoy Section ---------- */
1538
+ .convoy-overview {
1539
+ display: flex;
1540
+ flex-wrap: wrap;
1541
+ gap: 24px;
1542
+ margin-bottom: 20px;
1543
+ }
1544
+
1545
+ .convoy-stat {
1546
+ display: flex;
1547
+ flex-direction: column;
1548
+ gap: 4px;
1549
+ }
1550
+
1551
+ .convoy-stat__label {
1552
+ font-size: 0.75rem;
1553
+ color: var(--text-tertiary);
1554
+ text-transform: uppercase;
1555
+ letter-spacing: 0.05em;
1556
+ }
1557
+
1558
+ .convoy-stat__value {
1559
+ font-size: 0.95rem;
1560
+ color: var(--text-primary);
1561
+ }
1562
+
1563
+ /* Progress bar */
1564
+ .convoy-progress {
1565
+ display: flex;
1566
+ align-items: center;
1567
+ gap: 12px;
1568
+ margin-bottom: 20px;
1569
+ }
1570
+
1571
+ .convoy-progress__bar {
1572
+ flex: 1;
1573
+ height: 8px;
1574
+ background: var(--bg-tertiary);
1575
+ border-radius: 4px;
1576
+ overflow: hidden;
1577
+ }
1578
+
1579
+ .convoy-progress__fill {
1580
+ height: 100%;
1581
+ background: var(--gradient-accent);
1582
+ border-radius: 4px;
1583
+ transition: width var(--transition-base);
1584
+ }
1585
+
1586
+ .convoy-progress__label {
1587
+ font-size: 0.8rem;
1588
+ color: var(--text-secondary);
1589
+ white-space: nowrap;
1590
+ }
1591
+
1592
+ /* Task table inherits .sessions-table styles */
1593
+ .convoy-tasks {
1594
+ margin-top: 8px;
1595
+ }