import { json } from '@sveltejs/kit'; import { getScript, setScope, serversMatchingScope, ScriptError, type ScopeType } from '$lib/server/fleet/scripts.js'; import { unsetScopeAndDetach } from '$lib/server/fleet/beta.js'; import { db } from '$lib/server/db/index.js'; export async function GET({ params, url }: { params: { id: string }; url: URL }) { const id = Number(params.id); if (!Number.isSafeInteger(id) || id <= 0) return json({ error: 'Invalid file ID.' }, { status: 400 }); const script = getScript(id); if (!script) return json({ error: 'File not found.' }, { status: 404 }); const type = url.searchParams.get('type'); const value = url.searchParams.get('value'); if (type !== 'none' && type !== 'game' && type !== 'map' || type !== 'none' && !value) return json({ error: 'Choose a complete scope.' }, { status: 400 }); const before = serversMatchingScope(script.scope_type, script.scope_value); const after = type === 'none' ? [] : serversMatchingScope(type as ScopeType, value); const composed = script.kind === 'config' || script.kind === 'baseline'; const tracked = composed ? [] : (db().prepare(`SELECT DISTINCT server FROM server_scripts WHERE rel_path = ? AND script_version_id IN (SELECT id FROM script_versions WHERE script_id = ?) ORDER BY server`).all(script.target_rel_path, script.id) as Array<{ server: string }>).map(row => row.server); return json({ before, after, added: after.filter(name => !before.includes(name)), removed: before.filter(name => !after.includes(name)), affected: composed ? [...new Set([...before, ...after])].sort() : type === 'none' ? tracked : [...new Set([...before, ...after])].sort(), composed, detached: type === 'none' ? tracked : [] }, { headers: { 'Cache-Control': 'no-store' } }); } function parseId(raw: string): number | null { const n = Number.parseInt(raw, 10); return Number.isFinite(n) ? n : null; } // POST body: { scopeType: 'game' | 'map' | null, scopeValue?: string } // Scope edits preserve the promoted version; changes during an active beta // return 409 with kind='has-active-beta'. Un-scoping detaches inventory so the // orphaned files show up as drift on the deployments page for the operator // to reconcile manually — the same flow as any other drift. export async function POST({ params, request }: { params: { id: string }; request: Request }) { const scriptId = parseId(params.id); if (scriptId === null) return json({ ok: false, error: 'bad id' }, { status: 400 }); const script = getScript(scriptId); if (!script) return json({ ok: false, error: 'not found' }, { status: 404 }); const body = (await request.json().catch(() => ({}))) as { scopeType?: ScopeType | null; scopeValue?: string | null; }; const scopeType = body.scopeType ?? null; const scopeValue = body.scopeValue ?? null; try { if (scopeType === null) { const detached = unsetScopeAndDetach(scriptId); return json({ ok: true, detached }); } const updated = setScope({ scriptId, scopeType, scopeValue }); return json({ ok: true, script: updated }); } catch (err) { if (err instanceof ScriptError) { return json({ ok: false, error: err.message, kind: err.kind }, { status: 409 }); } throw err; } }