c34a0d1584
Adds a "stage / role" Group-by option alongside model and PR. Token spend is bucketed by pipeline stage (implementer / reviewer / estimator / conflict_resolver), derived from the session tag rather than the per-row agent — subagent turns inherit the wrapper session's tag, so their tokens roll into the stage that spawned them instead of scattering across sub-agent names. Legacy AUTO-* tags fold into the same buckets as controller tags; probe/overhead sessions sink to "other". - server.py: _stage_from_tag helper + a group_by="role" branch in _api_cost that SUMs per (session_tag, model, provider) so per-model pricing stays exact, then collapses to one row per stage. - app.js/index.html: new dropdown option; renderCost generalized so the pr and role aggregate views share layout. - 11 new tests, incl. a regression pin that a subagent turn rolls into its wrapper stage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
280 lines
14 KiB
HTML
280 lines
14 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="workflows" role="tab">Workflows</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="phases" role="tab">Phases</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">Needs operator attention</h3>
|
|
<div class="grid grid-2">
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<span>Workflows in attention states</span>
|
|
<span class="muted small" id="attention-breakdown">…</span>
|
|
</div>
|
|
<div id="attention-table" class="card-body table-wrap">loading…</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<span>Recently reaped attempts (24 h)</span>
|
|
<span class="muted small" id="reaped-count">…</span>
|
|
</div>
|
|
<div id="reaped-table" class="card-body table-wrap">loading…</div>
|
|
</div>
|
|
</div>
|
|
<h3 style="margin-top:24px">merge_drive — recent (24 h)</h3>
|
|
<div class="card">
|
|
<div class="card-body" id="overview-merge">loading…</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Live (controller run state + live OpenCode session tree) ── -->
|
|
<section class="pane" data-pane="live">
|
|
<h2>Live pipeline activity</h2>
|
|
<p class="muted small">
|
|
Refresh: 3 s · controller DB <code>workflows</code> /
|
|
<code>workflow_attempts</code> + live OpenCode <code>GET /session</code> ·
|
|
click a worker session to watch its transcript and drill into the
|
|
subagents it spawns
|
|
</p>
|
|
<div id="live-root"><div class="live-empty muted">loading…</div></div>
|
|
</section>
|
|
|
|
<!-- ── Workflows (controller DB state-machine inventory) ───── -->
|
|
<section class="pane" data-pane="workflows">
|
|
<h2>Workflows</h2>
|
|
<p class="muted small">Refresh: 15 s · source: controller DB <code>workflows</code> + <code>workflow_attempts</code> · click a row for timeline</p>
|
|
<div class="filter-row">
|
|
<label>State:
|
|
<select id="workflows-state-select">
|
|
<option value="" selected>— all states —</option>
|
|
</select>
|
|
</label>
|
|
<span id="workflows-states-summary" class="muted small"></span>
|
|
</div>
|
|
<div id="workflows-table" class="card-body table-wrap">loading…</div>
|
|
<div class="card hidden" id="workflow-detail-card">
|
|
<div class="card-head">
|
|
<span id="workflow-detail-title">Timeline</span>
|
|
<button class="btn btn-link" id="workflow-detail-close" type="button">close</button>
|
|
</div>
|
|
<div class="card-body" id="workflow-detail-body">
|
|
<span class="muted">click a row to inspect</span>
|
|
</div>
|
|
</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> · dispatcher + conflict drivers retired (see Workflows tab)</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>
|
|
</section>
|
|
|
|
<!-- ── PRs by controller state ─────────────────────────────── -->
|
|
<section class="pane" data-pane="prs">
|
|
<h2>Open PRs by controller state</h2>
|
|
<p class="muted small">Refresh: 30 s · live Forgejo query joined with controller DB <code>workflows.current_state</code></p>
|
|
<div class="filter-row">
|
|
<label>State:
|
|
<select id="pr-state-select">
|
|
<option value="" selected>— all open PRs —</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>
|
|
<label>Group by:
|
|
<select id="cost-group-select">
|
|
<option value="model" selected>model (total)</option>
|
|
<option value="pr">PR</option>
|
|
<option value="role">stage / role</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>
|
|
|
|
<!-- ── Phases ─────────────────────────────────────────────── -->
|
|
<section class="pane" data-pane="phases">
|
|
<h2>Time per phase</h2>
|
|
<p class="muted small">Refresh: 60 s · sources: controller DB <code>workflow_attempts</code> (worker phases) + legacy <code>ci_gate_events</code> (CI phase)</p>
|
|
<div class="filter-row">
|
|
<label>Window:
|
|
<select id="phases-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>
|
|
<label>Group by:
|
|
<select id="phases-group-select">
|
|
<option value="phase" selected>phase (total)</option>
|
|
<option value="pr">PR</option>
|
|
</select>
|
|
</label>
|
|
<span id="phases-totals" class="muted small"></span>
|
|
</div>
|
|
<div id="phases-table" class="card-body table-wrap">loading…</div>
|
|
<div id="phases-note" class="callout hidden"></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. Probed: <code>controller_master</code>, <code>controller_worker</code>, <code>merge_drive</code>, <code>opencode_builder</code>.</li>
|
|
<li><strong>Controller DB</strong> (system of record, SQLAlchemy via <code>CLEVERAGENTS_DB_URL</code>): <code>workflows</code> · <code>workflow_attempts</code> · <code>controller_events</code> drive the Workflows tab, attention backlog, Phases panel, and PR state column.</li>
|
|
<li><strong>Legacy SQLite cache</strong> <code>tools/.cache/forgejo.<owner>.<repo>.sqlite</code>: <code>merge_cycle</code> (Drivers tab), <code>ci_gate_events</code> (Phases CI column), <code>llm_activity</code> (Cost + Phases), <code>commits</code>/<code>pulls</code> (Velocity).</li>
|
|
<li><strong>PR list:</strong> live Forgejo <code>GET /repos/<owner>/<repo>/issues</code> joined with controller state per PR.</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</code> · prices from <code>prices.json</code>. Group by model or PR.</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>
|