@camstack/addon-pipeline 1.2.155 → 1.2.159

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.
Files changed (36) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/detection-pipeline/index.js +5 -5
  4. package/dist/detection-pipeline/index.mjs +4 -4
  5. package/dist/{dist-CmrtJ8RB.js → dist-bBqjZ7fo.js} +233 -7
  6. package/dist/{dist-CBRky_dz.mjs → dist-p8ruahJy.mjs} +233 -7
  7. package/dist/{lazy-sharp-BODx7w1t.js → lazy-sharp-Ct1Ow2zO.js} +1 -1
  8. package/dist/{local-frame-registry-BRG0GTmK.js → local-frame-registry-Bua7Qg7A.js} +1 -1
  9. package/dist/{local-frame-registry-ByKcWWPU.mjs → local-frame-registry-Ce3h-QcZ.mjs} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-BSeMUwxJ.js → node-DRc0Vqkz.js} +1 -1
  13. package/dist/{node-CQzPpwIR.mjs → node-kVYpLOax.mjs} +1 -1
  14. package/dist/pipeline-runner/index.js +5 -5
  15. package/dist/pipeline-runner/index.mjs +4 -4
  16. package/dist/{process-memory-DVmHBraW.js → process-memory-B4Id9YAE.js} +1 -1
  17. package/dist/{process-memory-DQX-DbHx.mjs → process-memory-D9s145SO.mjs} +1 -1
  18. package/dist/recorder/index.js +536 -27
  19. package/dist/recorder/index.mjs +533 -29
  20. package/dist/{segment-demux-js-DmqnujVx.js → segment-demux-js-DAgunuXf.js} +1 -1
  21. package/dist/{segment-demux-js-DGX1iGLL.mjs → segment-demux-js-VrM03R0O.mjs} +1 -1
  22. package/dist/session-decode/decode-worker-child.js +2 -2
  23. package/dist/session-decode/decode-worker-child.mjs +1 -1
  24. package/dist/stream-broker/_stub.js +2 -2
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DvfM0ofY.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B-FG8Phz.mjs} +2 -2
  26. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-lMPlG5w5.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-OnimCf2v.mjs} +1 -1
  27. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CSNuIp4J.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-_e_Px_mR.mjs} +1 -1
  28. package/dist/stream-broker/demux-worker-child.js +1 -1
  29. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  30. package/dist/stream-broker/{hostInit-DQGeMfoN.mjs → hostInit-DePWOWOH.mjs} +2 -2
  31. package/dist/stream-broker/index.js +3 -3
  32. package/dist/stream-broker/index.mjs +3 -3
  33. package/dist/stream-broker/remoteEntry.js +1 -1
  34. package/dist/{worker-protocol-C7jOjCsc.mjs → worker-protocol-BN0Q-eeu.mjs} +1 -1
  35. package/dist/{worker-protocol-S2nAdf6B.js → worker-protocol-CjWhvUMn.js} +1 -1
  36. package/package.json +1 -1
@@ -1,5 +1,9 @@
1
- const require_dist = require("../dist-CmrtJ8RB.js");
2
- const require_node = require("../node-BSeMUwxJ.js");
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ const require_dist = require("../dist-bBqjZ7fo.js");
6
+ const require_node = require("../node-DRc0Vqkz.js");
3
7
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
4
8
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
5
9
  const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
@@ -4211,6 +4215,377 @@ function sendDirectory(res, payload) {
4211
4215
  res.end(Buffer.from(body));
4212
4216
  }
4213
4217
  //#endregion
