@camstack/addon-pipeline 1.2.96 → 1.2.98

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 (31) hide show
  1. package/dist/{addon-utils-B6usm-YO.js → addon-utils-eUS6n_Zj.js} +1 -1
  2. package/dist/audio-analyzer/index.js +3 -3
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/detection-pipeline/index.js +5 -5
  5. package/dist/detection-pipeline/index.mjs +3 -3
  6. package/dist/{dist-w9QWDvWn.js → dist-dm3t4BOt.js} +29 -3
  7. package/dist/{dist-LIWfUNC_.mjs → dist-gXdWP96z.mjs} +29 -3
  8. package/dist/{event-loop-stall-monitor-DndI0S2s.js → event-loop-stall-monitor-Lki2_mYY.js} +1 -1
  9. package/dist/{event-loop-stall-monitor-BDC8qURn.mjs → event-loop-stall-monitor-TnQ21mu0.mjs} +1 -1
  10. package/dist/{lazy-sharp-BVqBydNU.js → lazy-sharp-oYppDIZR.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +51 -4
  14. package/dist/pipeline-runner/index.mjs +50 -3
  15. package/dist/{process-memory-k45KJaKH.mjs → process-memory-CKWh609B.mjs} +1 -1
  16. package/dist/{process-memory-C6zWIgos.js → process-memory-vkFVOdkV.js} +1 -1
  17. package/dist/recorder/index.js +296 -48
  18. package/dist/recorder/index.mjs +295 -47
  19. package/dist/session-decode/decode-worker-child.js +2 -2
  20. package/dist/session-decode/decode-worker-child.mjs +1 -1
  21. package/dist/stream-broker/_stub.js +1 -1
  22. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CJWMZMhQ.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B7ERJocD.mjs} +3 -3
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-zT-cYUdF.mjs +26 -0
  24. package/dist/stream-broker/{hostInit-CGIo3d4K.mjs → hostInit-Cm2rl8-e.mjs} +3 -3
  25. package/dist/stream-broker/index.js +21 -6
  26. package/dist/stream-broker/index.mjs +21 -6
  27. package/dist/stream-broker/remoteEntry.js +1 -1
  28. package/dist/{worker-protocol-DqrXmX0g.js → worker-protocol-BrlrMxXu.js} +1 -1
  29. package/dist/{worker-protocol-DTe7Ntat.mjs → worker-protocol-DJWdPLKF.mjs} +1 -1
  30. package/package.json +1 -1
  31. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D2WOTeYi.mjs +0 -26
@@ -1,4 +1,4 @@
1
- import { $ as recordingExportCapability, C as RecordingConfigSchema, Ct as nodePin, Et as selectAssignedProfileSlots, Ft as object, It as record, L as deriveRecordingMode, Lt as string, Pt as number, Q as recordingCapability, S as RECORDING_EXPORT_MAX_READ_BYTES, _t as DeviceType, ct as errMsg, f as EVENT_PAD_MS, it as storageEvictableCapability, kt as array, l as DEFAULT_EVENTS_BAND_BUFFER_SEC, m as ExportRecordSchema, mt as BaseAddon, nt as resolveRecordingProfiles, v as OpsLogEntrySchema, yt as hydrateSchema, zt as EventCategory } from "../dist-LIWfUNC_.mjs";
1
+ import { $ as recordingExportCapability, C as RecordingConfigSchema, Ct as nodePin, Et as selectAssignedProfileSlots, Ft as object, It as record, L as deriveRecordingMode, Lt as string, Pt as number, Q as recordingCapability, S as RECORDING_EXPORT_MAX_READ_BYTES, _t as DeviceType, ct as errMsg, f as EVENT_PAD_MS, it as storageEvictableCapability, kt as array, l as DEFAULT_EVENTS_BAND_BUFFER_SEC, m as ExportRecordSchema, mt as BaseAddon, nt as resolveRecordingProfiles, v as OpsLogEntrySchema, yt as hydrateSchema, zt as EventCategory } from "../dist-gXdWP96z.mjs";
2
2
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
3
  import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../addon-utils-A2S9D7pu.mjs";
4
4
  import { randomUUID } from "node:crypto";
