- Add domain_agent and domain_model to pr-lifecycle API response (data was queried but dropped before serialization — evaluator column showed blank) - Show model name tag next to evaluator (Gemini Flash, GPT-4o, etc.) - Re-attribute 1201 "pipeline (self-directed)" PRs to @m3taversal — these were Cory-directed, not autonomous overnight research - Re-attribute 252 NULL PRs to @m3taversal - Fix extract.py defaults: new PRs without proposed_by default to @m3taversal - Fix backfill script defaults: extract/ branches → @m3taversal, not "pipeline (self-directed)" - Only agent-named branches (rio/, theseus/, etc.) from research-session.sh remain as "(self-directed)" Pentagon-Agent: Ship <B8D06D3F-1589-4777-B2E7-B2460D51C81F>
561 lines
26 KiB
Python
561 lines
26 KiB
Python
"""PR Lifecycle dashboard — single-page view of every PR through the pipeline.
|
|
|
|
Sortable table: PR#, summary, claims, domain, contributor, outcome, evals, evaluator, cost, date.
|
|
Click any row to expand: claim titles, eval chain, timeline, reviews, issues.
|
|
Hero cards: total PRs, merge rate, total claims, est. 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 = """
|
|
.content-wrapper { 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: 28%; } /* Summary */
|
|
.pr-table th:nth-child(3) { width: 50px; } /* Claims */
|
|
.pr-table th:nth-child(4) { width: 11%; } /* Domain */
|
|
.pr-table th:nth-child(5) { width: 10%; } /* Contributor */
|
|
.pr-table th:nth-child(6) { width: 10%; } /* Outcome */
|
|
.pr-table th:nth-child(7) { width: 44px; } /* Evals */
|
|
.pr-table th:nth-child(8) { width: 12%; } /* Evaluator */
|
|
.pr-table th:nth-child(9) { width: 60px; } /* Cost */
|
|
.pr-table th:nth-child(10) { width: 80px; } /* 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: 10px; color: #6e7681; background: #161b22; border-radius: 3px; padding: 1px 4px; }
|
|
.pr-table td .contributor-tag { font-size: 11px; color: #d2a8ff; }
|
|
.pr-table td .contributor-self { font-size: 11px; color: #6e7681; font-style: italic; }
|
|
.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; }
|
|
.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 h4 { color: #58a6ff; font-size: 12px; margin: 12px 0 6px 0; }
|
|
.trace-panel h4:first-child { margin-top: 0; }
|
|
.claim-list { list-style: none; padding: 0; margin: 0; }
|
|
.claim-list li { padding: 4px 0 4px 16px; border-left: 2px solid #238636; color: #c9d1d9; font-size: 12px; line-height: 1.5; }
|
|
.claim-list li .claim-confidence { font-size: 10px; color: #8b949e; margin-left: 6px; }
|
|
.issues-box { background: #1c1210; border: 1px solid #f8514933; border-radius: 6px;
|
|
padding: 8px 12px; margin: 4px 0; font-size: 12px; color: #f85149; }
|
|
.eval-chain { background: #161b22; border-radius: 6px; padding: 8px 12px; margin: 4px 0; font-size: 12px; }
|
|
.eval-chain .chain-step { display: inline-block; margin-right: 6px; }
|
|
.eval-chain .chain-arrow { color: #484f58; margin: 0 4px; }
|
|
.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; }
|
|
.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">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-contributor"><option value="">All Contributors</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="submitted_by">Contributor <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_label">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>
|
|
"""
|
|
|
|
scripts = """<script>
|
|
var PAGE_SIZE = 50;
|
|
var FORGEJO = 'https://git.livingip.xyz/teleo/teleo-codex/pulls/';
|
|
var allData = [];
|
|
var filtered = [];
|
|
var sortCol = 'number';
|
|
var sortAsc = false;
|
|
var page = 0;
|
|
var expandedPr = null;
|
|
|
|
// Tier-based cost estimates (per eval round)
|
|
var TIER_COSTS = {
|
|
'DEEP': 0.145, // Haiku triage + Gemini Flash domain + Opus Leo
|
|
'STANDARD': 0.043, // Haiku triage + Gemini Flash domain + Sonnet Leo
|
|
'LIGHT': 0.027 // Haiku triage + Gemini Flash domain only
|
|
};
|
|
|
|
function estimateCost(pr) {
|
|
var tier = pr.tier || 'STANDARD';
|
|
var rounds = pr.eval_rounds || 1;
|
|
var baseCost = TIER_COSTS[tier] || TIER_COSTS['STANDARD'];
|
|
return baseCost * rounds;
|
|
}
|
|
|
|
function fmtCost(val) {
|
|
if (val == null || val === 0) return '--';
|
|
return '$' + val.toFixed(3);
|
|
}
|
|
|
|
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 || [];
|
|
// Compute derived fields
|
|
allData.forEach(function(p) {
|
|
p.est_cost = estimateCost(p);
|
|
// Evaluator label for sorting
|
|
p.evaluator_label = p.domain_agent || p.agent || '--';
|
|
});
|
|
populateFilters(allData);
|
|
updateKPIs(data);
|
|
applyFilters();
|
|
}).catch(function() {
|
|
document.getElementById('pr-tbody').innerHTML =
|
|
'<tr><td colspan="10" style="text-align:center;color:#f85149;">Failed to load data</td></tr>';
|
|
});
|
|
}
|
|
|
|
function populateFilters(prs) {
|
|
var domains = [], contribs = [], seenD = {}, seenC = {};
|
|
prs.forEach(function(p) {
|
|
if (p.domain && !seenD[p.domain]) { seenD[p.domain] = 1; domains.push(p.domain); }
|
|
var c = p.submitted_by || 'unknown';
|
|
if (!seenC[c]) { seenC[c] = 1; contribs.push(c); }
|
|
});
|
|
domains.sort(); contribs.sort();
|
|
var domSel = document.getElementById('filter-domain');
|
|
var conSel = document.getElementById('filter-contributor');
|
|
var curDom = domSel.value, curCon = conSel.value;
|
|
domSel.innerHTML = '<option value="">All Domains</option>' +
|
|
domains.map(function(d) { return '<option value="' + esc(d) + '">' + esc(d) + '</option>'; }).join('');
|
|
conSel.innerHTML = '<option value="">All Contributors</option>' +
|
|
contribs.map(function(c) { return '<option value="' + esc(c) + '">' + esc(c) + '</option>'; }).join('');
|
|
domSel.value = curDom; conSel.value = curCon;
|
|
}
|
|
|
|
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';
|
|
|
|
var totalClaims = 0, mergedClaims = 0, totalCost = 0;
|
|
(data.prs || []).forEach(function(p) {
|
|
totalClaims += (p.claims_count || 1);
|
|
if (p.status === 'merged') mergedClaims += (p.claims_count || 1);
|
|
totalCost += estimateCost(p);
|
|
});
|
|
document.getElementById('kpi-claims').textContent = fmtNum(totalClaims);
|
|
document.getElementById('kpi-claims-detail').textContent = fmtNum(mergedClaims) + ' merged';
|
|
|
|
document.getElementById('kpi-cost').textContent = '$' + totalCost.toFixed(2);
|
|
var perClaim = totalClaims > 0 ? totalCost / totalClaims : 0;
|
|
document.getElementById('kpi-cost-detail').textContent = '$' + perClaim.toFixed(3) + '/claim';
|
|
}
|
|
|
|
function applyFilters() {
|
|
var dom = document.getElementById('filter-domain').value;
|
|
var con = document.getElementById('filter-contributor').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 (con && (p.submitted_by || 'unknown') !== con) 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 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="10" 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: first claim title
|
|
var summary = p.summary || '--';
|
|
|
|
// Outcome with tier badge
|
|
var tierBadge = p.tier ? ' <span class="' + tierClass + '" style="font-size:10px;">' + esc(p.tier) + '</span>' : '';
|
|
|
|
// Review snippet for issues
|
|
var reviewSnippet = '';
|
|
if (p.review_snippet) {
|
|
reviewSnippet = '<div class="review-snippet">' + esc(truncate(p.review_snippet, 100)) + '</div>';
|
|
}
|
|
|
|
// Contributor display
|
|
var contributor = p.submitted_by || '--';
|
|
var contribClass = 'contributor-tag';
|
|
if (contributor.indexOf('self-directed') >= 0 || contributor === 'unknown') {
|
|
contribClass = 'contributor-self';
|
|
}
|
|
|
|
// Evaluator: domain agent + model tag
|
|
var evaluator = '';
|
|
if (p.domain_agent) {
|
|
var modelShort = '';
|
|
if (p.domain_model) {
|
|
var m = p.domain_model;
|
|
if (m.indexOf('gemini') >= 0) modelShort = 'Gemini Flash';
|
|
else if (m.indexOf('gpt-4o') >= 0) modelShort = 'GPT-4o';
|
|
else if (m.indexOf('sonnet') >= 0) modelShort = 'Sonnet';
|
|
else modelShort = m.split('/').pop();
|
|
}
|
|
evaluator = esc(p.domain_agent) + (modelShort ? ' <span class="model-tag">' + esc(modelShort) + '</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 || 1) + '</td>' +
|
|
'<td>' + esc(p.domain || '--') + '</td>' +
|
|
'<td><span class="' + contribClass + '">' + esc(truncate(contributor, 20)) + '</span></td>' +
|
|
'<td class="' + outClass + '">' + esc(p.status || '--') + tierBadge + '</td>' +
|
|
'<td style="text-align:center;">' + (p.eval_rounds || '--') + '</td>' +
|
|
'<td>' + evaluator + '</td>' +
|
|
'<td>' + fmtCost(p.est_cost) + '</td>' +
|
|
'<td>' + date + '</td>' +
|
|
'</tr>' +
|
|
'<tr id="trace-' + p.number + '" style="display:none;"><td colspan="10" style="padding:0;">' +
|
|
'<div class="trace-panel" id="panel-' + p.number + '">Loading...</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) {
|
|
// Find the PR data for claim titles
|
|
var prData = null;
|
|
for (var i = 0; i < allData.length; i++) {
|
|
if (allData[i].number == pr) { prData = allData[i]; break; }
|
|
}
|
|
|
|
fetch('/api/trace/' + pr).then(function(r) { return r.json(); }).then(function(data) {
|
|
var html = '';
|
|
|
|
// ─── Claims contained in this PR ───
|
|
if (prData && prData.description) {
|
|
var titles = prData.description.split('|').map(function(t) { return t.trim(); }).filter(Boolean);
|
|
if (titles.length > 0) {
|
|
html += '<h4>Claims (' + titles.length + ')</h4>';
|
|
html += '<ul class="claim-list">';
|
|
titles.forEach(function(t) {
|
|
html += '<li>' + esc(t) + '</li>';
|
|
});
|
|
html += '</ul>';
|
|
}
|
|
}
|
|
|
|
// ─── Issues (if any) ───
|
|
if (prData && prData.review_snippet) {
|
|
html += '<div class="issues-box">' + esc(prData.review_snippet) + '</div>';
|
|
}
|
|
|
|
// ─── Eval chain with models ───
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
|
|
html += '<div class="eval-chain"><strong style="color:#58a6ff;">Eval Chain:</strong> ';
|
|
var chain = [];
|
|
if (models['triage.haiku_triage'] || models['triage.deterministic_triage']) {
|
|
chain.push('<span class="chain-step">Triage <span class="model-tag">' +
|
|
esc(models['triage.haiku_triage'] || 'deterministic') + '</span></span>');
|
|
}
|
|
if (models['domain_review']) {
|
|
chain.push('<span class="chain-step">Domain <span class="model-tag">' +
|
|
esc(models['domain_review']) + '</span></span>');
|
|
}
|
|
if (models['leo_review']) {
|
|
chain.push('<span class="chain-step">Leo <span class="model-tag">' +
|
|
esc(models['leo_review']) + '</span></span>');
|
|
}
|
|
html += chain.length > 0 ? chain.join('<span class="chain-arrow">→</span>') :
|
|
'<span style="color:#484f58;">No model data</span>';
|
|
html += '</div>';
|
|
|
|
// ─── Source + contributor metadata ───
|
|
if (data.pr) {
|
|
html += '<div style="margin:8px 0;font-size:12px;color:#8b949e;">';
|
|
if (data.pr.source_path) html += 'Source: <span style="color:#c9d1d9;">' + esc(data.pr.source_path) + '</span> · ';
|
|
if (prData && prData.submitted_by) html += 'Contributor: <span style="color:#d2a8ff;">' + esc(prData.submitted_by) + '</span> · ';
|
|
if (data.pr.tier) html += 'Tier: <span style="color:#c9d1d9;">' + esc(data.pr.tier) + '</span> · ';
|
|
html += '<a class="pr-link" href="' + FORGEJO + pr + '" target="_blank">View on Forgejo</a>';
|
|
html += '</div>';
|
|
}
|
|
|
|
// ─── Timeline ───
|
|
if (data.timeline && data.timeline.length > 0) {
|
|
html += '<h4>Timeline</h4>';
|
|
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(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:8px 0;">No timeline events</div>';
|
|
}
|
|
|
|
// ─── Reviews ───
|
|
if (data.reviews && data.reviews.length > 0) {
|
|
html += '<h4>Reviews</h4>';
|
|
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(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-contributor', '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"),
|
|
)
|