"""PR Lifecycle dashboard — single-page view of every PR through the pipeline. Sortable table: PR#, summary, agent, domain, outcome, TTM, date. Click any row to expand the full trace (triage reasoning, review text, cascade). Hero cards: total PRs, merge rate, median TTM, median eval rounds. Data sources: prs table, audit_log (eval rounds), review_records. Owner: Ship """ from datetime import datetime from shared_ui import render_page EXTRA_CSS = """ .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: 60px; } /* PR# */ .pr-table th:nth-child(2) { width: 38%; } /* Summary */ .pr-table th:nth-child(3) { width: 10%; } /* Agent */ .pr-table th:nth-child(4) { width: 14%; } /* Domain */ .pr-table th:nth-child(5) { width: 10%; } /* Outcome */ .pr-table th:nth-child(6) { width: 7%; } /* TTM */ .pr-table th:nth-child(7) { width: 10%; } /* 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 .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-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; } .stat-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px; } .stat-row .mini-stat { font-size: 11px; color: #8b949e; } .stat-row .mini-stat span { color: #c9d1d9; font-weight: 600; } """ def render_prs_page(now: datetime) -> str: """Render the PR lifecycle page. All data loaded client-side via /api/pr-lifecycle.""" body = """