@camstack/system 1.2.163 → 1.2.164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +17 -5
- package/dist/builtins/device-manager/device-manager.addon.mjs +17 -5
- package/dist/builtins/device-manager/device-projection.d.ts +12 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-migration-coordinator.d.ts +1 -18
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +43 -75
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +43 -75
- package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +6 -7
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-B29Skpzo.js → dist-BhE8zNfY.js} +72 -0
- package/dist/{dist-DxFjna2a.mjs → dist-BpsHt11f.mjs} +72 -0
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{manifest-system-deps-CG9ESHhv.mjs → manifest-system-deps-Bk_XLYbB.mjs} +1 -1
- package/dist/{manifest-system-deps-DBje540e.js → manifest-system-deps-CJLqfJUs.js} +1 -1
- package/dist/{retired-settings-keys-CRL0qOnU.js → retired-settings-keys-CqDbI-vK.js} +1 -1
- package/dist/{retired-settings-keys-N0NZzDE3.mjs → retired-settings-keys-DLwieFXR.mjs} +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BhE8zNfY.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
let node_fs_promises = require("node:fs/promises");
|
|
9
9
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
@@ -237,39 +237,15 @@ var STORAGE_CLASSES = [
|
|
|
237
237
|
"recordings",
|
|
238
238
|
"recordingsLow",
|
|
239
239
|
"eventMedia",
|
|
240
|
-
"backups"
|
|
241
|
-
"galleryMedia"
|
|
240
|
+
"backups"
|
|
242
241
|
];
|
|
243
242
|
/** The classes footage/media movers actually own. Everything else is planned,
|
|
244
243
|
* reported and refused rather than silently accepted. */
|
|
245
244
|
var MOVER_CLASSES = [
|
|
246
245
|
"recordings",
|
|
247
246
|
"recordingsLow",
|
|
248
|
-
"eventMedia"
|
|
249
|
-
"galleryMedia"
|
|
247
|
+
"eventMedia"
|
|
250
248
|
];
|
|
251
|
-
/**
|
|
252
|
-
* Classes that can never span two locations at once.
|
|
253
|
-
*
|
|
254
|
-
* `galleryMedia` is `cardinality: 'single'` and `upsertLocation` refuses a
|
|
255
|
-
* second instance of a single type, so the "both locations are first-class for
|
|
256
|
-
* reads while the past drains" transient the non-blocking mode rests on is not
|
|
257
|
-
* merely undesirable there — it is not constructible. Its move is therefore
|
|
258
|
-
* stop-the-world, always. That is acceptable only because the enrolled gallery
|
|
259
|
-
* is a few KB per sample, and it is said here rather than discovered.
|
|
260
|
-
*/
|
|
261
|
-
var BLOCKING_ONLY_CLASSES = ["galleryMedia"];
|
|
262
|
-
/**
|
|
263
|
-
* Location types whose volume `galleryMedia` must NEVER land on.
|
|
264
|
-
*
|
|
265
|
-
* Verbatim from its own declaration: *"Deliberately NOT on the recordings
|
|
266
|
-
* volume: these images survive every retention sweep, so they must also survive
|
|
267
|
-
* a footage wipe, a quota eviction and a recordings disk swap."* A planner that
|
|
268
|
-
* accepted `galleryMedia` → the recordings volume would let an operator destroy
|
|
269
|
-
* the enrolment gallery on the next disk swap, months later, with no visible
|
|
270
|
-
* link back to this decision.
|
|
271
|
-
*/
|
|
272
|
-
var GALLERY_FORBIDDEN_NEIGHBOUR_TYPES = ["recordings", "recordingsLow"];
|
|
273
249
|
/** The two single-flight mover engines. Lanes run in parallel; within a lane,
|
|
274
250
|
* classes run FIFO because the engine refuses a second concurrent job. */
|
|
275
251
|
var MOVE_LANES = ["media", "footage"];
|
|
@@ -309,6 +285,7 @@ var StorageMigrationCoordinator = class {
|
|
|
309
285
|
this.deps = deps;
|
|
310
286
|
}
|
|
311
287
|
async plan(input) {
|
|
288
|
+
refuseRetiredGalleryClass(input.destinations);
|
|
312
289
|
const mode = input.mode ?? "blocking";
|
|
313
290
|
const moves = [];
|
|
314
291
|
const findings = [];
|
|
@@ -322,8 +299,6 @@ var StorageMigrationCoordinator = class {
|
|
|
322
299
|
if (target.type !== storageClass) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${storageClass}"`);
|
|
323
300
|
if (source.id === target.id) throw new Error(`Storage location "${targetId}" is already the "${storageClass}" default`);
|
|
324
301
|
if (!MOVER_CLASSES.includes(storageClass)) throw new Error(`No mover owns "${storageClass}" — the migration can repoint its default but cannot move its bytes. Move it by hand, then repoint the default with upsertLocation.`);
|
|
325
|
-
if (mode === "nonBlocking" && BLOCKING_ONLY_CLASSES.includes(storageClass)) throw new Error(`"${storageClass}" is a single-cardinality storage class: it can never exist at two locations at once, so it cannot be repointed first and drained behind. Use mode "blocking".`);
|
|
326
|
-
await this.refuseGalleryOntoRecordingsVolume(storageClass, targetId);
|
|
327
302
|
moves.push({
|
|
328
303
|
storageClass,
|
|
329
304
|
fromLocationId: source.id,
|
|
@@ -333,11 +308,6 @@ var StorageMigrationCoordinator = class {
|
|
|
333
308
|
error: null,
|
|
334
309
|
progress: null
|
|
335
310
|
});
|
|
336
|
-
if (BLOCKING_ONLY_CLASSES.includes(storageClass)) findings.push({
|
|
337
|
-
code: "blockingOnly",
|
|
338
|
-
storageClass,
|
|
339
|
-
message: `"${storageClass}" is single-cardinality: this move is stop-the-world for its whole duration, in every mode. It is a few KB per enrolled sample, which is why that is acceptable.`
|
|
340
|
-
});
|
|
341
311
|
findings.push(...await this.deviceFindings(storageClass, source.id, target.id));
|
|
342
312
|
}
|
|
343
313
|
if (moves.length === 0) throw new Error("select at least one storage class");
|
|
@@ -372,29 +342,6 @@ var StorageMigrationCoordinator = class {
|
|
|
372
342
|
findings
|
|
373
343
|
};
|
|
374
344
|
}
|
|
375
|
-
/**
|
|
376
|
-
* HARD refusal: `galleryMedia` onto a volume a recordings class lives on.
|
|
377
|
-
*
|
|
378
|
-
* This is the one same-device case that is a refusal rather than a finding,
|
|
379
|
-
* because its consequence is silent and arrives months later — the gallery is
|
|
380
|
-
* wiped by the next footage wipe, quota eviction or recordings disk swap, and
|
|
381
|
-
* nothing at that moment points back at this migration.
|
|
382
|
-
*
|
|
383
|
-
* It can only refuse what it can SEE. `deviceKeyOf` is a realpath, so it
|
|
384
|
-
* catches a symlink, a bind mount and a literally-shared directory — and
|
|
385
|
-
* misses two distinct directories on one filesystem, and any location pinned
|
|
386
|
-
* to another node. Those come back `null`/distinct and produce a
|
|
387
|
-
* `deviceIdentityUnknown` finding instead.
|
|
388
|
-
*/
|
|
389
|
-
async refuseGalleryOntoRecordingsVolume(storageClass, targetId) {
|
|
390
|
-
if (storageClass !== "galleryMedia") return;
|
|
391
|
-
const targetKey = await this.deviceKey(targetId);
|
|
392
|
-
if (targetKey === null) return;
|
|
393
|
-
for (const type of GALLERY_FORBIDDEN_NEIGHBOUR_TYPES) for (const neighbour of this.deps.locations.listLocations({ type })) {
|
|
394
|
-
if (await this.deviceKey(neighbour.id) !== targetKey) continue;
|
|
395
|
-
throw new Error(`Storage location "${targetId}" shares a volume with "${neighbour.id}" (${type}). galleryMedia is deliberately NOT on the recordings volume: enrolment images survive every retention sweep, so they must also survive a footage wipe, a quota eviction and a recordings disk swap. Choose a destination on another volume.`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
345
|
/** Same-device observations that are reported, not refused. */
|
|
399
346
|
async deviceFindings(storageClass, sourceId, targetId) {
|
|
400
347
|
if (this.deps.deviceKeyOf === void 0) return [];
|
|
@@ -526,7 +473,7 @@ var StorageMigrationCoordinator = class {
|
|
|
526
473
|
if (laneOf(storageClass) === "media") {
|
|
527
474
|
const count = await unanswerable(this.deps.participants.analytics.residue({
|
|
528
475
|
toLocationId: target.id,
|
|
529
|
-
mode:
|
|
476
|
+
mode: "move"
|
|
530
477
|
}));
|
|
531
478
|
if (count !== null && count.rows === 0) continue;
|
|
532
479
|
out.push({
|
|
@@ -805,9 +752,7 @@ var StorageMigrationCoordinator = class {
|
|
|
805
752
|
*
|
|
806
753
|
* Two lanes — the recorder's footage engine and post-analysis's media engine
|
|
807
754
|
* — run in parallel with each other and strictly FIFO within themselves.
|
|
808
|
-
* `
|
|
809
|
-
* the same `MediaRelocateEngine`, which refuses a second running job; starting
|
|
810
|
-
* both would fail the second one instantly.
|
|
755
|
+
* `eventMedia` is the only class on the media lane.
|
|
811
756
|
*/
|
|
812
757
|
async startOrResumeMoves(job) {
|
|
813
758
|
if (job.cancelRequested) return;
|
|
@@ -937,7 +882,7 @@ var StorageMigrationCoordinator = class {
|
|
|
937
882
|
const input = {
|
|
938
883
|
toLocationId: move.toLocationId,
|
|
939
884
|
throttleMbps: job.throttleMbps,
|
|
940
|
-
mode:
|
|
885
|
+
mode: "move"
|
|
941
886
|
};
|
|
942
887
|
return (unleased ? await this.deps.participants.analytics.startDrain(input) : await this.deps.participants.analytics.startMove({
|
|
943
888
|
...input,
|
|
@@ -1011,13 +956,18 @@ function isTerminal(job) {
|
|
|
1011
956
|
}
|
|
1012
957
|
/**
|
|
1013
958
|
* Which single-flight engine owns a class.
|
|
1014
|
-
*
|
|
1015
|
-
* `galleryMedia` is in the media lane not because it is event media — it
|
|
1016
|
-
* explicitly is not (D197) — but because it is moved by the same
|
|
1017
|
-
* `MediaRelocateEngine`, which refuses a second running job.
|
|
1018
959
|
*/
|
|
1019
960
|
function laneOf(storageClass) {
|
|
1020
|
-
return storageClass === "eventMedia"
|
|
961
|
+
return storageClass === "eventMedia" ? "media" : "footage";
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* The enrolled gallery is the analytics addon's data dir, not a storage class
|
|
965
|
+
* an operator migrates. A leftover destination from an older client is refused
|
|
966
|
+
* rather than silently dropped.
|
|
967
|
+
*/
|
|
968
|
+
function refuseRetiredGalleryClass(destinations) {
|
|
969
|
+
if (destinations.galleryMedia === void 0) return;
|
|
970
|
+
throw new Error("galleryMedia is no longer a storage class. Enrolled gallery lives in the pipeline-analytics addon data directory, not a location the operator places.");
|
|
1021
971
|
}
|
|
1022
972
|
/**
|
|
1023
973
|
* True when this phase runs with every writer already back up, so no
|
|
@@ -1472,14 +1422,13 @@ var StorageOrchestratorService = class {
|
|
|
1472
1422
|
/**
|
|
1473
1423
|
* Shout about a persisted row that is BOTH the type default and disabled.
|
|
1474
1424
|
*
|
|
1475
|
-
* `upsertLocation`
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1425
|
+
* `upsertLocation` can now produce this (enabled is the write set; `isDefault`
|
|
1426
|
+
* is store residue). Hydrate also writes rows straight into the map, so a
|
|
1427
|
+
* hand-edited store or a row written by another build can too. It matters
|
|
1428
|
+
* because every bare-type ref still resolves through {@link getDefaultLocation}
|
|
1429
|
+
* until Stage 5 of the enabled-write-set design: on such a row
|
|
1480
1430
|
* `storage.write({ location: 'eventMedia' })` and the migration coordinator's
|
|
1481
|
-
* drain destination both point at a disk the operator turned off.
|
|
1482
|
-
* one thing this model must never do silently.
|
|
1431
|
+
* drain destination both point at a disk the operator turned off.
|
|
1483
1432
|
*
|
|
1484
1433
|
* Reported, never repaired: flipping an operator's flag back on at boot would
|
|
1485
1434
|
* be the system overruling the switch instead of obeying it. The coordinator
|
|
@@ -1763,6 +1712,7 @@ var StorageOrchestratorService = class {
|
|
|
1763
1712
|
createdAt: existing?.createdAt ?? now,
|
|
1764
1713
|
updatedAt: now
|
|
1765
1714
|
};
|
|
1715
|
+
refuseLastWriteTargetDisabled(existing, next, this.locations.values());
|
|
1766
1716
|
const demoted = [];
|
|
1767
1717
|
if (next.isDefault) {
|
|
1768
1718
|
for (const [otherId, otherLoc] of this.locations) if (otherLoc.type === next.type && otherLoc.id !== next.id && otherLoc.isDefault) {
|
|
@@ -2061,9 +2011,27 @@ function resolveEnabled(input, existing, typeAlreadyHasLocation) {
|
|
|
2061
2011
|
if (existing) return {};
|
|
2062
2012
|
return { enabled: input.isDefault || !typeAlreadyHasLocation };
|
|
2063
2013
|
}
|
|
2064
|
-
if (input.isDefault && requested === false) return { enabled: true };
|
|
2065
2014
|
return { enabled: requested };
|
|
2066
2015
|
}
|
|
2016
|
+
/** Types that may have zero enabled locations. Backups with none is sad, not illegal. */
|
|
2017
|
+
var OPTIONAL_WRITE_TYPES = new Set(["backups"]);
|
|
2018
|
+
/**
|
|
2019
|
+
* Refuse an UPDATE that would leave a write-serving type with no enabled
|
|
2020
|
+
* location. Creates are exempt (an explicit `enabled: false` on the first
|
|
2021
|
+
* location is already a named test). `isDefault` is not consulted — enabled
|
|
2022
|
+
* is the write set.
|
|
2023
|
+
*/
|
|
2024
|
+
function refuseLastWriteTargetDisabled(existing, next, locations) {
|
|
2025
|
+
if (OPTIONAL_WRITE_TYPES.has(next.type)) return;
|
|
2026
|
+
if (next.enabled !== false) return;
|
|
2027
|
+
if (existing === void 0) return;
|
|
2028
|
+
if (existing.enabled === false) return;
|
|
2029
|
+
for (const loc of locations) {
|
|
2030
|
+
if (loc.id === next.id || loc.type !== next.type) continue;
|
|
2031
|
+
if (loc.enabled !== false) return;
|
|
2032
|
+
}
|
|
2033
|
+
throw new Error(`Cannot disable "${next.id}": it is the last enabled location of type "${next.type}". Enable another location of this type first.`);
|
|
2034
|
+
}
|
|
2067
2035
|
//#endregion
|
|
2068
2036
|
//#region src/builtins/storage-orchestrator/storage-pressure-manager.ts
|
|
2069
2037
|
var DEFAULT_MAX_ROUNDS = 8;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Mt as BaseAddon, N as collectSecretConfigKeys, Qt as parseJsonObject, S as StorageMigrationJobSchema, St as settingsStoreCapability, Tt as storageMigrationCapability, m as REDACTED_SECRET, wt as storageCapability, x as StorageLocationTypeSchema } from "../../dist-
|
|
1
|
+
import { Mt as BaseAddon, N as collectSecretConfigKeys, Qt as parseJsonObject, S as StorageMigrationJobSchema, St as settingsStoreCapability, Tt as storageMigrationCapability, m as REDACTED_SECRET, wt as storageCapability, x as StorageLocationTypeSchema } from "../../dist-BpsHt11f.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -230,39 +230,15 @@ var STORAGE_CLASSES = [
|
|
|
230
230
|
"recordings",
|
|
231
231
|
"recordingsLow",
|
|
232
232
|
"eventMedia",
|
|
233
|
-
"backups"
|
|
234
|
-
"galleryMedia"
|
|
233
|
+
"backups"
|
|
235
234
|
];
|
|
236
235
|
/** The classes footage/media movers actually own. Everything else is planned,
|
|
237
236
|
* reported and refused rather than silently accepted. */
|
|
238
237
|
var MOVER_CLASSES = [
|
|
239
238
|
"recordings",
|
|
240
239
|
"recordingsLow",
|
|
241
|
-
"eventMedia"
|
|
242
|
-
"galleryMedia"
|
|
240
|
+
"eventMedia"
|
|
243
241
|
];
|
|
244
|
-
/**
|
|
245
|
-
* Classes that can never span two locations at once.
|
|
246
|
-
*
|
|
247
|
-
* `galleryMedia` is `cardinality: 'single'` and `upsertLocation` refuses a
|
|
248
|
-
* second instance of a single type, so the "both locations are first-class for
|
|
249
|
-
* reads while the past drains" transient the non-blocking mode rests on is not
|
|
250
|
-
* merely undesirable there — it is not constructible. Its move is therefore
|
|
251
|
-
* stop-the-world, always. That is acceptable only because the enrolled gallery
|
|
252
|
-
* is a few KB per sample, and it is said here rather than discovered.
|
|
253
|
-
*/
|
|
254
|
-
var BLOCKING_ONLY_CLASSES = ["galleryMedia"];
|
|
255
|
-
/**
|
|
256
|
-
* Location types whose volume `galleryMedia` must NEVER land on.
|
|
257
|
-
*
|
|
258
|
-
* Verbatim from its own declaration: *"Deliberately NOT on the recordings
|
|
259
|
-
* volume: these images survive every retention sweep, so they must also survive
|
|
260
|
-
* a footage wipe, a quota eviction and a recordings disk swap."* A planner that
|
|
261
|
-
* accepted `galleryMedia` → the recordings volume would let an operator destroy
|
|
262
|
-
* the enrolment gallery on the next disk swap, months later, with no visible
|
|
263
|
-
* link back to this decision.
|
|
264
|
-
*/
|
|
265
|
-
var GALLERY_FORBIDDEN_NEIGHBOUR_TYPES = ["recordings", "recordingsLow"];
|
|
266
242
|
/** The two single-flight mover engines. Lanes run in parallel; within a lane,
|
|
267
243
|
* classes run FIFO because the engine refuses a second concurrent job. */
|
|
268
244
|
var MOVE_LANES = ["media", "footage"];
|
|
@@ -302,6 +278,7 @@ var StorageMigrationCoordinator = class {
|
|
|
302
278
|
this.deps = deps;
|
|
303
279
|
}
|
|
304
280
|
async plan(input) {
|
|
281
|
+
refuseRetiredGalleryClass(input.destinations);
|
|
305
282
|
const mode = input.mode ?? "blocking";
|
|
306
283
|
const moves = [];
|
|
307
284
|
const findings = [];
|
|
@@ -315,8 +292,6 @@ var StorageMigrationCoordinator = class {
|
|
|
315
292
|
if (target.type !== storageClass) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${storageClass}"`);
|
|
316
293
|
if (source.id === target.id) throw new Error(`Storage location "${targetId}" is already the "${storageClass}" default`);
|
|
317
294
|
if (!MOVER_CLASSES.includes(storageClass)) throw new Error(`No mover owns "${storageClass}" — the migration can repoint its default but cannot move its bytes. Move it by hand, then repoint the default with upsertLocation.`);
|
|
318
|
-
if (mode === "nonBlocking" && BLOCKING_ONLY_CLASSES.includes(storageClass)) throw new Error(`"${storageClass}" is a single-cardinality storage class: it can never exist at two locations at once, so it cannot be repointed first and drained behind. Use mode "blocking".`);
|
|
319
|
-
await this.refuseGalleryOntoRecordingsVolume(storageClass, targetId);
|
|
320
295
|
moves.push({
|
|
321
296
|
storageClass,
|
|
322
297
|
fromLocationId: source.id,
|
|
@@ -326,11 +301,6 @@ var StorageMigrationCoordinator = class {
|
|
|
326
301
|
error: null,
|
|
327
302
|
progress: null
|
|
328
303
|
});
|
|
329
|
-
if (BLOCKING_ONLY_CLASSES.includes(storageClass)) findings.push({
|
|
330
|
-
code: "blockingOnly",
|
|
331
|
-
storageClass,
|
|
332
|
-
message: `"${storageClass}" is single-cardinality: this move is stop-the-world for its whole duration, in every mode. It is a few KB per enrolled sample, which is why that is acceptable.`
|
|
333
|
-
});
|
|
334
304
|
findings.push(...await this.deviceFindings(storageClass, source.id, target.id));
|
|
335
305
|
}
|
|
336
306
|
if (moves.length === 0) throw new Error("select at least one storage class");
|
|
@@ -365,29 +335,6 @@ var StorageMigrationCoordinator = class {
|
|
|
365
335
|
findings
|
|
366
336
|
};
|
|
367
337
|
}
|
|
368
|
-
/**
|
|
369
|
-
* HARD refusal: `galleryMedia` onto a volume a recordings class lives on.
|
|
370
|
-
*
|
|
371
|
-
* This is the one same-device case that is a refusal rather than a finding,
|
|
372
|
-
* because its consequence is silent and arrives months later — the gallery is
|
|
373
|
-
* wiped by the next footage wipe, quota eviction or recordings disk swap, and
|
|
374
|
-
* nothing at that moment points back at this migration.
|
|
375
|
-
*
|
|
376
|
-
* It can only refuse what it can SEE. `deviceKeyOf` is a realpath, so it
|
|
377
|
-
* catches a symlink, a bind mount and a literally-shared directory — and
|
|
378
|
-
* misses two distinct directories on one filesystem, and any location pinned
|
|
379
|
-
* to another node. Those come back `null`/distinct and produce a
|
|
380
|
-
* `deviceIdentityUnknown` finding instead.
|
|
381
|
-
*/
|
|
382
|
-
async refuseGalleryOntoRecordingsVolume(storageClass, targetId) {
|
|
383
|
-
if (storageClass !== "galleryMedia") return;
|
|
384
|
-
const targetKey = await this.deviceKey(targetId);
|
|
385
|
-
if (targetKey === null) return;
|
|
386
|
-
for (const type of GALLERY_FORBIDDEN_NEIGHBOUR_TYPES) for (const neighbour of this.deps.locations.listLocations({ type })) {
|
|
387
|
-
if (await this.deviceKey(neighbour.id) !== targetKey) continue;
|
|
388
|
-
throw new Error(`Storage location "${targetId}" shares a volume with "${neighbour.id}" (${type}). galleryMedia is deliberately NOT on the recordings volume: enrolment images survive every retention sweep, so they must also survive a footage wipe, a quota eviction and a recordings disk swap. Choose a destination on another volume.`);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
338
|
/** Same-device observations that are reported, not refused. */
|
|
392
339
|
async deviceFindings(storageClass, sourceId, targetId) {
|
|
393
340
|
if (this.deps.deviceKeyOf === void 0) return [];
|
|
@@ -519,7 +466,7 @@ var StorageMigrationCoordinator = class {
|
|
|
519
466
|
if (laneOf(storageClass) === "media") {
|
|
520
467
|
const count = await unanswerable(this.deps.participants.analytics.residue({
|
|
521
468
|
toLocationId: target.id,
|
|
522
|
-
mode:
|
|
469
|
+
mode: "move"
|
|
523
470
|
}));
|
|
524
471
|
if (count !== null && count.rows === 0) continue;
|
|
525
472
|
out.push({
|
|
@@ -798,9 +745,7 @@ var StorageMigrationCoordinator = class {
|
|
|
798
745
|
*
|
|
799
746
|
* Two lanes — the recorder's footage engine and post-analysis's media engine
|
|
800
747
|
* — run in parallel with each other and strictly FIFO within themselves.
|
|
801
|
-
* `
|
|
802
|
-
* the same `MediaRelocateEngine`, which refuses a second running job; starting
|
|
803
|
-
* both would fail the second one instantly.
|
|
748
|
+
* `eventMedia` is the only class on the media lane.
|
|
804
749
|
*/
|
|
805
750
|
async startOrResumeMoves(job) {
|
|
806
751
|
if (job.cancelRequested) return;
|
|
@@ -930,7 +875,7 @@ var StorageMigrationCoordinator = class {
|
|
|
930
875
|
const input = {
|
|
931
876
|
toLocationId: move.toLocationId,
|
|
932
877
|
throttleMbps: job.throttleMbps,
|
|
933
|
-
mode:
|
|
878
|
+
mode: "move"
|
|
934
879
|
};
|
|
935
880
|
return (unleased ? await this.deps.participants.analytics.startDrain(input) : await this.deps.participants.analytics.startMove({
|
|
936
881
|
...input,
|
|
@@ -1004,13 +949,18 @@ function isTerminal(job) {
|
|
|
1004
949
|
}
|
|
1005
950
|
/**
|
|
1006
951
|
* Which single-flight engine owns a class.
|
|
1007
|
-
*
|
|
1008
|
-
* `galleryMedia` is in the media lane not because it is event media — it
|
|
1009
|
-
* explicitly is not (D197) — but because it is moved by the same
|
|
1010
|
-
* `MediaRelocateEngine`, which refuses a second running job.
|
|
1011
952
|
*/
|
|
1012
953
|
function laneOf(storageClass) {
|
|
1013
|
-
return storageClass === "eventMedia"
|
|
954
|
+
return storageClass === "eventMedia" ? "media" : "footage";
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* The enrolled gallery is the analytics addon's data dir, not a storage class
|
|
958
|
+
* an operator migrates. A leftover destination from an older client is refused
|
|
959
|
+
* rather than silently dropped.
|
|
960
|
+
*/
|
|
961
|
+
function refuseRetiredGalleryClass(destinations) {
|
|
962
|
+
if (destinations.galleryMedia === void 0) return;
|
|
963
|
+
throw new Error("galleryMedia is no longer a storage class. Enrolled gallery lives in the pipeline-analytics addon data directory, not a location the operator places.");
|
|
1014
964
|
}
|
|
1015
965
|
/**
|
|
1016
966
|
* True when this phase runs with every writer already back up, so no
|
|
@@ -1465,14 +1415,13 @@ var StorageOrchestratorService = class {
|
|
|
1465
1415
|
/**
|
|
1466
1416
|
* Shout about a persisted row that is BOTH the type default and disabled.
|
|
1467
1417
|
*
|
|
1468
|
-
* `upsertLocation`
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1418
|
+
* `upsertLocation` can now produce this (enabled is the write set; `isDefault`
|
|
1419
|
+
* is store residue). Hydrate also writes rows straight into the map, so a
|
|
1420
|
+
* hand-edited store or a row written by another build can too. It matters
|
|
1421
|
+
* because every bare-type ref still resolves through {@link getDefaultLocation}
|
|
1422
|
+
* until Stage 5 of the enabled-write-set design: on such a row
|
|
1473
1423
|
* `storage.write({ location: 'eventMedia' })` and the migration coordinator's
|
|
1474
|
-
* drain destination both point at a disk the operator turned off.
|
|
1475
|
-
* one thing this model must never do silently.
|
|
1424
|
+
* drain destination both point at a disk the operator turned off.
|
|
1476
1425
|
*
|
|
1477
1426
|
* Reported, never repaired: flipping an operator's flag back on at boot would
|
|
1478
1427
|
* be the system overruling the switch instead of obeying it. The coordinator
|
|
@@ -1756,6 +1705,7 @@ var StorageOrchestratorService = class {
|
|
|
1756
1705
|
createdAt: existing?.createdAt ?? now,
|
|
1757
1706
|
updatedAt: now
|
|
1758
1707
|
};
|
|
1708
|
+
refuseLastWriteTargetDisabled(existing, next, this.locations.values());
|
|
1759
1709
|
const demoted = [];
|
|
1760
1710
|
if (next.isDefault) {
|
|
1761
1711
|
for (const [otherId, otherLoc] of this.locations) if (otherLoc.type === next.type && otherLoc.id !== next.id && otherLoc.isDefault) {
|
|
@@ -2054,9 +2004,27 @@ function resolveEnabled(input, existing, typeAlreadyHasLocation) {
|
|
|
2054
2004
|
if (existing) return {};
|
|
2055
2005
|
return { enabled: input.isDefault || !typeAlreadyHasLocation };
|
|
2056
2006
|
}
|
|
2057
|
-
if (input.isDefault && requested === false) return { enabled: true };
|
|
2058
2007
|
return { enabled: requested };
|
|
2059
2008
|
}
|
|
2009
|
+
/** Types that may have zero enabled locations. Backups with none is sad, not illegal. */
|
|
2010
|
+
var OPTIONAL_WRITE_TYPES = new Set(["backups"]);
|
|
2011
|
+
/**
|
|
2012
|
+
* Refuse an UPDATE that would leave a write-serving type with no enabled
|
|
2013
|
+
* location. Creates are exempt (an explicit `enabled: false` on the first
|
|
2014
|
+
* location is already a named test). `isDefault` is not consulted — enabled
|
|
2015
|
+
* is the write set.
|
|
2016
|
+
*/
|
|
2017
|
+
function refuseLastWriteTargetDisabled(existing, next, locations) {
|
|
2018
|
+
if (OPTIONAL_WRITE_TYPES.has(next.type)) return;
|
|
2019
|
+
if (next.enabled !== false) return;
|
|
2020
|
+
if (existing === void 0) return;
|
|
2021
|
+
if (existing.enabled === false) return;
|
|
2022
|
+
for (const loc of locations) {
|
|
2023
|
+
if (loc.id === next.id || loc.type !== next.type) continue;
|
|
2024
|
+
if (loc.enabled !== false) return;
|
|
2025
|
+
}
|
|
2026
|
+
throw new Error(`Cannot disable "${next.id}": it is the last enabled location of type "${next.type}". Enable another location of this type first.`);
|
|
2027
|
+
}
|
|
2060
2028
|
//#endregion
|
|
2061
2029
|
//#region src/builtins/storage-orchestrator/storage-pressure-manager.ts
|
|
2062
2030
|
var DEFAULT_MAX_ROUNDS = 8;
|
|
@@ -186,14 +186,13 @@ export declare class StorageOrchestratorService {
|
|
|
186
186
|
/**
|
|
187
187
|
* Shout about a persisted row that is BOTH the type default and disabled.
|
|
188
188
|
*
|
|
189
|
-
* `upsertLocation`
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
189
|
+
* `upsertLocation` can now produce this (enabled is the write set; `isDefault`
|
|
190
|
+
* is store residue). Hydrate also writes rows straight into the map, so a
|
|
191
|
+
* hand-edited store or a row written by another build can too. It matters
|
|
192
|
+
* because every bare-type ref still resolves through {@link getDefaultLocation}
|
|
193
|
+
* until Stage 5 of the enabled-write-set design: on such a row
|
|
194
194
|
* `storage.write({ location: 'eventMedia' })` and the migration coordinator's
|
|
195
|
-
* drain destination both point at a disk the operator turned off.
|
|
196
|
-
* one thing this model must never do silently.
|
|
195
|
+
* drain destination both point at a disk the operator turned off.
|
|
197
196
|
*
|
|
198
197
|
* Reported, never repaired: flipping an operator's flag back on at boot would
|
|
199
198
|
* be the system overruling the switch instead of obeying it. The coordinator
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BhE8zNfY.js");
|
|
7
7
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
8
8
|
/**
|
|
9
9
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Mt as BaseAddon, Yt as hydrateSchema, jt as errMsg } from "../../dist-
|
|
1
|
+
import { Mt as BaseAddon, Yt as hydrateSchema, jt as errMsg } from "../../dist-BpsHt11f.mjs";
|
|
2
2
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
3
3
|
/**
|
|
4
4
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BhE8zNfY.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
let node_path = require("node:path");
|
|
9
9
|
node_path = require_chunk.__toESM(node_path);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Mt as BaseAddon, ot as logDestinationCapability } from "../../dist-
|
|
1
|
+
import { Mt as BaseAddon, ot as logDestinationCapability } from "../../dist-BpsHt11f.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
|
|
3
3
|
import * as path$1 from "node:path";
|
|
4
4
|
import path from "node:path";
|
|
@@ -18151,6 +18151,47 @@ var RebuildStatusSchema = zod.z.object({
|
|
|
18151
18151
|
/** Present when the pass ended by throwing. */
|
|
18152
18152
|
error: zod.z.string().nullable()
|
|
18153
18153
|
});
|
|
18154
|
+
/**
|
|
18155
|
+
* Acknowledgement that a debug-media reclaim STARTED.
|
|
18156
|
+
*
|
|
18157
|
+
* The pass is paced at ~2 MB/s over a standing 100 GB — tens of minutes — so
|
|
18158
|
+
* it runs detached and this returns immediately. Awaiting it is how the
|
|
18159
|
+
* `addons.custom` door hit the 60 s UDS deadline while the walk carried on
|
|
18160
|
+
* with no status. Poll {@link pipelineAnalyticsCapability.methods.getMediaReclaimStatus}.
|
|
18161
|
+
*/
|
|
18162
|
+
var MediaReclaimStartResultSchema = zod.z.object({
|
|
18163
|
+
started: zod.z.boolean(),
|
|
18164
|
+
/** True when a pass was already running; the new request is ignored. */
|
|
18165
|
+
alreadyRunning: zod.z.boolean()
|
|
18166
|
+
});
|
|
18167
|
+
var MediaReclaimInputSchema = zod.z.object({
|
|
18168
|
+
mode: zod.z.enum(["report", "reclaim"]).default("report"),
|
|
18169
|
+
scopes: zod.z.array(zod.z.enum(["motion-still", "track-filmstrip"])).min(1).optional(),
|
|
18170
|
+
deviceIds: zod.z.array(zod.z.number().int()).min(1).optional(),
|
|
18171
|
+
restart: zod.z.boolean().optional(),
|
|
18172
|
+
pageSize: zod.z.number().int().min(50).max(5e3).optional(),
|
|
18173
|
+
maxRowsPerDevice: zod.z.number().int().min(1).max(5e5).optional(),
|
|
18174
|
+
maxReclaimPerDevice: zod.z.number().int().min(1).max(5e5).optional(),
|
|
18175
|
+
maxBytesPerRun: zod.z.number().int().min(1).optional(),
|
|
18176
|
+
budgetMinutes: zod.z.number().int().min(1).max(720).optional(),
|
|
18177
|
+
throttleBytesPerSec: zod.z.number().int().min(64 * 1024).optional(),
|
|
18178
|
+
graceMinutes: zod.z.number().int().min(1).max(10080).optional()
|
|
18179
|
+
});
|
|
18180
|
+
var MediaReclaimStatusSchema = zod.z.object({
|
|
18181
|
+
running: zod.z.boolean(),
|
|
18182
|
+
mode: zod.z.enum(["report", "reclaim"]).nullable(),
|
|
18183
|
+
totalExamined: zod.z.number(),
|
|
18184
|
+
totalEligible: zod.z.number(),
|
|
18185
|
+
totalReclaimed: zod.z.number(),
|
|
18186
|
+
totalBytesReclaimed: zod.z.number(),
|
|
18187
|
+
totalRefused: zod.z.number(),
|
|
18188
|
+
/** Device+scope windows finished in this pass. */
|
|
18189
|
+
devicesDone: zod.z.number(),
|
|
18190
|
+
complete: zod.z.boolean().nullable(),
|
|
18191
|
+
startedAtMs: zod.z.number().nullable(),
|
|
18192
|
+
finishedAtMs: zod.z.number().nullable(),
|
|
18193
|
+
error: zod.z.string().nullable()
|
|
18194
|
+
});
|
|
18154
18195
|
var ReplayFrameInputSchema = zod.z.object({
|
|
18155
18196
|
timestamp: zod.z.number(),
|
|
18156
18197
|
frame: PipelineRunResultBridge
|
|
@@ -18574,6 +18615,20 @@ var pipelineAnalyticsCapability = {
|
|
|
18574
18615
|
auth: "admin"
|
|
18575
18616
|
}),
|
|
18576
18617
|
/**
|
|
18618
|
+
* Reclaim the standing debug-media backlog (motion-visit stills + track
|
|
18619
|
+
* filmstrips). DETACHED: returns `{ started }` immediately. The pass is
|
|
18620
|
+
* tens of minutes; awaiting it is a timeout, not a verdict. Dry-run unless
|
|
18621
|
+
* `mode: 'reclaim'`. Poll `getMediaReclaimStatus`.
|
|
18622
|
+
*/
|
|
18623
|
+
reclaimDebugMedia: method(MediaReclaimInputSchema, MediaReclaimStartResultSchema, {
|
|
18624
|
+
kind: "mutation",
|
|
18625
|
+
auth: "admin"
|
|
18626
|
+
}),
|
|
18627
|
+
getMediaReclaimStatus: method(zod.z.object({}), MediaReclaimStatusSchema, {
|
|
18628
|
+
kind: "query",
|
|
18629
|
+
auth: "admin"
|
|
18630
|
+
}),
|
|
18631
|
+
/**
|
|
18577
18632
|
* The CHEAP QUESTION, asked before any media moves: how big is the dataset
|
|
18578
18633
|
* the marked (`markForTrain`) tracks would produce?
|
|
18579
18634
|
*
|
|
@@ -37160,6 +37215,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
37160
37215
|
addonId: null,
|
|
37161
37216
|
access: "view"
|
|
37162
37217
|
},
|
|
37218
|
+
"pipelineAnalytics.getMediaReclaimStatus": {
|
|
37219
|
+
capName: "pipeline-analytics",
|
|
37220
|
+
capScope: "device",
|
|
37221
|
+
addonId: null,
|
|
37222
|
+
access: "view"
|
|
37223
|
+
},
|
|
37163
37224
|
"pipelineAnalytics.getMotionEvents": {
|
|
37164
37225
|
capName: "pipeline-analytics",
|
|
37165
37226
|
capScope: "device",
|
|
@@ -37334,6 +37395,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
37334
37395
|
addonId: null,
|
|
37335
37396
|
access: "create"
|
|
37336
37397
|
},
|
|
37398
|
+
"pipelineAnalytics.reclaimDebugMedia": {
|
|
37399
|
+
capName: "pipeline-analytics",
|
|
37400
|
+
capScope: "device",
|
|
37401
|
+
addonId: null,
|
|
37402
|
+
access: "create"
|
|
37403
|
+
},
|
|
37337
37404
|
"pipelineAnalytics.reconcileFromDisk": {
|
|
37338
37405
|
capName: "pipeline-analytics",
|
|
37339
37406
|
capScope: "device",
|
|
@@ -40950,6 +41017,11 @@ Object.freeze({
|
|
|
40950
41017
|
form: "single",
|
|
40951
41018
|
optional: true
|
|
40952
41019
|
}],
|
|
41020
|
+
"pipelineAnalytics.reclaimDebugMedia": [{
|
|
41021
|
+
name: "deviceIds",
|
|
41022
|
+
form: "array",
|
|
41023
|
+
optional: true
|
|
41024
|
+
}],
|
|
40953
41025
|
"pipelineAnalytics.reconcileFromDisk": [{
|
|
40954
41026
|
name: "deviceId",
|
|
40955
41027
|
form: "single",
|