feat: add shared team dashboard, regression alerts, weekly digest edge functions

Dashboard: Supabase edge function serving self-contained HTML with
PKCE OAuth, 6 parallel client-side REST queries, SVG charts, dark
theme, auto-refresh, who's-online from heartbeats. Public URL.

Regression alert: webhook on eval_runs INSERT, 5-min cooldown dedup
via alert_cooldowns, Slack notification on >5% pass rate drop.

Weekly digest: pg_cron Monday 9am UTC, aggregates 7-day team data,
Slack message with evals/ships/sessions/costs. 15 tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-16 02:44:47 -05:00
parent 46c82ce8ec
commit 78840c64a8
5 changed files with 1413 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
/**
* Dashboard edge function — serves the team dashboard HTML.
*
* Public URL: https://<project>.supabase.co/functions/v1/dashboard
* No auth required (the HTML page handles auth client-side via PKCE).
*/
import { getDashboardHTML } from './ui.ts';
Deno.serve((_req: Request) => {
const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? '';
const anonKey = Deno.env.get('SUPABASE_ANON_KEY') ?? '';
const html = getDashboardHTML(supabaseUrl, anonKey);
return new Response(html, {
status: 200,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
});
});
+909
View File
@@ -0,0 +1,909 @@
/**
* Dashboard UI — self-contained HTML page for gstack's team engineering intelligence platform.
*
* Served by a Supabase edge function. All auth (PKCE), data fetching, and rendering
* happen client-side. The server only injects supabaseUrl and anonKey into the template.
*/
export function getDashboardHTML(supabaseUrl: string, anonKey: string): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>gstack Dashboard</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #0a0a0a;
color: #e5e5e5;
font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
font-size: 14px;
line-height: 1.5;
}
/* ---------- Layout ---------- */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid #222;
}
.header h1 { font-size: 18px; font-weight: 600; }
.header-right { display: flex; align-items: center; gap: 16px; font-size: 13px; color: #888; }
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
display: inline-block; margin-right: 6px;
}
.status-dot.ok { background: #4ade80; }
.status-dot.err { background: #ef4444; }
.btn-signout {
background: transparent; border: 1px solid #333; color: #888;
padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 13px;
}
.btn-signout:hover { color: #e5e5e5; border-color: #555; }
/* ---------- Tabs ---------- */
.tabs {
display: flex; gap: 0; border-bottom: 1px solid #222;
padding: 0 24px; overflow-x: auto;
}
.tab {
padding: 10px 18px; cursor: pointer; color: #888;
border-bottom: 2px solid transparent; white-space: nowrap;
font-size: 13px; font-weight: 500; transition: color 0.15s;
}
.tab:hover { color: #ccc; }
.tab.active { color: #4ade80; border-bottom-color: #4ade80; }
/* ---------- Content ---------- */
.content { padding: 24px; max-width: 1200px; margin: 0 auto; }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ---------- Cards ---------- */
.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card {
background: #141414; border: 1px solid #222; border-radius: 10px; padding: 20px;
}
.stat-card .label { font-size: 12px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
.stat-card .value { font-size: 28px; font-weight: 700; font-family: 'SF Mono', 'Fira Code', monospace; }
/* ---------- Tables ---------- */
.panel { background: #141414; border: 1px solid #222; border-radius: 10px; padding: 20px; margin-bottom: 24px; }
.panel h3 { font-size: 14px; font-weight: 600; margin-bottom: 12px; }
table { width: 100%; border-collapse: collapse; }
th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #666; padding: 8px 12px; border-bottom: 1px solid #222; }
td { padding: 8px 12px; border-bottom: 1px solid #1a1a1a; font-size: 13px; }
td.mono { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
a { color: #4ade80; text-decoration: none; }
a:hover { text-decoration: underline; }
/* ---------- Health score colors ---------- */
.score-green { color: #4ade80; }
.score-yellow { color: #facc15; }
.score-red { color: #ef4444; }
/* ---------- Online dot ---------- */
.online-dot {
width: 6px; height: 6px; border-radius: 50%; background: #4ade80;
display: inline-block; margin-right: 6px;
}
/* ---------- Login ---------- */
.login-card {
position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
background: #0a0a0a;
}
.login-card .inner {
background: #141414; border: 1px solid #222; border-radius: 12px;
padding: 40px; text-align: center; max-width: 380px;
}
.login-card h2 { font-size: 20px; margin-bottom: 8px; }
.login-card p { color: #888; margin-bottom: 24px; font-size: 14px; }
.btn-github {
background: #e5e5e5; color: #0a0a0a; border: none; padding: 10px 24px;
border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer;
}
.btn-github:hover { background: #fff; }
/* ---------- Error / empty ---------- */
.panel-error { color: #888; font-size: 13px; padding: 16px 0; }
.panel-empty { color: #555; font-size: 13px; padding: 16px 0; font-style: italic; }
/* ---------- SVG charts ---------- */
.chart-container { margin-bottom: 16px; }
svg text { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; fill: #888; }
</style>
</head>
<body>
<!-- Login screen -->
<div id="login-screen" class="login-card" style="display:none">
<div class="inner">
<h2>gstack Dashboard</h2>
<p>Team engineering intelligence</p>
<button class="btn-github" id="btn-login">Sign in with GitHub</button>
</div>
</div>
<!-- Dashboard (shown after auth) -->
<div id="dashboard" style="display:none">
<div class="header">
<h1>gstack Dashboard</h1>
<div class="header-right">
<span><span id="status-dot" class="status-dot ok"></span><span id="status-text">Updated just now</span></span>
<button class="btn-signout" id="btn-signout">Sign out</button>
</div>
</div>
<div class="tabs">
<div class="tab active" data-tab="overview">Overview</div>
<div class="tab" data-tab="evals">Evals</div>
<div class="tab" data-tab="ships">Ships</div>
<div class="tab" data-tab="costs">Costs</div>
<div class="tab" data-tab="leaderboard">Leaderboard</div>
<div class="tab" data-tab="qa">QA</div>
</div>
<div class="content">
<!-- Overview -->
<div class="tab-panel active" id="panel-overview">
<div class="stat-row">
<div class="stat-card"><div class="label">Eval Runs This Week</div><div class="value" id="stat-evals-week">-</div></div>
<div class="stat-card"><div class="label">Ships This Week</div><div class="value" id="stat-ships-week">-</div></div>
<div class="stat-card"><div class="label">Active Now</div><div class="value" id="stat-active-now">-</div></div>
<div class="stat-card"><div class="label">Cost This Week</div><div class="value" id="stat-cost-week">-</div></div>
</div>
<div class="panel"><h3>Recent Eval Runs</h3><table id="tbl-overview-evals"><thead><tr><th>Date</th><th>Branch</th><th>Pass Rate</th><th>Cost</th></tr></thead><tbody></tbody></table><div class="panel-empty" id="empty-overview-evals" style="display:none">No data yet</div></div>
<div class="panel"><h3>Recent Ships</h3><table id="tbl-overview-ships"><thead><tr><th>Date</th><th>Version</th><th>Branch</th><th>PR</th></tr></thead><tbody></tbody></table><div class="panel-empty" id="empty-overview-ships" style="display:none">No data yet</div></div>
</div>
<!-- Evals -->
<div class="tab-panel" id="panel-evals">
<div class="panel"><h3>Pass Rate Trend</h3><div class="chart-container" id="chart-sparkline"></div></div>
<div class="panel"><h3>Recent Eval Runs</h3><table id="tbl-evals"><thead><tr><th>Date</th><th>User</th><th>Branch</th><th>Pass Rate</th><th>Cost</th><th>Tier</th></tr></thead><tbody></tbody></table><div class="panel-empty" id="empty-evals" style="display:none">No data yet</div></div>
</div>
<!-- Ships -->
<div class="tab-panel" id="panel-ships">
<div class="panel"><h3>Recent Ships</h3><table id="tbl-ships"><thead><tr><th>Date</th><th>Version</th><th>Branch</th><th>PR</th></tr></thead><tbody></tbody></table><div class="panel-empty" id="empty-ships" style="display:none">No data yet</div></div>
<div class="panel"><h3>Ships Per Person This Week</h3><div class="chart-container" id="chart-ships-per-person"></div></div>
</div>
<!-- Costs -->
<div class="tab-panel" id="panel-costs">
<div class="stat-row">
<div class="stat-card"><div class="label">Total All-Time Cost</div><div class="value" id="stat-cost-total">-</div></div>
</div>
<div class="panel"><h3>Weekly Cost Trend</h3><div class="chart-container" id="chart-weekly-cost"></div></div>
</div>
<!-- Leaderboard -->
<div class="tab-panel" id="panel-leaderboard">
<div class="panel"><h3>This Week</h3><table id="tbl-leaderboard"><thead><tr><th>#</th><th>Who</th><th>Ships</th><th>Evals</th><th>Sessions</th><th>Pass Rate</th><th>Cost</th></tr></thead><tbody></tbody></table><div class="panel-empty" id="empty-leaderboard" style="display:none">No data yet</div></div>
</div>
<!-- QA -->
<div class="tab-panel" id="panel-qa">
<div class="panel"><h3>Recent QA Reports</h3><table id="tbl-qa"><thead><tr><th>Date</th><th>Repo</th><th>Health Score</th></tr></thead><tbody></tbody></table><div class="panel-empty" id="empty-qa" style="display:none">No data yet</div></div>
</div>
</div>
</div>
<script>
(function() {
'use strict';
// ---- Config injected by the edge function ----
const SUPABASE_URL = ${JSON.stringify(supabaseUrl)};
const ANON_KEY = ${JSON.stringify(anonKey)};
// ---- DOM refs ----
const $login = document.getElementById('login-screen');
const $dashboard = document.getElementById('dashboard');
const $statusDot = document.getElementById('status-dot');
const $statusText = document.getElementById('status-text');
// ---- State ----
let accessToken = null;
let refreshToken = null;
let lastFetchTime = 0;
let refreshTimer = null;
// All fetched data
let data = { evalRuns: [], shipLogs: [], evalCosts: [], qaReports: [], transcripts: [], heartbeats: [] };
// ================================================================
// PKCE Auth helpers
// ================================================================
/** Generate a random string for PKCE code_verifier (43 chars, URL-safe) */
function generateVerifier() {
const arr = new Uint8Array(32);
crypto.getRandomValues(arr);
return btoa(String.fromCharCode(...arr))
.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')
.slice(0, 43);
}
/** SHA-256 hash, then base64url encode for code_challenge */
async function sha256Challenge(verifier) {
const encoded = new TextEncoder().encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', encoded);
return btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');
}
/** Decode a JWT payload (base64url middle segment) */
function decodeJWT(token) {
try {
const payload = token.split('.')[1];
const padded = payload.replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(atob(padded));
} catch { return null; }
}
/** Check if a JWT is expired (with 60s buffer) */
function isExpired(token) {
const claims = decodeJWT(token);
if (!claims || !claims.exp) return true;
return claims.exp * 1000 < Date.now() - 60000;
}
// ================================================================
// Auth flow
// ================================================================
/** Try to restore session from localStorage */
function restoreSession() {
accessToken = localStorage.getItem('sb-access-token');
refreshToken = localStorage.getItem('sb-refresh-token');
if (accessToken && !isExpired(accessToken)) return true;
if (accessToken && refreshToken) return tryRefresh();
return false;
}
/** Exchange refresh token for a new access token */
async function tryRefresh() {
try {
const res = await fetch(SUPABASE_URL + '/auth/v1/token?grant_type=refresh_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'apikey': ANON_KEY },
body: JSON.stringify({ refresh_token: refreshToken })
});
if (!res.ok) throw new Error('refresh failed');
const json = await res.json();
accessToken = json.access_token;
refreshToken = json.refresh_token;
localStorage.setItem('sb-access-token', accessToken);
localStorage.setItem('sb-refresh-token', refreshToken);
return true;
} catch {
signOut();
return false;
}
}
/** Exchange authorization code for tokens (PKCE) */
async function exchangeCode(code) {
const verifier = sessionStorage.getItem('pkce-verifier');
if (!verifier) { showLogin(); return; }
try {
const res = await fetch(SUPABASE_URL + '/auth/v1/token?grant_type=pkce', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'apikey': ANON_KEY },
body: JSON.stringify({ auth_code: code, code_verifier: verifier })
});
if (!res.ok) throw new Error('token exchange failed');
const json = await res.json();
accessToken = json.access_token;
refreshToken = json.refresh_token;
localStorage.setItem('sb-access-token', accessToken);
localStorage.setItem('sb-refresh-token', refreshToken);
sessionStorage.removeItem('pkce-verifier');
// Clean URL
window.history.replaceState({}, '', window.location.pathname);
showDashboard();
} catch {
showLogin();
}
}
/** Redirect to GitHub OAuth with PKCE */
async function startLogin() {
const verifier = generateVerifier();
sessionStorage.setItem('pkce-verifier', verifier);
const challenge = await sha256Challenge(verifier);
const redirectTo = window.location.origin + window.location.pathname;
const url = SUPABASE_URL + '/auth/v1/authorize'
+ '?provider=github'
+ '&redirect_to=' + encodeURIComponent(redirectTo)
+ '&flow_type=pkce'
+ '&code_challenge_method=s256'
+ '&code_challenge=' + challenge;
window.location.href = url;
}
function signOut() {
localStorage.removeItem('sb-access-token');
localStorage.removeItem('sb-refresh-token');
accessToken = null;
refreshToken = null;
showLogin();
}
function showLogin() {
$login.style.display = 'flex';
$dashboard.style.display = 'none';
stopAutoRefresh();
}
function showDashboard() {
$login.style.display = 'none';
$dashboard.style.display = 'block';
fetchAll();
startAutoRefresh();
}
// ================================================================
// Data fetching
// ================================================================
/** Fetch a single REST endpoint with auth headers */
async function apiFetch(path) {
const res = await fetch(SUPABASE_URL + '/rest/v1/' + path, {
headers: {
'apikey': ANON_KEY,
'Authorization': 'Bearer ' + accessToken
}
});
if (res.status === 401) {
const ok = await tryRefresh();
if (!ok) { signOut(); throw new Error('auth expired'); }
// Retry once with new token
const retry = await fetch(SUPABASE_URL + '/rest/v1/' + path, {
headers: {
'apikey': ANON_KEY,
'Authorization': 'Bearer ' + accessToken
}
});
if (!retry.ok) throw new Error('fetch failed: ' + path);
return retry.json();
}
if (!res.ok) throw new Error('fetch failed: ' + path);
return res.json();
}
/** Fetch all 6 data sources in parallel */
async function fetchAll() {
const fetches = [
apiFetch('eval_runs?order=timestamp.desc&limit=100').then(d => { data.evalRuns = d; }).catch(() => { data.evalRuns = null; }),
apiFetch('ship_logs?order=created_at.desc&limit=100').then(d => { data.shipLogs = d; }).catch(() => { data.shipLogs = null; }),
apiFetch('eval_costs?order=created_at.desc&limit=200').then(d => { data.evalCosts = d; }).catch(() => { data.evalCosts = null; }),
apiFetch('qa_reports?order=created_at.desc&limit=100').then(d => { data.qaReports = d; }).catch(() => { data.qaReports = null; }),
apiFetch('session_transcripts?order=started_at.desc&limit=100').then(d => { data.transcripts = d; }).catch(() => { data.transcripts = null; }),
apiFetch('sync_heartbeats?order=timestamp.desc&limit=50').then(d => { data.heartbeats = d; }).catch(() => { data.heartbeats = null; }),
];
await Promise.allSettled(fetches);
lastFetchTime = Date.now();
updateStatus(true);
renderAll();
}
// ================================================================
// Auto-refresh (30s, pauses when tab hidden)
// ================================================================
function startAutoRefresh() {
stopAutoRefresh();
refreshTimer = setInterval(() => { fetchAll(); }, 30000);
}
function stopAutoRefresh() {
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
}
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
stopAutoRefresh();
} else {
// Fetch immediately when tab becomes visible, then resume timer
if (accessToken) { fetchAll(); startAutoRefresh(); }
}
});
function updateStatus(ok) {
$statusDot.className = 'status-dot ' + (ok ? 'ok' : 'err');
updateStatusText();
}
function updateStatusText() {
const elapsed = Math.floor((Date.now() - lastFetchTime) / 1000);
if (elapsed < 5) { $statusText.textContent = 'Updated just now'; }
else { $statusText.textContent = 'Updated ' + elapsed + 's ago'; }
}
// Update the "Xs ago" label every 5 seconds
setInterval(updateStatusText, 5000);
// ================================================================
// Helpers
// ================================================================
/** Format an ISO date string to a short readable form */
function fmtDate(iso) {
if (!iso) return '-';
const d = new Date(iso);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
/** Format USD cost */
function fmtCost(n) {
if (n == null || isNaN(n)) return '-';
return '$' + Number(n).toFixed(2);
}
/** Get the start of the current ISO week (Monday) */
function weekStart() {
const now = new Date();
const day = now.getDay();
const diff = now.getDate() - day + (day === 0 ? -6 : 1);
const monday = new Date(now);
monday.setHours(0, 0, 0, 0);
monday.setDate(diff);
return monday;
}
/** Check if a timestamp is within the last N minutes */
function withinMinutes(iso, mins) {
if (!iso) return false;
return (Date.now() - new Date(iso).getTime()) < mins * 60 * 1000;
}
/** Create a table row, setting textContent for each cell (XSS safe) */
function makeRow(cells) {
const tr = document.createElement('tr');
cells.forEach(function(cell) {
const td = document.createElement('td');
if (typeof cell === 'object' && cell.href) {
// Clickable link — still safe, we control the href
const a = document.createElement('a');
a.href = cell.href;
a.target = '_blank';
a.rel = 'noopener';
a.textContent = cell.text || cell.href;
td.appendChild(a);
} else if (typeof cell === 'object' && cell.html) {
// For inline dots — only used with controlled content
td.innerHTML = cell.html;
} else {
td.textContent = String(cell != null ? cell : '-');
if (typeof cell === 'number' || (typeof cell === 'string' && /^[\\$\\d\\.\\-]+$/.test(cell))) {
td.classList.add('mono');
}
}
tr.appendChild(td);
});
return tr;
}
/** Show/hide empty placeholder */
function toggleEmpty(tableId, emptyId, hasData) {
const tbl = document.getElementById(tableId);
const empty = document.getElementById(emptyId);
if (!tbl || !empty) return;
tbl.style.display = hasData ? '' : 'none';
empty.style.display = hasData ? 'none' : '';
}
/** Show error text inside a panel */
function showPanelError(tableId, emptyId) {
const empty = document.getElementById(emptyId);
if (empty) {
empty.textContent = 'Could not load data';
empty.className = 'panel-error';
empty.style.display = '';
}
const tbl = document.getElementById(tableId);
if (tbl) tbl.style.display = 'none';
}
/** Clear a table body */
function clearTbody(tableId) {
const tbl = document.getElementById(tableId);
if (!tbl) return null;
const tbody = tbl.querySelector('tbody');
if (tbody) tbody.innerHTML = '';
return tbody;
}
// ================================================================
// SVG chart builders
// ================================================================
/** Sparkline: dots connected by a polyline */
function renderSparkline(containerId, values) {
const el = document.getElementById(containerId);
if (!el) return;
if (!values || values.length === 0) { el.innerHTML = '<span class="panel-empty">No data yet</span>'; return; }
const W = 600, H = 120, PAD = 30;
const max = Math.max(...values, 1);
const min = Math.min(...values, 0);
const range = max - min || 1;
const pts = values.map(function(v, i) {
const x = PAD + (i / Math.max(values.length - 1, 1)) * (W - PAD * 2);
const y = H - PAD - ((v - min) / range) * (H - PAD * 2);
return { x: x, y: y, v: v };
});
const polyline = pts.map(function(p) { return p.x + ',' + p.y; }).join(' ');
const dots = pts.map(function(p) {
return '<circle cx="' + p.x + '" cy="' + p.y + '" r="4" fill="#4ade80" />';
}).join('');
// Y-axis labels
const yLabels = '<text x="4" y="' + (PAD + 4) + '">' + Math.round(max * 100) + '%</text>'
+ '<text x="4" y="' + (H - PAD + 4) + '">' + Math.round(min * 100) + '%</text>';
el.innerHTML = '<svg viewBox="0 0 ' + W + ' ' + H + '" width="100%" height="' + H + '">'
+ yLabels
+ '<polyline points="' + polyline + '" fill="none" stroke="#4ade80" stroke-width="2" />'
+ dots
+ '</svg>';
}
/** Horizontal bar chart */
function renderHBarChart(containerId, items) {
const el = document.getElementById(containerId);
if (!el) return;
if (!items || items.length === 0) { el.innerHTML = '<span class="panel-empty">No data yet</span>'; return; }
const W = 600, barH = 28, gap = 6;
const H = items.length * (barH + gap) + 20;
const maxVal = Math.max(...items.map(function(d) { return d.value; }), 1);
const LABEL_W = 120, BAR_AREA = W - LABEL_W - 60;
let bars = '';
items.forEach(function(item, i) {
const y = i * (barH + gap) + 10;
const bw = Math.max((item.value / maxVal) * BAR_AREA, 2);
bars += '<text x="' + (LABEL_W - 8) + '" y="' + (y + barH / 2 + 4) + '" text-anchor="end" fill="#ccc" font-size="12">'
+ escapeHTML(item.label) + '</text>';
bars += '<rect x="' + LABEL_W + '" y="' + y + '" width="' + bw + '" height="' + barH + '" rx="4" fill="#4ade80" opacity="0.8" />';
bars += '<text x="' + (LABEL_W + bw + 8) + '" y="' + (y + barH / 2 + 4) + '" fill="#888" font-size="12">' + item.value + '</text>';
});
el.innerHTML = '<svg viewBox="0 0 ' + W + ' ' + H + '" width="100%" height="' + H + '">' + bars + '</svg>';
}
/** Vertical bar chart (for weekly cost) */
function renderVBarChart(containerId, items) {
const el = document.getElementById(containerId);
if (!el) return;
if (!items || items.length === 0) { el.innerHTML = '<span class="panel-empty">No data yet</span>'; return; }
const W = 600, H = 180, PAD_BOTTOM = 40, PAD_TOP = 20, PAD_LEFT = 50;
const maxVal = Math.max(...items.map(function(d) { return d.value; }), 0.01);
const barW = Math.min(50, (W - PAD_LEFT - 20) / items.length - 8);
const chartH = H - PAD_BOTTOM - PAD_TOP;
let bars = '';
items.forEach(function(item, i) {
const x = PAD_LEFT + i * ((W - PAD_LEFT - 20) / items.length) + 4;
const bh = Math.max((item.value / maxVal) * chartH, 2);
const y = PAD_TOP + chartH - bh;
bars += '<rect x="' + x + '" y="' + y + '" width="' + barW + '" height="' + bh + '" rx="3" fill="#4ade80" opacity="0.8" />';
// Value on top
bars += '<text x="' + (x + barW / 2) + '" y="' + (y - 6) + '" text-anchor="middle" font-size="10" fill="#888">$' + item.value.toFixed(2) + '</text>';
// Label at bottom
bars += '<text x="' + (x + barW / 2) + '" y="' + (H - 10) + '" text-anchor="middle" font-size="10" fill="#666">' + escapeHTML(item.label) + '</text>';
});
// Y-axis line
bars += '<line x1="' + PAD_LEFT + '" y1="' + PAD_TOP + '" x2="' + PAD_LEFT + '" y2="' + (H - PAD_BOTTOM) + '" stroke="#333" />';
el.innerHTML = '<svg viewBox="0 0 ' + W + ' ' + H + '" width="100%" height="' + H + '">' + bars + '</svg>';
}
/** Minimal HTML escape for chart labels (numbers + short strings we control) */
function escapeHTML(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ================================================================
// Render functions
// ================================================================
function renderAll() {
renderOverview();
renderEvals();
renderShips();
renderCosts();
renderLeaderboard();
renderQA();
}
// ---- Overview ----
function renderOverview() {
const ws = weekStart();
// Stat cards
if (data.evalRuns) {
const thisWeek = data.evalRuns.filter(function(r) { return new Date(r.timestamp) >= ws; });
document.getElementById('stat-evals-week').textContent = thisWeek.length;
}
if (data.shipLogs) {
const thisWeek = data.shipLogs.filter(function(r) { return new Date(r.created_at) >= ws; });
document.getElementById('stat-ships-week').textContent = thisWeek.length;
}
if (data.heartbeats) {
const active = data.heartbeats.filter(function(h) { return withinMinutes(h.timestamp, 15); });
// Count unique hostnames
const uniqueHosts = new Set(active.map(function(h) { return h.hostname || h.user_id; }));
document.getElementById('stat-active-now').textContent = uniqueHosts.size;
}
if (data.evalCosts) {
const thisWeek = data.evalCosts.filter(function(c) { return new Date(c.created_at) >= ws; });
const total = thisWeek.reduce(function(s, c) { return s + Number(c.estimated_cost_usd || 0); }, 0);
document.getElementById('stat-cost-week').textContent = fmtCost(total);
}
// Recent eval runs table (last 10)
if (data.evalRuns === null) {
showPanelError('tbl-overview-evals', 'empty-overview-evals');
} else {
const tbody = clearTbody('tbl-overview-evals');
const rows = data.evalRuns.slice(0, 10);
rows.forEach(function(r) {
const rate = r.total_tests ? ((r.passed / r.total_tests) * 100).toFixed(0) + '%' : '-';
tbody.appendChild(makeRow([fmtDate(r.timestamp), r.branch || '-', rate, fmtCost(r.total_cost_usd)]));
});
toggleEmpty('tbl-overview-evals', 'empty-overview-evals', rows.length > 0);
}
// Recent ships table (last 10)
if (data.shipLogs === null) {
showPanelError('tbl-overview-ships', 'empty-overview-ships');
} else {
const tbody = clearTbody('tbl-overview-ships');
const rows = data.shipLogs.slice(0, 10);
rows.forEach(function(r) {
const pr = r.pr_url ? { href: r.pr_url, text: 'PR' } : '-';
tbody.appendChild(makeRow([fmtDate(r.created_at), r.version || '-', r.branch || '-', pr]));
});
toggleEmpty('tbl-overview-ships', 'empty-overview-ships', rows.length > 0);
}
}
// ---- Evals ----
function renderEvals() {
if (data.evalRuns === null) {
showPanelError('tbl-evals', 'empty-evals');
return;
}
// Sparkline — pass rate for last 20 runs (oldest first for left-to-right)
const last20 = data.evalRuns.slice(0, 20).reverse();
const rates = last20.map(function(r) {
return r.total_tests ? r.passed / r.total_tests : 0;
});
renderSparkline('chart-sparkline', rates);
// Table
const tbody = clearTbody('tbl-evals');
const rows = data.evalRuns.slice(0, 20);
rows.forEach(function(r) {
const rate = r.total_tests ? ((r.passed / r.total_tests) * 100).toFixed(0) + '%' : '-';
tbody.appendChild(makeRow([
fmtDate(r.timestamp),
r.hostname || '-',
r.branch || '-',
rate,
fmtCost(r.total_cost_usd),
r.tier || '-'
]));
});
toggleEmpty('tbl-evals', 'empty-evals', rows.length > 0);
}
// ---- Ships ----
function renderShips() {
if (data.shipLogs === null) {
showPanelError('tbl-ships', 'empty-ships');
return;
}
// Table
const tbody = clearTbody('tbl-ships');
const rows = data.shipLogs.slice(0, 20);
rows.forEach(function(r) {
const pr = r.pr_url ? { href: r.pr_url, text: 'PR' } : '-';
tbody.appendChild(makeRow([fmtDate(r.created_at), r.version || '-', r.branch || '-', pr]));
});
toggleEmpty('tbl-ships', 'empty-ships', rows.length > 0);
// Ships per person this week (horizontal bar chart)
const ws = weekStart();
const thisWeek = data.shipLogs.filter(function(r) { return new Date(r.created_at) >= ws; });
const counts = {};
thisWeek.forEach(function(r) {
const who = r.user_id || 'unknown';
counts[who] = (counts[who] || 0) + 1;
});
const items = Object.keys(counts).map(function(k) {
return { label: k.slice(0, 8), value: counts[k] };
}).sort(function(a, b) { return b.value - a.value; });
renderHBarChart('chart-ships-per-person', items);
}
// ---- Costs ----
function renderCosts() {
if (data.evalCosts === null) return;
// Total all-time
const allTime = data.evalCosts.reduce(function(s, c) { return s + Number(c.estimated_cost_usd || 0); }, 0);
document.getElementById('stat-cost-total').textContent = fmtCost(allTime);
// Weekly cost trend — last 8 weeks
const now = new Date();
const weeks = [];
for (let i = 7; i >= 0; i--) {
const start = new Date(now);
start.setDate(start.getDate() - (i * 7 + now.getDay()));
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(end.getDate() + 7);
const label = (start.getMonth() + 1) + '/' + start.getDate();
const weekCosts = data.evalCosts.filter(function(c) {
const d = new Date(c.created_at);
return d >= start && d < end;
});
const total = weekCosts.reduce(function(s, c) { return s + Number(c.estimated_cost_usd || 0); }, 0);
weeks.push({ label: label, value: total });
}
renderVBarChart('chart-weekly-cost', weeks);
}
// ---- Leaderboard ----
function renderLeaderboard() {
const ws = weekStart();
const hasAnyData = data.evalRuns || data.shipLogs || data.transcripts;
if (!hasAnyData) {
showPanelError('tbl-leaderboard', 'empty-leaderboard');
return;
}
// Aggregate per user_id
const board = {};
function ensure(uid) {
if (!board[uid]) board[uid] = { ships: 0, evals: 0, sessions: 0, passed: 0, total_tests: 0, cost: 0, online: false };
}
if (data.shipLogs) {
data.shipLogs.filter(function(r) { return new Date(r.created_at) >= ws; }).forEach(function(r) {
const uid = r.user_id || 'unknown';
ensure(uid);
board[uid].ships++;
});
}
if (data.evalRuns) {
data.evalRuns.filter(function(r) { return new Date(r.timestamp) >= ws; }).forEach(function(r) {
const uid = r.user_id || r.hostname || 'unknown';
ensure(uid);
board[uid].evals++;
board[uid].passed += r.passed || 0;
board[uid].total_tests += r.total_tests || 0;
board[uid].cost += Number(r.total_cost_usd || 0);
});
}
if (data.transcripts) {
data.transcripts.filter(function(r) { return new Date(r.started_at) >= ws; }).forEach(function(r) {
const uid = r.user_id || 'unknown';
ensure(uid);
board[uid].sessions++;
});
}
// Online status from heartbeats
if (data.heartbeats) {
data.heartbeats.forEach(function(h) {
const uid = h.user_id || h.hostname;
if (uid && board[uid] && withinMinutes(h.timestamp, 15)) {
board[uid].online = true;
}
});
}
// Sort by ships desc, then evals desc
const sorted = Object.keys(board).map(function(uid) {
return Object.assign({ uid: uid }, board[uid]);
}).sort(function(a, b) { return (b.ships + b.evals) - (a.ships + a.evals); });
const tbody = clearTbody('tbl-leaderboard');
sorted.forEach(function(entry, i) {
const rate = entry.total_tests ? ((entry.passed / entry.total_tests) * 100).toFixed(0) + '%' : '-';
const nameCell = entry.online
? { html: '<span class="online-dot"></span>' + escapeHTML(entry.uid.slice(0, 8)) }
: entry.uid.slice(0, 8);
tbody.appendChild(makeRow([
i + 1,
nameCell,
entry.ships,
entry.evals,
entry.sessions,
rate,
fmtCost(entry.cost)
]));
});
toggleEmpty('tbl-leaderboard', 'empty-leaderboard', sorted.length > 0);
}
// ---- QA ----
function renderQA() {
if (data.qaReports === null) {
showPanelError('tbl-qa', 'empty-qa');
return;
}
const tbody = clearTbody('tbl-qa');
const rows = data.qaReports.slice(0, 20);
rows.forEach(function(r) {
const score = Number(r.health_score);
const scoreText = isNaN(score) ? '-' : score.toFixed(0);
const cls = score > 80 ? 'score-green' : (score >= 50 ? 'score-yellow' : 'score-red');
const scoreCell = { html: '<span class="' + cls + '">' + escapeHTML(scoreText) + '</span>' };
tbody.appendChild(makeRow([fmtDate(r.created_at), r.repo_slug || '-', scoreCell]));
});
toggleEmpty('tbl-qa', 'empty-qa', rows.length > 0);
}
// ================================================================
// Tab switching
// ================================================================
document.querySelectorAll('.tab').forEach(function(tab) {
tab.addEventListener('click', function() {
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
tab.classList.add('active');
const panelId = 'panel-' + tab.getAttribute('data-tab');
document.getElementById(panelId).classList.add('active');
});
});
// ================================================================
// Event listeners
// ================================================================
document.getElementById('btn-login').addEventListener('click', startLogin);
document.getElementById('btn-signout').addEventListener('click', signOut);
// ================================================================
// Init
// ================================================================
(async function init() {
// Check for OAuth callback code in URL
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
if (code) {
await exchangeCode(code);
return;
}
// Try to restore existing session
const restored = await restoreSession();
if (restored) {
showDashboard();
} else {
showLogin();
}
})();
})();
</script>
</body>
</html>`;
}
@@ -0,0 +1,171 @@
/**
* Regression alert edge function.
*
* Trigger: Database webhook on eval_runs INSERT.
* Logic: Compare new run's pass rate against recent baseline.
* If >5% drop, POST to team's Slack webhook.
* Dedup via alert_cooldowns table (5-min window).
*
* Uses service_role key (bypasses RLS) — standard for webhooks.
*/
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
interface WebhookPayload {
type: 'INSERT';
table: string;
record: {
id: string;
team_id: string;
repo_slug: string;
branch: string;
passed: number;
total_tests: number;
timestamp: string;
};
schema: string;
}
// --- Pure functions (testable) ---
export function computePassRate(passed: number, total: number): number | null {
return total > 0 ? (passed / total) * 100 : null;
}
export function shouldAlert(
currentRate: number | null,
baselineRate: number | null,
thresholdPct: number = 5,
): boolean {
if (currentRate === null || baselineRate === null) return false;
return baselineRate - currentRate > thresholdPct;
}
export function formatSlackMessage(opts: {
repoSlug: string;
branch: string;
previousRate: number;
currentRate: number;
}): string {
const delta = opts.currentRate - opts.previousRate;
const arrow = delta < 0 ? 'regressed' : 'improved';
return [
`:warning: *Eval ${arrow}* on \`${opts.branch}\` (${opts.repoSlug})`,
`Pass rate: ${opts.previousRate.toFixed(0)}% → ${opts.currentRate.toFixed(0)}% (${delta > 0 ? '+' : ''}${delta.toFixed(0)}%)`,
].join('\n');
}
// --- Main handler ---
Deno.serve(async (req: Request) => {
try {
const payload: WebhookPayload = await req.json();
const { record } = payload;
if (!record || !record.team_id || !record.total_tests) {
return new Response('OK (skipped: missing fields)', { status: 200 });
}
const currentRate = computePassRate(record.passed, record.total_tests);
if (currentRate === null) {
return new Response('OK (skipped: total_tests=0)', { status: 200 });
}
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const serviceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
const supabase = createClient(supabaseUrl, serviceKey);
// Check cooldown (5-min dedup)
const { data: cooldown } = await supabase
.from('alert_cooldowns')
.select('last_sent_at')
.eq('team_id', record.team_id)
.eq('repo_slug', record.repo_slug)
.eq('alert_type', 'regression')
.single();
if (cooldown?.last_sent_at) {
const cooldownMs = Date.now() - new Date(cooldown.last_sent_at).getTime();
if (cooldownMs < 5 * 60 * 1000) {
return new Response('OK (cooldown active)', { status: 200 });
}
}
// Get previous runs for baseline
const { data: previousRuns } = await supabase
.from('eval_runs')
.select('passed, total_tests')
.eq('team_id', record.team_id)
.eq('repo_slug', record.repo_slug)
.neq('id', record.id)
.order('timestamp', { ascending: false })
.limit(19);
if (!previousRuns || previousRuns.length < 2) {
return new Response('OK (not enough history)', { status: 200 });
}
// Compute baseline pass rate
const rates = previousRuns
.map(r => computePassRate(r.passed, r.total_tests))
.filter((r): r is number => r !== null);
if (rates.length === 0) {
return new Response('OK (no valid baseline)', { status: 200 });
}
const baselineRate = rates.reduce((a, b) => a + b, 0) / rates.length;
if (!shouldAlert(currentRate, baselineRate)) {
return new Response('OK (no regression)', { status: 200 });
}
// Get Slack webhook URL
const { data: setting } = await supabase
.from('team_settings')
.select('value')
.eq('team_id', record.team_id)
.eq('key', 'slack-webhook')
.single();
if (!setting?.value) {
console.log(`Regression detected but no Slack webhook configured for team ${record.team_id}`);
return new Response('OK (no webhook configured)', { status: 200 });
}
// Send Slack alert
const message = formatSlackMessage({
repoSlug: record.repo_slug,
branch: record.branch,
previousRate: baselineRate,
currentRate,
});
const slackRes = await fetch(setting.value, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message }),
});
if (!slackRes.ok) {
console.error(`Slack webhook failed: ${slackRes.status} ${await slackRes.text()}`);
}
// Update cooldown
await supabase
.from('alert_cooldowns')
.upsert({
team_id: record.team_id,
repo_slug: record.repo_slug,
alert_type: 'regression',
last_sent_at: new Date().toISOString(),
});
console.log(`Regression alert sent: ${record.repo_slug} ${baselineRate.toFixed(0)}% → ${currentRate.toFixed(0)}%`);
return new Response('OK (alert sent)', { status: 200 });
} catch (err) {
console.error(`Regression alert error: ${err}`);
return new Response('OK (error logged)', { status: 200 });
}
});
+228
View File
@@ -0,0 +1,228 @@
/**
* Weekly digest edge function.
*
* Trigger: pg_cron every Monday 9am UTC.
* Logic: For each team with digest_enabled=true, aggregate 7-day data
* and POST a summary to their Slack webhook.
*
* Uses service_role key (bypasses RLS) — standard for cron functions.
*/
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
// --- Pure functions (testable) ---
interface DigestData {
teamSlug: string;
evalRuns: number;
evalPassRate: number | null;
evalPassRateDelta: number | null;
shipsByPerson: Array<{ email: string; count: number }>;
totalShips: number;
sessionCount: number;
topTools: Array<{ tool: string; count: number }>;
totalCost: number;
}
export function formatDigestMessage(data: DigestData): string {
const lines: string[] = [];
lines.push(`:bar_chart: *Weekly gstack Digest* — ${data.teamSlug}`);
lines.push('');
// Evals
if (data.evalRuns > 0) {
let evalLine = `:white_check_mark: *Evals:* ${data.evalRuns} runs`;
if (data.evalPassRate !== null) {
evalLine += `, ${data.evalPassRate.toFixed(0)}% pass rate`;
if (data.evalPassRateDelta !== null) {
const sign = data.evalPassRateDelta >= 0 ? '+' : '';
evalLine += ` (${sign}${data.evalPassRateDelta.toFixed(0)}% from last week)`;
}
}
lines.push(evalLine);
}
// Ships
if (data.totalShips > 0) {
const people = data.shipsByPerson
.sort((a, b) => b.count - a.count)
.slice(0, 5)
.map(p => `${p.email.split('@')[0]}: ${p.count}`)
.join(', ');
lines.push(`:rocket: *Ships:* ${data.totalShips} PRs (${people})`);
}
// Sessions
if (data.sessionCount > 0) {
let sessionLine = `:robot_face: *AI Sessions:* ${data.sessionCount}`;
if (data.topTools.length > 0) {
const tools = data.topTools.slice(0, 5).map(t => `${t.tool}(${t.count})`).join(', ');
sessionLine += ` — top tools: ${tools}`;
}
lines.push(sessionLine);
}
// Cost
if (data.totalCost > 0) {
lines.push(`:moneybag: *Eval spend:* $${data.totalCost.toFixed(2)}`);
}
// Quiet week fallback
if (data.evalRuns === 0 && data.totalShips === 0 && data.sessionCount === 0) {
lines.push('_Quiet week — no evals, ships, or sessions recorded._');
}
return lines.join('\n');
}
// --- Main handler ---
Deno.serve(async (_req: Request) => {
try {
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const serviceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
const supabase = createClient(supabaseUrl, serviceKey);
const weekAgo = new Date(Date.now() - 7 * 86_400_000).toISOString();
const twoWeeksAgo = new Date(Date.now() - 14 * 86_400_000).toISOString();
// Find all teams with digest enabled
const { data: digestSettings } = await supabase
.from('team_settings')
.select('team_id, value')
.eq('key', 'digest-enabled')
.eq('value', 'true');
if (!digestSettings || digestSettings.length === 0) {
console.log('No teams have digest enabled');
return new Response('OK (no teams)', { status: 200 });
}
let sentCount = 0;
for (const setting of digestSettings) {
const teamId = setting.team_id;
// Get Slack webhook
const { data: webhookSetting } = await supabase
.from('team_settings')
.select('value')
.eq('team_id', teamId)
.eq('key', 'slack-webhook')
.single();
if (!webhookSetting?.value) {
console.log(`Team ${teamId}: digest enabled but no Slack webhook`);
continue;
}
// Get team slug
const { data: team } = await supabase
.from('teams')
.select('slug')
.eq('id', teamId)
.single();
// Fetch this week's data
const [evalRes, shipRes, sessionRes] = await Promise.all([
supabase.from('eval_runs')
.select('passed, total_tests, total_cost_usd, user_id')
.eq('team_id', teamId)
.gte('timestamp', weekAgo),
supabase.from('ship_logs')
.select('user_id, email')
.eq('team_id', teamId)
.gte('created_at', weekAgo),
supabase.from('session_transcripts')
.select('tools_used')
.eq('team_id', teamId)
.gte('started_at', weekAgo),
]);
const evalRuns = evalRes.data || [];
const shipLogs = shipRes.data || [];
const sessions = sessionRes.data || [];
// Compute pass rate
let passRate: number | null = null;
const validRuns = evalRuns.filter(r => r.total_tests > 0);
if (validRuns.length > 0) {
const totalPassed = validRuns.reduce((s, r) => s + r.passed, 0);
const totalTests = validRuns.reduce((s, r) => s + r.total_tests, 0);
passRate = totalTests > 0 ? (totalPassed / totalTests) * 100 : null;
}
// Compute previous week's pass rate for delta
let passRateDelta: number | null = null;
const { data: prevWeekRuns } = await supabase
.from('eval_runs')
.select('passed, total_tests')
.eq('team_id', teamId)
.gte('timestamp', twoWeeksAgo)
.lt('timestamp', weekAgo);
if (prevWeekRuns && prevWeekRuns.length > 0 && passRate !== null) {
const prevValid = prevWeekRuns.filter(r => r.total_tests > 0);
if (prevValid.length > 0) {
const prevPassed = prevValid.reduce((s, r) => s + r.passed, 0);
const prevTotal = prevValid.reduce((s, r) => s + r.total_tests, 0);
const prevRate = prevTotal > 0 ? (prevPassed / prevTotal) * 100 : null;
if (prevRate !== null) passRateDelta = passRate - prevRate;
}
}
// Ships by person
const shipsByPerson = new Map<string, number>();
for (const log of shipLogs) {
const key = String(log.email || log.user_id || 'unknown');
shipsByPerson.set(key, (shipsByPerson.get(key) || 0) + 1);
}
// Top tools from sessions
const toolCounts = new Map<string, number>();
for (const s of sessions) {
const tools = (s.tools_used as string[]) || [];
for (const t of tools) {
toolCounts.set(t, (toolCounts.get(t) || 0) + 1);
}
}
const totalCost = evalRuns.reduce((s, r) => s + (Number(r.total_cost_usd) || 0), 0);
const digest: DigestData = {
teamSlug: team?.slug || 'unknown',
evalRuns: evalRuns.length,
evalPassRate: passRate,
evalPassRateDelta: passRateDelta,
shipsByPerson: [...shipsByPerson.entries()].map(([email, count]) => ({ email, count })),
totalShips: shipLogs.length,
sessionCount: sessions.length,
topTools: [...toolCounts.entries()]
.map(([tool, count]) => ({ tool, count }))
.sort((a, b) => b.count - a.count),
totalCost,
};
const message = formatDigestMessage(digest);
// Send to Slack
const slackRes = await fetch(webhookSetting.value, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message }),
});
if (slackRes.ok) {
sentCount++;
console.log(`Digest sent for team ${team?.slug || teamId}`);
} else {
console.error(`Slack failed for team ${teamId}: ${slackRes.status}`);
}
}
return new Response(`OK (${sentCount} digests sent)`, { status: 200 });
} catch (err) {
console.error(`Weekly digest error: ${err}`);
return new Response('OK (error logged)', { status: 200 });
}
});
+82
View File
@@ -0,0 +1,82 @@
/**
* Tests for dashboard UI HTML generation.
*/
import { describe, test, expect } from 'bun:test';
import { getDashboardHTML } from '../supabase/functions/dashboard/ui';
const SUPABASE_URL = 'https://test-project.supabase.co';
const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test-anon-key';
describe('getDashboardHTML', () => {
const html = getDashboardHTML(SUPABASE_URL, ANON_KEY);
test('returns valid HTML document', () => {
expect(html).toContain('<!DOCTYPE html>');
expect(html).toContain('<html');
expect(html).toContain('</html>');
});
test('contains page title', () => {
expect(html).toContain('<title>gstack Dashboard</title>');
});
test('embeds supabase URL', () => {
expect(html).toContain(SUPABASE_URL);
});
test('embeds anon key', () => {
expect(html).toContain(ANON_KEY);
});
test('contains login UI elements', () => {
expect(html).toContain('Sign in with GitHub');
});
test('contains tab navigation', () => {
expect(html).toContain('Overview');
expect(html).toContain('Evals');
expect(html).toContain('Ships');
expect(html).toContain('Costs');
expect(html).toContain('Leaderboard');
expect(html).toContain('QA');
});
test('contains auto-refresh logic', () => {
expect(html).toContain('visibilitychange');
expect(html).toContain('setInterval');
});
test('contains PKCE auth code', () => {
expect(html).toContain('code_challenge');
expect(html).toContain('code_verifier');
});
test('uses textContent for XSS prevention', () => {
expect(html).toContain('textContent');
});
test('contains dark theme styling', () => {
expect(html).toContain('#0a0a0a');
});
test('contains SVG chart elements', () => {
expect(html).toContain('svg');
});
test('fetches from eval_runs endpoint', () => {
expect(html).toContain('eval_runs');
});
test('fetches from ship_logs endpoint', () => {
expect(html).toContain('ship_logs');
});
test('fetches from sync_heartbeats for who\'s online', () => {
expect(html).toContain('sync_heartbeats');
});
test('contains sign out functionality', () => {
expect(html).toContain('Sign out');
});
});