import { json } from '@sveltejs/kit'; import { listBetas, openBeta, BetaError } from '$lib/server/fleet/beta.js'; import { betaScriptMetadata } from '$lib/server/fleet/library-metadata.js'; import { db } from '$lib/server/db/index.js'; import type { BetaServerRow } from '$lib/server/fleet/beta.js'; import type { StoredPlan } from '$lib/server/fleet/beta-tests.js'; import { kick } from '$lib/server/pipeline/worker.js'; import { parseBetaPlan, type BetaPlan } from '$lib/beta-plan.js'; export async function GET({ url }: { url: URL }) { const stateFilter = url.searchParams.get('state') as | 'running' | 'ready-promote' | 'promoted' | 'rolled-back' | null; const betas = listBetas(stateFilter ?? undefined); const ids = JSON.stringify(betas.map(beta => beta.id)); const bundles = betaScriptMetadata(betas.map(beta => beta.id)); const plans = new Map((db().prepare(`SELECT p.*, COALESCE((SELECT started_at FROM beta_revisions r WHERE r.beta_id=p.beta_id ORDER BY revision DESC LIMIT 1), b.opened_at) started_at FROM beta_test_plans p JOIN betas b ON b.id=p.beta_id WHERE p.beta_id IN (SELECT value FROM json_each(?))`).all(ids) as Array).map(plan => [plan.beta_id, plan])); const members = db().prepare('SELECT * FROM beta_servers WHERE beta_id IN (SELECT value FROM json_each(?)) ORDER BY server').all(ids) as BetaServerRow[]; const rows = betas.map(b => { const scripts = bundles.get(b.id) ?? []; const test = plans.get(b.id); const plan = test ? parseBetaPlan(JSON.parse(test.plan)) : null; return { ...b, testOutcome: test?.outcome ?? null, testReason: test?.reason ?? null, deadlineAt: plan ? new Date(Date.parse(test!.started_at) + plan.timeoutHours * 3600000).toISOString() : null, scripts, servers: members.filter(member => member.beta_id === b.id) }; }); return json({ betas: rows }); } // POST body accepts either: // { artifacts: [{ id, versionId }], ... } — polymorphic (script/config/plugin/baseline) // or the legacy form: // { scripts: [{ scriptId, scriptVersionId }], ... } export async function POST({ request }: { request: Request }) { const body = (await request.json().catch(() => ({}))) as { artifacts?: Array<{ id?: number; versionId?: number }>; scripts?: Array<{ scriptId?: number; scriptVersionId?: number }>; servers?: string[]; waitForHealth?: boolean; waitForMatch?: boolean; notes?: string; testPlan?: BetaPlan; }; if (!body || typeof body !== 'object' || Array.isArray(body)) return json({ ok: false, error: 'Expected a JSON object' }, { status: 400 }); // Normalize into the openBeta shape. Both fields feed the same list. const rawEntries: Array<{ scriptId?: number; scriptVersionId?: number }> = []; if (Array.isArray(body.artifacts)) { for (const a of body.artifacts) rawEntries.push({ scriptId: a.id, scriptVersionId: a.versionId }); } if (Array.isArray(body.scripts)) { for (const s of body.scripts) rawEntries.push({ scriptId: s.scriptId, scriptVersionId: s.scriptVersionId }); } if (rawEntries.length === 0 || !Array.isArray(body.servers)) { return json( { ok: false, error: 'artifacts[]/scripts[] (non-empty) and servers[] are required' }, { status: 400 } ); } if (body.servers.length === 0) { return json({ ok: false, error: 'at least one server is required' }, { status: 400 }); } const normalized: Array<{ scriptId: number; scriptVersionId: number }> = []; for (const s of rawEntries) { if (typeof s.scriptId !== 'number' || typeof s.scriptVersionId !== 'number') { return json( { ok: false, error: 'each entry needs scriptId/id and scriptVersionId/versionId' }, { status: 400 } ); } normalized.push({ scriptId: s.scriptId, scriptVersionId: s.scriptVersionId }); } try { let testPlan: BetaPlan | undefined; try { if (body.testPlan !== undefined) testPlan = parseBetaPlan(body.testPlan); } catch (error) { return json({ ok: false, error: (error as Error).message }, { status: 400 }); } const result = await openBeta({ testPlan, scripts: normalized, servers: body.servers, waitForHealth: body.waitForHealth ?? true, waitForMatch: body.waitForMatch ?? false, notes: body.notes }); void kick(); return json({ ok: true, result }); } catch (err) { if (err instanceof BetaError) { return json({ ok: false, error: err.message, kind: err.kind, details: err.details }, { status: 409 }); } throw err; } }