Paginate beta history and refresh pages from events
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { db } from '../db/index.js';
|
||||
import type { BetaRow } from './beta.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);
|
||||
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')");
|
||||
if (search) {
|
||||
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))`);
|
||||
args.push(search, search);
|
||||
}
|
||||
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 };
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type { GameTelemetry } from '../../telemetry.js';
|
||||
import { applySample, applyGameEvent, emptyMetrics, evaluateRequirements, type TestMetrics } from './beta-evaluation.js';
|
||||
import { ONLINE_MAX_MS } from '../../server-freshness.js';
|
||||
import { currentBetaRevision } from './beta-revisions.js';
|
||||
import { invalidateBeta } from '../pipeline/bus.js';
|
||||
|
||||
export interface TestRun { id: string; beta_id: number; server: string; phase: number; created_at: string; observing_at: string | null; metrics: string }
|
||||
export interface StoredPlan { beta_id: number; plan: string; outcome: string; reason: string | null }
|
||||
@@ -15,7 +16,7 @@ const activeParticipant = (id: number, server: string) => !!db().prepare(`SELECT
|
||||
WHERE beta_id = ? AND server = ? AND server_deleted_at IS NULL`).get(id, server);
|
||||
export const storedPlan = (id: number) => db().prepare('SELECT * FROM beta_test_plans WHERE beta_id = ?').get(id) as StoredPlan | undefined;
|
||||
export const currentRun = (id: number, server: string) => db().prepare('SELECT * FROM beta_test_runs WHERE beta_id = ? AND server = ? AND retired_at IS NULL').get(id, server) as TestRun | undefined;
|
||||
export function activity(id: number, message: string) { db().prepare('INSERT INTO beta_events (beta_id, message) VALUES (?, ?)').run(id, message); }
|
||||
export function activity(id: number, message: string) { db().prepare('INSERT INTO beta_events (beta_id, message) VALUES (?, ?)').run(id, message); invalidateBeta(id); }
|
||||
export function setOutcome(id: number, outcome: string, reason: string | null): void {
|
||||
const previous = storedPlan(id);
|
||||
if (!previous || (previous.outcome === outcome && previous.reason === reason)) return;
|
||||
@@ -23,7 +24,7 @@ export function setOutcome(id: number, outcome: string, reason: string | null):
|
||||
activity(id, `Test plan ${outcome}${reason ? `: ${reason}` : ''}`);
|
||||
}
|
||||
export function readMetrics(run: TestRun): TestMetrics { return { ...emptyMetrics(), ...JSON.parse(run.metrics) }; }
|
||||
export function saveMetrics(run: TestRun, metrics: TestMetrics): void { db().prepare('UPDATE beta_test_runs SET metrics = ? WHERE id = ?').run(JSON.stringify(metrics), run.id); }
|
||||
export function saveMetrics(run: TestRun, metrics: TestMetrics): void { db().prepare('UPDATE beta_test_runs SET metrics = ? WHERE id = ?').run(JSON.stringify(metrics), run.id); invalidateBeta(run.beta_id); }
|
||||
export function evidence(run: TestRun, eventId: string, kind: string, payload: unknown): boolean {
|
||||
return db().prepare('INSERT OR IGNORE INTO beta_test_evidence (run_id, event_id, received_at, kind, payload) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(run.id, eventId, nowIso(), kind, JSON.stringify(payload)).changes > 0;
|
||||
|
||||
@@ -46,6 +46,7 @@ export type BetaStage =
|
||||
export type BetaState = 'running' | 'ready-promote' | 'promoted' | 'rolled-back';
|
||||
|
||||
export type PipelineEvent =
|
||||
| { type: 'beta-changed'; betaId: number }
|
||||
| { type: 'fleet-invalidated' }
|
||||
| { type: 'server-telemetry'; entries: GameTelemetry[] }
|
||||
| { type: 'deployment-opened'; deploymentId: string; action: string; servers: string[] }
|
||||
@@ -79,6 +80,15 @@ class Bus extends EventEmitter {
|
||||
}
|
||||
|
||||
const bus = new Bus();
|
||||
const changedBetas = new Set<number>();
|
||||
export function invalidateBeta(betaId: number): void {
|
||||
const scheduled = changedBetas.size > 0;
|
||||
changedBetas.add(betaId);
|
||||
if (!scheduled) queueMicrotask(() => {
|
||||
const ids = [...changedBetas]; changedBetas.clear();
|
||||
for (const betaId of ids) bus.emit('event', { type: 'beta-changed', betaId });
|
||||
});
|
||||
}
|
||||
let invalidationPending = false;
|
||||
export function invalidateFleet(): void {
|
||||
if (invalidationPending) return;
|
||||
@@ -90,6 +100,7 @@ export function invalidateFleet(): void {
|
||||
export function emit(event: PipelineEvent): void {
|
||||
bus.emit('event', event);
|
||||
if (event.type === 'beta-opened' || event.type === 'beta-state' || event.type === 'beta-server-stage') invalidateFleet();
|
||||
if ('betaId' in event && event.type !== 'beta-changed') invalidateBeta(event.betaId);
|
||||
}
|
||||
|
||||
export function subscribe(listener: (event: PipelineEvent) => void): () => void {
|
||||
|
||||
Reference in New Issue
Block a user