179b420270
A sidecar + web UI that turns a running 4-hour dispatcher test from
"tail three logs and guess" into a real-time status grid.
tools/live_log_writer.py — sidecar. Tails the implementer + review
dispatcher logs, watches OpenCode session archives + Phase-4
telemetry JSONL, polls host process state, and emits two artifacts:
<run_dir>/events.jsonl — append-only event stream, closed event set
<run_dir>/snapshot.json — rolling status grid, atomically replaced
Restart-safe: replays events.jsonl to rebuild counters, resumes each
tailer from a per-tailer offset file. Offsets are saved BEFORE the
line is handled, so an unclean crash can drop <=1 event but never
double-count.
.opencode/telemetry/{server.py,app.js,index.html,style.css} — the
"Live" tab. server.py serves /runs + /snapshot + /events (the last
with a byte-offset cache so a 3s UI poll is O(new events), not
O(file size)); app.js renders the process/service grids, the
implementer/reviewer cards, and a bounded event feed.
scripts/dispatchers-launcher.sh — wires the sidecar in as a
best-effort child: respawned outside the dispatcher crash-loop
budget, its exit never propagates to the launcher's, with a
rapid-crash backoff floor so a writer that crashes on boot can't
tight-loop.
Hardened through three review rounds (23 fixes) before landing:
partial-read race in the dir watchers, a shared offset-temp-file
race, replay/offset double-count, dead event types, the /events
O(n)-per-poll scan, the app.js↔snapshot field-contract drift, a
respawn tight-loop, and more.
Tests: tests/auto_agents/test_live_log_writer.py — 46 tests covering
every log-pattern handler, replay↔handler counter agreement, atomic
snapshot write, idempotent restart, partial-read resilience, the
/events offset cache, a cross-language app.js↔snapshot contract test,
a schema-doc consistency test, and two end-to-end smoke tests that
run the actual sidecar process. Full tests/auto_agents/ suite: 1548
passed, 3 skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
257 lines
13 KiB
HTML
257 lines
13 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
<title>Pipeline Telemetry Console</title>
|
|
<link rel="stylesheet" href="/static/style.css" />
|
|
</head>
|
|
<body>
|
|
<header class="topbar">
|
|
<div class="brand">
|
|
<span class="dot dot-pulse" id="conn-dot" title="connection status"></span>
|
|
<strong>Pipeline Telemetry</strong>
|
|
<span class="sep">·</span>
|
|
<span id="repo-target" class="muted">loading…</span>
|
|
</div>
|
|
<div class="meta-extra">
|
|
<span id="cache-info" class="muted small"></span>
|
|
<span class="sep">·</span>
|
|
<span id="server-time" class="muted small"></span>
|
|
</div>
|
|
</header>
|
|
|
|
<nav class="tabs" role="tablist">
|
|
<button class="tab" data-tab="overview" role="tab" aria-selected="true">Overview</button>
|
|
<button class="tab" data-tab="live" role="tab">Live</button>
|
|
<button class="tab" data-tab="drivers" role="tab">Drivers</button>
|
|
<button class="tab" data-tab="prs" role="tab">PRs</button>
|
|
<button class="tab" data-tab="velocity" role="tab">Velocity</button>
|
|
<button class="tab" data-tab="llm" role="tab">LLM</button>
|
|
<button class="tab" data-tab="sessions" role="tab">Sessions</button>
|
|
<button class="tab" data-tab="cost" role="tab">Cost</button>
|
|
<button class="tab" data-tab="about" role="tab">About</button>
|
|
</nav>
|
|
|
|
<main>
|
|
<!-- ── Overview ───────────────────────────────────────────── -->
|
|
<section class="pane active" data-pane="overview">
|
|
<h2>Pipeline health</h2>
|
|
<p class="muted small">Refresh: 5 s · daemon discovery via /proc · OpenCode probed at <code id="oc-url">…</code></p>
|
|
<div id="daemon-grid" class="grid grid-4">
|
|
<div class="card placeholder">loading…</div>
|
|
</div>
|
|
<h3 style="margin-top:24px">Last cycles (24 h)</h3>
|
|
<div class="grid grid-2">
|
|
<div class="card">
|
|
<div class="card-head">merge_drive — recent</div>
|
|
<div id="overview-merge" class="card-body">loading…</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head">conflict_drive — recent</div>
|
|
<div id="overview-conflict" class="card-body">loading…</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head">dispatch_review — recent</div>
|
|
<div id="overview-dispatch_review" class="card-body">loading…</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head">dispatch_implementer — recent</div>
|
|
<div id="overview-dispatch_implementer" class="card-body">loading…</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Live (per-run status grid driven by /runs, /snapshot, /events) ── -->
|
|
<section class="pane" data-pane="live">
|
|
<div class="live-layout">
|
|
<aside class="live-runs">
|
|
<div class="live-runs-head">
|
|
<strong>Runs</strong>
|
|
<span class="muted small" id="live-runs-meta">…</span>
|
|
</div>
|
|
<div id="live-runs-list" class="live-runs-list">loading…</div>
|
|
<p class="muted small live-runs-foot">
|
|
Source: <code id="live-runs-root">…</code><br>
|
|
Click a row to inspect. Refresh: 5 s.
|
|
</p>
|
|
</aside>
|
|
<section class="live-panel" id="live-panel">
|
|
<div class="live-empty muted">Select a run from the left to see live status.</div>
|
|
</section>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Drivers ────────────────────────────────────────────── -->
|
|
<section class="pane" data-pane="drivers">
|
|
<h2>Driver telemetry</h2>
|
|
<p class="muted small">Refresh: 15 s · source: <code id="cache-path-drivers">…</code></p>
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<span>merge_cycle</span>
|
|
<span class="muted small">last 24h breakdown: <span id="merge-breakdown">…</span></span>
|
|
</div>
|
|
<div id="merge-table" class="card-body table-wrap">loading…</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<span>conflict_drive_cycles</span>
|
|
<span class="muted small">last 24h breakdown: <span id="conflict-breakdown">…</span></span>
|
|
</div>
|
|
<div id="conflict-table" class="card-body table-wrap">loading…</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<span>dispatch_review_cycles</span>
|
|
<span class="muted small">last 24h breakdown (terminal_state/worker_outcome): <span id="dispatch_review-breakdown">…</span></span>
|
|
</div>
|
|
<div id="dispatch_review-table" class="card-body table-wrap">loading…</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<span>dispatch_implementer_cycles</span>
|
|
<span class="muted small">last 24h breakdown (terminal_state/worker_outcome): <span id="dispatch_implementer-breakdown">…</span></span>
|
|
</div>
|
|
<div id="dispatch_implementer-table" class="card-body table-wrap">loading…</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── PRs by label ───────────────────────────────────────── -->
|
|
<section class="pane" data-pane="prs">
|
|
<h2>Open PRs by <code>auto/*</code> label</h2>
|
|
<p class="muted small">Refresh: 30 s · live Forgejo query (no cache)</p>
|
|
<div class="filter-row">
|
|
<label>Filter:
|
|
<select id="pr-label-select">
|
|
<option value="">— all open auto/* PRs —</option>
|
|
<option value="auto/claimed-merge">auto/claimed-merge</option>
|
|
<option value="auto/needs-conflict-resolution">auto/needs-conflict-resolution</option>
|
|
<option value="auto/needs-implementer">auto/needs-implementer</option>
|
|
<option value="auto/blocked-by-deps">auto/blocked-by-deps</option>
|
|
<option value="auto/restart-throttled">auto/restart-throttled</option>
|
|
<option value="auto/ci-timeout">auto/ci-timeout</option>
|
|
<option value="auto/stale-inactivity">auto/stale-inactivity</option>
|
|
<option value="auto/unstable">auto/unstable</option>
|
|
<option value="auto/sentinel">auto/sentinel</option>
|
|
</select>
|
|
</label>
|
|
<span id="pr-count" class="muted small"></span>
|
|
</div>
|
|
<div id="pr-table" class="card-body table-wrap">loading…</div>
|
|
</section>
|
|
|
|
<!-- ── Velocity ───────────────────────────────────────────── -->
|
|
<section class="pane" data-pane="velocity">
|
|
<h2>Velocity windows</h2>
|
|
<p class="muted small">Refresh: 5 min · cache-backed</p>
|
|
<div id="velocity-grid" class="grid grid-4">
|
|
<div class="card placeholder">loading…</div>
|
|
</div>
|
|
<p class="muted small" style="margin-top:24px">
|
|
For the full historical view (weekly throughput, top authors, monthly volume,
|
|
daily series, oldest open PRs), keep
|
|
<code>tools/reports.sh</code> rendering the canvas — the console focuses on
|
|
live/short-window pipeline state. Charts here mirror the canvas but are
|
|
cache-driven and refresh on demand.
|
|
</p>
|
|
</section>
|
|
|
|
<!-- ── LLM sessions ───────────────────────────────────────── -->
|
|
<section class="pane" data-pane="llm">
|
|
<h2>OpenCode sessions</h2>
|
|
<p class="muted small">Refresh: 10 s · OpenCode at <code id="oc-url-2">…</code></p>
|
|
<div id="llm-table" class="card-body table-wrap">loading…</div>
|
|
</section>
|
|
|
|
<!-- ── Archived sessions ──────────────────────────────────── -->
|
|
<section class="pane" data-pane="sessions">
|
|
<h2>Archived worker sessions</h2>
|
|
<p class="muted small">
|
|
Refresh: 30 s ·
|
|
snapshot of every <code>pr-review-worker</code> /
|
|
<code>implementation-worker</code> session before it was deleted ·
|
|
source: <code id="archive-dir-info">…</code>
|
|
</p>
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<span>Sessions (newest first)</span>
|
|
<span class="muted small" id="archive-count">…</span>
|
|
</div>
|
|
<div id="archive-table" class="card-body table-wrap">loading…</div>
|
|
</div>
|
|
<div class="card hidden" id="archive-detail-card">
|
|
<div class="card-head">
|
|
<span id="archive-detail-title">Detail</span>
|
|
<button class="btn btn-link" id="archive-detail-close" type="button">close</button>
|
|
</div>
|
|
<div class="card-body" id="archive-detail-body">
|
|
<span class="muted">click a row to inspect</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Cost ───────────────────────────────────────────────── -->
|
|
<section class="pane" data-pane="cost">
|
|
<h2>Token usage & cost</h2>
|
|
<p class="muted small">Refresh: 60 s · source: <code>llm_activity</code> table · prices: <code>.opencode/telemetry/prices.json</code> (override)</p>
|
|
<div class="filter-row">
|
|
<label>Window:
|
|
<select id="cost-days-select">
|
|
<option value="1">last 24 h</option>
|
|
<option value="7" selected>last 7 d</option>
|
|
<option value="30">last 30 d</option>
|
|
<option value="90">last 90 d</option>
|
|
</select>
|
|
</label>
|
|
<span id="cost-totals" class="muted small"></span>
|
|
</div>
|
|
<div id="cost-table" class="card-body table-wrap">loading…</div>
|
|
<div id="cost-instrumentation-note" class="callout hidden">
|
|
<strong>Instrumentation pending.</strong>
|
|
The <code>llm_activity</code> table has 0 rows for this window — drivers are
|
|
not yet writing per-dispatch token usage. The data shape is ready;
|
|
wiring the writers is a follow-up. This tab will populate automatically
|
|
once <code>conflict_drive.py</code> and the LLM supervisors record their
|
|
dispatches into <code>llm_activity</code>.
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── About ──────────────────────────────────────────────── -->
|
|
<section class="pane" data-pane="about">
|
|
<h2>About this console</h2>
|
|
<div class="card">
|
|
<div class="card-head">Environment audit</div>
|
|
<div class="card-body">
|
|
<dl id="about-dl"></dl>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head">Where the data comes from</div>
|
|
<div class="card-body">
|
|
<ul class="tight">
|
|
<li><strong>Daemons + heartbeats:</strong> <code>/proc</code> walk for PIDs · file-mtime probes for heartbeats.</li>
|
|
<li><strong>Cycle telemetry:</strong> per-repo SQLite cache <code>tools/.cache/forgejo.<owner>.<repo>.sqlite</code> · tables <code>merge_cycle</code> and <code>conflict_drive_cycles</code>.</li>
|
|
<li><strong>PR label flow:</strong> live Forgejo API <code>GET /repos/<owner>/<repo>/issues</code>.</li>
|
|
<li><strong>OpenCode sessions:</strong> live <code>GET /session</code> (and per-session <code>/message</code>).</li>
|
|
<li><strong>Cost:</strong> aggregated from <code>llm_activity(model, tokens_in, tokens_out, cached_tokens)</code> · prices from <code>prices.json</code>.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head">Operator notes</div>
|
|
<div class="card-body">
|
|
<ul class="tight">
|
|
<li>Console is <strong>read-only</strong>. It never writes to Forgejo, OpenCode, or the SQLite cache.</li>
|
|
<li>Default bind: <strong>127.0.0.1 (loopback only)</strong>. Set <code>TELEMETRY_HOST=0.0.0.0</code> only if you've added auth in a reverse proxy.</li>
|
|
<li>Source the same shell the drivers run from (<code>source tools/launch_fork.sh</code>) so the console reads the right cache and hits the right Forgejo target.</li>
|
|
<li>To switch repos without restarting, change <code>FORGEJO_OWNER</code>/<code>FORGEJO_REPO</code> in the launching shell and restart the console — the cache file is selected at startup.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<script src="/static/app.js"></script>
|
|
</body>
|
|
</html>
|