camstack 1.2.70 → 1.2.71

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.
@@ -23633,9 +23633,9 @@ var require_zod = __commonJS({
23633
23633
  }
23634
23634
  });
23635
23635
 
23636
- // ../system/dist/dist-BhE8zNfY.js
23637
- var require_dist_BhE8zNfY = __commonJS({
23638
- "../system/dist/dist-BhE8zNfY.js"(exports) {
23636
+ // ../system/dist/dist-CFTKQGym.js
23637
+ var require_dist_CFTKQGym = __commonJS({
23638
+ "../system/dist/dist-CFTKQGym.js"(exports) {
23639
23639
  "use strict";
23640
23640
  var zod = require_zod();
23641
23641
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
@@ -26326,6 +26326,13 @@ var require_dist_BhE8zNfY = __commonJS({
26326
26326
  ]);
26327
26327
  var RelocateMediaInputSchema = zod.z.object({
26328
26328
  toLocationId: zod.z.string(),
26329
+ /**
26330
+ * Restrict the pass to rows currently on this location. Omitted / `'*'` =
26331
+ * every row that is not already on `toLocationId` (the historical
26332
+ * behaviour). A named source is what a from→to migration needs: without it
26333
+ * "move events off disk 2" also emptied disk 1.
26334
+ */
26335
+ fromLocationId: zod.z.string().optional(),
26329
26336
  throttleMbps: zod.z.number().min(1).max(1e3).optional(),
26330
26337
  /** Omitted = `move`, the pre-existing behaviour. */
26331
26338
  mode: MediaRelocateModeSchema.optional()
@@ -26357,9 +26364,18 @@ var require_dist_BhE8zNfY = __commonJS({
26357
26364
  backups: zod.z.string().min(1).optional(),
26358
26365
  galleryMedia: zod.z.string().min(1).optional()
26359
26366
  }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
26367
+ var StorageMigrationSourcesSchema = zod.z.object({
26368
+ recordings: zod.z.string().min(1).optional(),
26369
+ recordingsLow: zod.z.string().min(1).optional(),
26370
+ eventMedia: zod.z.string().min(1).optional(),
26371
+ backups: zod.z.string().min(1).optional(),
26372
+ galleryMedia: zod.z.string().min(1).optional()
26373
+ }).optional();
26360
26374
  var StorageMigrationModeSchema = zod.z.enum(["blocking", "nonBlocking"]);
26361
26375
  var StorageMigrationInputSchema = zod.z.object({
26362
26376
  destinations: StorageMigrationDestinationsSchema,
26377
+ /** Omitted = each class's current default. */
26378
+ sources: StorageMigrationSourcesSchema,
26363
26379
  throttleMbps: zod.z.number().min(1).max(1e3).optional(),
26364
26380
  /** Omitted = `blocking`, which stays the default. */
26365
26381
  mode: StorageMigrationModeSchema.optional()
@@ -26405,6 +26421,13 @@ var require_dist_BhE8zNfY = __commonJS({
26405
26421
  storageClass: StorageMigrationClassSchema,
26406
26422
  fromLocationId: zod.z.string(),
26407
26423
  toLocationId: zod.z.string(),
26424
+ /**
26425
+ * True when `from` was NOT the class default at plan time. The move still
26426
+ * copies bytes, but the default is left alone and the source is disabled
26427
+ * once the copy verifies. Absent on jobs planned before this field existed
26428
+ * — those jobs always repointed, which is `false`.
26429
+ */
26430
+ freezeSource: zod.z.boolean().optional(),
26408
26431
  moverJobId: zod.z.string().nullable(),
26409
26432
  state: RelocateJobStateSchema.nullable(),
26410
26433
  error: zod.z.string().nullable(),
@@ -26418,6 +26441,7 @@ var require_dist_BhE8zNfY = __commonJS({
26418
26441
  * can tell a seconds-long cutover from a thirty-hour one. */
26419
26442
  mode: StorageMigrationModeSchema,
26420
26443
  destinations: StorageMigrationDestinationsSchema,
26444
+ sources: StorageMigrationSourcesSchema,
26421
26445
  throttleMbps: zod.z.number(),
26422
26446
  moves: zod.z.array(StorageMigrationMoveSchema),
26423
26447
  pauseLeaseId: zod.z.string().nullable(),
@@ -26444,6 +26468,7 @@ var require_dist_BhE8zNfY = __commonJS({
26444
26468
  });
26445
26469
  var StorageMigrationPlanSchema = zod.z.object({
26446
26470
  destinations: StorageMigrationDestinationsSchema,
26471
+ sources: StorageMigrationSourcesSchema,
26447
26472
  /** The mode this plan was built for. A plan is only valid for its mode: the
26448
26473
  * `eventMedia` seal gate and the single-cardinality refusal both depend on
26449
26474
  * it. */
@@ -26451,7 +26476,8 @@ var require_dist_BhE8zNfY = __commonJS({
26451
26476
  moves: zod.z.array(zod.z.object({
26452
26477
  storageClass: StorageMigrationClassSchema,
26453
26478
  fromLocationId: zod.z.string(),
26454
- toLocationId: zod.z.string()
26479
+ toLocationId: zod.z.string(),
26480
+ freezeSource: zod.z.boolean().optional()
26455
26481
  })),
26456
26482
  findings: zod.z.array(StorageMigrationFindingSchema)
26457
26483
  });
@@ -26563,9 +26589,42 @@ var require_dist_BhE8zNfY = __commonJS({
26563
26589
  var RelocatableMediaCountSchema = zod.z.object({ rows: zod.z.number().int().nonnegative() }).nullable();
26564
26590
  var RelocatableMediaCountInputSchema = zod.z.object({
26565
26591
  toLocationId: zod.z.string().min(1),
26592
+ /** Restrict the count to one source; omitted / `'*'` = every non-target row. */
26593
+ fromLocationId: zod.z.string().optional(),
26566
26594
  /** Omitted = `move`. */
26567
26595
  mode: MediaRelocateModeSchema.optional()
26568
26596
  });
26597
+ var StorageCleanupPhaseSchema = zod.z.enum([
26598
+ "orphans",
26599
+ "debug-media",
26600
+ "ghost-ledger",
26601
+ "done",
26602
+ "failed",
26603
+ "cancelled"
26604
+ ]);
26605
+ var StorageCleanupInputSchema = zod.z.object({
26606
+ /** Also walk motion stills / track filmstrips. Off by default. */
26607
+ includeDebugMedia: zod.z.boolean().optional()
26608
+ });
26609
+ var StorageCleanupJobSchema = zod.z.object({
26610
+ jobId: zod.z.string(),
26611
+ phase: StorageCleanupPhaseSchema,
26612
+ includeDebugMedia: zod.z.boolean(),
26613
+ orphansReclaimed: zod.z.number().int().nonnegative(),
26614
+ orphanBytesReclaimed: zod.z.number().int().nonnegative(),
26615
+ debugMediaReclaimed: zod.z.number().int().nonnegative(),
26616
+ debugMediaBytesReclaimed: zod.z.number().int().nonnegative(),
26617
+ ghostsForgotten: zod.z.number().int().nonnegative(),
26618
+ ghostBytesForgotten: zod.z.number().int().nonnegative(),
26619
+ /** Short operator-facing line: current collection, pass, or location. */
26620
+ detail: zod.z.string().nullable(),
26621
+ cancelRequested: zod.z.boolean(),
26622
+ startedAt: zod.z.number(),
26623
+ updatedAt: zod.z.number(),
26624
+ finishedAt: zod.z.number().nullable(),
26625
+ error: zod.z.string().nullable()
26626
+ });
26627
+ var StorageCleanupStatusInputSchema = zod.z.object({ jobId: zod.z.string().optional() });
26569
26628
  var StorageLocationTypeSchema = zod.z.string().regex(/^[a-z][a-zA-Z0-9-]*$/);
26570
26629
  var StorageLocationSchema = zod.z.object({
26571
26630
  id: zod.z.string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/),
@@ -40343,7 +40402,23 @@ var require_dist_BhE8zNfY = __commonJS({
40343
40402
  drain: method(StorageMigrationDrainInputSchema, zod.z.object({ jobId: zod.z.string() }), {
40344
40403
  kind: "mutation",
40345
40404
  auth: "admin"
40346
- })
40405
+ }),
40406
+ /**
40407
+ * One operator cleanup of leftover analytics (DB + blobs) and ghost
40408
+ * ledger rows on frozen footage locations. Optional debug-media sweep.
40409
+ * Returns immediately; poll {@link cleanupStatus}.
40410
+ */
40411
+ cleanupStart: method(StorageCleanupInputSchema, zod.z.object({ jobId: zod.z.string() }), {
40412
+ kind: "mutation",
40413
+ auth: "admin"
40414
+ }),
40415
+ cleanupStatus: method(StorageCleanupStatusInputSchema, StorageCleanupJobSchema.nullable(), { auth: "admin" }),
40416
+ cleanupCancel: method(zod.z.object({ jobId: zod.z.string() }), zod.z.object({ cancelled: zod.z.boolean() }), {
40417
+ kind: "mutation",
40418
+ auth: "admin"
40419
+ }),
40420
+ /** Recent finished migration jobs, newest first. The live job is `status`. */
40421
+ history: method(zod.z.object({}), zod.z.array(StorageMigrationJobSchema).readonly(), { auth: "admin" })
40347
40422
  }
40348
40423
  };
40349
40424
  var ProviderInfoSchema = zod.z.discriminatedUnion("shouldSaveDiskSpace", [zod.z.object({
@@ -47135,6 +47210,26 @@ var require_dist_BhE8zNfY = __commonJS({
47135
47210
  /** Ignore piles smaller than this (default 1 GB). */
47136
47211
  minMoveGb: zod.z.number().min(0).optional()
47137
47212
  });
47213
+ var RecordingDevicePlacementSchema = zod.z.object({
47214
+ deviceId: zod.z.number().int(),
47215
+ profile: zod.z.string(),
47216
+ locationId: zod.z.string()
47217
+ });
47218
+ var RecordingDevicePinSchema = zod.z.object({
47219
+ deviceId: zod.z.number().int(),
47220
+ /** Recordings-class location this camera is pinned to. */
47221
+ locationId: zod.z.string()
47222
+ });
47223
+ var RecordingPlacementViewSchema = zod.z.object({
47224
+ assignments: zod.z.array(RecordingDevicePlacementSchema),
47225
+ pins: zod.z.array(RecordingDevicePinSchema),
47226
+ defaultLocations: zod.z.record(zod.z.string(), zod.z.string())
47227
+ });
47228
+ var RecordingSetDevicePlacementInputSchema = zod.z.object({
47229
+ deviceId: zod.z.number().int(),
47230
+ /** `null` = Auto. Otherwise a `recordings:*` location id. */
47231
+ locationId: zod.z.string().nullable()
47232
+ });
47138
47233
  var LocateSegmentResultSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
47139
47234
  kind: zod.z.literal("segment"),
47140
47235
  startMs: zod.z.number(),
@@ -47453,6 +47548,22 @@ var require_dist_BhE8zNfY = __commonJS({
47453
47548
  startStorageRebalance: method(RecordingRebalanceInputSchema, RecordingRebalancePlanSchema, {
47454
47549
  kind: "mutation",
47455
47550
  auth: "admin"
47551
+ }),
47552
+ /**
47553
+ * The placement plan in force plus operator pins. Drives the Locations
47554
+ * admin page: Auto vs a named recordings location, per camera.
47555
+ */
47556
+ getPlacement: method(zod.z.object({}), RecordingPlacementViewSchema, {
47557
+ kind: "query",
47558
+ auth: "admin"
47559
+ }),
47560
+ /**
47561
+ * Pin a camera to a recordings location, or clear the pin (Auto). High and
47562
+ * mid follow the pin; low stays with the recordingsLow planner.
47563
+ */
47564
+ setDevicePlacement: method(RecordingSetDevicePlacementInputSchema, zod.z.object({ ok: zod.z.literal(true) }), {
47565
+ kind: "mutation",
47566
+ auth: "admin"
47456
47567
  })
47457
47568
  }
47458
47569
  };
@@ -54676,6 +54787,12 @@ var require_dist_BhE8zNfY = __commonJS({
54676
54787
  addonId: null,
54677
54788
  access: "view"
54678
54789
  },
54790
+ "recording.getPlacement": {
54791
+ capName: "recording",
54792
+ capScope: "system",
54793
+ addonId: null,
54794
+ access: "view"
54795
+ },
54679
54796
  "recording.getPlaybackManifest": {
54680
54797
  capName: "recording",
54681
54798
  capScope: "system",
@@ -54802,6 +54919,12 @@ var require_dist_BhE8zNfY = __commonJS({
54802
54919
  addonId: null,
54803
54920
  access: "create"
54804
54921
  },
54922
+ "recording.setDevicePlacement": {
54923
+ capName: "recording",
54924
+ capScope: "system",
54925
+ addonId: null,
54926
+ access: "create"
54927
+ },
54805
54928
  "recording.startStorageMigrationMove": {
54806
54929
  capName: "recording",
54807
54930
  capScope: "system",
@@ -55246,12 +55369,36 @@ var require_dist_BhE8zNfY = __commonJS({
55246
55369
  addonId: null,
55247
55370
  access: "create"
55248
55371
  },
55372
+ "storageMigration.cleanupCancel": {
55373
+ capName: "storage-migration",
55374
+ capScope: "system",
55375
+ addonId: null,
55376
+ access: "create"
55377
+ },
55378
+ "storageMigration.cleanupStart": {
55379
+ capName: "storage-migration",
55380
+ capScope: "system",
55381
+ addonId: null,
55382
+ access: "create"
55383
+ },
55384
+ "storageMigration.cleanupStatus": {
55385
+ capName: "storage-migration",
55386
+ capScope: "system",
55387
+ addonId: null,
55388
+ access: "view"
55389
+ },
55249
55390
  "storageMigration.drain": {
55250
55391
  capName: "storage-migration",
55251
55392
  capScope: "system",
55252
55393
  addonId: null,
55253
55394
  access: "create"
55254
55395
  },
55396
+ "storageMigration.history": {
55397
+ capName: "storage-migration",
55398
+ capScope: "system",
55399
+ addonId: null,
55400
+ access: "view"
55401
+ },
55255
55402
  "storageMigration.movers": {
55256
55403
  capName: "storage-migration",
55257
55404
  capScope: "system",
@@ -57738,6 +57885,11 @@ var require_dist_BhE8zNfY = __commonJS({
57738
57885
  form: "single",
57739
57886
  optional: false
57740
57887
  }],
57888
+ "recording.setDevicePlacement": [{
57889
+ name: "deviceId",
57890
+ form: "single",
57891
+ optional: false
57892
+ }],
57741
57893
  "recording.startStorageMigrationMove": [{
57742
57894
  name: "deviceId",
57743
57895
  form: "single",
@@ -58978,6 +59130,12 @@ var require_dist_BhE8zNfY = __commonJS({
58978
59130
  return ScopedTokenSchema;
58979
59131
  }
58980
59132
  });
59133
+ Object.defineProperty(exports, "StorageCleanupJobSchema", {
59134
+ enumerable: true,
59135
+ get: function() {
59136
+ return StorageCleanupJobSchema;
59137
+ }
59138
+ });
58981
59139
  Object.defineProperty(exports, "StorageLocationTypeSchema", {
58982
59140
  enumerable: true,
58983
59141
  get: function() {
@@ -59482,7 +59640,7 @@ var require_alerts_addon = __commonJS({
59482
59640
  [Symbol.toStringTag]: { value: "Module" }
59483
59641
  });
59484
59642
  require_chunk_Cek0wNdY();
59485
- var require_dist10 = require_dist_BhE8zNfY();
59643
+ var require_dist10 = require_dist_CFTKQGym();
59486
59644
  function selectExpired(alerts, cutoffMs) {
59487
59645
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
59488
59646
  }
@@ -60301,7 +60459,7 @@ var require_console_logging = __commonJS({
60301
60459
  [Symbol.toStringTag]: { value: "Module" }
60302
60460
  });
60303
60461
  require_chunk_Cek0wNdY();
60304
- var require_dist10 = require_dist_BhE8zNfY();
60462
+ var require_dist10 = require_dist_CFTKQGym();
60305
60463
  var require_formatter = require_formatter_DqAKDlvN();
60306
60464
  var LEVEL_RANK = {
60307
60465
  debug: 0,
@@ -60395,7 +60553,7 @@ var require_core_blocks_addon = __commonJS({
60395
60553
  "use strict";
60396
60554
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
60397
60555
  var require_chunk = require_chunk_Cek0wNdY();
60398
- var require_dist10 = require_dist_BhE8zNfY();
60556
+ var require_dist10 = require_dist_CFTKQGym();
60399
60557
  var node_crypto = __require("crypto");
60400
60558
  var node_fs_promises = __require("fs/promises");
60401
60559
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -61292,11 +61450,11 @@ var require_core_blocks = __commonJS({
61292
61450
  }
61293
61451
  });
61294
61452
 
61295
- // ../system/dist/retired-settings-keys-CqDbI-vK.js
61296
- var require_retired_settings_keys_CqDbI_vK = __commonJS({
61297
- "../system/dist/retired-settings-keys-CqDbI-vK.js"(exports) {
61453
+ // ../system/dist/retired-settings-keys-dehyu2N3.js
61454
+ var require_retired_settings_keys_dehyu2N3 = __commonJS({
61455
+ "../system/dist/retired-settings-keys-dehyu2N3.js"(exports) {
61298
61456
  "use strict";
61299
- var require_dist10 = require_dist_BhE8zNfY();
61457
+ var require_dist10 = require_dist_CFTKQGym();
61300
61458
  function settingsStoreIsAuthoritativeHere(env) {
61301
61459
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
61302
61460
  return raw === "" || raw === "hub";
@@ -63510,8 +63668,8 @@ var require_device_manager_addon = __commonJS({
63510
63668
  [Symbol.toStringTag]: { value: "Module" }
63511
63669
  });
63512
63670
  require_chunk_Cek0wNdY();
63513
- var require_dist10 = require_dist_BhE8zNfY();
63514
- var require_retired_settings_keys = require_retired_settings_keys_CqDbI_vK();
63671
+ var require_dist10 = require_dist_CFTKQGym();
63672
+ var require_retired_settings_keys = require_retired_settings_keys_dehyu2N3();
63515
63673
  var node_crypto = __require("crypto");
63516
63674
  var _camstack_types_node = require_node();
63517
63675
  var JOB_HISTORY = 20;
@@ -68327,7 +68485,7 @@ var require_hub_forwarder = __commonJS({
68327
68485
  [Symbol.toStringTag]: { value: "Module" }
68328
68486
  });
68329
68487
  require_chunk_Cek0wNdY();
68330
- var require_dist10 = require_dist_BhE8zNfY();
68488
+ var require_dist10 = require_dist_CFTKQGym();
68331
68489
  var require_formatter = require_formatter_DqAKDlvN();
68332
68490
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
68333
68491
  var HubForwarderDestination = class {
@@ -68464,7 +68622,7 @@ var require_liveness_monitor_addon = __commonJS({
68464
68622
  "use strict";
68465
68623
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
68466
68624
  require_chunk_Cek0wNdY();
68467
- var require_dist10 = require_dist_BhE8zNfY();
68625
+ var require_dist10 = require_dist_CFTKQGym();
68468
68626
  var NO_DEVICES = "liveness:no-devices";
68469
68627
  var ALL_OFFLINE = "liveness:all-devices-offline";
68470
68628
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -68654,7 +68812,7 @@ var require_local_auth_addon = __commonJS({
68654
68812
  [Symbol.toStringTag]: { value: "Module" }
68655
68813
  });
68656
68814
  var require_chunk = require_chunk_Cek0wNdY();
68657
- var require_dist10 = require_dist_BhE8zNfY();
68815
+ var require_dist10 = require_dist_CFTKQGym();
68658
68816
  var node_crypto = __require("crypto");
68659
68817
  node_crypto = require_chunk.__toESM(node_crypto);
68660
68818
  var crypto$1 = __require("crypto");
@@ -76467,7 +76625,7 @@ var require_loki_logging = __commonJS({
76467
76625
  [Symbol.toStringTag]: { value: "Module" }
76468
76626
  });
76469
76627
  require_chunk_Cek0wNdY();
76470
- var require_dist10 = require_dist_BhE8zNfY();
76628
+ var require_dist10 = require_dist_CFTKQGym();
76471
76629
  function sanitizeLabelName(raw) {
76472
76630
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
76473
76631
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -77032,7 +77190,7 @@ var require_native_metrics_addon = __commonJS({
77032
77190
  [Symbol.toStringTag]: { value: "Module" }
77033
77191
  });
77034
77192
  var require_chunk = require_chunk_Cek0wNdY();
77035
- var require_dist10 = require_dist_BhE8zNfY();
77193
+ var require_dist10 = require_dist_CFTKQGym();
77036
77194
  var node_fs_promises = __require("fs/promises");
77037
77195
  var node_child_process = __require("child_process");
77038
77196
  var node_util = __require("util");
@@ -79654,7 +79812,7 @@ var require_filesystem_storage_addon = __commonJS({
79654
79812
  [Symbol.toStringTag]: { value: "Module" }
79655
79813
  });
79656
79814
  var require_chunk = require_chunk_Cek0wNdY();
79657
- var require_dist10 = require_dist_BhE8zNfY();
79815
+ var require_dist10 = require_dist_CFTKQGym();
79658
79816
  var node_crypto = __require("crypto");
79659
79817
  var node_fs_promises = __require("fs/promises");
79660
79818
  var node_path = __require("path");
@@ -80770,8 +80928,8 @@ var require_sqlite_settings_addon = __commonJS({
80770
80928
  [Symbol.toStringTag]: { value: "Module" }
80771
80929
  });
80772
80930
  var require_chunk = require_chunk_Cek0wNdY();
80773
- var require_dist10 = require_dist_BhE8zNfY();
80774
- var require_retired_settings_keys = require_retired_settings_keys_CqDbI_vK();
80931
+ var require_dist10 = require_dist_CFTKQGym();
80932
+ var require_retired_settings_keys = require_retired_settings_keys_dehyu2N3();
80775
80933
  var node_crypto = __require("crypto");
80776
80934
  var node_fs = __require("fs");
80777
80935
  var node_module = __require("module");
@@ -83180,7 +83338,8 @@ var require_storage_orchestrator_addon = __commonJS({
83180
83338
  [Symbol.toStringTag]: { value: "Module" }
83181
83339
  });
83182
83340
  var require_chunk = require_chunk_Cek0wNdY();
83183
- var require_dist10 = require_dist_BhE8zNfY();
83341
+ var require_dist10 = require_dist_CFTKQGym();
83342
+ var zod = require_zod();
83184
83343
  var node_crypto = __require("crypto");
83185
83344
  var node_fs_promises = __require("fs/promises");
83186
83345
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -83402,17 +83561,22 @@ var require_storage_orchestrator_addon = __commonJS({
83402
83561
  for (const storageClass of STORAGE_CLASSES) {
83403
83562
  const targetId = input.destinations[storageClass];
83404
83563
  if (targetId === void 0) continue;
83405
- const source = this.deps.locations.getDefaultLocation(storageClass);
83406
- if (!source) throw new Error(`No default storage location for "${storageClass}"`);
83564
+ const namedSourceId = input.sources?.[storageClass];
83565
+ const defaultLoc = this.deps.locations.getDefaultLocation(storageClass);
83566
+ const source = namedSourceId ? this.deps.locations.getLocationById(namedSourceId) : defaultLoc;
83567
+ if (!source) throw new Error(namedSourceId ? `Storage location "${namedSourceId}" not found` : `No default storage location for "${storageClass}"`);
83568
+ if (source.type !== storageClass) throw new Error(`Storage location "${source.id}" is type "${source.type}", expected "${storageClass}"`);
83407
83569
  const target = this.deps.locations.getLocationById(targetId);
83408
83570
  if (!target) throw new Error(`Storage location "${targetId}" not found`);
83409
83571
  if (target.type !== storageClass) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${storageClass}"`);
83410
- if (source.id === target.id) throw new Error(`Storage location "${targetId}" is already the "${storageClass}" default`);
83572
+ if (source.id === target.id) throw new Error(namedSourceId ? `Source and destination are the same location ("${targetId}")` : `Storage location "${targetId}" is already the "${storageClass}" default`);
83573
+ const freezeSource = defaultLoc !== void 0 && source.id !== defaultLoc.id;
83411
83574
  if (!MOVER_CLASSES.includes(storageClass)) throw new Error(`No mover owns "${storageClass}" \u2014 the migration can repoint its default but cannot move its bytes. Move it by hand, then repoint the default with upsertLocation.`);
83412
83575
  moves.push({
83413
83576
  storageClass,
83414
83577
  fromLocationId: source.id,
83415
83578
  toLocationId: target.id,
83579
+ freezeSource,
83416
83580
  moverJobId: null,
83417
83581
  state: null,
83418
83582
  error: null,
@@ -83443,11 +83607,13 @@ var require_storage_orchestrator_addon = __commonJS({
83443
83607
  }
83444
83608
  return {
83445
83609
  destinations: input.destinations,
83610
+ sources: input.sources,
83446
83611
  mode,
83447
- moves: moves.map(({ storageClass, fromLocationId, toLocationId }) => ({
83612
+ moves: moves.map(({ storageClass, fromLocationId, toLocationId, freezeSource }) => ({
83448
83613
  storageClass,
83449
83614
  fromLocationId,
83450
- toLocationId
83615
+ toLocationId,
83616
+ freezeSource
83451
83617
  })),
83452
83618
  findings
83453
83619
  };
@@ -83477,12 +83643,12 @@ var require_storage_orchestrator_addon = __commonJS({
83477
83643
  this.startReserved = true;
83478
83644
  try {
83479
83645
  const existing = await this.status();
83480
- if (existing && isTerminal(existing) && existing.pausedParticipants.length > 0) {
83646
+ if (existing && isTerminal$1(existing) && existing.pausedParticipants.length > 0) {
83481
83647
  await this.releaseAfterTerminal(existing);
83482
83648
  await this.persist(existing);
83483
83649
  if (existing.pausedParticipants.length > 0) throw new Error(`storage migration ${existing.jobId} still holds maintenance leases`);
83484
83650
  }
83485
- if (existing && !isTerminal(existing)) throw new Error(`storage migration is already active (${existing.jobId})`);
83651
+ if (existing && !isTerminal$1(existing)) throw new Error(`storage migration is already active (${existing.jobId})`);
83486
83652
  const plan = await this.plan(input);
83487
83653
  const now = this.deps.now();
83488
83654
  const job = {
@@ -83490,6 +83656,7 @@ var require_storage_orchestrator_addon = __commonJS({
83490
83656
  phase: "planning",
83491
83657
  mode: plan.mode,
83492
83658
  destinations: input.destinations,
83659
+ sources: input.sources,
83493
83660
  throttleMbps: input.throttleMbps ?? 40,
83494
83661
  moves: plan.moves.map((move) => ({
83495
83662
  ...move,
@@ -83521,10 +83688,15 @@ var require_storage_orchestrator_addon = __commonJS({
83521
83688
  if (jobId !== void 0 && job?.jobId !== jobId) return null;
83522
83689
  return job;
83523
83690
  }
83691
+ /** Finished jobs, newest first, excluding the one `status` currently shows. */
83692
+ async history() {
83693
+ const current = await this.status();
83694
+ return (await this.deps.history?.get() ?? []).filter((job) => job.jobId !== current?.jobId);
83695
+ }
83524
83696
  async cancel(jobId) {
83525
83697
  const job = await this.status(jobId);
83526
83698
  const cutoverInFlight = job !== null && job.repointed && (job.phase === "refreshing" || job.phase === "resuming");
83527
- if (!job || isTerminal(job) || cutoverInFlight) return false;
83699
+ if (!job || isTerminal$1(job) || cutoverInFlight) return false;
83528
83700
  job.cancelRequested = true;
83529
83701
  await this.persist(job);
83530
83702
  await Promise.all(job.moves.filter((move) => move.moverJobId !== null).map((move) => this.cancelMove(move)));
@@ -83581,18 +83753,22 @@ var require_storage_orchestrator_addon = __commonJS({
83581
83753
  const target = this.deps.locations.getDefaultLocation(storageClass);
83582
83754
  if (!target) continue;
83583
83755
  if (laneOf(storageClass) === "media") {
83584
- const count = await unanswerable(this.deps.participants.analytics.residue({
83585
- toLocationId: target.id,
83586
- mode: "move"
83587
- }));
83588
- if (count !== null && count.rows === 0) continue;
83589
- out.push({
83590
- storageClass,
83591
- fromLocationId: "*",
83592
- toLocationId: target.id,
83593
- items: count?.rows ?? null,
83594
- bytes: null
83595
- });
83756
+ for (const source of this.deps.locations.listLocations({ type: storageClass })) {
83757
+ if (source.id === target.id) continue;
83758
+ const count = await unanswerable(this.deps.participants.analytics.residue({
83759
+ toLocationId: target.id,
83760
+ fromLocationId: source.id,
83761
+ mode: "move"
83762
+ }));
83763
+ if (count !== null && count.rows === 0) continue;
83764
+ out.push({
83765
+ storageClass,
83766
+ fromLocationId: source.id,
83767
+ toLocationId: target.id,
83768
+ items: count?.rows ?? null,
83769
+ bytes: null
83770
+ });
83771
+ }
83596
83772
  continue;
83597
83773
  }
83598
83774
  for (const source of this.deps.locations.listLocations({ type: storageClass })) {
@@ -83646,12 +83822,12 @@ var require_storage_orchestrator_addon = __commonJS({
83646
83822
  this.startReserved = true;
83647
83823
  try {
83648
83824
  const existing = await this.status();
83649
- if (existing && isTerminal(existing) && existing.pausedParticipants.length > 0) {
83825
+ if (existing && isTerminal$1(existing) && existing.pausedParticipants.length > 0) {
83650
83826
  await this.releaseAfterTerminal(existing);
83651
83827
  await this.persist(existing);
83652
83828
  if (existing.pausedParticipants.length > 0) throw new Error(`storage migration ${existing.jobId} still holds maintenance leases`);
83653
83829
  }
83654
- if (existing && !isTerminal(existing)) throw new Error(`storage migration is already active (${existing.jobId})`);
83830
+ if (existing && !isTerminal$1(existing)) throw new Error(`storage migration is already active (${existing.jobId})`);
83655
83831
  for (const storageClass of input.classes) {
83656
83832
  if (MOVER_CLASSES.includes(storageClass)) continue;
83657
83833
  throw new Error(`No mover owns "${storageClass}" \u2014 there is nothing that can drain it. Move it by hand.`);
@@ -83716,7 +83892,7 @@ var require_storage_orchestrator_addon = __commonJS({
83716
83892
  await this.persist(job);
83717
83893
  return;
83718
83894
  }
83719
- if (isTerminal(job)) {
83895
+ if (isTerminal$1(job)) {
83720
83896
  if (job.pausedParticipants.length > 0) {
83721
83897
  await this.releaseAfterTerminal(job);
83722
83898
  await this.persist(job);
@@ -83777,11 +83953,12 @@ var require_storage_orchestrator_addon = __commonJS({
83777
83953
  if (job.phase === "verifying" && !nonBlocking) {
83778
83954
  await this.verifyMoves(job);
83779
83955
  if (job.cancelRequested) return this.finishCancelled(job);
83956
+ await this.freezeDrainedSources(job);
83780
83957
  await this.setPhase(job, "repointing");
83781
83958
  }
83782
83959
  if (job.phase === "repointing") {
83783
- const targets = new Map(job.moves.map((move) => [move.storageClass, move.toLocationId]));
83784
- await this.deps.locations.setDefaultLocations(targets);
83960
+ const targets = new Map(job.moves.filter((move) => move.freezeSource !== true).map((move) => [move.storageClass, move.toLocationId]));
83961
+ if (targets.size > 0) await this.deps.locations.setDefaultLocations(targets);
83785
83962
  job.repointed = true;
83786
83963
  await this.setPhase(job, "refreshing");
83787
83964
  }
@@ -83804,6 +83981,7 @@ var require_storage_orchestrator_addon = __commonJS({
83804
83981
  if (job.phase === "verifying" && nonBlocking) {
83805
83982
  await this.verifyMoves(job);
83806
83983
  if (job.cancelRequested) return this.finishCancelled(job);
83984
+ await this.freezeDrainedSources(job);
83807
83985
  await this.setPhase(job, "done");
83808
83986
  }
83809
83987
  } catch (err) {
@@ -83819,7 +83997,7 @@ var require_storage_orchestrator_addon = __commonJS({
83819
83997
  await this.releaseAfterTerminal(job);
83820
83998
  await this.persist(job);
83821
83999
  } finally {
83822
- if (isTerminal(job)) {
84000
+ if (isTerminal$1(job)) {
83823
84001
  await this.releaseAfterTerminal(job);
83824
84002
  this.active = job;
83825
84003
  }
@@ -83980,6 +84158,24 @@ var require_storage_orchestrator_addon = __commonJS({
83980
84158
  for (const move of job.moves) if (move.state !== "done") throw new Error(`${move.storageClass} move did not complete verification`);
83981
84159
  }
83982
84160
  /**
84161
+ * After a from→to copy whose source was NOT the class default: stop new
84162
+ * writes to `from`. The default is unchanged (that is the whole point of
84163
+ * naming a non-default source). A location that is already disabled is
84164
+ * left alone.
84165
+ */
84166
+ freezeDrainedSources(job) {
84167
+ for (const move of job.moves) {
84168
+ if (move.freezeSource !== true) continue;
84169
+ const loc = this.deps.locations.getLocationById(move.fromLocationId);
84170
+ if (!loc || loc.enabled === false) continue;
84171
+ const { createdAt: _c, updatedAt: _u, capacity: _cap, ...rest } = loc;
84172
+ this.deps.locations.upsertLocation({
84173
+ ...rest,
84174
+ enabled: false
84175
+ });
84176
+ }
84177
+ }
84178
+ /**
83983
84179
  * Arm one class's mover.
83984
84180
  *
83985
84181
  * `draining` runs after every writer has been resumed, so there is no lease
@@ -83992,6 +84188,7 @@ var require_storage_orchestrator_addon = __commonJS({
83992
84188
  if (laneOf(move.storageClass) === "media") {
83993
84189
  const input2 = {
83994
84190
  toLocationId: move.toLocationId,
84191
+ fromLocationId: move.fromLocationId,
83995
84192
  throttleMbps: job.throttleMbps,
83996
84193
  mode: "move"
83997
84194
  };
@@ -84025,7 +84222,7 @@ var require_storage_orchestrator_addon = __commonJS({
84025
84222
  }
84026
84223
  async setPhase(job, phase) {
84027
84224
  job.phase = phase;
84028
- if (isTerminal(job)) job.finishedAt = this.deps.now();
84225
+ if (isTerminal$1(job)) job.finishedAt = this.deps.now();
84029
84226
  await this.persist(job);
84030
84227
  }
84031
84228
  async finishCancelled(job) {
@@ -84037,6 +84234,9 @@ var require_storage_orchestrator_addon = __commonJS({
84037
84234
  async persist(job) {
84038
84235
  job.updatedAt = this.deps.now();
84039
84236
  await this.deps.state.set(job);
84237
+ if (!isTerminal$1(job) || this.deps.history === void 0) return;
84238
+ const prev = [...await this.deps.history.get() ?? []];
84239
+ await this.deps.history.set([job, ...prev.filter((row) => row.jobId !== job.jobId)].slice(0, HISTORY_CAP));
84040
84240
  }
84041
84241
  };
84042
84242
  async function unanswerable(read) {
@@ -84050,9 +84250,10 @@ var require_storage_orchestrator_addon = __commonJS({
84050
84250
  if (job.pauseLeaseId === null) throw new Error("storage migration has no maintenance lease");
84051
84251
  return job.pauseLeaseId;
84052
84252
  }
84053
- function isTerminal(job) {
84253
+ function isTerminal$1(job) {
84054
84254
  return job.phase === "done" || job.phase === "failed" || job.phase === "cancelled";
84055
84255
  }
84256
+ var HISTORY_CAP = 20;
84056
84257
  function laneOf(storageClass) {
84057
84258
  return storageClass === "eventMedia" ? "media" : "footage";
84058
84259
  }
@@ -84105,6 +84306,46 @@ var require_storage_orchestrator_addon = __commonJS({
84105
84306
  function canProbeOccupancyLocally(locality) {
84106
84307
  return locality === true;
84107
84308
  }
84309
+ var STORAGE_CLASS_SUBTREES = {
84310
+ eventMedia: ["events"],
84311
+ recordingsLow: ["low"],
84312
+ recordings: ["high", "mid"]
84313
+ };
84314
+ function isDotEntry(name) {
84315
+ return name.startsWith(".");
84316
+ }
84317
+ async function dirHasAnyEntry(dir) {
84318
+ let handle;
84319
+ try {
84320
+ handle = await (0, node_fs_promises.opendir)(dir);
84321
+ } catch (err) {
84322
+ if (err.code === "ENOENT") return false;
84323
+ throw err;
84324
+ }
84325
+ try {
84326
+ return await handle.read() !== null;
84327
+ } finally {
84328
+ await handle.close();
84329
+ }
84330
+ }
84331
+ async function probeLocalDirectoryOccupancy(input) {
84332
+ let names;
84333
+ try {
84334
+ names = await (0, node_fs_promises.readdir)(input.basePath);
84335
+ } catch (err) {
84336
+ if (err.code === "ENOENT") return "empty";
84337
+ return "unknown";
84338
+ }
84339
+ const visible = names.filter((name) => !isDotEntry(name));
84340
+ const subtrees = STORAGE_CLASS_SUBTREES[input.type];
84341
+ if (subtrees === void 0) return visible.length > 0 ? "occupied" : "empty";
84342
+ try {
84343
+ for (const camera of visible) for (const subtree of subtrees) if (await dirHasAnyEntry(node_path.default.join(input.basePath, camera, subtree))) return "occupied";
84344
+ } catch {
84345
+ return "unknown";
84346
+ }
84347
+ return "empty";
84348
+ }
84108
84349
  function resolveEngine(getEngines) {
84109
84350
  const engines = getEngines();
84110
84351
  if (engines.length === 0) throw new Error("settings-store: no data-store-provider engine is registered \u2014 the data door has nothing behind it");
@@ -84194,6 +84435,164 @@ var require_storage_orchestrator_addon = __commonJS({
84194
84435
  }
84195
84436
  return out;
84196
84437
  }
84438
+ var FOOTAGE_TYPES = /* @__PURE__ */ new Set(["recordings", "recordingsLow"]);
84439
+ function isTerminal(job) {
84440
+ return job.phase === "done" || job.phase === "failed" || job.phase === "cancelled";
84441
+ }
84442
+ function footageFrozen(location) {
84443
+ return location.enabled === false || location.config["readOnly"] === true;
84444
+ }
84445
+ var StorageCleanupCoordinator = class {
84446
+ deps;
84447
+ active = null;
84448
+ startReserved = false;
84449
+ constructor(deps) {
84450
+ this.deps = deps;
84451
+ }
84452
+ async start(input) {
84453
+ if (this.startReserved) throw new Error("storage cleanup is already active");
84454
+ this.startReserved = true;
84455
+ try {
84456
+ const existing = await this.status();
84457
+ if (existing && !isTerminal(existing)) throw new Error(`storage cleanup is already active (${existing.jobId})`);
84458
+ const now = this.deps.now();
84459
+ const job = {
84460
+ jobId: this.deps.newId(),
84461
+ phase: "orphans",
84462
+ includeDebugMedia: input.includeDebugMedia === true,
84463
+ orphansReclaimed: 0,
84464
+ orphanBytesReclaimed: 0,
84465
+ debugMediaReclaimed: 0,
84466
+ debugMediaBytesReclaimed: 0,
84467
+ ghostsForgotten: 0,
84468
+ ghostBytesForgotten: 0,
84469
+ detail: "Starting orphan reclaim",
84470
+ cancelRequested: false,
84471
+ startedAt: now,
84472
+ updatedAt: now,
84473
+ finishedAt: null,
84474
+ error: null
84475
+ };
84476
+ await this.persist(job);
84477
+ this.active = job;
84478
+ this.run(job);
84479
+ return job.jobId;
84480
+ } finally {
84481
+ this.startReserved = false;
84482
+ }
84483
+ }
84484
+ async status(jobId) {
84485
+ const job = this.active ?? await this.deps.state.get();
84486
+ if (jobId !== void 0 && job?.jobId !== jobId) return null;
84487
+ return job;
84488
+ }
84489
+ async cancel(jobId) {
84490
+ const job = await this.status(jobId);
84491
+ if (!job || isTerminal(job)) return false;
84492
+ job.cancelRequested = true;
84493
+ job.detail = "Cancel requested \u2014 finishing the current step";
84494
+ await this.persist(job);
84495
+ return true;
84496
+ }
84497
+ async run(job) {
84498
+ try {
84499
+ await this.reclaimOrphans(job);
84500
+ if (await this.stopped(job)) return;
84501
+ if (job.includeDebugMedia) {
84502
+ await this.setPhase(job, "debug-media", "Reclaiming debug media");
84503
+ await this.reclaimDebugMedia(job);
84504
+ if (await this.stopped(job)) return;
84505
+ }
84506
+ await this.setPhase(job, "ghost-ledger", "Forgetting ghost footage rows");
84507
+ await this.forgetGhosts(job);
84508
+ if (await this.stopped(job)) return;
84509
+ await this.finish(job, "done", null);
84510
+ } catch (err) {
84511
+ const message = err instanceof Error ? err.message : String(err);
84512
+ await this.finish(job, "failed", message);
84513
+ }
84514
+ }
84515
+ async reclaimOrphans(job) {
84516
+ let restart = true;
84517
+ for (; ; ) {
84518
+ if (await this.stopped(job)) return;
84519
+ const start = await this.deps.participants.startOrphan(restart);
84520
+ restart = false;
84521
+ if (!start.started && !start.alreadyRunning) throw new Error("orphan reclaim did not start");
84522
+ const status = await this.waitUntilIdle(() => this.deps.participants.orphanStatus(), job, (s) => `Orphan reclaim: ${s.totalReclaimed} rows / ${String(s.totalBytesReclaimed)} bytes`);
84523
+ job.orphansReclaimed += status.totalReclaimed;
84524
+ job.orphanBytesReclaimed += status.totalBytesReclaimed;
84525
+ job.detail = `Orphans: ${job.orphansReclaimed} rows, ${String(job.orphanBytesReclaimed)} bytes`;
84526
+ await this.persist(job);
84527
+ if (status.error) throw new Error(status.error);
84528
+ if (status.complete === true) return;
84529
+ }
84530
+ }
84531
+ async reclaimDebugMedia(job) {
84532
+ const start = await this.deps.participants.startDebugMedia();
84533
+ if (!start.started && !start.alreadyRunning) throw new Error("debug-media reclaim did not start");
84534
+ const status = await this.waitUntilIdle(() => this.deps.participants.debugMediaStatus(), job, (s) => `Debug media: ${s.totalReclaimed} files`);
84535
+ job.debugMediaReclaimed = status.totalReclaimed;
84536
+ job.debugMediaBytesReclaimed = status.totalBytesReclaimed;
84537
+ await this.persist(job);
84538
+ if (status.error) throw new Error(status.error);
84539
+ }
84540
+ async forgetGhosts(job) {
84541
+ const frozen = this.deps.locations.listLocations().filter((l) => FOOTAGE_TYPES.has(l.type) && footageFrozen(l));
84542
+ if (frozen.length === 0) {
84543
+ job.detail = "No frozen footage locations";
84544
+ await this.persist(job);
84545
+ return;
84546
+ }
84547
+ for (const loc of frozen) {
84548
+ if (await this.stopped(job)) return;
84549
+ job.detail = `Ghost ledger: ${loc.id}`;
84550
+ await this.persist(job);
84551
+ const report = await this.deps.participants.walkLedger({
84552
+ locationId: loc.id,
84553
+ apply: true
84554
+ });
84555
+ job.ghostsForgotten += report.forgottenSegments;
84556
+ job.ghostBytesForgotten += report.forgottenBytes;
84557
+ await this.persist(job);
84558
+ }
84559
+ }
84560
+ async waitUntilIdle(read, job, detail) {
84561
+ const pollMs = this.deps.pollMs ?? 2e3;
84562
+ const sleep = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
84563
+ for (; ; ) {
84564
+ const status = await read();
84565
+ job.detail = detail(status);
84566
+ await this.persist(job);
84567
+ if (!status.running) return status;
84568
+ await sleep(pollMs);
84569
+ }
84570
+ }
84571
+ async stopped(job) {
84572
+ if ((await this.status(job.jobId))?.cancelRequested === true) {
84573
+ await this.finish(job, "cancelled", null);
84574
+ return true;
84575
+ }
84576
+ return false;
84577
+ }
84578
+ async setPhase(job, phase, detail) {
84579
+ job.phase = phase;
84580
+ job.detail = detail;
84581
+ await this.persist(job);
84582
+ }
84583
+ async finish(job, phase, error) {
84584
+ job.phase = phase;
84585
+ job.error = error;
84586
+ job.finishedAt = this.deps.now();
84587
+ job.detail = phase === "done" ? "Cleanup finished" : job.detail;
84588
+ await this.persist(job);
84589
+ }
84590
+ async persist(job) {
84591
+ job.updatedAt = this.deps.now();
84592
+ this.active = job;
84593
+ await this.deps.state.set(job);
84594
+ }
84595
+ };
84197
84596
  var SESSION_VOCABULARY = {
84198
84597
  upload: {
84199
84598
  idLabel: "uploadId",
@@ -84281,7 +84680,7 @@ var require_storage_orchestrator_addon = __commonJS({
84281
84680
  * Reconciles both directions so operator edits survive a reboot:
84282
84681
  * 1. hydrate — DB rows win over any early in-memory seed (restores
84283
84682
  * operator config like `minFreePercent` that the pre-store boot
84284
- * can't see), with the same `isSystem` upgrade as {@link initialize};
84683
+ * can't see);
84285
84684
  * 2. backfill — in-memory locations the DB doesn't have yet (the
84286
84685
  * pre-store seed defaults on a fresh install) are persisted, so the
84287
84686
  * store becomes the durable source of truth from here on.
@@ -84303,15 +84702,7 @@ var require_storage_orchestrator_addon = __commonJS({
84303
84702
  const rows = await store.loadAll();
84304
84703
  this.locationStore = store;
84305
84704
  const dbIds = new Set(rows.map((r) => r.id));
84306
- for (const loc of rows) {
84307
- const upgraded = !loc.isSystem && loc.id === `${loc.type}:default` ? {
84308
- ...loc,
84309
- isSystem: true
84310
- } : loc;
84311
- this.locations.set(upgraded.id, upgraded);
84312
- if (upgraded !== loc) store.upsert(upgraded).catch(() => {
84313
- });
84314
- }
84705
+ for (const loc of rows) this.locations.set(loc.id, loc);
84315
84706
  const backfill = [...this.locations.values()].filter((l) => !dbIds.has(l.id));
84316
84707
  for (const loc of backfill) store.upsert(loc).catch((err) => {
84317
84708
  this.logger.warn("storage-orchestrator: attachStore backfill persist failed", { meta: {
@@ -84403,25 +84794,8 @@ var require_storage_orchestrator_addon = __commonJS({
84403
84794
  async initialize() {
84404
84795
  if (!this.locationStore) return;
84405
84796
  const rows = await this.locationStore.loadAll();
84406
- let upgraded = 0;
84407
- for (const loc of rows) if (!loc.isSystem && loc.id === `${loc.type}:default`) {
84408
- const upgradedLoc = {
84409
- ...loc,
84410
- isSystem: true
84411
- };
84412
- this.locations.set(upgradedLoc.id, upgradedLoc);
84413
- this.locationStore.upsert(upgradedLoc).catch((err) => {
84414
- this.logger.warn("storage-orchestrator: isSystem upgrade persist failed", { meta: {
84415
- id: upgradedLoc.id,
84416
- error: err instanceof Error ? err.message : String(err)
84417
- } });
84418
- });
84419
- upgraded++;
84420
- } else this.locations.set(loc.id, loc);
84421
- this.logger.info("storage-orchestrator: hydrated locations from store", { meta: {
84422
- loaded: this.locations.size,
84423
- isSystemUpgraded: upgraded
84424
- } });
84797
+ for (const loc of rows) this.locations.set(loc.id, loc);
84798
+ this.logger.info("storage-orchestrator: hydrated locations from store", { meta: { loaded: this.locations.size } });
84425
84799
  this.reportDisabledDefaults();
84426
84800
  }
84427
84801
  /**
@@ -84607,10 +84981,6 @@ var require_storage_orchestrator_addon = __commonJS({
84607
84981
  ...input,
84608
84982
  nodeId: "hub"
84609
84983
  };
84610
- if (existing?.isSystem === true) input = {
84611
- ...input,
84612
- isSystem: true
84613
- };
84614
84984
  input = {
84615
84985
  ...input,
84616
84986
  ...resolveEnabled(input, existing, this.hasAnyLocationOfType(input.type))
@@ -84657,21 +85027,22 @@ var require_storage_orchestrator_addon = __commonJS({
84657
85027
  return next;
84658
85028
  }
84659
85029
  /**
84660
- * Remove a location. Refuses to remove the default of a type unless a
84661
- * sibling default exists (defensive `upsertLocation` already ensures
84662
- * at most one default per type, but the logic guards against future
84663
- * bypass paths e.g. SQLite migration that imports two defaults).
84664
- *
84665
- * Persistence (Task 6) mirrors the delete asynchronously, with errors
84666
- * routed to the logger — see `upsertLocation` for the rationale.
85030
+ * Remove a location. Refuses the last location of a type and the last
85031
+ * enabled location of a type. `isSystem` is informational — a frozen
85032
+ * recordings `:default` is deletable once a sibling is live. Deleting
85033
+ * the flagged default promotes an enabled sibling. Occupancy still
85034
+ * refuses unless `force` is set; force never overrides uniqueness /
85035
+ * last-enabled.
84667
85036
  */
84668
85037
  async deleteLocation(id, options) {
84669
85038
  const loc = this.locations.get(id);
84670
85039
  if (!loc) throw new Error(`Storage location "${id}" not found`);
84671
- if (loc.isSystem) throw new Error(`Storage location "${id}" is system-managed and cannot be deleted. Edit its config (path / providerId) instead.`);
84672
- if (loc.isDefault) {
84673
- if (![...this.locations.values()].find((l) => l.type === loc.type && l.id !== id && l.isDefault)) throw new Error(`Cannot delete default location "${id}" for type "${loc.type}" \u2014 promote another location to default first`);
84674
- }
85040
+ const ofType = [...this.locations.values()].filter((l) => l.type === loc.type);
85041
+ if (ofType.length <= 1) throw new Error(`Cannot delete "${id}" \u2014 it is the only location for type "${loc.type}"`);
85042
+ const enabledOfType = ofType.filter((l) => l.enabled !== false);
85043
+ if (loc.enabled !== false && enabledOfType.length <= 1) throw new Error(`Cannot delete "${id}" \u2014 it is the only enabled location for type "${loc.type}"`);
85044
+ const successor = loc.isDefault === true ? ofType.find((l) => l.id !== id && l.enabled !== false) : void 0;
85045
+ if (loc.isDefault === true && successor === void 0) throw new Error(`Cannot delete "${id}" \u2014 it is the only enabled location for type "${loc.type}"`);
84675
85046
  const occupancy = await this.probeOccupancy(loc);
84676
85047
  if (occupancy !== "empty") {
84677
85048
  if (options?.force !== true) throw new Error(occupancy === "occupied" ? `Storage location "${id}" still holds data \u2014 drain it first (storage migration / relocate), or pass force to delete the record anyway and strand what is on it` : `Storage location "${id}" could not be checked for remaining data (unreachable provider, another node, or an unmounted root) \u2014 pass force to delete the record anyway`);
@@ -84681,6 +85052,21 @@ var require_storage_orchestrator_addon = __commonJS({
84681
85052
  occupancy
84682
85053
  } });
84683
85054
  }
85055
+ if (successor !== void 0 && successor.isDefault !== true) {
85056
+ const now = Date.now();
85057
+ const promoted = {
85058
+ ...successor,
85059
+ isDefault: true,
85060
+ updatedAt: now
85061
+ };
85062
+ this.locations.set(successor.id, promoted);
85063
+ if (this.locationStore) this.locationStore.upsert(promoted).catch((err) => {
85064
+ this.logger.error("storage-orchestrator: default promotion persist failed", { meta: {
85065
+ id: successor.id,
85066
+ error: err instanceof Error ? err.message : String(err)
85067
+ } });
85068
+ });
85069
+ }
84684
85070
  this.locations.delete(id);
84685
85071
  if (this.locationStore) this.locationStore.delete(id).catch((err) => {
84686
85072
  this.logger.error("storage-orchestrator: delete persistence failed", { meta: {
@@ -84707,15 +85093,14 @@ var require_storage_orchestrator_addon = __commonJS({
84707
85093
  }
84708
85094
  }
84709
85095
  /**
84710
- * Remove system-seeded locations whose type is no longer declared by any
84711
- * addon (stale defaults from a removed location type). Operator-added
84712
- * (non-system) locations of an undeclared type are KEPT but warned — the
84713
- * operator owns them. Requires the registry to be set first.
85096
+ * Remove seed-shaped locations (`<type>:default`) whose type is no longer
85097
+ * declared by any addon (stale defaults from a removed location type).
85098
+ * Operator-added locations of an undeclared type are KEPT but warned.
84714
85099
  *
84715
85100
  * FAIL-SAFE: if the registry is EMPTY (no addon declared any location), we
84716
85101
  * refuse to prune anything. An empty registry almost always means
84717
85102
  * declarations failed to load (boot ordering, a stale install) — pruning
84718
- * "everything undeclared" in that state would wipe every system location
85103
+ * "everything undeclared" in that state would wipe every seeded location
84719
85104
  * (data/logs/recordings/…). Better to keep stale rows than destroy live ones.
84720
85105
  */
84721
85106
  pruneUndeclaredSystemLocations() {
@@ -84726,7 +85111,7 @@ var require_storage_orchestrator_addon = __commonJS({
84726
85111
  }
84727
85112
  for (const [id, loc] of Array.from(this.locations)) {
84728
85113
  if (this.registry.cardinalityOf(loc.type) !== null) continue;
84729
- if (loc.isSystem) {
85114
+ if (loc.id === `${loc.type}:default`) {
84730
85115
  this.locations.delete(id);
84731
85116
  if (this.locationStore) this.locationStore.delete(id).catch((err) => {
84732
85117
  this.logger.error("storage-orchestrator: prune persistence failed", { meta: {
@@ -84734,7 +85119,7 @@ var require_storage_orchestrator_addon = __commonJS({
84734
85119
  error: err instanceof Error ? err.message : String(err)
84735
85120
  } });
84736
85121
  });
84737
- this.logger.info("storage-orchestrator: pruned stale system location", { meta: {
85122
+ this.logger.info("storage-orchestrator: pruned stale seeded location", { meta: {
84738
85123
  id,
84739
85124
  type: loc.type
84740
85125
  } });
@@ -84894,7 +85279,7 @@ var require_storage_orchestrator_addon = __commonJS({
84894
85279
  providerId: input.providerId,
84895
85280
  config: { basePath: base },
84896
85281
  isDefault: true,
84897
- isSystem: true
85282
+ isSystem: false
84898
85283
  });
84899
85284
  added++;
84900
85285
  }
@@ -85076,6 +85461,7 @@ var require_storage_orchestrator_addon = __commonJS({
85076
85461
  pressureTimer = null;
85077
85462
  pressureSweepInFlight = false;
85078
85463
  migration = null;
85464
+ cleanup = null;
85079
85465
  /**
85080
85466
  * Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
85081
85467
  * synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
@@ -85133,7 +85519,7 @@ var require_storage_orchestrator_addon = __commonJS({
85133
85519
  const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
85134
85520
  return Promise.all(rows.map(async (loc) => ({
85135
85521
  ...this.redacted(loc),
85136
- capacity: await this.localCapacityOf(loc)
85522
+ capacity: loc.enabled === false ? null : await this.localCapacityOf(loc)
85137
85523
  })));
85138
85524
  },
85139
85525
  getDefaultLocation: async ({ type }) => {
@@ -85301,6 +85687,7 @@ var require_storage_orchestrator_addon = __commonJS({
85301
85687
  const migration = new StorageMigrationCoordinator({
85302
85688
  locations: service,
85303
85689
  state: this.state("storage-migration", require_dist10.StorageMigrationJobSchema.nullable(), null),
85690
+ history: this.state("storage-migration-history", zod.z.array(require_dist10.StorageMigrationJobSchema), []),
85304
85691
  participants: {
85305
85692
  pipeline: {
85306
85693
  pause: async (leaseId) => {
@@ -85351,6 +85738,36 @@ var require_storage_orchestrator_addon = __commonJS({
85351
85738
  deviceKeyOf: (locationId) => this.locationDeviceKey(locationId)
85352
85739
  });
85353
85740
  this.migration = migration;
85741
+ const cleanup = new StorageCleanupCoordinator({
85742
+ locations: service,
85743
+ state: this.state("storage-cleanup", require_dist10.StorageCleanupJobSchema.nullable(), null),
85744
+ participants: {
85745
+ startOrphan: async (restart) => {
85746
+ return parseStarted(await this.ctx.api.addons.custom.mutate({
85747
+ addonId: "pipeline-analytics",
85748
+ action: "retention.orphanAudit",
85749
+ input: {
85750
+ mode: "reclaim",
85751
+ restart,
85752
+ maxRowsPerScope: 2e4
85753
+ }
85754
+ }));
85755
+ },
85756
+ orphanStatus: async () => {
85757
+ return parseOrphanStatus(await this.ctx.api.addons.custom.mutate({
85758
+ addonId: "pipeline-analytics",
85759
+ action: "retention.orphanAuditStatus",
85760
+ input: {}
85761
+ }));
85762
+ },
85763
+ startDebugMedia: () => this.ctx.api.pipelineAnalytics.reclaimDebugMedia.mutate({ mode: "reclaim" }),
85764
+ debugMediaStatus: () => this.ctx.api.pipelineAnalytics.getMediaReclaimStatus.query({}),
85765
+ walkLedger: (input) => this.ctx.api.recording.reconcileLedgerAgainstDisk.mutate(input)
85766
+ },
85767
+ now: () => Date.now(),
85768
+ newId: () => (0, node_crypto.randomUUID)()
85769
+ });
85770
+ this.cleanup = cleanup;
85354
85771
  const migrationProvider = {
85355
85772
  plan: (input) => migration.plan(input),
85356
85773
  start: async (input) => ({ jobId: await migration.start(input) }),
@@ -85358,7 +85775,11 @@ var require_storage_orchestrator_addon = __commonJS({
85358
85775
  cancel: async ({ jobId }) => ({ cancelled: await migration.cancel(jobId) }),
85359
85776
  movers: () => migration.movers(),
85360
85777
  residue: () => migration.residue(),
85361
- drain: async (input) => ({ jobId: await migration.drain(input) })
85778
+ drain: async (input) => ({ jobId: await migration.drain(input) }),
85779
+ cleanupStart: async (input) => ({ jobId: await cleanup.start(input) }),
85780
+ cleanupStatus: ({ jobId }) => cleanup.status(jobId),
85781
+ cleanupCancel: async ({ jobId }) => ({ cancelled: await cleanup.cancel(jobId) }),
85782
+ history: () => migration.history()
85362
85783
  };
85363
85784
  await this.seedFromDeclarations();
85364
85785
  const eventBus = this.ctx.eventBus;
@@ -85603,11 +86024,14 @@ var require_storage_orchestrator_addon = __commonJS({
85603
86024
  }
85604
86025
  }
85605
86026
  /**
85606
- * Does this location still hold anything? A SHALLOW `readdir` of its
85607
- * `basePath` — one syscall, never a walk. `/recordings` holds one entry per
85608
- * camera, so this is cheap on a volume with millions of segments, and a
85609
- * `find`-style walk of a saturated recordings disk is precisely what must
85610
- * not happen here.
86027
+ * Does this location still hold anything of ITS class?
86028
+ *
86029
+ * Shared roots (`/recordings` hosting high + low + events) are scored by
86030
+ * class subtree (`<camera>/high|mid`, `/low`, `/events`), not by a raw
86031
+ * `readdir` of the root — camera dirs and `.camstack-location` would
86032
+ * otherwise make every class look occupied. The walk stays shallow: one
86033
+ * dirent per camera, then first-entry of the named subtree. A `find` of
86034
+ * segments is precisely what must not happen here.
85611
86035
  *
85612
86036
  * **What it cannot see, stated rather than papered over:**
85613
86037
  *
@@ -85616,9 +86040,10 @@ var require_storage_orchestrator_addon = __commonJS({
85616
86040
  * `unknown`, never `empty`.
85617
86041
  * - A location with no `basePath` — same answer.
85618
86042
  * - The difference between "holds live footage" and "holds one stray
85619
- * lock-file". It measures BYTES ON DISK, not durable rows, so it refuses
85620
- * a location holding orphan files that no index names. That direction is
85621
- * the safe one, and `force` is the escape hatch for it.
86043
+ * lock-file". It measures ON-DISK entries of this class, not durable
86044
+ * rows, so it refuses a location holding orphan files that no index
86045
+ * names. That direction is the safe one, and `force` is the escape
86046
+ * hatch for it.
85622
86047
  *
85623
86048
  * A missing root (`ENOENT`) is `empty`, not `unknown`: an unmounted disk and
85624
86049
  * a deleted directory are indistinguishable here, and if the record's own
@@ -85636,16 +86061,16 @@ var require_storage_orchestrator_addon = __commonJS({
85636
86061
  if ((location.nodeId === void 0 || location.nodeId === "" ? HUB_NODE_ID : location.nodeId) !== (this.service?.getLocalNodeId() ?? HUB_NODE_ID)) return "unknown";
85637
86062
  const basePath = this.locationBasePath(location.id);
85638
86063
  if (basePath === null) return "unknown";
85639
- try {
85640
- return (await node_fs_promises.readdir(basePath)).length > 0 ? "occupied" : "empty";
85641
- } catch (err) {
85642
- if (err instanceof Error && "code" in err && err.code === "ENOENT") return "empty";
85643
- this.ctx.logger.warn("storage-orchestrator: could not read a location root to check it", { meta: {
85644
- id: location.id,
85645
- error: err instanceof Error ? err.message : String(err)
85646
- } });
85647
- return "unknown";
85648
- }
86064
+ const occupancy = await probeLocalDirectoryOccupancy({
86065
+ type: location.type,
86066
+ basePath
86067
+ });
86068
+ if (occupancy === "unknown") this.ctx.logger.warn("storage-orchestrator: could not read a location root to check it", { meta: {
86069
+ id: location.id,
86070
+ basePath,
86071
+ type: location.type
86072
+ } });
86073
+ return occupancy;
85649
86074
  }
85650
86075
  /** Free capacity (%) on a location's volume via `statfs`; 100 (guard inert) when unstattable. */
85651
86076
  async locationFreePercent(locationId) {
@@ -85721,6 +86146,38 @@ var require_storage_orchestrator_addon = __commonJS({
85721
86146
  }
85722
86147
  }
85723
86148
  };
86149
+ function parseStarted(raw) {
86150
+ if (raw !== null && typeof raw === "object") {
86151
+ const o = raw;
86152
+ if (typeof o["started"] === "boolean" && typeof o["alreadyRunning"] === "boolean") return {
86153
+ started: o["started"],
86154
+ alreadyRunning: o["alreadyRunning"]
86155
+ };
86156
+ }
86157
+ return {
86158
+ started: false,
86159
+ alreadyRunning: false
86160
+ };
86161
+ }
86162
+ function parseOrphanStatus(raw) {
86163
+ if (raw !== null && typeof raw === "object") {
86164
+ const o = raw;
86165
+ return {
86166
+ running: o["running"] === true,
86167
+ complete: typeof o["complete"] === "boolean" ? o["complete"] : null,
86168
+ totalReclaimed: typeof o["totalReclaimed"] === "number" ? o["totalReclaimed"] : 0,
86169
+ totalBytesReclaimed: typeof o["totalBytesReclaimed"] === "number" ? o["totalBytesReclaimed"] : 0,
86170
+ error: typeof o["error"] === "string" ? o["error"] : null
86171
+ };
86172
+ }
86173
+ return {
86174
+ running: false,
86175
+ complete: true,
86176
+ totalReclaimed: 0,
86177
+ totalBytesReclaimed: 0,
86178
+ error: "orphan audit status unreadable"
86179
+ };
86180
+ }
85724
86181
  exports.SqliteLocationStore = SqliteLocationStore;
85725
86182
  exports.StorageMigrationCoordinator = StorageMigrationCoordinator;
85726
86183
  exports.StorageOrchestratorAddon = StorageOrchestratorAddon;
@@ -85757,7 +86214,7 @@ var require_system_config_addon = __commonJS({
85757
86214
  [Symbol.toStringTag]: { value: "Module" }
85758
86215
  });
85759
86216
  require_chunk_Cek0wNdY();
85760
- var require_dist10 = require_dist_BhE8zNfY();
86217
+ var require_dist10 = require_dist_CFTKQGym();
85761
86218
  var SECTION_TITLES = {
85762
86219
  server: "Server",
85763
86220
  auth: "Authentication"
@@ -103818,7 +104275,7 @@ var require_winston_logging = __commonJS({
103818
104275
  [Symbol.toStringTag]: { value: "Module" }
103819
104276
  });
103820
104277
  var require_chunk = require_chunk_Cek0wNdY();
103821
- var require_dist10 = require_dist_BhE8zNfY();
104278
+ var require_dist10 = require_dist_CFTKQGym();
103822
104279
  var require_formatter = require_formatter_DqAKDlvN();
103823
104280
  var node_path = __require("path");
103824
104281
  node_path = require_chunk.__toESM(node_path);
@@ -116320,12 +116777,12 @@ var require_dist2 = __commonJS({
116320
116777
  }
116321
116778
  });
116322
116779
 
116323
- // ../system/dist/manifest-system-deps-uflHvHDB.js
116324
- var require_manifest_system_deps_uflHvHDB = __commonJS({
116325
- "../system/dist/manifest-system-deps-uflHvHDB.js"(exports) {
116780
+ // ../system/dist/manifest-system-deps-BIxY1Y5e.js
116781
+ var require_manifest_system_deps_BIxY1Y5e = __commonJS({
116782
+ "../system/dist/manifest-system-deps-BIxY1Y5e.js"(exports) {
116326
116783
  "use strict";
116327
116784
  var require_chunk = require_chunk_Cek0wNdY();
116328
- require_dist_BhE8zNfY();
116785
+ require_dist_CFTKQGym();
116329
116786
  var node_crypto = __require("crypto");
116330
116787
  node_crypto = require_chunk.__toESM(node_crypto);
116331
116788
  var _camstack_types_node = require_node();
@@ -128473,7 +128930,7 @@ var require_dist3 = __commonJS({
128473
128930
  "use strict";
128474
128931
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
128475
128932
  var require_chunk = require_chunk_Cek0wNdY();
128476
- var require_dist10 = require_dist_BhE8zNfY();
128933
+ var require_dist10 = require_dist_CFTKQGym();
128477
128934
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
128478
128935
  require_alerts();
128479
128936
  var require_formatter = require_formatter_DqAKDlvN();
@@ -128499,7 +128956,7 @@ var require_dist3 = __commonJS({
128499
128956
  var require_builtins_winston_logging_index = require_winston_logging();
128500
128957
  var require_file_data_plane = require_file_data_plane_DO8KbxCe();
128501
128958
  var require_tls$1 = require_tls_BxQlomxd();
128502
- var require_manifest_system_deps = require_manifest_system_deps_uflHvHDB();
128959
+ var require_manifest_system_deps = require_manifest_system_deps_BIxY1Y5e();
128503
128960
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
128504
128961
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
128505
128962
  var zod = require_zod();
@@ -210740,6 +211197,13 @@ var require_dist4 = __commonJS({
210740
211197
  ]);
210741
211198
  var RelocateMediaInputSchema = zod.z.object({
210742
211199
  toLocationId: zod.z.string(),
211200
+ /**
211201
+ * Restrict the pass to rows currently on this location. Omitted / `'*'` =
211202
+ * every row that is not already on `toLocationId` (the historical
211203
+ * behaviour). A named source is what a from→to migration needs: without it
211204
+ * "move events off disk 2" also emptied disk 1.
211205
+ */
211206
+ fromLocationId: zod.z.string().optional(),
210743
211207
  throttleMbps: zod.z.number().min(1).max(1e3).optional(),
210744
211208
  /** Omitted = `move`, the pre-existing behaviour. */
210745
211209
  mode: MediaRelocateModeSchema.optional()
@@ -210771,9 +211235,18 @@ var require_dist4 = __commonJS({
210771
211235
  backups: zod.z.string().min(1).optional(),
210772
211236
  galleryMedia: zod.z.string().min(1).optional()
210773
211237
  }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
211238
+ var StorageMigrationSourcesSchema = zod.z.object({
211239
+ recordings: zod.z.string().min(1).optional(),
211240
+ recordingsLow: zod.z.string().min(1).optional(),
211241
+ eventMedia: zod.z.string().min(1).optional(),
211242
+ backups: zod.z.string().min(1).optional(),
211243
+ galleryMedia: zod.z.string().min(1).optional()
211244
+ }).optional();
210774
211245
  var StorageMigrationModeSchema = zod.z.enum(["blocking", "nonBlocking"]);
210775
211246
  var StorageMigrationInputSchema = zod.z.object({
210776
211247
  destinations: StorageMigrationDestinationsSchema,
211248
+ /** Omitted = each class's current default. */
211249
+ sources: StorageMigrationSourcesSchema,
210777
211250
  throttleMbps: zod.z.number().min(1).max(1e3).optional(),
210778
211251
  /** Omitted = `blocking`, which stays the default. */
210779
211252
  mode: StorageMigrationModeSchema.optional()
@@ -210819,6 +211292,13 @@ var require_dist4 = __commonJS({
210819
211292
  storageClass: StorageMigrationClassSchema,
210820
211293
  fromLocationId: zod.z.string(),
210821
211294
  toLocationId: zod.z.string(),
211295
+ /**
211296
+ * True when `from` was NOT the class default at plan time. The move still
211297
+ * copies bytes, but the default is left alone and the source is disabled
211298
+ * once the copy verifies. Absent on jobs planned before this field existed
211299
+ * — those jobs always repointed, which is `false`.
211300
+ */
211301
+ freezeSource: zod.z.boolean().optional(),
210822
211302
  moverJobId: zod.z.string().nullable(),
210823
211303
  state: RelocateJobStateSchema.nullable(),
210824
211304
  error: zod.z.string().nullable(),
@@ -210832,6 +211312,7 @@ var require_dist4 = __commonJS({
210832
211312
  * can tell a seconds-long cutover from a thirty-hour one. */
210833
211313
  mode: StorageMigrationModeSchema,
210834
211314
  destinations: StorageMigrationDestinationsSchema,
211315
+ sources: StorageMigrationSourcesSchema,
210835
211316
  throttleMbps: zod.z.number(),
210836
211317
  moves: zod.z.array(StorageMigrationMoveSchema),
210837
211318
  pauseLeaseId: zod.z.string().nullable(),
@@ -210858,6 +211339,7 @@ var require_dist4 = __commonJS({
210858
211339
  });
210859
211340
  var StorageMigrationPlanSchema = zod.z.object({
210860
211341
  destinations: StorageMigrationDestinationsSchema,
211342
+ sources: StorageMigrationSourcesSchema,
210861
211343
  /** The mode this plan was built for. A plan is only valid for its mode: the
210862
211344
  * `eventMedia` seal gate and the single-cardinality refusal both depend on
210863
211345
  * it. */
@@ -210865,7 +211347,8 @@ var require_dist4 = __commonJS({
210865
211347
  moves: zod.z.array(zod.z.object({
210866
211348
  storageClass: StorageMigrationClassSchema,
210867
211349
  fromLocationId: zod.z.string(),
210868
- toLocationId: zod.z.string()
211350
+ toLocationId: zod.z.string(),
211351
+ freezeSource: zod.z.boolean().optional()
210869
211352
  })),
210870
211353
  findings: zod.z.array(StorageMigrationFindingSchema)
210871
211354
  });
@@ -210977,9 +211460,42 @@ var require_dist4 = __commonJS({
210977
211460
  var RelocatableMediaCountSchema = zod.z.object({ rows: zod.z.number().int().nonnegative() }).nullable();
210978
211461
  var RelocatableMediaCountInputSchema = zod.z.object({
210979
211462
  toLocationId: zod.z.string().min(1),
211463
+ /** Restrict the count to one source; omitted / `'*'` = every non-target row. */
211464
+ fromLocationId: zod.z.string().optional(),
210980
211465
  /** Omitted = `move`. */
210981
211466
  mode: MediaRelocateModeSchema.optional()
210982
211467
  });
211468
+ var StorageCleanupPhaseSchema = zod.z.enum([
211469
+ "orphans",
211470
+ "debug-media",
211471
+ "ghost-ledger",
211472
+ "done",
211473
+ "failed",
211474
+ "cancelled"
211475
+ ]);
211476
+ var StorageCleanupInputSchema = zod.z.object({
211477
+ /** Also walk motion stills / track filmstrips. Off by default. */
211478
+ includeDebugMedia: zod.z.boolean().optional()
211479
+ });
211480
+ var StorageCleanupJobSchema = zod.z.object({
211481
+ jobId: zod.z.string(),
211482
+ phase: StorageCleanupPhaseSchema,
211483
+ includeDebugMedia: zod.z.boolean(),
211484
+ orphansReclaimed: zod.z.number().int().nonnegative(),
211485
+ orphanBytesReclaimed: zod.z.number().int().nonnegative(),
211486
+ debugMediaReclaimed: zod.z.number().int().nonnegative(),
211487
+ debugMediaBytesReclaimed: zod.z.number().int().nonnegative(),
211488
+ ghostsForgotten: zod.z.number().int().nonnegative(),
211489
+ ghostBytesForgotten: zod.z.number().int().nonnegative(),
211490
+ /** Short operator-facing line: current collection, pass, or location. */
211491
+ detail: zod.z.string().nullable(),
211492
+ cancelRequested: zod.z.boolean(),
211493
+ startedAt: zod.z.number(),
211494
+ updatedAt: zod.z.number(),
211495
+ finishedAt: zod.z.number().nullable(),
211496
+ error: zod.z.string().nullable()
211497
+ });
211498
+ var StorageCleanupStatusInputSchema = zod.z.object({ jobId: zod.z.string().optional() });
210983
211499
  var SUB_DETECTION_TYPES = ["face", "plate"];
210984
211500
  var RECOGNITION_TYPES = [
210985
211501
  "face",
@@ -226903,7 +227419,23 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
226903
227419
  drain: require_sleep.method(StorageMigrationDrainInputSchema, zod.z.object({ jobId: zod.z.string() }), {
226904
227420
  kind: "mutation",
226905
227421
  auth: "admin"
226906
- })
227422
+ }),
227423
+ /**
227424
+ * One operator cleanup of leftover analytics (DB + blobs) and ghost
227425
+ * ledger rows on frozen footage locations. Optional debug-media sweep.
227426
+ * Returns immediately; poll {@link cleanupStatus}.
227427
+ */
227428
+ cleanupStart: require_sleep.method(StorageCleanupInputSchema, zod.z.object({ jobId: zod.z.string() }), {
227429
+ kind: "mutation",
227430
+ auth: "admin"
227431
+ }),
227432
+ cleanupStatus: require_sleep.method(StorageCleanupStatusInputSchema, StorageCleanupJobSchema.nullable(), { auth: "admin" }),
227433
+ cleanupCancel: require_sleep.method(zod.z.object({ jobId: zod.z.string() }), zod.z.object({ cancelled: zod.z.boolean() }), {
227434
+ kind: "mutation",
227435
+ auth: "admin"
227436
+ }),
227437
+ /** Recent finished migration jobs, newest first. The live job is `status`. */
227438
+ history: require_sleep.method(zod.z.object({}), zod.z.array(StorageMigrationJobSchema).readonly(), { auth: "admin" })
226907
227439
  }
226908
227440
  };
226909
227441
  var ProviderInfoSchema = zod.z.discriminatedUnion("shouldSaveDiskSpace", [zod.z.object({
@@ -233704,6 +234236,26 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
233704
234236
  /** Ignore piles smaller than this (default 1 GB). */
233705
234237
  minMoveGb: zod.z.number().min(0).optional()
233706
234238
  });
234239
+ var RecordingDevicePlacementSchema = zod.z.object({
234240
+ deviceId: zod.z.number().int(),
234241
+ profile: zod.z.string(),
234242
+ locationId: zod.z.string()
234243
+ });
234244
+ var RecordingDevicePinSchema = zod.z.object({
234245
+ deviceId: zod.z.number().int(),
234246
+ /** Recordings-class location this camera is pinned to. */
234247
+ locationId: zod.z.string()
234248
+ });
234249
+ var RecordingPlacementViewSchema = zod.z.object({
234250
+ assignments: zod.z.array(RecordingDevicePlacementSchema),
234251
+ pins: zod.z.array(RecordingDevicePinSchema),
234252
+ defaultLocations: zod.z.record(zod.z.string(), zod.z.string())
234253
+ });
234254
+ var RecordingSetDevicePlacementInputSchema = zod.z.object({
234255
+ deviceId: zod.z.number().int(),
234256
+ /** `null` = Auto. Otherwise a `recordings:*` location id. */
234257
+ locationId: zod.z.string().nullable()
234258
+ });
233707
234259
  var LocateSegmentResultSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
233708
234260
  kind: zod.z.literal("segment"),
233709
234261
  startMs: zod.z.number(),
@@ -234022,6 +234574,22 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
234022
234574
  startStorageRebalance: require_sleep.method(RecordingRebalanceInputSchema, RecordingRebalancePlanSchema, {
234023
234575
  kind: "mutation",
234024
234576
  auth: "admin"
234577
+ }),
234578
+ /**
234579
+ * The placement plan in force plus operator pins. Drives the Locations
234580
+ * admin page: Auto vs a named recordings location, per camera.
234581
+ */
234582
+ getPlacement: require_sleep.method(zod.z.object({}), RecordingPlacementViewSchema, {
234583
+ kind: "query",
234584
+ auth: "admin"
234585
+ }),
234586
+ /**
234587
+ * Pin a camera to a recordings location, or clear the pin (Auto). High and
234588
+ * mid follow the pin; low stays with the recordingsLow planner.
234589
+ */
234590
+ setDevicePlacement: require_sleep.method(RecordingSetDevicePlacementInputSchema, zod.z.object({ ok: zod.z.literal(true) }), {
234591
+ kind: "mutation",
234592
+ auth: "admin"
234025
234593
  })
234026
234594
  }
234027
234595
  };
@@ -244181,6 +244749,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244181
244749
  addonId: null,
244182
244750
  access: "view"
244183
244751
  },
244752
+ "recording.getPlacement": {
244753
+ capName: "recording",
244754
+ capScope: "system",
244755
+ addonId: null,
244756
+ access: "view"
244757
+ },
244184
244758
  "recording.getPlaybackManifest": {
244185
244759
  capName: "recording",
244186
244760
  capScope: "system",
@@ -244307,6 +244881,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244307
244881
  addonId: null,
244308
244882
  access: "create"
244309
244883
  },
244884
+ "recording.setDevicePlacement": {
244885
+ capName: "recording",
244886
+ capScope: "system",
244887
+ addonId: null,
244888
+ access: "create"
244889
+ },
244310
244890
  "recording.startStorageMigrationMove": {
244311
244891
  capName: "recording",
244312
244892
  capScope: "system",
@@ -244751,12 +245331,36 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244751
245331
  addonId: null,
244752
245332
  access: "create"
244753
245333
  },
245334
+ "storageMigration.cleanupCancel": {
245335
+ capName: "storage-migration",
245336
+ capScope: "system",
245337
+ addonId: null,
245338
+ access: "create"
245339
+ },
245340
+ "storageMigration.cleanupStart": {
245341
+ capName: "storage-migration",
245342
+ capScope: "system",
245343
+ addonId: null,
245344
+ access: "create"
245345
+ },
245346
+ "storageMigration.cleanupStatus": {
245347
+ capName: "storage-migration",
245348
+ capScope: "system",
245349
+ addonId: null,
245350
+ access: "view"
245351
+ },
244754
245352
  "storageMigration.drain": {
244755
245353
  capName: "storage-migration",
244756
245354
  capScope: "system",
244757
245355
  addonId: null,
244758
245356
  access: "create"
244759
245357
  },
245358
+ "storageMigration.history": {
245359
+ capName: "storage-migration",
245360
+ capScope: "system",
245361
+ addonId: null,
245362
+ access: "view"
245363
+ },
244760
245364
  "storageMigration.movers": {
244761
245365
  capName: "storage-migration",
244762
245366
  capScope: "system",
@@ -247501,6 +248105,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
247501
248105
  form: "single",
247502
248106
  optional: false
247503
248107
  }],
248108
+ "recording.setDevicePlacement": [{
248109
+ name: "deviceId",
248110
+ form: "single",
248111
+ optional: false
248112
+ }],
247504
248113
  "recording.startStorageMigrationMove": [{
247505
248114
  name: "deviceId",
247506
248115
  form: "single",
@@ -247970,6 +248579,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
247970
248579
  "recording.renderGif",
247971
248580
  "recording.rescanStorage",
247972
248581
  "recording.setDeviceConfig",
248582
+ "recording.setDevicePlacement",
247973
248583
  "recording.startStorageMigrationMove",
247974
248584
  "recordingExport.createExport",
247975
248585
  "recordingExport.listExports",
@@ -248853,7 +249463,9 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
248853
249463
  reconcileLedgerAgainstDisk: (input) => dispatch("recording", "reconcileLedgerAgainstDisk", "mutation", input),
248854
249464
  cancelRelocateJob: (input) => dispatch("recording", "cancelRelocateJob", "mutation", input),
248855
249465
  planStorageRebalance: (input) => dispatch("recording", "planStorageRebalance", "query", input),
248856
- startStorageRebalance: (input) => dispatch("recording", "startStorageRebalance", "mutation", input)
249466
+ startStorageRebalance: (input) => dispatch("recording", "startStorageRebalance", "mutation", input),
249467
+ getPlacement: (input) => dispatch("recording", "getPlacement", "query", input),
249468
+ setDevicePlacement: (input) => dispatch("recording", "setDevicePlacement", "mutation", input)
248857
249469
  },
248858
249470
  recordingExport: {
248859
249471
  createExport: (input) => dispatch("recordingExport", "createExport", "mutation", input),
@@ -248917,7 +249529,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
248917
249529
  cancel: (input) => dispatch("storageMigration", "cancel", "mutation", input),
248918
249530
  movers: (input) => dispatch("storageMigration", "movers", "query", input),
248919
249531
  residue: (input) => dispatch("storageMigration", "residue", "query", input),
248920
- drain: (input) => dispatch("storageMigration", "drain", "mutation", input)
249532
+ drain: (input) => dispatch("storageMigration", "drain", "mutation", input),
249533
+ cleanupStart: (input) => dispatch("storageMigration", "cleanupStart", "mutation", input),
249534
+ cleanupStatus: (input) => dispatch("storageMigration", "cleanupStatus", "query", input),
249535
+ cleanupCancel: (input) => dispatch("storageMigration", "cleanupCancel", "mutation", input),
249536
+ history: (input) => dispatch("storageMigration", "history", "query", input)
248921
249537
  },
248922
249538
  streamBroker: {
248923
249539
  fetchEventMedia: (input) => dispatch("streamBroker", "fetchEventMedia", "mutation", input),
@@ -252534,6 +253150,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
252534
253150
  exports.StorageBeginDownloadResultSchema = BeginDownloadResultSchema;
252535
253151
  exports.StorageBeginUploadInputSchema = BeginUploadInputSchema;
252536
253152
  exports.StorageBeginUploadResultSchema = BeginUploadResultSchema;
253153
+ exports.StorageCleanupInputSchema = StorageCleanupInputSchema;
253154
+ exports.StorageCleanupJobSchema = StorageCleanupJobSchema;
253155
+ exports.StorageCleanupPhaseSchema = StorageCleanupPhaseSchema;
253156
+ exports.StorageCleanupStatusInputSchema = StorageCleanupStatusInputSchema;
252537
253157
  exports.StorageEndDownloadInputSchema = EndDownloadInputSchema;
252538
253158
  exports.StorageFinalizeUploadInputSchema = FinalizeUploadInputSchema;
252539
253159
  exports.StorageLocationDeclarationSchema = StorageLocationDeclarationSchema;
@@ -252559,6 +253179,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
252559
253179
  exports.StorageMigrationPhaseSchema = StorageMigrationPhaseSchema;
252560
253180
  exports.StorageMigrationPlanSchema = StorageMigrationPlanSchema;
252561
253181
  exports.StorageMigrationResidueSchema = StorageMigrationResidueSchema;
253182
+ exports.StorageMigrationSourcesSchema = StorageMigrationSourcesSchema;
252562
253183
  exports.StorageProviderInfoSchema = ProviderInfoSchema;
252563
253184
  exports.StorageReadChunkInputSchema = ReadChunkInputSchema;
252564
253185
  exports.StorageTestLocationResultSchema = TestLocationResultSchema;