import { db } from '../db/index.js'; import type { BetaRow } from './beta.js'; import { serverTitle } from '../../ui/server-labels.js'; const positive = (value: string | null, fallback: number) => { const number = Number(value); return Number.isSafeInteger(number) && number > 0 ? number : fallback; }; export function betaListPage(params: URLSearchParams) { const state = params.get('state'), view = params.get('view'), search = (params.get('q') ?? '').slice(0, 200).trim(); const predicates: string[] = [], args: (string | number)[] = []; if (state) { predicates.push('b.state=?'); args.push(state); } else if (view === 'active') predicates.push("b.state IN ('running','ready-promote')"); else if (view === 'history') predicates.push("b.state IN ('promoted','rolled-back')"); const status = params.get('status'); if ( status && ['ready', 'testing', 'attention', 'paused', 'promoted', 'rolled-back'].includes(status) ) { // Match betaStatus: closed lifecycle wins over the final test outcome. predicates.push(`(CASE WHEN b.state IN ('promoted','rolled-back') THEN b.state WHEN EXISTS (SELECT 1 FROM beta_servers m WHERE m.beta_id=b.id AND (m.server_deleted_at IS NOT NULL OR m.stage='failed')) OR EXISTS (SELECT 1 FROM beta_test_plans p WHERE p.beta_id=b.id AND p.outcome IN ('failed','inconclusive')) THEN 'attention' WHEN EXISTS (SELECT 1 FROM beta_test_plans p WHERE p.beta_id=b.id AND p.outcome='paused') THEN 'paused' WHEN b.state='ready-promote' THEN 'ready' ELSE 'testing' END)=?`); args.push(status); } if (search) { const names = ( db().prepare('SELECT DISTINCT server FROM beta_servers').all() as { server: string }[] ) .filter((row) => `${row.server} ${serverTitle(row.server)}` .toLowerCase() .includes(search.toLowerCase()) ) .map((row) => row.server); predicates.push(`(instr(lower('#'||b.id||' '||COALESCE(b.notes,'')),lower(?))>0 OR EXISTS ( SELECT 1 FROM beta_scripts bs JOIN scripts s ON s.id=bs.script_id WHERE bs.beta_id=b.id AND (instr(lower(s.name),lower(?))>0 OR instr(lower(s.target_rel_path),lower(?))>0)) OR EXISTS (SELECT 1 FROM scripts s WHERE s.id=b.script_id AND (instr(lower(s.name),lower(?))>0 OR instr(lower(s.target_rel_path),lower(?))>0)) OR EXISTS (SELECT 1 FROM beta_servers m WHERE m.beta_id=b.id AND m.server IN (SELECT value FROM json_each(?))))`); args.push(search, search, search, search, search, JSON.stringify(names)); } const where = predicates.length ? ' WHERE ' + predicates.join(' AND ') : ''; const total = ( db() .prepare('SELECT COUNT(*) n FROM betas b' + where) .get(...args) as { n: number } ).n; const pageSize = Math.min(100, positive(params.get('limit'), 25)); const totalPages = Math.max(1, Math.ceil(total / pageSize)); const page = Math.min(totalPages, positive(params.get('page'), 1)); const betas = db() .prepare('SELECT b.* FROM betas b' + where + ' ORDER BY b.id DESC LIMIT ? OFFSET ?') .all(...args, pageSize, (page - 1) * pageSize) as BetaRow[]; const counts = db() .prepare( `SELECT COUNT(CASE WHEN state IN ('running','ready-promote') THEN 1 END) active, COUNT(CASE WHEN state IN ('promoted','rolled-back') THEN 1 END) history FROM betas` ) .get() as { active: number; history: number }; return { betas, page, pageSize, total, totalPages, counts }; } export function betaActivityPage(id: number, params = new URLSearchParams()) { const before = positive(params.get('before'), Number.MAX_SAFE_INTEGER); const limit = Math.min(100, positive(params.get('limit'), 50)); const rows = db() .prepare( 'SELECT id,created_at,message FROM beta_events WHERE beta_id=? AND id limit; const events = rows.slice(0, limit).reverse(); return { events, nextBefore: more ? events[0].id : null }; }