4218
+ //#region src/recorder/addon/location-discard-actions.ts
4219
+ var DiscardStartInput = require_dist.object({ locationId: require_dist.string().min(1) });
4220
+ var DiscardStarted = require_dist.object({
4221
+ started: require_dist.boolean(),
4222
+ alreadyRunning: require_dist.boolean(),
4223
+ jobId: require_dist.string().nullable()
4224
+ });
4225
+ var DiscardStatusSchema = require_dist.object({
4226
+ running: require_dist.boolean(),
4227
+ locationId: require_dist.string().nullable(),
4228
+ jobId: require_dist.string().nullable(),
4229
+ phase: require_dist._enum([
4230
+ "idle",
4231
+ "scanning",
4232
+ "wiping-fs",
4233
+ "wiping-db",
4234
+ "cascading",
4235
+ "complete",
4236
+ "cancelled",
4237
+ "error"
4238
+ ]),
4239
+ camerasTotal: require_dist.number(),
4240
+ camerasDone: require_dist.number(),
4241
+ foldersRemoved: require_dist.number(),
4242
+ dbRowsDeleted: require_dist.number(),
4243
+ ownersCascaded: require_dist.number(),
4244
+ detail: require_dist.string(),
4245
+ startedAtMs: require_dist.number().nullable(),
4246
+ finishedAtMs: require_dist.number().nullable(),
4247
+ error: require_dist.string().nullable()
4248
+ });
4249
+ var DiscardCancel = require_dist.object({ requested: require_dist.boolean() });
4250
+ var locationDiscardActions = require_dist.defineCustomActions({
4251
+ "storage.discardLocation": require_dist.customAction(DiscardStartInput, DiscardStarted, { auth: "admin" }),
4252
+ "storage.discardLocationStatus": require_dist.customAction(require_dist.object({}), DiscardStatusSchema, { auth: "admin" }),
4253
+ "storage.discardLocationCancel": require_dist.customAction(require_dist.object({}), DiscardCancel, { auth: "admin" })
4254
+ });
4255
+ //#endregion
4256
+ //#region src/recorder/addon/location-class-wipe.ts
4257
+ /**
4258
+ * Class-subtree wipe — the filesystem half of discarding a frozen location
4259
+ * that SHARES its root with another class (today: Toshiba `/recordings`
4260
+ * holds events + low + high).
4261
+ *
4262
+ * The only directories this module will `rm` are
4263
+ * `<root>/<camera>/<subtree>` for `subtree` in the class map. It refuses
4264
+ * a path that is not exactly two levels under the root, a symlink, or a
4265
+ * subtree the class did not name. `rm` of the location root is not a
4266
+ * code path.
4267
+ */
4268
+ var DISCARD_SUBTREES$1 = {
4269
+ eventMedia: ["events"],
4270
+ recordingsLow: ["low"],
4271
+ recordings: ["high", "mid"]
4272
+ };
4273
+ function discardSubtreesForType(type) {
4274
+ return DISCARD_SUBTREES$1[type] ?? null;
4275
+ }
4276
+ var DEFAULT_FS = {
4277
+ readdir: (dir) => node_fs.promises.readdir(dir, { withFileTypes: true }),
4278
+ lstat: (target) => node_fs.promises.lstat(target),
4279
+ rm: (target) => node_fs.promises.rm(target, {
4280
+ recursive: true,
4281
+ force: true
4282
+ })
4283
+ };
4284
+ /**
4285
+ * Resolve `<root>/<camera>/<subtree>` and refuse anything that is not
4286
+ * exactly that shape under `root`. The check is lexical (after
4287
+ * `path.resolve`) so `../high` in a camera name cannot escape.
4288
+ */
4289
+ function resolveClassDir(root, cameraId, subtree) {
4290
+ if (cameraId.length === 0 || cameraId === "." || cameraId === "..") throw new Error(`discard: illegal camera id ${cameraId}`);
4291
+ if (subtree.includes("/") || subtree.includes("\\") || subtree === ".." || subtree === ".") throw new Error(`discard: illegal subtree ${subtree}`);
4292
+ const rootResolved = node_path.default.resolve(root);
4293
+ const target = node_path.default.resolve(rootResolved, cameraId, subtree);
4294
+ const rel = node_path.default.relative(rootResolved, target);
4295
+ if (rel.startsWith("..") || node_path.default.isAbsolute(rel)) throw new Error(`discard: path escaped root: ${target}`);
4296
+ const parts = rel.split(node_path.default.sep).filter((p) => p.length > 0);
4297
+ if (parts.length !== 2 || parts[0] !== cameraId || parts[1] !== subtree) throw new Error(`discard: path is not a class subtree: ${target}`);
4298
+ return target;
4299
+ }
4300
+ async function wipeClassSubtrees(input) {
4301
+ const subtrees = discardSubtreesForType(input.type);
4302
+ if (subtrees === null) throw new Error(`discard: unknown location type ${input.type}`);
4303
+ const io = input.io ?? DEFAULT_FS;
4304
+ const rootResolved = node_path.default.resolve(input.root);
4305
+ let cameras;
4306
+ try {
4307
+ cameras = await io.readdir(rootResolved);
4308
+ } catch (err) {
4309
+ if (err.code === "ENOENT") return {
4310
+ camerasDone: 0,
4311
+ foldersRemoved: 0,
4312
+ cancelled: false
4313
+ };
4314
+ throw err;
4315
+ }
4316
+ const dirs = cameras.filter((e) => e.isDirectory() && e.name !== "." && e.name !== "..");
4317
+ let camerasDone = 0;
4318
+ let foldersRemoved = 0;
4319
+ for (const dir of dirs) {
4320
+ if (input.stop()) return {
4321
+ camerasDone,
4322
+ foldersRemoved,
4323
+ cancelled: true
4324
+ };
4325
+ for (const subtree of subtrees) {
4326
+ const target = resolveClassDir(rootResolved, dir.name, subtree);
4327
+ let st;
4328
+ try {
4329
+ st = await io.lstat(target);
4330
+ } catch (err) {
4331
+ if (err.code === "ENOENT") continue;
4332
+ throw err;
4333
+ }
4334
+ if (st.isSymbolicLink()) throw new Error(`discard: refusing to wipe symlink ${target}`);
4335
+ if (!st.isDirectory()) continue;
4336
+ await io.rm(target);
4337
+ foldersRemoved += 1;
4338
+ }
4339
+ camerasDone += 1;
4340
+ input.onProgress({
4341
+ camerasTotal: dirs.length,
4342
+ camerasDone,
4343
+ cameraId: dir.name,
4344
+ foldersRemoved
4345
+ });
4346
+ if (input.pace) await input.pace();
4347
+ }
4348
+ return {
4349
+ camerasDone,
4350
+ foldersRemoved,
4351
+ cancelled: false
4352
+ };
4353
+ }
4354
+ //#endregion
4355
+ //#region src/recorder/addon/location-discard-policy.ts
4356
+ /**
4357
+ * Server-side eligibility for discarding a frozen location's leftover
4358
+ * class subtree. Same rules as the admin-ui card: frozen, known class,
4359
+ * writable sibling of the same type. The last writable of a type is
4360
+ * refused here even if the UI is bypassed.
4361
+ */
4362
+ var DISCARD_SUBTREES = {
4363
+ eventMedia: ["events"],
4364
+ recordingsLow: ["low"],
4365
+ recordings: ["high", "mid"]
4366
+ };
4367
+ function locationIsFrozen(location) {
4368
+ const enabled = location.enabled !== false;
4369
+ const readOnly = location.config["readOnly"] === true;
4370
+ return !enabled || readOnly;
4371
+ }
4372
+ function discardEligible(location, siblings) {
4373
+ if (!(location.type in DISCARD_SUBTREES)) return false;
4374
+ if (!locationIsFrozen(location)) return false;
4375
+ return siblings.some((s) => {
4376
+ if (s.id === location.id) return false;
4377
+ if (s.type !== location.type) return false;
4378
+ const enabled = s.enabled !== false;
4379
+ const readOnly = s.config["readOnly"] === true;
4380
+ return enabled && !readOnly;
4381
+ });
4382
+ }
4383
+ function assertDiscardAllowed(location, all, localNodeId) {
4384
+ if (!discardEligible(location, all.filter((s) => s.type === location.type))) throw new Error(`discard refused: ${location.id} is not a frozen location with a writable sibling of type ${location.type}`);
4385
+ const nodeId = location.nodeId ?? "hub";
4386
+ if (nodeId !== localNodeId) throw new Error(`discard refused: ${location.id} lives on node ${nodeId}, this process is ${localNodeId}`);
4387
+ }
4388
+ function locationBasePath(location) {
4389
+ const basePath = location.config["basePath"];
4390
+ if (typeof basePath !== "string" || basePath.length === 0) throw new Error(`discard refused: ${location.id} has no basePath`);
4391
+ return basePath;
4392
+ }
4393
+ //#endregion
4394
+ //#region src/recorder/addon/location-discard-job.ts
4395
+ /**
4396
+ * Footage discard job: wipe `<camera>/low/` (or `high/`+`mid/` for the
4397
+ * recordings class) on a frozen location, then forget the hour rows
4398
+ * stamped with that `locationId` and drop them from the RAM index.
4399
+ *
4400
+ * The class-subtree wipe is the load-bearing safety: `recordingsLow:default`
4401
+ * shares `/recordings` with `recordings:default`. This job never `rm`s the
4402
+ * location root. Profiles passed to the ledger forget are the class's own
4403
+ * (`low` vs `high`/`mid`) so a mis-stamped hour of the other class is not
4404
+ * dropped.
4405
+ *
4406
+ * Start returns immediately. Progress is the status snapshot.
4407
+ */
4408
+ var IDLE_DISCARD = {
4409
+ running: false,
4410
+ locationId: null,
4411
+ jobId: null,
4412
+ phase: "idle",
4413
+ camerasTotal: 0,
4414
+ camerasDone: 0,
4415
+ foldersRemoved: 0,
4416
+ dbRowsDeleted: 0,
4417
+ ownersCascaded: 0,
4418
+ detail: "",
4419
+ startedAtMs: null,
4420
+ finishedAtMs: null,
4421
+ error: null
4422
+ };
4423
+ var USB_PACE_MS = 80;
4424
+ var FOOTAGE_TYPES = new Set(["recordings", "recordingsLow"]);
4425
+ function sleep(ms) {
4426
+ return new Promise((resolve) => setTimeout(resolve, ms));
4427
+ }
4428
+ function profilesForType(type) {
4429
+ return type === "recordingsLow" ? ["low"] : ["high", "mid"];
4430
+ }
4431
+ function createFootageDiscardJob(deps) {
4432
+ let state = { ...IDLE_DISCARD };
4433
+ let cancelRequested = false;
4434
+ let inflight = null;
4435
+ const report = (patch) => {
4436
+ state = {
4437
+ ...state,
4438
+ ...patch
4439
+ };
4440
+ };
4441
+ const status = () => ({ ...state });
4442
+ const cancel = () => {
4443
+ if (!state.running) return { requested: false };
4444
+ cancelRequested = true;
4445
+ report({ detail: "cancel requested — finishing current camera" });
4446
+ return { requested: true };
4447
+ };
4448
+ const start = async (input) => {
4449
+ if (state.running) return {
4450
+ started: false,
4451
+ alreadyRunning: true,
4452
+ jobId: state.jobId
4453
+ };
4454
+ const jobId = (0, node_crypto.randomUUID)();
4455
+ cancelRequested = false;
4456
+ report({
4457
+ ...IDLE_DISCARD,
4458
+ running: true,
4459
+ locationId: input.locationId,
4460
+ jobId,
4461
+ phase: "scanning",
4462
+ startedAtMs: deps.now(),
4463
+ detail: "starting"
4464
+ });
4465
+ try {
4466
+ const all = await deps.listLocations();
4467
+ const location = all.find((row) => row.id === input.locationId);
4468
+ if (!location) throw new Error(`discard refused: unknown location ${input.locationId}`);
4469
+ if (!FOOTAGE_TYPES.has(location.type)) throw new Error(`discard refused: recorder handles footage, not ${location.type}`);
4470
+ assertDiscardAllowed(location, all, deps.localNodeId);
4471
+ const root = locationBasePath(location);
4472
+ report({ detail: `scanning ${root}` });
4473
+ inflight = run(location, root).finally(() => {
4474
+ inflight = null;
4475
+ });
4476
+ return {
4477
+ started: true,
4478
+ alreadyRunning: false,
4479
+ jobId
4480
+ };
4481
+ } catch (err) {
4482
+ report({
4483
+ ...IDLE_DISCARD,
4484
+ phase: "error",
4485
+ error: err instanceof Error ? err.message : String(err),
4486
+ finishedAtMs: deps.now()
4487
+ });
4488
+ throw err;
4489
+ }
4490
+ };
4491
+ const run = async (location, root) => {
4492
+ const wipe = deps.wipe ?? wipeClassSubtrees;
4493
+ const paceMs = deps.paceMs ?? USB_PACE_MS;
4494
+ try {
4495
+ report({
4496
+ phase: "wiping-fs",
4497
+ detail: `wiping ${profilesForType(location.type).join("/")} under ${root}`
4498
+ });
4499
+ const fsResult = await wipe({
4500
+ root,
4501
+ type: location.type,
4502
+ stop: () => cancelRequested,
4503
+ onProgress: (p) => {
4504
+ report({
4505
+ phase: "wiping-fs",
4506
+ camerasTotal: p.camerasTotal,
4507
+ camerasDone: p.camerasDone,
4508
+ foldersRemoved: p.foldersRemoved,
4509
+ detail: `camera ${p.cameraId} (${p.camerasDone}/${p.camerasTotal})`
4510
+ });
4511
+ },
4512
+ pace: () => sleep(paceMs)
4513
+ });
4514
+ if (fsResult.cancelled || cancelRequested) {
4515
+ report({
4516
+ running: false,
4517
+ phase: "cancelled",
4518
+ camerasDone: fsResult.camerasDone,
4519
+ foldersRemoved: fsResult.foldersRemoved,
4520
+ finishedAtMs: deps.now(),
4521
+ detail: "cancelled"
4522
+ });
4523
+ return;
4524
+ }
4525
+ report({
4526
+ phase: "wiping-db",
4527
+ camerasDone: fsResult.camerasDone,
4528
+ foldersRemoved: fsResult.foldersRemoved,
4529
+ detail: "forgetting ledger hours"
4530
+ });
4531
+ const forgotten = await forgetLedger(location);
4532
+ if (cancelRequested) {
4533
+ report({
4534
+ running: false,
4535
+ phase: "cancelled",
4536
+ dbRowsDeleted: forgotten,
4537
+ finishedAtMs: deps.now(),
4538
+ detail: "cancelled"
4539
+ });
4540
+ return;
4541
+ }
4542
+ report({
4543
+ running: false,
4544
+ phase: "complete",
4545
+ dbRowsDeleted: forgotten,
4546
+ finishedAtMs: deps.now(),
4547
+ detail: `done — ${fsResult.foldersRemoved} folders, ${forgotten} ledger segments`,
4548
+ error: null
4549
+ });
4550
+ } catch (err) {
4551
+ report({
4552
+ running: false,
4553
+ phase: "error",
4554
+ finishedAtMs: deps.now(),
4555
+ error: err instanceof Error ? err.message : String(err),
4556
+ detail: "failed"
4557
+ });
4558
+ }
4559
+ };
4560
+ const forgetLedger = async (location) => {
4561
+ const ledger = deps.ledger;
4562
+ if (ledger === null) return 0;
4563
+ let forgotten = 0;
4564
+ const profiles = profilesForType(location.type);
4565
+ for await (const page of ledger.streamSegments({
4566
+ locationId: location.id,
4567
+ profiles
4568
+ })) {
4569
+ if (cancelRequested) return forgotten;
4570
+ deps.index.removeSegments(page.map((row) => row.path));
4571
+ await ledger.dropSegments(page);
4572
+ forgotten += page.length;
4573
+ report({
4574
+ dbRowsDeleted: forgotten,
4575
+ detail: `forgot ${forgotten} ledger segments`
4576
+ });
4577
+ await sleep(0);
4578
+ }
4579
+ return forgotten;
4580
+ };
4581
+ return {
4582
+ start,
4583
+ status,
4584
+ cancel,
4585
+ waitForIdle: () => inflight
4586
+ };
4587
+ }
4588
+ //#endregion
4214
4589
  //#region src/recorder/addon/export-dense-map.ts