@@ -4332,9 +4332,15 @@ function buildRecordingProvider(deps) {
4332
4332
  const first = group[0];
4333
4333
  const cap = await deps.capacity(first.root);
4334
4334
  const ids = group.map((l) => l.id);
4335
+ if (cap.error !== void 0) deps.logger.warn("recorder: volume capacity unavailable (statfs failed)", { meta: {
4336
+ locationId: first.id,
4337
+ root: first.root,
4338
+ error: cap.error
4339
+ } });
4335
4340
  return {
4336
4341
  locationId: first.id,
4337
4342
  locationIds: ids,
4343
+ root: first.root,
4338
4344
  usedBytes: deps.index.accountingForLocations(new Set(ids)).bytes,
4339
4345
  availableBytes: cap.availableBytes,
4340
4346
  totalBytes: cap.totalBytes
@@ -5568,13 +5574,20 @@ async function probeLocationVolume(location, rememberedVolumeId, deps) {
5568
5574
  }
5569
5575
  /**
5570
5576
  * Where to write instead when a location fails its volume probe: the normal
5571
- * placement chain over every OTHER location. With N locations this is a
5572
- * sibling of the same type; with one it is the unusable location itself —
5573
- * a shouting recorder beats a stopped camera, and refusing to record is the
5574
- * one outcome no operator asked for.
5575
- */
5576
- function failoverForUnusableLocation(unusable, locations, profile) {
5577
- const siblings = locations.filter((l) => l.id !== unusable.id);
5577
+ * placement chain over every location NOT already known unusable. With N
5578
+ * locations this is a healthy sibling of the same type; with none left it is
5579
+ * the unusable location itself — a shouting recorder beats a stopped camera,
5580
+ * and refusing to record is the one outcome no operator asked for.
5581
+ *
5582
+ * `alreadyUnusable` is not an optimisation. Without it the search happily
5583
+ * returned a sibling that had ALSO failed its probe, so with two broken disks
5584
+ * every attach handed back the other one and the stored assignment flipped
5585
+ * A→B→A→B forever — one durable write and one audit row per attach, per camera,
5586
+ * per profile. Observed live on 2026-08-19 (the UD disk did not come back after
5587
+ * a reboot): 716 EACCES/min, with the ops-log ring flooded by the flapping.
5588
+ */
5589
+ function failoverForUnusableLocation(unusable, locations, profile, alreadyUnusable = /* @__PURE__ */ new Set()) {
5590
+ const siblings = locations.filter((l) => l.id !== unusable.id && !alreadyUnusable.has(l.id));
5578
5591
  try {
5579
5592
  return resolvePlacement(siblings, profile);
5580
5593
  } catch {
@@ -5625,6 +5638,13 @@ var PlacementService = class {
5625
5638
  verified = /* @__PURE__ */ new Set();
5626
5639
  /** Locations the probe declared unusable — re-probed after a change. */
5627
5640
  unusable = /* @__PURE__ */ new Set();
5641
+ /** Cameras that have already announced "every location is unusable". One line
5642
+ * per camera per episode: the fault is asked per-camera, and repeating it per
5643
+ * attach is the noise this state exists to end. */
5644
+ allUnusableReported = /* @__PURE__ */ new Set();
5645
+ /** When the current all-unusable episode last (re-)probed. `null` ⇒ no
5646
+ * episode in progress. */
5647
+ allUnusableProbedAt = null;
5628
5648
  /** The fair tie-break the PURE planner defers to (D37). Advisory state, and
5629
5649
  * consulted only for a tie the capacity signal cannot separate. */
5630
5650
  rotation = new PlacementRotation();
@@ -5634,6 +5654,12 @@ var PlacementService = class {
5634
5654
  get probeDeps() {
5635
5655
  return this.deps.probe ?? fsVolumeProbeDeps;
5636
5656
  }
5657
+ get now() {
5658
+ return this.deps.now?.() ?? Date.now();
5659
+ }
5660
+ get reprobeMs() {
5661
+ return this.deps.reprobeMs ?? 6e4;
5662
+ }
5637
5663
  /**
5638
5664
  * Forget every probe verdict. Called whenever the location set is re-resolved
5639
5665
  * (rescan, storage migration) — a remount is exactly the event the probe
@@ -5642,6 +5668,8 @@ var PlacementService = class {
5642
5668
  onLocationsChanged() {
5643
5669
  this.verified.clear();
5644
5670
  this.unusable.clear();
5671
+ this.allUnusableReported.clear();
5672
+ this.allUnusableProbedAt = null;
5645
5673
  }
5646
5674
  /**
5647
5675
  * Resolve the location for one (camera, profile) at an attach boundary.
@@ -5650,12 +5678,92 @@ var PlacementService = class {
5650
5678
  */
5651
5679
  async place(deviceId, profile) {
5652
5680
  const pool = this.pool();
5681
+ this.reprobeAfterBackoff(pool);
5653
5682
  const resolved = await this.assignedLocation(deviceId, profile, pool) ?? resolvePlacement(pool, profile);
5654
5683
  const usable = await this.ensureUsable(resolved, pool, profile, deviceId);
5684
+ if (this.everyLocationUnusable(pool)) {
5685
+ this.reportEveryLocationUnusable(deviceId, profile, resolved, pool);
5686
+ return resolved;
5687
+ }
5688
+ this.allUnusableReported.delete(deviceId);
5655
5689
  await this.recordDecision(deviceId, profile, usable, resolved.id === usable.id);
5656
5690
  return usable;
5657
5691
  }
5658
5692
  /**
5693
+ * Has the marker probe declared this location's volume NOT the one we last
5694
+ * wrote to?
5695
+ *
5696
+ * Read by the segment-hour ledger: a walk over a root whose volume failed its
5697
+ * probe found nothing because nothing is mounted there, and that must never be
5698
+ * allowed to delete durable rows (D49). Unprobed ⇒ usable, which is honest —
5699
+ * the probe runs at the first writer attach, so at boot it has usually not run
5700
+ * at all, and the ledger's own magnitude guard covers that window.
5701
+ */
5702
+ isLocationUsable(locationId) {
5703
+ return !this.unusable.has(locationId);
5704
+ }
5705
+ /** True when the pool is non-empty and the probe has failed EVERY member. */
5706
+ everyLocationUnusable(pool) {
5707
+ return pool.length > 0 && pool.every((location) => this.unusable.has(location.id));
5708
+ }
5709
+ /**
5710
+ * Announce the terminal state — ONCE per camera per episode.
5711
+ *
5712
+ * Before this, two unusable locations produced an infinite failover
5713
+ * ping-pong: `failoverForUnusableLocation` did not consult the unusable set,
5714
+ * so each attach handed back the other broken disk, `recordDecision` saw a
5715
+ * changed assignment and wrote the store plus an audit row, and the writer
5716
+ * hit EACCES on a path that was never going to work. Measured on the live hub
5717
+ * on 2026-08-19 with the UD disk unmounted: 716 EACCES/min.
5718
+ *
5719
+ * Recording is NOT refused. With a single location this state is reached the
5720
+ * moment that one location fails, and the recorder keeps attaching to it on
5721
+ * purpose — a shouting recorder beats a stopped camera. What changes is that
5722
+ * it shouts once instead of once per attach, and writes nothing durable while
5723
+ * shouting.
5724
+ */
5725
+ reportEveryLocationUnusable(deviceId, profile, target, pool) {
5726
+ this.allUnusableProbedAt ??= this.now;
5727
+ if (this.allUnusableReported.has(deviceId)) return;
5728
+ this.allUnusableReported.add(deviceId);
5729
+ this.deps.logger.warn("recorder placement: EVERY recordings location failed its volume probe — the assignment is FROZEN on its current target and nothing more is written until a disk comes back", {
5730
+ tags: { deviceId },
5731
+ meta: {
5732
+ profile,
5733
+ targetLocationId: target.id,
5734
+ root: target.root,
5735
+ locationIds: pool.map((location) => location.id),
5736
+ reprobeMs: this.reprobeMs
5737
+ }
5738
+ });
5739
+ }
5740
+ /**
5741
+ * While every location is unusable, re-probe them all — at most once per
5742
+ * {@link ALL_UNUSABLE_REPROBE_MS}. This is the backoff: without it the probe
5743
+ * memo would hold for the life of the process (only `onLocationsChanged`
5744
+ * clears it) and a disk remounted by an operator would need an addon restart
5745
+ * to be noticed.
5746
+ */
5747
+ reprobeAfterBackoff(pool) {
5748
+ if (!this.everyLocationUnusable(pool)) {
5749
+ this.allUnusableProbedAt = null;
5750
+ return;
5751
+ }
5752
+ const now = this.now;
5753
+ if (this.allUnusableProbedAt === null) {
5754
+ this.allUnusableProbedAt = now;
5755
+ return;
5756
+ }
5757
+ if (now - this.allUnusableProbedAt < this.reprobeMs) return;
5758
+ this.allUnusableProbedAt = now;
5759
+ this.deps.logger.info("recorder placement: re-probing every storage location after backoff — a disk that came back is picked up here", { meta: {
5760
+ locationIds: pool.map((location) => location.id),
5761
+ backoffMs: this.reprobeMs
5762
+ } });
5763
+ this.unusable.clear();
5764
+ this.verified.clear();
5765
+ }
5766
+ /**
5659
5767
  * The locations placement may write to: operator-ENABLED and alias-guarded.
5660
5768
  * A location the operator has not opted in is still read, swept and drained —
5661
5769
  * it is simply never a write target. Creating a disk must not start writing
@@ -5804,13 +5912,13 @@ var PlacementService = class {
5804
5912
  * ONCE per location per location-set generation — it is a claim, not a poll.
5805
5913
  */
5806
5914
  async ensureUsable(chosen, locations, profile, deviceId) {
5807
- if (this.unusable.has(chosen.id)) return failoverForUnusableLocation(chosen, locations, profile);
5915
+ if (this.unusable.has(chosen.id)) return failoverForUnusableLocation(chosen, locations, profile, this.unusable);
5808
5916
  if (this.verified.has(chosen.id)) return chosen;
5809
5917
  const result = await probeLocationVolume(chosen, (await readPlacementState(this.deps.state)).volumeIds[chosen.id] ?? null, this.probeDeps);
5810
5918
  if (result.outcome === "mismatch") {
5811
5919
  this.unusable.add(chosen.id);
5812
5920
  this.reportMountFault(result, deviceId);
5813
- return failoverForUnusableLocation(chosen, locations, profile);
5921
+ return failoverForUnusableLocation(chosen, locations, profile, this.unusable);
5814
5922
  }
5815
5923
  if (result.outcome === "unreadable") {
5816
5924
  this.deps.logger.warn("recorder placement: volume probe unreadable — assignment unchanged", {
@@ -7924,6 +8032,34 @@ function makeBrokerCall(handle) {
7924
8032
  return (fn) => handle.call(fn, BROKER_READY_TIMEOUT_MS);
7925
8033
  }
7926
8034
  //#endregion
8035
+ //#region src/recorder/addon/location-capacity.ts
8036
+ /** Human-readable failure text for a thrown value of unknown shape. */
8037
+ function reasonOf(err) {
8038
+ return err instanceof Error ? err.message : String(err);
8039
+ }
8040
+ /**
8041
+ * Free + total bytes on the volume backing `root`.
8042
+ *
8043
+ * Never throws: an unreachable volume is one degraded ROW, not a failed
8044
+ * `getStorageUsage` — a NAS that stops answering must not blank the capacity of
8045
+ * every other disk on the node.
8046
+ */
8047
+ async function readLocationCapacity(root, statfs) {
8048
+ try {
8049
+ const st = await statfs(root);
8050
+ return {
8051
+ availableBytes: st.bavail * st.bsize,
8052
+ totalBytes: st.blocks * st.bsize
8053
+ };
8054
+ } catch (err) {
8055
+ return {
8056
+ availableBytes: null,
8057
+ totalBytes: null,
8058
+ error: reasonOf(err)
8059
+ };
8060
+ }
8061
+ }
8062
+ //#endregion
7927
8063
  //#region src/durable/durable-ledger.ts
7928
8064
  /** Default reseed cap — every current consumer's row set is installation-bounded. */
7929
8065
  var DEFAULT_LOAD_LIMIT = 1e5;
@@ -8211,6 +8347,17 @@ var RECORDING_SEGMENT_HOURS_INDEXES = [{
8211
8347
  name: "idx_recorder_segment_hours_hour",
8212
8348
  columns: ["hourStartMs"]
8213
8349
  }];
8350
+ /**
8351
+ * The largest share of a device's held hours a single walk may delete.
8352
+ *
8353
+ * On 2026-08-19 the recorder respawned while `/recordings` was an empty tmpfs —
8354
+ * the UD disk had not mounted — and one walk took the ledger from 873 hours to
8355
+ * 32 (a 96% drop). The ledger is precisely what lets the NEXT boot skip a
8356
+ * ~400 s recursive walk, so a single bad walk taxed every boot after it. A
8357
+ * retention sweep does not delete half a camera's archive in one pass; a
8358
+ * `readdir` over an unmounted root deletes all of it.
8359
+ */
8360
+ var LEDGER_PRUNE_MAX_SHARE = .5;
8214
8361
  function hourStartMs(startMs) {
8215
8362
  return Math.floor(startMs / HOUR_MS$1) * HOUR_MS$1;
8216
8363
  }
@@ -8284,8 +8431,10 @@ var SPEC = {
8284
8431
  var SegmentHourLedger = class {
8285
8432
  ledger;
8286
8433
  logger;
8434
+ isLocationUsable;
8287
8435
  constructor(deps) {
8288
8436
  this.logger = deps.logger;
8437
+ this.isLocationUsable = deps.isLocationUsable ?? (() => true);
8289
8438
  this.ledger = new DurableLedger({
8290
8439
  spec: SPEC,
8291
8440
  store: deps.store,
@@ -8375,6 +8524,10 @@ var SegmentHourLedger = class {
8375
8524
  *
8376
8525
  * Devices not in `deviceIds` are left untouched (a partial walk must not
8377
8526
  * prune cameras it did not look at).
8527
+ *
8528
+ * And a walk this ledger does not TRUST prunes nothing at all — see
8529
+ * {@link refusedPrunes}. What such a walk FOUND is still real, so its paths
8530
+ * are unioned in; what it did not find decides nothing (D49).
8378
8531
  */
8379
8532
  async reconcileFromIndex(index, deviceIds, nowMs) {
8380
8533
  const walked = new Set(deviceIds);
@@ -8396,15 +8549,17 @@ var SegmentHourLedger = class {
8396
8549
  paths: [...existing.paths, seg.path]
8397
8550
  });
8398
8551
  }
8552
+ const refused = this.refusedPrunes(walked, fromIndex);
8399
8553
  for (const row of this.ledger.snapshot()) {
8400
8554
  if (!walked.has(row.deviceId)) continue;
8401
8555
  const disk = fromIndex.get(row.key);
8556
+ const additiveOnly = row.hourStartMs === currentHour || refused.devices.has(row.deviceId) || refused.locations.has(locationKey(row.deviceId, row.locationId));
8402
8557
  if (disk === void 0) {
8403
- if (row.hourStartMs === currentHour) continue;
8558
+ if (additiveOnly) continue;
8404
8559
  await this.ledger.forget(row.key);
8405
8560
  continue;
8406
8561
  }
8407
- if (row.hourStartMs === currentHour) {
8562
+ if (additiveOnly) {
8408
8563
  const union = uniquePaths([...disk.paths, ...row.paths]);
8409
8564
  if (!samePaths(union, row.paths)) await this.ledger.put({
8410
8565
  ...row,
@@ -8419,7 +8574,85 @@ var SegmentHourLedger = class {
8419
8574
  await this.ledger.put(disk);
8420
8575
  }
8421
8576
  }
8577
+ /**
8578
+ * Which of this walk's deletions are REFUSED, and why.
8579
+ *
8580
+ * Two independent reasons, because the live fault of 2026-08-19 would have
8581
+ * escaped either one alone:
8582
+ *
8583
+ * - **the volume said no.** `PlacementService`'s marker probe knows the bytes
8584
+ * under a root are not the bytes we wrote. A walk of such a root found
8585
+ * nothing because nothing is mounted there, not because the footage is
8586
+ * gone.
8587
+ * - **the magnitude said no.** At BOOT the probe has not run yet — it runs at
8588
+ * the first writer attach — so on the morning the recorder respawned onto an
8589
+ * empty tmpfs the volume was not flagged and the prune went through: 873
8590
+ * hours to 32. A walk that deletes more than
8591
+ * {@link LEDGER_PRUNE_MAX_SHARE} of a device's held hours is refused on its
8592
+ * size alone.
8593
+ *
8594
+ * A refusal is one line per device (or per device+location), carrying the
8595
+ * delta, tagged with `deviceId` — a ledger that silently declined to prune and
8596
+ * a ledger with nothing to prune must never read the same.
8597
+ */
8598
+ refusedPrunes(walked, fromIndex) {
8599
+ const heldByDevice = /* @__PURE__ */ new Map();
8600
+ const unusable = /* @__PURE__ */ new Map();
8601
+ for (const row of this.ledger.snapshot()) {
8602
+ if (!walked.has(row.deviceId)) continue;
8603
+ heldByDevice.set(row.deviceId, (heldByDevice.get(row.deviceId) ?? 0) + 1);
8604
+ if (this.isLocationUsable(row.locationId)) continue;
8605
+ const key = locationKey(row.deviceId, row.locationId);
8606
+ const seen = unusable.get(key);
8607
+ unusable.set(key, seen === void 0 ? {
8608
+ deviceId: row.deviceId,
8609
+ locationId: row.locationId,
8610
+ held: 1
8611
+ } : {
8612
+ ...seen,
8613
+ held: seen.held + 1
8614
+ });
8615
+ }
8616
+ const onDiskByDevice = /* @__PURE__ */ new Map();
8617
+ for (const row of fromIndex.values()) onDiskByDevice.set(row.deviceId, (onDiskByDevice.get(row.deviceId) ?? 0) + 1);
8618
+ const devices = /* @__PURE__ */ new Set();
8619
+ for (const [deviceId, held] of heldByDevice) {
8620
+ if (held < 8) continue;
8621
+ const onDisk = onDiskByDevice.get(deviceId) ?? 0;
8622
+ if (onDisk >= held * .5) continue;
8623
+ devices.add(deviceId);
8624
+ this.logger.warn("recorder: segment-hour ledger prune REFUSED — this walk would delete most of the camera archive, which is what an unmounted root looks like; keeping the previous ledger", {
8625
+ tags: { deviceId },
8626
+ meta: {
8627
+ held,
8628
+ onDisk,
8629
+ dropped: held - onDisk,
8630
+ maxShare: LEDGER_PRUNE_MAX_SHARE
8631
+ }
8632
+ });
8633
+ }
8634
+ const locations = /* @__PURE__ */ new Set();
8635
+ for (const [key, entry] of unusable) {
8636
+ if (devices.has(entry.deviceId)) continue;
8637
+ locations.add(key);
8638
+ this.logger.warn("recorder: segment-hour ledger prune REFUSED — the volume under this location failed its probe, so the walk found nothing because nothing is mounted there", {
8639
+ tags: { deviceId: entry.deviceId },
8640
+ meta: {
8641
+ locationId: entry.locationId,
8642
+ held: entry.held,
8643
+ onDisk: onDiskByDevice.get(entry.deviceId) ?? 0
8644
+ }
8645
+ });
8646
+ }
8647
+ return {
8648
+ devices,
8649
+ locations
8650
+ };
8651
+ }
8422
8652
  };
8653
+ function locationKey(deviceId, locationId) {
8654
+ return `${deviceId}|${locationId}`;
8655
+ }
8423
8656
  //#endregion
8424
8657
  //#region src/recorder/addon/recording-export-provider.ts
8425
8658
  /** Content type of every rendered export — the engine only ever writes MP4. */
@@ -8909,7 +9142,9 @@ var macrotask = () => new Promise((resolve) => {
8909
9142
  * unnoticed for six weeks precisely because nothing said anything.
8910
9143
  */
8911
9144
  async function recoverStagingOrphans(dir, input, deps) {
8912
- let names;
9145
+ const chunkSize = deps.chunkSize ?? DEFAULT_CHUNK_SIZE;
9146
+ const yieldBetween = deps.yieldBetween ?? macrotask;
9147
+ let names = null;
8913
9148
  try {
8914
9149
  names = await deps.listDir(dir);
8915
9150
  } catch (err) {
@@ -8917,13 +9152,14 @@ async function recoverStagingOrphans(dir, input, deps) {
8917
9152
  dir,
8918
9153
  error: err instanceof Error ? err.message : String(err)
8919
9154
  } });
8920
- return {
8921
- found: 0,
8922
- recovered: 0,
8923
- failed: 0,
8924
- aborted: false
8925
- };
8926
9155
  }
9156
+ await yieldBetween();
9157
+ if (names === null) return {
9158
+ found: 0,
9159
+ recovered: 0,
9160
+ failed: 0,
9161
+ aborted: false
9162
+ };
8927
9163
  let playlistNames;
8928
9164
  const body = await deps.readPlaylist(dir).catch(() => null);
8929
9165
  if (body !== null) playlistNames = new Set(parseLivePlaylist(body).map((e) => e.segPath.replace(/^.*\//, "")));
@@ -8945,8 +9181,6 @@ async function recoverStagingOrphans(dir, input, deps) {
8945
9181
  oldestMs: plan[0]?.startMs,
8946
9182
  newestMs: plan[plan.length - 1]?.startMs
8947
9183
  } });
8948
- const chunkSize = deps.chunkSize ?? DEFAULT_CHUNK_SIZE;
8949
- const yieldBetween = deps.yieldBetween ?? macrotask;
8950
9184
  let recovered = 0;
8951
9185
  let failed = 0;
8952
9186
  let aborted = false;
@@ -9046,24 +9280,41 @@ async function recoverAllStagedOrphans(deps) {
9046
9280
  locations: locations.map((l) => l.id),
9047
9281
  bootMs: deps.bootMs
9048
9282
  } });
9049
- const work = [];
9050
- for (const location of locations) {
9051
- const stagingRoot = `${location.root}/${STAGING_DIR_NAME}`;
9052
- let deviceDirs;
9283
+ /**
9284
+ * One directory listing on the walk, paced through the pass's own pacer.
9285
+ *
9286
+ * "A handful of listings" is what this walk was costed as, and on a WARM
9287
+ * share it is — 4.1 s for the whole thing. Cold, after a power-cycle, each
9288
+ * one is a ~9.4 s FUSE round-trip and 58 of them summed to 547 s with the
9289
+ * loop to themselves, while the controller re-pinned writers 118 times and
9290
+ * the first recording landed at t+714 s.
9291
+ *
9292
+ * Paced per LISTING and not per entry, because that is where the latency
9293
+ * is; and through the SAME `yieldBetween` the recovery uses, so there is one
9294
+ * pacer, one back-off state and one set of counters for the whole pass —
9295
+ * never a second scheduling authority (D167).
9296
+ */
9297
+ const listPaced = async (dir) => {
9298
+ let entries = null;
9053
9299
  try {
9054
- deviceDirs = await deps.listDir(stagingRoot);
9300
+ entries = await deps.listDir(dir);
9055
9301
  } catch {
9056
- continue;
9302
+ entries = null;
9057
9303
  }
9304
+ await yieldBetween();
9305
+ return entries;
9306
+ };
9307
+ const walkStartedMs = Date.now();
9308
+ const work = [];
9309
+ for (const location of locations) {
9310
+ const stagingRoot = `${location.root}/${STAGING_DIR_NAME}`;
9311
+ const deviceDirs = await listPaced(stagingRoot);
9312
+ if (deviceDirs === null) continue;
9058
9313
  for (const deviceDir of deviceDirs) {
9059
9314
  const deviceId = Number(deviceDir);
9060
9315
  if (!Number.isInteger(deviceId) || deviceId <= 0) continue;
9061
- let profileDirs;
9062
- try {
9063
- profileDirs = await deps.listDir(`${stagingRoot}/${deviceDir}`);
9064
- } catch {
9065
- continue;
9066
- }
9316
+ const profileDirs = await listPaced(`${stagingRoot}/${deviceDir}`);
9317
+ if (profileDirs === null) continue;
9067
9318
  const segmentSeconds = await deps.segmentSecondsFor(deviceId);
9068
9319
  for (const profile of profileDirs) {
9069
9320
  if (!KNOWN_PROFILES.has(profile)) continue;
@@ -9077,6 +9328,7 @@ async function recoverAllStagedOrphans(deps) {
9077
9328
  }
9078
9329
  }
9079
9330
  }
9331
+ const walkMs = Date.now() - walkStartedMs;
9080
9332
  const queue = [...work];
9081
9333
  const lane = async () => {
9082
9334
  for (;;) {
@@ -9119,6 +9371,7 @@ async function recoverAllStagedOrphans(deps) {
9119
9371
  failed,
9120
9372
  aborted,
9121
9373
  ms: Date.now() - startedMs,
9374
+ walkMs,
9122
9375
  ...pacer === null ? {} : { pacing: pacer.stats() }
9123
9376
  } });
9124
9377
  return {
@@ -9371,7 +9624,8 @@ var RecorderV2Addon = class extends BaseAddon {
9371
9624
  }
9372
9625
  this.segmentHours = new SegmentHourLedger({
9373
9626
  store: this.ctx.api.settingsStore,
9374
- logger: this.ctx.logger
9627
+ logger: this.ctx.logger,
9628
+ isLocationUsable: (locationId) => this.placement?.isLocationUsable(locationId) ?? true
9375
9629
  });
9376
9630
  try {
9377
9631
  await this.segmentHours.declare();
@@ -10200,20 +10454,14 @@ var RecorderV2Addon = class extends BaseAddon {
10200
10454
  playbackRoots() {
10201
10455
  return [path.join(this.ctx.dataDir, "playback"), ...this.resolvedLocations.map((l) => l.root)];
10202
10456
  }
10203
- /** Free + total bytes on a location's volume (`statfs`); null when unstattable. */
10457
+ /**
10458
+ * Free + total bytes on a location's volume (`statfs`); null plus a reason
10459
+ * when the volume will not answer. The reason is NOT logged here — the caller
10460
+ * (`getStorageUsage`) knows which location the root belongs to and logs it
10461
+ * against that location.
10462
+ */
10204
10463
  async locationCapacity(root) {
10205
- try {
10206
- const st = await promises.statfs(root);
10207
- return {
10208
- availableBytes: st.bavail * st.bsize,
10209
- totalBytes: st.blocks * st.bsize
10210
- };
10211
- } catch {
10212
- return {
10213
- availableBytes: null,
10214
- totalBytes: null
10215
- };
10216
- }
10464
+ return readLocationCapacity(root, (target) => promises.statfs(target));
10217
10465
  }
10218
10466
  /**
10219
10467
  * Global settings surfaced in the admin UI (and persisted to the addon
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_worker_protocol = require("../worker-protocol-DqrXmX0g.js");
3
- const require_lazy_sharp = require("../lazy-sharp-BVqBydNU.js");
2
+ const require_worker_protocol = require("../worker-protocol-BrlrMxXu.js");
3
+ const require_lazy_sharp = require("../lazy-sharp-oYppDIZR.js");
4
4
  //#region src/session-decode/color-conversion.ts
5
5
  /**
6
6
  * Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
@@ -1,4 +1,4 @@
1
- import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-DTe7Ntat.mjs";
1
+ import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-DJWdPLKF.mjs";
2
2
  import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
3
3
  //#region src/session-decode/color-conversion.ts
4
4
  /**
@@ -1,7 +1,7 @@
1
1
  import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, p as c, r as l, s as u, t as d, u as f } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DsIi4G5Q.mjs";
2
2
  import { a as p, i as m, n as h, o as g, r as _, t as v } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs";
3
3
  import { n as y, r as b, t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs";
4
- import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D2WOTeYi.mjs";
4
+ import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-zT-cYUdF.mjs";
5
5
  import { n as C, t as w } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs";
6
6
  //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
7
7
  var T = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), E = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), D = (e) => {
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.24",
6
+ version: "1.2.25",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_stream_broker_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.88",
21
+ version: "1.2.90",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_stream_broker_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.60",
36
+ version: "1.2.62",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_stream_broker_widgets",