Fix delete FK exhaustively; unset-scope surfaces as drift

deleteScript: previous fix missed beta_servers.prior_version_id,
beta_server_scripts.prior_version_id, and deployments.beta_id. Cascade
from betas.script_id -> beta_servers/beta_server_scripts left dangling
prior_version_id refs, and deletions of cascaded betas dangled
deployments.beta_id. Now nulls every NO ACTION reference before the
DELETE inside a single transaction.

Scope unset no longer touches disk from the API. Instead it detaches
inventory rows for scripts/plugins so drift Pass 1 flags the on-disk
copies as untracked; configs rely on drift Pass 2's compose-diff, which
already handles scope=null naturally. The operator reconciles from
/deployments using the existing rescan + reconcile buttons — same
visible flow as any other drift, no more silent synchronous file
removal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 23:59:43 -04:00
co-authored by Claude Opus 4.7
parent 34dc2ff00f
commit 30b0d1d4da
3 changed files with 51 additions and 60 deletions
+6 -19
View File
@@ -1,28 +1,17 @@
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';
import { unsetScopeAndDetach } from '$lib/server/fleet/beta.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
// servers.
// 409 with kind='scope-requires-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 });
@@ -39,12 +28,10 @@ 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 detached = unsetScopeAndDetach(scriptId);
return json({ ok: true, detached });
}
const updated = setScope({ scriptId, scopeType, scopeValue });
triggerDrift();
return json({ ok: true, script: updated });
} catch (err) {
if (err instanceof ScriptError) {