4215
4590
  /**
4216
4591
  * Shortest clip worth keeping, in ms.
@@ -6339,7 +6714,10 @@ function buildRecordingProvider(deps) {
6339
6714
  }
6340
6715
  const locationUsages = await Promise.all([...byPhysical.values()].map(async (group) => {
6341
6716
  const first = group[0];
6342
- const cap = await deps.capacity(first.root);
6717
+ const cap = group.some((l) => l.enabled !== false) ? await deps.capacity(first.root) : {
6718
+ availableBytes: null,
6719
+ totalBytes: null
6720
+ };
6343
6721
  const ids = group.map((l) => l.id);
6344
6722
  if (cap.error !== void 0) deps.logger.warn("recorder: volume capacity unavailable (statfs failed)", { meta: {
6345
6723
  locationId: first.id,
@@ -6742,6 +7120,15 @@ function buildRecordingProvider(deps) {
6742
7120
  cancelRelocateJob: async ({ jobId }) => ({ cancelled: deps.relocate?.cancel(jobId) ?? false }),
6743
7121
  planStorageRebalance: (input) => runRebalance(deps, input, false),
6744
7122
  startStorageRebalance: (input) => runRebalance(deps, input, true),
7123
+ getPlacement: async () => {
7124
+ if (!deps.placementView) throw new Error("placement is not active on this node");
7125
+ return deps.placementView();
7126
+ },
7127
+ setDevicePlacement: async ({ deviceId, locationId }) => {
7128
+ if (!deps.setDevicePlacement) throw new Error("placement is not active on this node");
7129
+ await deps.setDevicePlacement(deviceId, locationId);
7130
+ return { ok: true };
7131
+ },
6745
7132
  getDeviceSettingsContribution: async ({ deviceId }) => {
6746
7133
  if (deps.isCameraDevice && !await deps.isCameraDevice(deviceId)) return null;
6747
7134
  return require_dist.hydrateSchema(buildRecordingDeviceSchema(), {});
@@ -9615,6 +10002,7 @@ Object.freeze({
9615
10002
  "getSensorEvents": { "limit": 1e3 },
9616
10003
  "listGroups": { "limit": 40 },
9617
10004
  "listRecentTracks": { "limit": 200 },
10005
+ "reclaimDebugMedia": { "mode": "report" },
9618
10006
  "searchObjectEvents": {
9619
10007
  "limit": 50,
9620
10008
  "minScore": .2
@@ -10702,6 +11090,11 @@ var RECORDING_PLACEMENT_KEY = "recordingPlacement";
10702
11090
  var RecordingPlacementBlobSchema = require_dist.object({
10703
11091
  /** `<deviceId>:<profile>` → locationId. */
