Files consolidated: - dashboard_routes.py: root copy (39K) overwrites teleo-codex (34K) — has cost fix + connection leak fix - dashboard_prs.py: root copy overwrites — has cost display rewrite - dashboard_epistemic.py: root copy overwrites — has Ship rename - research_tracking.py: new file, existed only in root /diagnostics/ (reviewed by Ganymede, never committed here) - research_routes.py: new file, same situation - ops/db.py: new file, unique to root /diagnostics/ops/ After this commit, root /diagnostics/ contains only stale copies and patch files — safe to delete. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
564 lines
26 KiB
Python
564 lines
26 KiB
Python
"""PR Lifecycle dashboard — single-page view of every PR through the pipeline.
|
|
|
|
Sortable table: PR#, summary, claims, domain, outcome, evals, evaluator, cost, date.
|
|
Click any row to expand: timeline, claim list, issues summary.
|
|
Hero cards: total PRs, merge rate, median eval rounds, total claims, total cost.
|
|
|
|
Data sources: prs table, audit_log (eval rounds), review_records.
|
|
Owner: Ship
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from shared_ui import render_page
|
|
|
|
|
|
EXTRA_CSS = """
|
|
.page-content { max-width: 1600px !important; }
|
|
.filters { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
.filters select, .filters input {
|
|
background: #161b22; color: #c9d1d9; border: 1px solid #30363d;
|
|
border-radius: 6px; padding: 6px 10px; font-size: 12px; }
|
|
.filters select:focus, .filters input:focus { border-color: #58a6ff; outline: none; }
|
|
.pr-table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
|
|
.pr-table th:nth-child(1) { width: 50px; } /* PR# */
|
|
.pr-table th:nth-child(2) { width: 30%; } /* Summary */
|
|
.pr-table th:nth-child(3) { width: 50px; } /* Claims */
|
|
.pr-table th:nth-child(4) { width: 12%; } /* Domain */
|
|
.pr-table th:nth-child(5) { width: 10%; } /* Outcome */
|
|
.pr-table th:nth-child(6) { width: 50px; } /* Evals */
|
|
.pr-table th:nth-child(7) { width: 16%; } /* Evaluator */
|
|
.pr-table th:nth-child(8) { width: 70px; } /* Cost */
|
|
.pr-table th:nth-child(9) { width: 90px; } /* Date */
|
|
.pr-table td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 8px 6px; }
|
|
.pr-table td:nth-child(2) { white-space: normal; overflow: visible; line-height: 1.4; }
|
|
.pr-table th { cursor: pointer; user-select: none; position: relative; padding: 8px 18px 8px 6px; }
|
|
.pr-table th:hover { color: #58a6ff; }
|
|
.pr-table th .sort-arrow { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); font-size: 10px; opacity: 0.5; }
|
|
.pr-table th.sorted .sort-arrow { opacity: 1; color: #58a6ff; }
|
|
.pr-table tr { cursor: pointer; transition: background 0.1s; }
|
|
.pr-table tbody tr:hover { background: #161b22; }
|
|
.pr-table .outcome-merged { color: #3fb950; }
|
|
.pr-table .outcome-closed { color: #f85149; }
|
|
.pr-table .outcome-open { color: #d29922; }
|
|
.pr-table .tier-deep { color: #bc8cff; font-weight: 600; }
|
|
.pr-table .tier-standard { color: #58a6ff; }
|
|
.pr-table .tier-light { color: #8b949e; }
|
|
.pr-table .pr-link { color: #58a6ff; text-decoration: none; }
|
|
.pr-table .pr-link:hover { text-decoration: underline; }
|
|
.pr-table td .summary-text { font-size: 12px; color: #c9d1d9; }
|
|
.pr-table td .review-snippet { font-size: 11px; color: #f85149; margin-top: 2px; opacity: 0.8; }
|
|
.pr-table td .model-tag { font-size: 9px; color: #6e7681; background: #21262d; border-radius: 3px; padding: 1px 4px; display: inline-block; margin: 1px 0; }
|
|
.pr-table td .expand-chevron { display: inline-block; width: 12px; color: #484f58; font-size: 10px; transition: transform 0.2s; }
|
|
.pr-table tr.expanded .expand-chevron { transform: rotate(90deg); color: #58a6ff; }
|
|
.pr-table td .cost-val { font-size: 12px; color: #8b949e; }
|
|
.pr-table td .claims-count { font-size: 13px; color: #c9d1d9; text-align: center; }
|
|
.pr-table td .evals-count { font-size: 13px; text-align: center; }
|
|
.trace-panel { background: #0d1117; border: 1px solid #30363d; border-radius: 8px;
|
|
padding: 16px; margin: 4px 0 8px 0; font-size: 12px; display: none; }
|
|
.trace-panel.open { display: block; }
|
|
.trace-panel .section-title { color: #58a6ff; font-size: 12px; font-weight: 600; margin: 12px 0 6px; }
|
|
.trace-panel .section-title:first-child { margin-top: 0; }
|
|
.trace-panel .claim-list { list-style: none; padding: 0; margin: 0; }
|
|
.trace-panel .claim-list li { padding: 4px 0; border-bottom: 1px solid #21262d; color: #c9d1d9; font-size: 12px; }
|
|
.trace-panel .claim-list li:last-child { border-bottom: none; }
|
|
.trace-panel .issues-box { background: #1c1017; border: 1px solid #f8514930; border-radius: 6px;
|
|
padding: 8px 12px; margin: 4px 0; font-size: 12px; color: #f85149; }
|
|
.trace-timeline { list-style: none; padding: 0; }
|
|
.trace-timeline li { padding: 4px 0; border-left: 2px solid #30363d; padding-left: 12px; margin-left: 8px; }
|
|
.trace-timeline li .ts { color: #484f58; font-size: 11px; }
|
|
.trace-timeline li .ev { font-weight: 600; }
|
|
.trace-timeline li.ev-approved .ev { color: #3fb950; }
|
|
.trace-timeline li.ev-rejected .ev { color: #f85149; }
|
|
.trace-timeline li.ev-changes .ev { color: #d29922; }
|
|
.review-text { background: #161b22; padding: 8px 12px; border-radius: 4px;
|
|
margin: 4px 0; white-space: pre-wrap; font-size: 11px; color: #8b949e; max-height: 200px; overflow-y: auto; }
|
|
.eval-chain { background: #161b22; border-radius: 6px; padding: 8px 12px; margin: 4px 0 8px;
|
|
font-size: 12px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
|
|
.eval-chain .step { display: flex; align-items: center; gap: 4px; }
|
|
.eval-chain .step-label { color: #8b949e; font-size: 11px; }
|
|
.eval-chain .step-model { color: #c9d1d9; font-size: 11px; font-weight: 600; }
|
|
.eval-chain .arrow { color: #484f58; }
|
|
.pagination { display: flex; gap: 8px; align-items: center; justify-content: center; margin-top: 16px; }
|
|
.pagination button { background: #161b22; color: #c9d1d9; border: 1px solid #30363d;
|
|
border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; }
|
|
.pagination button:hover { border-color: #58a6ff; }
|
|
.pagination button:disabled { opacity: 0.4; cursor: default; }
|
|
.pagination .page-info { color: #8b949e; font-size: 12px; }
|
|
"""
|
|
|
|
|
|
def render_prs_page(now: datetime) -> str:
|
|
"""Render the PR lifecycle page. All data loaded client-side via /api/pr-lifecycle."""
|
|
|
|
body = """
|
|
<!-- Hero cards (populated by JS) -->
|
|
<div class="grid" id="hero-cards">
|
|
<div class="card"><div class="label">Total PRs</div><div class="value blue" id="kpi-total">--</div><div class="detail" id="kpi-total-detail"></div></div>
|
|
<div class="card"><div class="label">Merge Rate</div><div class="value green" id="kpi-merge-rate">--</div><div class="detail" id="kpi-merge-detail"></div></div>
|
|
<div class="card"><div class="label">Median Eval Rounds</div><div class="value" id="kpi-rounds">--</div><div class="detail" id="kpi-rounds-detail"></div></div>
|
|
<div class="card"><div class="label">Total Claims</div><div class="value blue" id="kpi-claims">--</div><div class="detail" id="kpi-claims-detail"></div></div>
|
|
<div class="card"><div class="label">Est. Cost</div><div class="value" id="kpi-cost">--</div><div class="detail" id="kpi-cost-detail"></div></div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="filters">
|
|
<select id="filter-domain"><option value="">All Domains</option></select>
|
|
<select id="filter-outcome">
|
|
<option value="">All Outcomes</option>
|
|
<option value="merged">Merged</option>
|
|
<option value="closed">Rejected</option>
|
|
<option value="open">Open</option>
|
|
</select>
|
|
<select id="filter-tier">
|
|
<option value="">All Tiers</option>
|
|
<option value="DEEP">Deep</option>
|
|
<option value="STANDARD">Standard</option>
|
|
<option value="LIGHT">Light</option>
|
|
</select>
|
|
<select id="filter-days">
|
|
<option value="7">Last 7 days</option>
|
|
<option value="30" selected>Last 30 days</option>
|
|
<option value="90">Last 90 days</option>
|
|
<option value="0">All time</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- PR table -->
|
|
<div class="card" style="padding: 0; overflow: hidden;">
|
|
<table class="pr-table">
|
|
<thead>
|
|
<tr>
|
|
<th data-col="number">PR# <span class="sort-arrow">▲</span></th>
|
|
<th data-col="summary">Summary <span class="sort-arrow">▲</span></th>
|
|
<th data-col="claims_count">Claims <span class="sort-arrow">▲</span></th>
|
|
<th data-col="domain">Domain <span class="sort-arrow">▲</span></th>
|
|
<th data-col="status">Outcome <span class="sort-arrow">▲</span></th>
|
|
<th data-col="eval_rounds">Evals <span class="sort-arrow">▲</span></th>
|
|
<th data-col="evaluator">Evaluator <span class="sort-arrow">▲</span></th>
|
|
<th data-col="est_cost">Cost <span class="sort-arrow">▲</span></th>
|
|
<th data-col="created_at">Date <span class="sort-arrow">▲</span></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="pr-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div class="pagination">
|
|
<button id="pg-prev" disabled>« Prev</button>
|
|
<span class="page-info" id="pg-info">--</span>
|
|
<button id="pg-next" disabled>Next »</button>
|
|
</div>
|
|
"""
|
|
|
|
# Use single-quoted JS strings throughout to avoid Python/HTML escaping issues
|
|
scripts = """<script>
|
|
const PAGE_SIZE = 50;
|
|
const FORGEJO = 'https://git.livingip.xyz/teleo/teleo-codex/pulls/';
|
|
let allData = [];
|
|
let filtered = [];
|
|
let sortCol = 'number';
|
|
let sortAsc = false;
|
|
let page = 0;
|
|
let expandedPr = null;
|
|
|
|
function loadData() {
|
|
var days = document.getElementById('filter-days').value;
|
|
var url = '/api/pr-lifecycle' + (days !== '0' ? '?days=' + days : '?days=9999');
|
|
fetch(url).then(function(r) { return r.json(); }).then(function(data) {
|
|
allData = data.prs || [];
|
|
populateFilters(allData);
|
|
updateKPIs(data);
|
|
applyFilters();
|
|
}).catch(function() {
|
|
document.getElementById('pr-tbody').innerHTML =
|
|
'<tr><td colspan="9" style="text-align:center;color:#f85149;">Failed to load data</td></tr>';
|
|
});
|
|
}
|
|
|
|
function populateFilters(prs) {
|
|
var domains = [], seenD = {};
|
|
prs.forEach(function(p) {
|
|
if (p.domain && !seenD[p.domain]) { seenD[p.domain] = 1; domains.push(p.domain); }
|
|
});
|
|
domains.sort();
|
|
var domSel = document.getElementById('filter-domain');
|
|
var curDom = domSel.value;
|
|
domSel.innerHTML = '<option value="">All Domains</option>' +
|
|
domains.map(function(d) { return '<option value="' + esc(d) + '">' + esc(d) + '</option>'; }).join('');
|
|
domSel.value = curDom;
|
|
}
|
|
|
|
function updateKPIs(data) {
|
|
document.getElementById('kpi-total').textContent = fmtNum(data.total);
|
|
document.getElementById('kpi-total-detail').textContent =
|
|
fmtNum(data.merged) + ' merged, ' + fmtNum(data.closed) + ' rejected';
|
|
|
|
var rate = data.total > 0 ? data.merged / (data.merged + data.closed) : 0;
|
|
document.getElementById('kpi-merge-rate').textContent = fmtPct(rate);
|
|
document.getElementById('kpi-merge-detail').textContent = fmtNum(data.open) + ' open';
|
|
|
|
document.getElementById('kpi-rounds').textContent =
|
|
data.median_rounds != null ? data.median_rounds.toFixed(1) : '--';
|
|
document.getElementById('kpi-rounds-detail').textContent =
|
|
data.max_rounds != null ? 'max: ' + data.max_rounds : '';
|
|
|
|
var totalClaims = 0, mergedClaims = 0;
|
|
var totalCost = 0;
|
|
var actualCount = 0, estCount = 0;
|
|
(data.prs || []).forEach(function(p) {
|
|
totalClaims += (p.claims_count || 1);
|
|
if (p.status === 'merged') mergedClaims += (p.claims_count || 1);
|
|
totalCost += (p.cost || 0);
|
|
if (p.cost_is_actual) actualCount++; else estCount++;
|
|
});
|
|
document.getElementById('kpi-claims').textContent = fmtNum(totalClaims);
|
|
document.getElementById('kpi-claims-detail').textContent = fmtNum(mergedClaims) + ' merged';
|
|
|
|
// Show actual DB total if available, otherwise sum from PRs
|
|
var costLabel = '';
|
|
if (data.actual_total_cost > 0) {
|
|
document.getElementById('kpi-cost').textContent = '$' + data.actual_total_cost.toFixed(2);
|
|
costLabel = 'from costs table';
|
|
} else if (actualCount > 0) {
|
|
document.getElementById('kpi-cost').textContent = '$' + totalCost.toFixed(2);
|
|
costLabel = actualCount + ' actual, ' + estCount + ' est.';
|
|
} else {
|
|
document.getElementById('kpi-cost').textContent = '$' + totalCost.toFixed(2);
|
|
costLabel = 'ALL ESTIMATED';
|
|
}
|
|
var costPerClaim = totalClaims > 0 ? totalCost / totalClaims : 0;
|
|
document.getElementById('kpi-cost-detail').textContent =
|
|
'$' + costPerClaim.toFixed(3) + '/claim \u00b7 ' + costLabel;
|
|
}
|
|
|
|
function applyFilters() {
|
|
var dom = document.getElementById('filter-domain').value;
|
|
var out = document.getElementById('filter-outcome').value;
|
|
var tier = document.getElementById('filter-tier').value;
|
|
|
|
filtered = allData.filter(function(p) {
|
|
if (dom && p.domain !== dom) return false;
|
|
if (out && p.status !== out) return false;
|
|
if (tier && p.tier !== tier) return false;
|
|
return true;
|
|
});
|
|
|
|
sortData();
|
|
page = 0;
|
|
renderTable();
|
|
}
|
|
|
|
function sortData() {
|
|
filtered.sort(function(a, b) {
|
|
var va = a[sortCol], vb = b[sortCol];
|
|
if (va == null) va = '';
|
|
if (vb == null) vb = '';
|
|
if (typeof va === 'number' && typeof vb === 'number') {
|
|
return sortAsc ? va - vb : vb - va;
|
|
}
|
|
va = String(va).toLowerCase();
|
|
vb = String(vb).toLowerCase();
|
|
return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
|
|
});
|
|
}
|
|
|
|
function truncate(s, n) {
|
|
if (!s) return '';
|
|
return s.length > n ? s.substring(0, n) + '...' : s;
|
|
}
|
|
|
|
function shortModel(m) {
|
|
if (!m) return '';
|
|
// Shorten model names for display
|
|
if (m.indexOf('gemini-2.5-flash') !== -1) return 'Gemini Flash';
|
|
if (m.indexOf('claude-sonnet') !== -1 || m.indexOf('sonnet-4') !== -1) return 'Sonnet';
|
|
if (m.indexOf('claude-opus') !== -1 || m.indexOf('opus') !== -1) return 'Opus';
|
|
if (m.indexOf('haiku') !== -1) return 'Haiku';
|
|
if (m.indexOf('gpt-4o') !== -1) return 'GPT-4o';
|
|
// fallback: strip provider prefix
|
|
var parts = m.split('/');
|
|
return parts[parts.length - 1];
|
|
}
|
|
|
|
function renderTable() {
|
|
var tbody = document.getElementById('pr-tbody');
|
|
var start = page * PAGE_SIZE;
|
|
var slice = filtered.slice(start, start + PAGE_SIZE);
|
|
var totalPages = Math.ceil(filtered.length / PAGE_SIZE);
|
|
|
|
if (slice.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;color:#8b949e;">No PRs match filters</td></tr>';
|
|
return;
|
|
}
|
|
|
|
var rows = [];
|
|
slice.forEach(function(p) {
|
|
var outClass = p.status === 'merged' ? 'outcome-merged' :
|
|
p.status === 'closed' ? 'outcome-closed' : 'outcome-open';
|
|
var tierClass = (p.tier || '').toLowerCase() === 'deep' ? 'tier-deep' :
|
|
(p.tier || '').toLowerCase() === 'standard' ? 'tier-standard' : 'tier-light';
|
|
var date = p.created_at ? p.created_at.substring(0, 10) : '--';
|
|
|
|
// Summary
|
|
var summary = p.summary || '--';
|
|
var reviewSnippet = '';
|
|
if (p.status === 'closed' && p.review_snippet) {
|
|
reviewSnippet = '<div class="review-snippet">' + esc(truncate(p.review_snippet, 120)) + '</div>';
|
|
}
|
|
|
|
// Outcome with tier badge
|
|
var outcomeLabel = esc(p.status || '--');
|
|
var tierBadge = p.tier ? ' <span class="' + tierClass + '" style="font-size:10px;">' + esc(p.tier) + '</span>' : '';
|
|
|
|
// Evaluator column: domain agent + model
|
|
var evaluator = '';
|
|
if (p.domain_agent) {
|
|
evaluator = '<div style="font-size:12px;color:#c9d1d9;">' + esc(p.domain_agent) + '</div>';
|
|
}
|
|
if (p.domain_model) {
|
|
evaluator += '<div class="model-tag">' + esc(shortModel(p.domain_model)) + '</div>';
|
|
}
|
|
if (p.leo_model) {
|
|
evaluator += '<div class="model-tag">' + esc(shortModel(p.leo_model)) + '</div>';
|
|
}
|
|
if (!evaluator) evaluator = '<span style="color:#484f58;">--</span>';
|
|
|
|
// Cost — actual from DB or estimated (flagged)
|
|
var costStr;
|
|
if (p.cost != null && p.cost > 0) {
|
|
if (p.cost_is_actual) {
|
|
costStr = '<span class="cost-val">$' + p.cost.toFixed(3) + '</span>';
|
|
} else {
|
|
costStr = '<span class="cost-val" style="opacity:0.5;" title="Estimated — no actual cost tracked">~$' + p.cost.toFixed(3) + '</span>';
|
|
}
|
|
} else {
|
|
costStr = '<span style="color:#484f58;">--</span>';
|
|
}
|
|
|
|
rows.push(
|
|
'<tr data-pr="' + p.number + '">' +
|
|
'<td><span class="expand-chevron">▶</span> ' +
|
|
'<a class="pr-link" href="' + FORGEJO + p.number + '" target="_blank" rel="noopener" onclick="event.stopPropagation();">#' + p.number + '</a></td>' +
|
|
'<td style="white-space:normal;"><span class="summary-text">' + esc(summary) + '</span>' + reviewSnippet + '</td>' +
|
|
'<td style="text-align:center;">' + (p.claims_count || '--') + '</td>' +
|
|
'<td>' + esc(p.domain || '--') + '</td>' +
|
|
'<td class="' + outClass + '">' + outcomeLabel + tierBadge + '</td>' +
|
|
'<td style="text-align:center;">' + (p.eval_rounds || '--') + '</td>' +
|
|
'<td>' + evaluator + '</td>' +
|
|
'<td>' + costStr + '</td>' +
|
|
'<td>' + date + '</td>' +
|
|
'</tr>' +
|
|
'<tr id="trace-' + p.number + '" style="display:none;"><td colspan="9" style="padding:0;">' +
|
|
'<div class="trace-panel" id="panel-' + p.number + '">Loading trace...</div>' +
|
|
'</td></tr>'
|
|
);
|
|
});
|
|
tbody.innerHTML = rows.join('');
|
|
|
|
// Pagination
|
|
document.getElementById('pg-info').textContent =
|
|
'Page ' + (totalPages > 0 ? page + 1 : 0) + ' of ' + totalPages +
|
|
' (' + filtered.length + ' PRs)';
|
|
document.getElementById('pg-prev').disabled = page <= 0;
|
|
document.getElementById('pg-next').disabled = page >= totalPages - 1;
|
|
|
|
// Update sort arrows
|
|
document.querySelectorAll('.pr-table th').forEach(function(th) {
|
|
th.classList.toggle('sorted', th.dataset.col === sortCol);
|
|
var arrow = th.querySelector('.sort-arrow');
|
|
if (arrow) arrow.innerHTML = (th.dataset.col === sortCol && sortAsc) ? '▲' : '▼';
|
|
});
|
|
}
|
|
|
|
// Sort click
|
|
document.querySelectorAll('.pr-table th').forEach(function(th) {
|
|
th.addEventListener('click', function() {
|
|
var col = th.dataset.col;
|
|
if (col === sortCol) { sortAsc = !sortAsc; }
|
|
else { sortCol = col; sortAsc = col === 'number' ? false : true; }
|
|
sortData();
|
|
renderTable();
|
|
});
|
|
});
|
|
|
|
// Row click -> trace expand
|
|
document.getElementById('pr-tbody').addEventListener('click', function(e) {
|
|
if (e.target.closest('a')) return;
|
|
var row = e.target.closest('tr[data-pr]');
|
|
if (!row) return;
|
|
var pr = row.dataset.pr;
|
|
var traceRow = document.getElementById('trace-' + pr);
|
|
var panel = document.getElementById('panel-' + pr);
|
|
if (!traceRow) return;
|
|
|
|
if (traceRow.style.display === 'none') {
|
|
if (expandedPr && expandedPr !== pr) {
|
|
var prev = document.getElementById('trace-' + expandedPr);
|
|
if (prev) prev.style.display = 'none';
|
|
var prevRow = document.querySelector('tr[data-pr="' + expandedPr + '"]');
|
|
if (prevRow) prevRow.classList.remove('expanded');
|
|
}
|
|
traceRow.style.display = '';
|
|
panel.classList.add('open');
|
|
row.classList.add('expanded');
|
|
expandedPr = pr;
|
|
loadTrace(pr, panel);
|
|
} else {
|
|
traceRow.style.display = 'none';
|
|
panel.classList.remove('open');
|
|
row.classList.remove('expanded');
|
|
expandedPr = null;
|
|
}
|
|
});
|
|
|
|
function loadTrace(pr, panel) {
|
|
// Also find this PR in allData for claim list
|
|
var prData = null;
|
|
allData.forEach(function(p) { if (p.number == pr) prData = p; });
|
|
|
|
fetch('/api/trace/' + pr).then(function(r) { return r.json(); }).then(function(data) {
|
|
var html = '';
|
|
|
|
// --- Claims contained in this PR ---
|
|
if (prData && prData.claim_titles && prData.claim_titles.length > 0) {
|
|
html += '<div class="section-title">Claims (' + prData.claim_titles.length + ')</div>';
|
|
html += '<ul class="claim-list">';
|
|
prData.claim_titles.forEach(function(t) {
|
|
html += '<li>' + esc(t) + '</li>';
|
|
});
|
|
html += '</ul>';
|
|
}
|
|
|
|
// --- Issues summary ---
|
|
var issues = [];
|
|
if (data.timeline) {
|
|
data.timeline.forEach(function(ev) {
|
|
if (ev.detail && ev.detail.issues) {
|
|
var iss = ev.detail.issues;
|
|
if (typeof iss === 'string') { try { iss = JSON.parse(iss); } catch(e) { iss = [iss]; } }
|
|
if (Array.isArray(iss)) {
|
|
iss.forEach(function(i) {
|
|
var label = String(i).replace(/_/g, ' ');
|
|
if (issues.indexOf(label) === -1) issues.push(label);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if (prData && prData.review_snippet) {
|
|
html += '<div class="issues-box">' + esc(prData.review_snippet) + '</div>';
|
|
} else if (issues.length > 0) {
|
|
html += '<div class="issues-box">Issues: ' + issues.map(esc).join(', ') + '</div>';
|
|
}
|
|
|
|
// --- Eval chain (who reviewed with what model) ---
|
|
var models = {};
|
|
if (data.timeline) {
|
|
data.timeline.forEach(function(ev) {
|
|
if (ev.detail) {
|
|
if (ev.detail.model) models[ev.stage + '.' + ev.event] = ev.detail.model;
|
|
if (ev.detail.domain_model) models['domain_review'] = ev.detail.domain_model;
|
|
if (ev.detail.leo_model) models['leo_review'] = ev.detail.leo_model;
|
|
}
|
|
});
|
|
}
|
|
if (Object.keys(models).length > 0) {
|
|
html += '<div class="eval-chain">';
|
|
html += '<strong style="color:#58a6ff;">Eval chain:</strong> ';
|
|
var parts = [];
|
|
if (models['triage.haiku_triage'] || models['triage.deterministic_triage'])
|
|
parts.push('<span class="step"><span class="step-label">Triage</span> <span class="step-model">' + shortModel(models['triage.haiku_triage'] || 'deterministic') + '</span></span>');
|
|
if (models['domain_review'])
|
|
parts.push('<span class="step"><span class="step-label">Domain</span> <span class="step-model">' + shortModel(models['domain_review']) + '</span></span>');
|
|
if (models['leo_review'])
|
|
parts.push('<span class="step"><span class="step-label">Leo</span> <span class="step-model">' + shortModel(models['leo_review']) + '</span></span>');
|
|
html += parts.length > 0 ? parts.join(' <span class="arrow">→</span> ') : '<span style="color:#484f58;">No model data</span>';
|
|
html += '</div>';
|
|
}
|
|
|
|
// --- Timeline ---
|
|
if (data.timeline && data.timeline.length > 0) {
|
|
html += '<div class="section-title">Timeline</div>';
|
|
html += '<ul class="trace-timeline">';
|
|
data.timeline.forEach(function(ev) {
|
|
var cls = ev.event === 'approved' ? 'ev-approved' :
|
|
(ev.event === 'domain_rejected' || ev.event === 'tier05_rejected') ? 'ev-rejected' :
|
|
ev.event === 'changes_requested' ? 'ev-changes' : '';
|
|
var ts = ev.timestamp ? ev.timestamp.substring(0, 19).replace('T', ' ') : '';
|
|
var detail = '';
|
|
if (ev.detail) {
|
|
if (ev.detail.tier) detail += ' tier=' + ev.detail.tier;
|
|
if (ev.detail.reason) detail += ' — ' + esc(ev.detail.reason);
|
|
if (ev.detail.model) detail += ' [' + esc(shortModel(ev.detail.model)) + ']';
|
|
if (ev.detail.review_text) {
|
|
detail += '<div class="review-text">' + esc(ev.detail.review_text).substring(0, 2000) + '</div>';
|
|
}
|
|
if (ev.detail.domain_review_text) {
|
|
detail += '<div class="review-text"><strong>Domain review:</strong><br>' + esc(ev.detail.domain_review_text).substring(0, 2000) + '</div>';
|
|
}
|
|
if (ev.detail.leo_review_text) {
|
|
detail += '<div class="review-text"><strong>Leo review:</strong><br>' + esc(ev.detail.leo_review_text).substring(0, 2000) + '</div>';
|
|
}
|
|
}
|
|
html += '<li class="' + cls + '">' +
|
|
'<span class="ts">' + ts + '</span> ' +
|
|
'<span class="ev">' + esc(ev.stage + '.' + ev.event) + '</span>' +
|
|
detail + '</li>';
|
|
});
|
|
html += '</ul>';
|
|
} else {
|
|
html += '<div style="color:#484f58;font-size:12px;margin-top:8px;">No timeline events</div>';
|
|
}
|
|
|
|
// --- Reviews ---
|
|
if (data.reviews && data.reviews.length > 0) {
|
|
html += '<div class="section-title">Reviews</div>';
|
|
data.reviews.forEach(function(r) {
|
|
var cls = r.outcome === 'approved' ? 'badge-green' :
|
|
r.outcome === 'rejected' ? 'badge-red' : 'badge-yellow';
|
|
html += '<div style="margin:4px 0;">' +
|
|
'<span class="badge ' + cls + '">' + esc(r.outcome) + '</span> ' +
|
|
'<span style="color:#8b949e;font-size:11px;">' + esc(r.reviewer || '') + ' ' +
|
|
(r.model ? '[' + esc(shortModel(r.model)) + ']' : '') + ' ' +
|
|
(r.reviewed_at || '').substring(0, 19) + '</span>';
|
|
if (r.rejection_reason) {
|
|
html += ' <code>' + esc(r.rejection_reason) + '</code>';
|
|
}
|
|
if (r.notes) {
|
|
html += '<div class="review-text">' + esc(r.notes) + '</div>';
|
|
}
|
|
html += '</div>';
|
|
});
|
|
}
|
|
|
|
panel.innerHTML = html || '<div style="color:#484f58;font-size:12px;">No trace data</div>';
|
|
}).catch(function() {
|
|
panel.innerHTML = '<div style="color:#f85149;font-size:12px;">Failed to load trace</div>';
|
|
});
|
|
}
|
|
|
|
// Filter listeners
|
|
['filter-domain', 'filter-outcome', 'filter-tier'].forEach(function(id) {
|
|
document.getElementById(id).addEventListener('change', applyFilters);
|
|
});
|
|
document.getElementById('filter-days').addEventListener('change', loadData);
|
|
|
|
// Pagination
|
|
document.getElementById('pg-prev').addEventListener('click', function() { page--; renderTable(); });
|
|
document.getElementById('pg-next').addEventListener('click', function() { page++; renderTable(); });
|
|
|
|
// Init
|
|
loadData();
|
|
</script>"""
|
|
|
|
return render_page(
|
|
title="PR Lifecycle",
|
|
subtitle="Every PR through the pipeline — triage to merge",
|
|
active_path="/prs",
|
|
body_html=body,
|
|
scripts=scripts,
|
|
extra_css=EXTRA_CSS,
|
|
timestamp=now.strftime("%Y-%m-%d %H:%M UTC"),
|
|
)
|