diff --git a/src/lib/server/fleet/scripts.ts b/src/lib/server/fleet/scripts.ts index 2be4a05..543106c 100644 --- a/src/lib/server/fleet/scripts.ts +++ b/src/lib/server/fleet/scripts.ts @@ -337,7 +337,39 @@ export function deleteScript(scriptId: number): void { ) .get(scriptId) as { id: number } | undefined; if (beta) throw new ScriptError(`beta ${beta.id} is still running against this script`, 'has-active-beta'); - db().prepare('DELETE FROM scripts WHERE id = ?').run(scriptId); + + // Null out deployment-history FKs that would otherwise block the cascade. + // deployments.script_id/script_version_id/prior_version_id and + // deployment_scripts.prior_version_id are historical audit references + // with no ON DELETE action; keep the deployment rows intact but drop + // the FK to the script so we can remove it. + const handle = db(); + const tx = handle.transaction((id: number) => { + handle + .prepare( + `UPDATE deployments + SET script_version_id = NULL + WHERE script_version_id IN (SELECT id FROM script_versions WHERE script_id = ?)` + ) + .run(id); + handle + .prepare( + `UPDATE deployments + SET prior_version_id = NULL + WHERE prior_version_id IN (SELECT id FROM script_versions WHERE script_id = ?)` + ) + .run(id); + handle.prepare('UPDATE deployments SET script_id = NULL WHERE script_id = ?').run(id); + handle + .prepare( + `UPDATE deployment_scripts + SET prior_version_id = NULL + WHERE prior_version_id IN (SELECT id FROM script_versions WHERE script_id = ?)` + ) + .run(id); + handle.prepare('DELETE FROM scripts WHERE id = ?').run(id); + }); + tx(scriptId); } // Servers that match a script's scope. Empty for unscoped scripts — those are diff --git a/src/routes/api/scripts/[id]/scope/+server.ts b/src/routes/api/scripts/[id]/scope/+server.ts index 4f31e9a..ec6ccd7 100644 --- a/src/routes/api/scripts/[id]/scope/+server.ts +++ b/src/routes/api/scripts/[id]/scope/+server.ts @@ -1,12 +1,24 @@ import { json } from '@sveltejs/kit'; import { getScript, setScope, ScriptError, type ScopeType } from '$lib/server/fleet/scripts.js'; import { unsetScopeAndCleanup } from '$lib/server/fleet/beta.js'; +import { runDriftDeployment } from '$lib/server/fleet/drift.js'; +import { kick } from '$lib/server/pipeline/worker.js'; function parseId(raw: string): number | null { const n = Number.parseInt(raw, 10); return Number.isFinite(n) ? n : null; } +// Fire-and-forget: after a scope mutation, reconcile any drift the change may +// have introduced (orphan files on servers that no longer match, or missing +// files on servers that now do). Runs in the background so the API returns +// promptly; the deployment will surface on /deployments if there's real drift. +function triggerDrift(): void { + void runDriftDeployment({ triggeredBy: 'ui' }) + .then((result) => { if (result.deploymentId) void kick(); }) + .catch(() => { /* surfaced on /deployments if it fails */ }); +} + // POST body: { scopeType: 'game' | 'map' | null, scopeValue?: string } // Setting/changing scope on a script with a current_version_id returns a // 409 with kind='scope-requires-beta'; unsetting cleans the file off matching @@ -28,9 +40,11 @@ export async function POST({ params, request }: { params: { id: string }; reques try { if (scopeType === null) { const cleaned = await unsetScopeAndCleanup(scriptId); + triggerDrift(); return json({ ok: true, cleaned }); } const updated = setScope({ scriptId, scopeType, scopeValue }); + triggerDrift(); return json({ ok: true, script: updated }); } catch (err) { if (err instanceof ScriptError) {