10704
11092
  assignments: require_dist.record(require_dist.string(), require_dist.string()).default({}),
11093
+ /**
11094
+ * Operator pin: `String(deviceId)` → a `recordings:*` location id.
11095
+ * Absent = Auto. High and mid follow the pin; low is not pinned here.
11096
+ */
11097
+ pins: require_dist.record(require_dist.string(), require_dist.string()).default({}),
10705
11098
  /** locationId → the volume id last verified at that location's root. */
10706
11099
  volumeIds: require_dist.record(require_dist.string(), require_dist.string()).default({}),
10707
11100
  /**
@@ -10720,6 +11113,7 @@ var RecordingPlacementBlobSchema = require_dist.object({
10720
11113
  /** Cold-start value: nothing assigned, nothing claimed, no authority recorded. */
10721
11114
  var EMPTY_PLACEMENT = {
10722
11115
  assignments: {},
11116
+ pins: {},
10723
11117
  volumeIds: {},
10724
11118
  defaultLocations: {}
10725
11119
  };
@@ -10805,6 +11199,25 @@ async function rememberVolumeId(state, locationId, volumeId) {
10805
11199
  updatedAt: Date.now()
10806
11200
  });
10807
11201
  }
11202
+ /** Operator pin: `locationId` null clears Auto. High/mid assignment keys follow. */
11203
+ async function writeDevicePin(state, deviceId, locationId) {
11204
+ const blob = await state.get();
11205
+ const pins = { ...blob.pins };
11206
+ const assignments = { ...blob.assignments };
11207
+ const key = String(deviceId);
11208
+ if (locationId === null) delete pins[key];
11209
+ else {
11210
+ pins[key] = locationId;
11211
+ assignments[assignmentKey(deviceId, "high")] = locationId;
11212
+ assignments[assignmentKey(deviceId, "mid")] = locationId;
11213
+ }
11214
+ await state.set({
11215
+ ...blob,
11216
+ pins,
11217
+ assignments,
11218
+ updatedAt: Date.now()
11219
+ });
11220
+ }
10808
11221
  //#endregion
