83 lines
4.2 KiB
TypeScript
83 lines
4.2 KiB
TypeScript
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<? ORDER BY id DESC LIMIT ?'
|
|
)
|
|
.all(id, before, limit + 1) as { id: number; created_at: string; message: string }[];
|
|
const more = rows.length > limit;
|
|
const events = rows.slice(0, limit).reverse();
|
|
return { events, nextBefore: more ? events[0].id : null };
|
|
}
|