diff --git a/docs/server-admin-container-migration.md b/docs/server-admin-container-migration.md index 60bb15e8..e4797fd3 100644 --- a/docs/server-admin-container-migration.md +++ b/docs/server-admin-container-migration.md @@ -474,6 +474,7 @@ Implemented on 2026-09-14: - Added the early streaming `/video` middleware with `http-proxy-middleware`. Express removes the public prefix before forwarding WHEP/WHIP requests to `127.0.0.1:8889`, while root-relative MediaMTX session locations receive the prefix again so subsequent browser `PATCH` and `DELETE` requests follow the same path. MediaMTX signaling now binds to loopback; its ICE UDP/TCP listener remains directly reachable on port 8189. - Replaced Discord's `siteUrl`, the inter-instance profile's `publicUrl`, and media `whepBaseUrl` with one top-level `publicUrl`. A numbered internal database migration transforms every saved configuration revision before current validation, and the media section now contains only optional additional ICE hosts. WHEP and microphone WHIP URLs are fixed relative paths, so they work through the current origin without knowing its hostname. - Discord command authorization and lockdown moderation recipients now read the live administrator registry, so setup imports and later Discord-ID or role edits take effect without restarting the server. +- Full-data restore now leaves `runtime/` untouched, matching its existing exclusion from backup archives and preventing the non-root application from trying to remove lifecycle-controller state owned by the root controller container. - Fixed inter-instance public payload generation to read feature flags and social links from the same live configuration revision. Social links enabled through the new configuration system no longer trigger an undefined legacy-config reference and an HTTP 500 response. Local verification completed: diff --git a/server/src/services/backupRestoreService/backupRestore.test.js b/server/src/services/backupRestoreService/backupRestore.test.js index 37e244eb..daeb81f6 100644 --- a/server/src/services/backupRestoreService/backupRestore.test.js +++ b/server/src/services/backupRestoreService/backupRestore.test.js @@ -116,7 +116,9 @@ test('applies validated replacement data and removes rollback only after startup const applied = startupRestore.applyPendingRestore(); assert.equal(applied.status, 'awaiting-health'); assert.equal(fs.existsSync(path.join(temporaryRoot, 'old-state.txt')), false); - assert.equal(fs.existsSync(path.join(temporaryRoot, 'runtime')), false); + // Runtime is outside the durable backup payload and can contain state owned + // by the separate lifecycle controller, so applying a restore preserves it. + assert.equal(fs.readFileSync(path.join(temporaryRoot, 'runtime', 'active.tmp'), 'utf8'), 'discard during restore'); assert.equal(fs.readFileSync(path.join(temporaryRoot, 'state.json'), 'utf8'), '{"preserved":true}\n'); assert.equal(fs.existsSync(path.join(temporaryRoot, 'backup-restore', 'rollback', 'old-state.txt')), true); diff --git a/server/src/services/backupRestoreService/startupRestore.js b/server/src/services/backupRestoreService/startupRestore.js index cc9aded3..33496c89 100644 --- a/server/src/services/backupRestoreService/startupRestore.js +++ b/server/src/services/backupRestoreService/startupRestore.js @@ -35,7 +35,11 @@ function listActiveDataEntries({ includeRuntime = true } = {}) { } function removeActiveData() { - for (const name of listActiveDataEntries()) { + // Runtime contains disposable work owned by active companion processes as + // well as the lifecycle controller's status directory. It is deliberately + // absent from backup archives, so restore must leave it untouched instead + // of trying to delete root-owned controller state from the non-root server. + for (const name of listActiveDataEntries({ includeRuntime: false })) { fs.rmSync(path.join(resolveDataDir(), name), { recursive: true, force: true }); } }