10809
11222
  //#region src/recorder/addon/placement-volume.ts
10810
11223
  /**
@@ -11027,6 +11440,17 @@ function sameStamp(stamped, authority) {
11027
11440
  for (const [type, locationId] of authority) if (stamped.get(type) !== locationId) return false;
11028
11441
  return true;
11029
11442
  }
11443
+ /** Operator pins win over the planner for high/mid of the pinned camera. */
11444
+ function overlayPins(planned, pins) {
11445
+ const next = new Map(planned);
11446
+ for (const [id, locationId] of Object.entries(pins)) {
11447
+ const deviceId = Number(id);
11448
+ if (!Number.isFinite(deviceId)) continue;
11449
+ next.set(assignmentKey(deviceId, "high"), locationId);
11450
+ next.set(assignmentKey(deviceId, "mid"), locationId);
11451
+ }
11452
+ return next;
11453
+ }
11030
11454
  var PlacementService = class {
11031
11455
  deps;
11032
11456
  /** Locations whose volume has been probed since the last location change. */
@@ -11220,6 +11644,11 @@ var PlacementService = class {
11220
11644
  return null;
11221
11645
  }
11222
11646
  const assignedId = blob.assignments[assignmentKey(deviceId, profile)];
11647
+ const pinId = blob.pins[String(deviceId)];
11648
+ if (pinId !== void 0 && (profile === "high" || profile === "mid")) {
11649
+ const pinned = pool.find((l) => l.id === pinId && !l.readOnly) ?? null;
11650
+ if (pinned !== null) return pinned;
11651
+ }
11223
11652
  if (assignedId === void 0) return null;
11224
11653
  const location = pool.find((l) => l.id === assignedId && !l.readOnly) ?? null;
11225
11654
  if (location === null) return null;
@@ -11278,7 +11707,7 @@ var PlacementService = class {
11278
11707
  return NO_PLAN;
11279
11708
  }
11280
11709
  const stamped = new Map(Object.entries(blob.defaultLocations));
11281
- const repoints = this.detectRepoints(blob.assignments, stamped, authority);
11710
+ const repoints = this.detectRepoints(blob.assignments, stamped, authority, blob.pins);
11282
11711
  if (repoints.length === 0) this.repointAnnounced.clear();
11283
11712
  for (const repoint of repoints) this.announceRepoint(repoint);
11284
11713
  const repointed = /* @__PURE__ */ new Map();
@@ -11299,7 +11728,7 @@ var PlacementService = class {
11299
11728
  });
11300
11729
  const pass = {
11301
11730
  repointed,
11302
- planned: plan.assignments,
11731
+ planned: overlayPins(plan.assignments, blob.pins),
11303
11732
  defaultLocations: authority
11304
11733
  };
11305
11734
  this.reportDivergence(applyPlacementPass(blob.assignments, pass), authority);
@@ -11336,15 +11765,17 @@ var PlacementService = class {
11336
11765
  * from an absent stamp would throw away every deliberate assignment on the
11337
11766
  * first tick after a deploy.
11338
11767
  */
11339
- detectRepoints(stored, stamped, authority) {
11768
+ detectRepoints(stored, stamped, authority, pins) {
11340
11769
  const out = [];
11341
11770
  for (const [type, to] of authority) {
11342
11771
  const from = stamped.get(type);
11343
11772
  if (from === void 0 || from === to) continue;
11344
11773
  const keys = [];
11345
11774
  for (const [key, locationId] of Object.entries(stored)) {
11346
- if (placementTypeForProfile(key.split(":")[1] ?? "") !== type) continue;
11775
+ const [devicePart, profilePart] = key.split(":");
11776
+ if (placementTypeForProfile(profilePart ?? "") !== type) continue;
11347
11777
  if (locationId === to) continue;
11778
+ if (type === "recordings" && devicePart !== void 0 && pins[devicePart] !== void 0) continue;
11348
11779
  keys.push(key);
11349
11780
  }
11350
11781
  out.push({
@@ -11480,6 +11911,40 @@ var PlacementService = class {
11480
11911
  return /* @__PURE__ */ new Map();
11481
11912
  }
11482
11913
  }
11914
+ async placementView() {
11915
+ const blob = await readPlacementState(this.deps.state);
11916
+ return {
11917
+ assignments: Object.entries(blob.assignments).flatMap(([key, locationId]) => {
11918
+ const [idPart, profile] = key.split(":");
11919
+ const deviceId = Number(idPart);
11920
+ if (!Number.isFinite(deviceId) || profile === void 0) return [];
11921
+ return [{
11922
+ deviceId,
11923
+ profile,
11924
+ locationId
11925
+ }];
11926
+ }),
11927
+ pins: Object.entries(blob.pins).flatMap(([id, locationId]) => {
11928
+ const deviceId = Number(id);
11929
+ if (!Number.isFinite(deviceId)) return [];
11930
+ return [{
11931
+ deviceId,
11932
+ locationId
11933
+ }];
11934
+ }),
11935
+ defaultLocations: blob.defaultLocations
11936
+ };
11937
+ }
11938
+ async setDevicePin(deviceId, locationId) {
11939
+ if (locationId !== null) {
11940
+ if (!this.pool().find((l) => l.id === locationId && l.type === "recordings" && !l.readOnly)) throw new Error(`Cannot pin camera ${String(deviceId)} to "${locationId}" — it is not a writable recordings location`);
11941
+ }
11942
+ await writeDevicePin(this.deps.state, deviceId, locationId);
11943
+ this.deps.logger.info("recorder placement: operator pin updated", {
11944
+ tags: { deviceId },
11945
+ meta: { locationId }
11946
+ });
11947
+ }
11483
11948
  /**
11484
11949
  * Turn each pool location into a planner candidate. Headroom is
11485
11950
  * `min(free − minFree floor, maxUsedGb − used)`, clamped at zero: the first
@@ -15472,6 +15937,14 @@ function withDeviceTag(logger, deviceId) {
15472
15937
  * provider until the controller swaps the manifest entry in a later step — this
15473
15938
  * file ships dormant (separate tree, not wired into the manifest yet).
15474
15939
  */
15940
+ /**
15941
+ * Hub harvests this named export into CustomActionRegistry. Actions that
15942
+ * exist only on the onInitialize envelope stay invisible to `addons.custom`.
15943
+ */
15944
+ var customActions = {
15945
+ ...recorderDebugActions,
15946
+ ...locationDiscardActions
15947
+ };
15475
15948
  /** Default designated recording node (`recordingNodeId` global setting). The
15476
15949
  * stream-broker is hub-only and recording is a single-node global function, so
15477
15950
  * the hub is the safe default — mirrors pipeline-analytics' post-processing
@@ -15639,6 +16112,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
15639
16112
  * deferred archive walk reconciles it against disk.
15640
16113
  */
15641
16114
  segmentHours = null;
16115
+ /** Frozen-location leftover wipe (`low/` or `high/`+`mid/`). Detached. */
16116
+ footageDiscard = null;
15642
16117
  /** Why the last placement pass planned nothing — latched so the line is one
15643
16118
  * per episode, not one per 30 s tick. `null` ⇒ the last pass had work. */
15644
16119
  placementWorkGap = null;
@@ -16065,6 +16540,16 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
16065
16540
  refreshStorageLocationsForMigration: (leaseId) => this.refreshStorageLocationsForMigration(leaseId),
16066
16541
  assertStorageMigrationLease: (leaseId) => this.assertStorageMigrationLease(leaseId),
16067
16542
  placementSnapshot: () => this.placementSnapshot(),
16543
+ placementView: () => {
16544
+ const placement = this.placement;
16545
+ if (!placement) throw new Error("placement is not active on this node");
16546
+ return placement.placementView();
16547
+ },
16548
+ setDevicePlacement: (deviceId, locationId) => {
16549
+ const placement = this.placement;
16550
+ if (!placement) throw new Error("placement is not active on this node");
16551
+ return placement.setDevicePin(deviceId, locationId);
16552
+ },
16068
16553
  dataDir: this.ctx.dataDir,
16069
16554
  playbackBaseUrl: () => this.playbackBaseUrl,
16070
16555
  isCameraDevice: async (deviceId) => {
@@ -16227,27 +16712,50 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
16227
16712
  }
16228
16713
  });
16229
16714
  warnLostBands(this.configStore(), this.ctx.logger);
16715
+ const providers = [
16716
+ {
16717
+ capability: require_dist.recordingCapability,
16718
+ provider: recordingProvider
16719
+ },
16720
+ {
16721
+ capability: require_dist.storageEvictableCapability,
16722
+ provider: evictableProvider
16723
+ },
16724
+ {
16725
+ capability: require_dist.recordingExportCapability,
16726
+ provider: exportProvider
16727
+ },
16728
+ {
16729
+ capability: require_dist.loadContributionCapability,
16730
+ provider: { list: () => this.costRegistry.contributions() }
16731
+ }
16732
+ ];
16733
+ this.footageDiscard = createFootageDiscardJob({
16734
+ listLocations: async () => this.ctx.api.storage.listLocations.query({}),
16735
+ localNodeId: this.nodeId,
16736
+ index: this.index,
16737
+ ledger: this.segmentHours === null ? null : {
16738
+ streamSegments: (scope) => this.segmentHours.streamSegments(scope),
16739
+ dropSegments: (rows) => this.segmentHours.dropSegments(rows)
16740
+ },
16741
+ now: () => Date.now()
16742
+ });
16230
16743
  return {
16231
- providers: [
16232
- {
16233
- capability: require_dist.recordingCapability,
16234
- provider: recordingProvider
16235
- },
16236
- {
16237
- capability: require_dist.storageEvictableCapability,
16238
- provider: evictableProvider
16239
- },
16240
- {
16241
- capability: require_dist.recordingExportCapability,
16242
- provider: exportProvider
16744
+ providers,
16745
+ customActions: {
16746
+ ...recorderDebugActions,
16747
+ ...locationDiscardActions
16748
+ },
16749
+ actionHandlers: {
16750
+ "debug.storeCensus": async (input) => handleStoreCensusAction(this.readCensus, this.ctx.logger.child("Census"), input),
16751
+ "storage.discardLocation": async (input) => this.footageDiscard?.start(input) ?? {
16752
+ started: false,
16753
+ alreadyRunning: false,
16754
+ jobId: null
16243
16755
  },
16244
- {
16245
- capability: require_dist.loadContributionCapability,
16246
- provider: { list: () => this.costRegistry.contributions() }
16247
- }
16248
- ],
16249
- customActions: recorderDebugActions,
16250
- actionHandlers: { "debug.storeCensus": async (input) => handleStoreCensusAction(this.readCensus, this.ctx.logger.child("Census"), input) }
16756
+ "storage.discardLocationStatus": async () => this.footageDiscard?.status() ?? IDLE_DISCARD,
16757
+ "storage.discardLocationCancel": async () => this.footageDiscard?.cancel() ?? { requested: false }
16758
+ }
16251
16759
  };
16252
16760
  }
16253
16761
  async onShutdown() {
@@ -17254,4 +17762,5 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
17254
17762
  }
17255
17763
  };
17256
17764
  //#endregion
17257
- module.exports = RecorderV2Addon;
17765
+ exports.customActions = customActions;
17766
+ exports.default = RecorderV2Addon;