camstack 1.2.112 → 1.2.113

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-jZb-HKI5.js
23637
- var require_dist_jZb_HKI5 = __commonJS({
23638
- "../system/dist/dist-jZb-HKI5.js"(exports) {
23636
+ // ../system/dist/dist-BFvWv_bz.js
23637
+ var require_dist_BFvWv_bz = __commonJS({
23638
+ "../system/dist/dist-BFvWv_bz.js"(exports) {
23639
23639
  "use strict";
23640
23640
  var zod = require_zod();
23641
23641
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
@@ -23684,6 +23684,7 @@ var require_dist_jZb_HKI5 = __commonJS({
23684
23684
  EventCategory2["ProcessCrashed"] = "process.crashed";
23685
23685
  EventCategory2["ProcessRestartScheduled"] = "process.restart_scheduled";
23686
23686
  EventCategory2["ProcessRestarted"] = "process.restarted";
23687
+ EventCategory2["StorageLocationsChanged"] = "storage.locations-changed";
23687
23688
  EventCategory2["RecordingStarted"] = "recording.started";
23688
23689
  EventCategory2["RecordingStopped"] = "recording.stopped";
23689
23690
  EventCategory2["RecordingError"] = "recording.error";
@@ -26656,6 +26657,71 @@ var require_dist_jZb_HKI5 = __commonJS({
26656
26657
  error: zod.z.string().nullable()
26657
26658
  });
26658
26659
  var StorageCleanupStatusInputSchema = zod.z.object({ jobId: zod.z.string().optional() });
26660
+ var STORAGE_LOCATION_MODES = [
26661
+ "active",
26662
+ "readonly",
26663
+ "drain",
26664
+ "disabled"
26665
+ ];
26666
+ var StorageLocationModeSchema = zod.z.enum(STORAGE_LOCATION_MODES);
26667
+ zod.z.enum([
26668
+ "normal",
26669
+ "never",
26670
+ "drain"
26671
+ ]);
26672
+ var LEGACY_READ_ONLY_CONFIG_KEY = "readOnly";
26673
+ function modeMayWrite(mode) {
26674
+ return mode === "active";
26675
+ }
26676
+ function modeMayRead(mode) {
26677
+ return mode !== "disabled";
26678
+ }
26679
+ function evictionPolicyForMode(mode) {
26680
+ switch (mode) {
26681
+ case "active":
26682
+ return "normal";
26683
+ case "drain":
26684
+ return "drain";
26685
+ case "readonly":
26686
+ case "disabled":
26687
+ return "never";
26688
+ }
26689
+ }
26690
+ function legacyModeOf(location) {
26691
+ if (location.mode !== void 0) return null;
26692
+ if (location.config["readOnly"] === true) return "readonly";
26693
+ if (location.enabled === false) return "readonly";
26694
+ return null;
26695
+ }
26696
+ function resolveLocationMode(location) {
26697
+ return (isStorageLocationMode(location.mode) ? location.mode : void 0) ?? legacyModeOf(location) ?? "active";
26698
+ }
26699
+ function isStorageLocationMode(value) {
26700
+ return STORAGE_LOCATION_MODES.some((mode) => mode === value);
26701
+ }
26702
+ function mayWriteToLocation(location) {
26703
+ return modeMayWrite(resolveLocationMode(location));
26704
+ }
26705
+ function mayReadLocation(location) {
26706
+ return modeMayRead(resolveLocationMode(location));
26707
+ }
26708
+ function evictionPolicyOfLocation(location) {
26709
+ return evictionPolicyForMode(resolveLocationMode(location));
26710
+ }
26711
+ function strippedOfLegacyReadOnly(config) {
26712
+ if (!("readOnly" in config)) return config;
26713
+ const next = { ...config };
26714
+ delete next[LEGACY_READ_ONLY_CONFIG_KEY];
26715
+ return next;
26716
+ }
26717
+ function withLocationMode(location, mode) {
26718
+ return {
26719
+ ...location,
26720
+ mode,
26721
+ enabled: modeMayWrite(mode),
26722
+ config: strippedOfLegacyReadOnly(location.config)
26723
+ };
26724
+ }
26659
26725
  var StorageLocationTypeSchema = zod.z.string().regex(/^[a-z][a-zA-Z0-9-]*$/);
26660
26726
  var StorageLocationSchema = zod.z.object({
26661
26727
  id: zod.z.string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/),
@@ -26687,6 +26753,21 @@ var require_dist_jZb_HKI5 = __commonJS({
26687
26753
  * stops existing rather than being re-derived on every read.
26688
26754
  */
26689
26755
  enabled: zod.z.boolean().optional(),
26756
+ /**
26757
+ * THE state of this location (D385), and the only authority on what may be
26758
+ * written, read or evicted here. Interpreted in exactly one place —
26759
+ * `storage-location-mode.ts` — which also folds the legacy
26760
+ * `enabled` / `config.readOnly` pair into a mode so an old row is never
26761
+ * ambiguous.
26762
+ *
26763
+ * OPTIONAL only for the wire and for rows written before D385: absence is
26764
+ * resolved by `resolveLocationMode`, and the orchestrator stamps every
26765
+ * unstamped row ONCE at hydrate so absence stops existing rather than being
26766
+ * re-derived on every read. `enabled` survives one release as a DERIVED
26767
+ * mirror (`mode === 'active'`); `withLocationMode` is the only writer of
26768
+ * either, so the two cannot disagree.
26769
+ */
26770
+ mode: StorageLocationModeSchema.optional(),
26690
26771
  /** COMPUTED at read time by the orchestrator (statfs of the backing volume
26691
26772
  * for node-local locations it can reach) — never persisted, absent when the
26692
26773
  * volume is remote/unreachable. The single capacity truth every UI reads. */
@@ -26694,13 +26775,47 @@ var require_dist_jZb_HKI5 = __commonJS({
26694
26775
  totalBytes: zod.z.number(),
26695
26776
  availableBytes: zod.z.number()
26696
26777
  }).nullable().optional(),
26778
+ /**
26779
+ * How much of that volume CamStack ITSELF holds on this location (D388) —
26780
+ * COMPUTED at read time from the `storage-occupancy` providers' own figures,
26781
+ * never persisted, never a filesystem walk.
26782
+ *
26783
+ * **ABSENT MEANS UNKNOWN, never zero.** No provider has reported for this
26784
+ * location yet — nobody stores here, the owning addon is down, or the first
26785
+ * refresh has not completed. A UI must omit the segment rather than draw it
26786
+ * at zero, which would claim we occupy nothing (D315). It is an OBJECT and
26787
+ * not a bare number precisely so that a `?? 0` on the consuming side has to
26788
+ * be spelled out loud instead of appearing by accident.
26789
+ *
26790
+ * `measuredAtMs` is the OLDEST contributing measurement, so it is honest
26791
+ * about the whole figure rather than about its freshest part.
26792
+ */
26793
+ owned: zod.z.object({
26794
+ bytes: zod.z.number().int().nonnegative(),
26795
+ measuredAtMs: zod.z.number().int().nonnegative()
26796
+ }).optional(),
26697
26797
  createdAt: zod.z.number(),
26698
26798
  updatedAt: zod.z.number()
26699
26799
  });
26700
26800
  var LegacyStorageLocationDefaultSchema = zod.z.object({ isDefault: zod.z.boolean().optional() });
26701
26801
  function isLocationEnabled(location) {
26702
- return location.enabled !== false;
26802
+ return mayWriteToLocation({
26803
+ ...location,
26804
+ config: location.config ?? {}
26805
+ });
26806
+ }
26807
+ var STORAGE_BYTES_PER_GB = 1024 ** 3;
26808
+ function gbToBytes(gb) {
26809
+ return Math.floor(gb * STORAGE_BYTES_PER_GB);
26703
26810
  }
26811
+ var StorageDrainProgressSchema = zod.z.object({
26812
+ locationId: zod.z.string(),
26813
+ startedAtMs: zod.z.number(),
26814
+ startBytes: zod.z.number(),
26815
+ bytesRemaining: zod.z.number(),
26816
+ drained: zod.z.boolean(),
26817
+ estimatedEmptyAtMs: zod.z.number().nullable()
26818
+ });
26704
26819
  var StorageLocationRefSchema = zod.z.union([StorageLocationTypeSchema, zod.z.string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/)]);
26705
26820
  var StorageAccessSchema = zod.z.enum(["local-path", "cap-mediated"]);
26706
26821
  var STORAGE_ACCESS_FALLBACK = "local-path";
@@ -40702,6 +40817,12 @@ var require_dist_jZb_HKI5 = __commonJS({
40702
40817
  kind: "mutation",
40703
40818
  auth: "admin"
40704
40819
  }),
40820
+ /**
40821
+ * How far each draining location has got (D386). A pure READ of what the
40822
+ * last pressure sweep computed — it starts no work, and a location that is
40823
+ * not draining simply does not appear.
40824
+ */
40825
+ listDrainProgress: method(zod.z.void(), zod.z.array(StorageDrainProgressSchema).readonly()),
40705
40826
  testLocation: method(zod.z.object({ id: zod.z.string() }), zod.z.object({
40706
40827
  ok: zod.z.boolean(),
40707
40828
  error: zod.z.string().optional()
@@ -40831,6 +40952,37 @@ var require_dist_jZb_HKI5 = __commonJS({
40831
40952
  history: method(zod.z.object({}), zod.z.array(StorageMigrationJobSchema).readonly(), { auth: "admin" })
40832
40953
  }
40833
40954
  };
40955
+ var StorageOccupancyReportSchema = zod.z.object({
40956
+ locationId: zod.z.string(),
40957
+ /** Bytes this provider holds on THAT location — not its eviction domain, and
40958
+ * not net of what it is willing to delete. */
40959
+ ownedBytes: zod.z.number().int().nonnegative(),
40960
+ /** When the provider last actually measured this. The orchestrator carries it
40961
+ * through so a UI can say how old the figure is instead of implying "now". */
40962
+ measuredAtMs: zod.z.number().int().nonnegative()
40963
+ });
40964
+ var storageOccupancyCapability = {
40965
+ name: "storage-occupancy",
40966
+ scope: "system",
40967
+ mode: "collection",
40968
+ internal: true,
40969
+ methods: {
40970
+ /**
40971
+ * Occupancy for the given locations, in ONE round trip.
40972
+ *
40973
+ * A provider answers only for the locations it actually holds bytes on and
40974
+ * OMITS the rest — an omitted location is "I hold nothing measurable here",
40975
+ * which the orchestrator merges as a contribution of nothing rather than as
40976
+ * a claim that the location is empty. Only a location no provider reports
40977
+ * at all stays unknown.
40978
+ *
40979
+ * This must be CHEAP and must never walk a filesystem: it is on the admin
40980
+ * UI's `listLocations` path. The owner keeps its own figure fresh (D224) and
40981
+ * answers from what it already has.
40982
+ */
40983
+ getOccupancy: method(zod.z.object({ locationIds: zod.z.array(zod.z.string()).readonly() }), zod.z.array(StorageOccupancyReportSchema).readonly(), { auth: "admin" })
40984
+ }
40985
+ };
40834
40986
  var ProviderInfoSchema = zod.z.discriminatedUnion("shouldSaveDiskSpace", [zod.z.object({
40835
40987
  providerId: zod.z.string().min(1),
40836
40988
  displayName: zod.z.string().min(1),
@@ -42788,83 +42940,6 @@ var require_dist_jZb_HKI5 = __commonJS({
42788
42940
  * whether persisting is worth a SQLite commit. */
42789
42941
  volatileStateFields: ["lastUpdated"]
42790
42942
  };
42791
- var NetworkLinkStatusSchema = zod.z.object({
42792
- /** The link the device is on. `'unknown'` = not read yet, not "no link". */
42793
- type: zod.z.enum([
42794
- "wifi",
42795
- "ethernet",
42796
- "cellular",
42797
- "unknown"
42798
- ]),
42799
- /**
42800
- * Link quality, 0..100 inclusive, normalised by the provider from whatever
42801
- * the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
42802
- * KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
42803
- * one whose reading has not landed must not be drawn at 0 %. Consumers
42804
- * SKIP a null rather than coerce it.
42805
- */
42806
- signalPercent: zod.z.number().min(0).max(100).nullable(),
42807
- /** Raw received signal strength in dBm, when the firmware reports one. */
42808
- rssiDbm: zod.z.number().optional(),
42809
- /** Network name of a wireless link, when the firmware reports it. */
42810
- ssid: zod.z.string().optional(),
42811
- /** Ms epoch of the last observation. Lets consumers reason about freshness. */
42812
- lastUpdated: zod.z.number()
42813
- });
42814
- var networkLinkCapability = {
42815
- name: "network-link",
42816
- scope: "device",
42817
- deviceNative: true,
42818
- mode: "singleton",
42819
- deviceTypes: [
42820
- DeviceType.Camera,
42821
- DeviceType.Sensor,
42822
- DeviceType.Button,
42823
- DeviceType.Switch,
42824
- DeviceType.Light,
42825
- DeviceType.Lock,
42826
- DeviceType.Siren
42827
- ],
42828
- methods: {},
42829
- events: {
42830
- /**
42831
- * Emitted whenever the cached status changes (a link switch, a signal
42832
- * reading that moved). Mirrored on the parent chain by the
42833
- * DeviceEventPropagator like `battery.onStatusChanged`.
42834
- */
42835
- onStatusChanged: { data: zod.z.object({
42836
- deviceId: zod.z.number(),
42837
- status: NetworkLinkStatusSchema
42838
- }) }
42839
- },
42840
- status: {
42841
- schema: NetworkLinkStatusSchema,
42842
- kind: "push",
42843
- empty: {
42844
- type: "unknown",
42845
- signalPercent: null,
42846
- lastUpdated: 0
42847
- }
42848
- },
42849
- /**
42850
- * Runtime-state slice — every provider stores the same shape under
42851
- * `device.runtimeState['network-link']`, read once by the badge and the
42852
- * Home Assistant projector regardless of the driver.
42853
- */
42854
- runtimeState: NetworkLinkStatusSchema,
42855
- /**
42856
- * Runtime-state durability: **restored** — a link reading is slow to
42857
- * change and a sleeping battery camera may not report for hours; the
42858
- * restored slice is what the badge shows until the next read.
42859
- *
42860
- * See `RuntimeStateDurability`. Enforced by
42861
- * `scripts/check-runtime-state-durability.ts`.
42862
- */
42863
- durability: "restored",
42864
- /** Clock fields: written, but excluded from the compare that decides
42865
- * whether persisting is worth a SQLite commit. */
42866
- volatileStateFields: ["lastUpdated"]
42867
- };
42868
42943
  var BinaryStatusSchema = zod.z.object({
42869
42944
  on: zod.z.boolean(),
42870
42945
  /** Ms epoch of the last transition. 0 if never observed. */
@@ -46005,6 +46080,246 @@ var require_dist_jZb_HKI5 = __commonJS({
46005
46080
  * whether persisting is worth a SQLite commit. */
46006
46081
  volatileStateFields: ["lastFetchedAt"]
46007
46082
  };
46083
+ var NavigationMoveCommandSchema = zod.z.object({
46084
+ pan: zod.z.number().min(-1).max(1).optional(),
46085
+ tilt: zod.z.number().min(-1).max(1).optional(),
46086
+ speed: zod.z.number().min(0).max(1).optional()
46087
+ });
46088
+ var NavigationActionIdSchema = zod.z.enum([
46089
+ "goHome",
46090
+ "locate",
46091
+ "spotClean",
46092
+ "findPet",
46093
+ "personFollow",
46094
+ "stop",
46095
+ "startClean",
46096
+ "pauseClean",
46097
+ "dockWash",
46098
+ "autoEmpty",
46099
+ "flashOn",
46100
+ "flashOff"
46101
+ ]);
46102
+ var NavigationEntryKindSchema = zod.z.enum(["action", "sound"]);
46103
+ var NavigationActionEntrySchema = zod.z.object({
46104
+ id: zod.z.string(),
46105
+ kind: NavigationEntryKindSchema,
46106
+ label: zod.z.string(),
46107
+ icon: zod.z.string(),
46108
+ /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
46109
+ soundId: zod.z.number().int().optional(),
46110
+ /** Per-device feature flag — render this entry only when true. */
46111
+ enabled: zod.z.boolean()
46112
+ });
46113
+ var NavigationPointSchema = zod.z.object({
46114
+ x: zod.z.number(),
46115
+ y: zod.z.number()
46116
+ });
46117
+ var NavigationFeaturesSchema = zod.z.object({
46118
+ move: zod.z.boolean(),
46119
+ stop: zod.z.boolean(),
46120
+ goToPoint: zod.z.boolean(),
46121
+ runAction: zod.z.boolean(),
46122
+ playSound: zod.z.boolean(),
46123
+ light: zod.z.boolean(),
46124
+ lightMode: zod.z.boolean()
46125
+ });
46126
+ var NavigationLightModeSchema = zod.z.enum(["auto", "manual"]);
46127
+ var NavigationModeSchema = zod.z.enum([
46128
+ "idle",
46129
+ "cleaning",
46130
+ "spot",
46131
+ "following",
46132
+ "goto",
46133
+ "returning",
46134
+ "paused",
46135
+ "unknown"
46136
+ ]);
46137
+ var NavigationStatusSchema = zod.z.object({
46138
+ mode: NavigationModeSchema,
46139
+ following: zod.z.boolean(),
46140
+ flash: zod.z.boolean(),
46141
+ lightMode: NavigationLightModeSchema,
46142
+ lightLevel: zod.z.number().min(40).max(100),
46143
+ /** Ms epoch when the slice was last updated. */
46144
+ lastChangedAt: zod.z.number()
46145
+ });
46146
+ var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: zod.z.number() });
46147
+ var navigationCapability = {
46148
+ name: "navigation",
46149
+ scope: "device",
46150
+ deviceNative: true,
46151
+ mode: "singleton",
46152
+ deviceTypes: [DeviceType.Camera],
46153
+ deviceConfig: { ui: {
46154
+ kind: "widget",
46155
+ widgetId: "host/navigation-panel",
46156
+ tab: "navigation",
46157
+ topTab: true,
46158
+ label: "Navigation",
46159
+ order: 0
46160
+ } },
46161
+ methods: {
46162
+ /**
46163
+ * Momentary drive nudge (the robot moves). `protected` — mirrors
46164
+ * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
46165
+ * path) works for any authenticated user, not admin-only. The UI sends
46166
+ * these at ~1 Hz while a control is held; the provider forwards each one to
46167
+ * a single drive write WITHOUT debouncing.
46168
+ */
46169
+ move: method(NavigationMoveCommandSchema.extend({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
46170
+ /** Halt all motion immediately (zero drive vector). */
46171
+ stop: method(zod.z.object({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
46172
+ /** Send the robot to a point on its live map. */
46173
+ goToPoint: method(NavigationPointSchema.extend({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
46174
+ /**
46175
+ * Enumerate the discrete controls THIS device supports (data-driven UI +
46176
+ * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
46177
+ */
46178
+ listActions: method(zod.z.object({ deviceId: zod.z.number() }), zod.z.array(NavigationActionEntrySchema)),
46179
+ /**
46180
+ * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
46181
+ * unsupported action ids are rejected by the provider.
46182
+ */
46183
+ runAction: method(zod.z.object({
46184
+ deviceId: zod.z.number(),
46185
+ actionId: NavigationActionIdSchema
46186
+ }), zod.z.void(), { kind: "mutation" }),
46187
+ /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
46188
+ playSound: method(zod.z.object({
46189
+ deviceId: zod.z.number(),
46190
+ soundId: zod.z.number().int()
46191
+ }), zod.z.void(), { kind: "mutation" }),
46192
+ /**
46193
+ * Turn the on-camera fill light on / off (the `OpenFullLight` control —
46194
+ * works anytime, no active stream required).
46195
+ */
46196
+ setLightOn: method(zod.z.object({
46197
+ deviceId: zod.z.number(),
46198
+ on: zod.z.boolean()
46199
+ }), zod.z.void(), { kind: "mutation" }),
46200
+ /**
46201
+ * Set the fill-light mode (auto vs manual). `manual` optionally carries the
46202
+ * initial `level`. The auto/manual + level control is a CAMERA-service
46203
+ * action that generally needs an active camera stream/monitor session — the
46204
+ * UI shows the manual level slider ONLY when `mode === 'manual'`.
46205
+ */
46206
+ setLightMode: method(zod.z.object({
46207
+ deviceId: zod.z.number(),
46208
+ mode: NavigationLightModeSchema,
46209
+ level: zod.z.number().min(40).max(100).optional()
46210
+ }), zod.z.void(), { kind: "mutation" }),
46211
+ /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
46212
+ setLightLevel: method(zod.z.object({
46213
+ deviceId: zod.z.number(),
46214
+ level: zod.z.number().min(40).max(100)
46215
+ }), zod.z.void(), { kind: "mutation" }),
46216
+ /**
46217
+ * Per-device FEATURE-FLAG report for the general primitives — drives which
46218
+ * controls the UI shows (the per-entry flags for the dictionary come back on
46219
+ * `listActions`).
46220
+ */
46221
+ getFeatures: method(zod.z.object({ deviceId: zod.z.number() }), NavigationFeaturesSchema)
46222
+ },
46223
+ events: { onStatusChanged: { data: zod.z.object({
46224
+ deviceId: zod.z.number(),
46225
+ status: NavigationStatusSchema
46226
+ }) } },
46227
+ status: {
46228
+ schema: NavigationStatusSchema,
46229
+ kind: "push"
46230
+ },
46231
+ /**
46232
+ * Runtime-state slice mirrored by the kernel. The navigation panel watches it
46233
+ * for live mode / follow / flash changes.
46234
+ */
46235
+ runtimeState: NavigationRuntimeStateSchema,
46236
+ /**
46237
+ * Runtime-state durability: **session** — like `vacuum-control`, a restored
46238
+ * `mode: cleaning` / `following: true` is a robot that is not actually doing
46239
+ * that. The live handle re-publishes on connect.
46240
+ *
46241
+ * See `RuntimeStateDurability`. Enforced by
46242
+ * `scripts/check-runtime-state-durability.ts`.
46243
+ */
46244
+ durability: "session"
46245
+ };
46246
+ var NetworkLinkStatusSchema = zod.z.object({
46247
+ /** The link the device is on. `'unknown'` = not read yet, not "no link". */
46248
+ type: zod.z.enum([
46249
+ "wifi",
46250
+ "ethernet",
46251
+ "cellular",
46252
+ "unknown"
46253
+ ]),
46254
+ /**
46255
+ * Link quality, 0..100 inclusive, normalised by the provider from whatever
46256
+ * the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
46257
+ * KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
46258
+ * one whose reading has not landed must not be drawn at 0 %. Consumers
46259
+ * SKIP a null rather than coerce it.
46260
+ */
46261
+ signalPercent: zod.z.number().min(0).max(100).nullable(),
46262
+ /** Raw received signal strength in dBm, when the firmware reports one. */
46263
+ rssiDbm: zod.z.number().optional(),
46264
+ /** Network name of a wireless link, when the firmware reports it. */
46265
+ ssid: zod.z.string().optional(),
46266
+ /** Ms epoch of the last observation. Lets consumers reason about freshness. */
46267
+ lastUpdated: zod.z.number()
46268
+ });
46269
+ var networkLinkCapability = {
46270
+ name: "network-link",
46271
+ scope: "device",
46272
+ deviceNative: true,
46273
+ mode: "singleton",
46274
+ deviceTypes: [
46275
+ DeviceType.Camera,
46276
+ DeviceType.Sensor,
46277
+ DeviceType.Button,
46278
+ DeviceType.Switch,
46279
+ DeviceType.Light,
46280
+ DeviceType.Lock,
46281
+ DeviceType.Siren
46282
+ ],
46283
+ methods: {},
46284
+ events: {
46285
+ /**
46286
+ * Emitted whenever the cached status changes (a link switch, a signal
46287
+ * reading that moved). Mirrored on the parent chain by the
46288
+ * DeviceEventPropagator like `battery.onStatusChanged`.
46289
+ */
46290
+ onStatusChanged: { data: zod.z.object({
46291
+ deviceId: zod.z.number(),
46292
+ status: NetworkLinkStatusSchema
46293
+ }) }
46294
+ },
46295
+ status: {
46296
+ schema: NetworkLinkStatusSchema,
46297
+ kind: "push",
46298
+ empty: {
46299
+ type: "unknown",
46300
+ signalPercent: null,
46301
+ lastUpdated: 0
46302
+ }
46303
+ },
46304
+ /**
46305
+ * Runtime-state slice — every provider stores the same shape under
46306
+ * `device.runtimeState['network-link']`, read once by the badge and the
46307
+ * Home Assistant projector regardless of the driver.
46308
+ */
46309
+ runtimeState: NetworkLinkStatusSchema,
46310
+ /**
46311
+ * Runtime-state durability: **restored** — a link reading is slow to
46312
+ * change and a sleeping battery camera may not report for hours; the
46313
+ * restored slice is what the badge shows until the next read.
46314
+ *
46315
+ * See `RuntimeStateDurability`. Enforced by
46316
+ * `scripts/check-runtime-state-durability.ts`.
46317
+ */
46318
+ durability: "restored",
46319
+ /** Clock fields: written, but excluded from the compare that decides
46320
+ * whether persisting is worth a SQLite commit. */
46321
+ volatileStateFields: ["lastUpdated"]
46322
+ };
46008
46323
  var StreamNetworkStatsSchema = zod.z.object({
46009
46324
  nominalBitrateKbps: zod.z.number(),
46010
46325
  observedBitrateKbps: zod.z.number(),
@@ -47570,169 +47885,6 @@ var require_dist_jZb_HKI5 = __commonJS({
47570
47885
  */
47571
47886
  durability: "session"
47572
47887
  };
47573
- var NavigationMoveCommandSchema = zod.z.object({
47574
- pan: zod.z.number().min(-1).max(1).optional(),
47575
- tilt: zod.z.number().min(-1).max(1).optional(),
47576
- speed: zod.z.number().min(0).max(1).optional()
47577
- });
47578
- var NavigationActionIdSchema = zod.z.enum([
47579
- "goHome",
47580
- "locate",
47581
- "spotClean",
47582
- "findPet",
47583
- "personFollow",
47584
- "stop",
47585
- "startClean",
47586
- "pauseClean",
47587
- "dockWash",
47588
- "autoEmpty",
47589
- "flashOn",
47590
- "flashOff"
47591
- ]);
47592
- var NavigationEntryKindSchema = zod.z.enum(["action", "sound"]);
47593
- var NavigationActionEntrySchema = zod.z.object({
47594
- id: zod.z.string(),
47595
- kind: NavigationEntryKindSchema,
47596
- label: zod.z.string(),
47597
- icon: zod.z.string(),
47598
- /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
47599
- soundId: zod.z.number().int().optional(),
47600
- /** Per-device feature flag — render this entry only when true. */
47601
- enabled: zod.z.boolean()
47602
- });
47603
- var NavigationPointSchema = zod.z.object({
47604
- x: zod.z.number(),
47605
- y: zod.z.number()
47606
- });
47607
- var NavigationFeaturesSchema = zod.z.object({
47608
- move: zod.z.boolean(),
47609
- stop: zod.z.boolean(),
47610
- goToPoint: zod.z.boolean(),
47611
- runAction: zod.z.boolean(),
47612
- playSound: zod.z.boolean(),
47613
- light: zod.z.boolean(),
47614
- lightMode: zod.z.boolean()
47615
- });
47616
- var NavigationLightModeSchema = zod.z.enum(["auto", "manual"]);
47617
- var NavigationModeSchema = zod.z.enum([
47618
- "idle",
47619
- "cleaning",
47620
- "spot",
47621
- "following",
47622
- "goto",
47623
- "returning",
47624
- "paused",
47625
- "unknown"
47626
- ]);
47627
- var NavigationStatusSchema = zod.z.object({
47628
- mode: NavigationModeSchema,
47629
- following: zod.z.boolean(),
47630
- flash: zod.z.boolean(),
47631
- lightMode: NavigationLightModeSchema,
47632
- lightLevel: zod.z.number().min(40).max(100),
47633
- /** Ms epoch when the slice was last updated. */
47634
- lastChangedAt: zod.z.number()
47635
- });
47636
- var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: zod.z.number() });
47637
- var navigationCapability = {
47638
- name: "navigation",
47639
- scope: "device",
47640
- deviceNative: true,
47641
- mode: "singleton",
47642
- deviceTypes: [DeviceType.Camera],
47643
- deviceConfig: { ui: {
47644
- kind: "widget",
47645
- widgetId: "host/navigation-panel",
47646
- tab: "navigation",
47647
- topTab: true,
47648
- label: "Navigation",
47649
- order: 0
47650
- } },
47651
- methods: {
47652
- /**
47653
- * Momentary drive nudge (the robot moves). `protected` — mirrors
47654
- * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
47655
- * path) works for any authenticated user, not admin-only. The UI sends
47656
- * these at ~1 Hz while a control is held; the provider forwards each one to
47657
- * a single drive write WITHOUT debouncing.
47658
- */
47659
- move: method(NavigationMoveCommandSchema.extend({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
47660
- /** Halt all motion immediately (zero drive vector). */
47661
- stop: method(zod.z.object({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
47662
- /** Send the robot to a point on its live map. */
47663
- goToPoint: method(NavigationPointSchema.extend({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
47664
- /**
47665
- * Enumerate the discrete controls THIS device supports (data-driven UI +
47666
- * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
47667
- */
47668
- listActions: method(zod.z.object({ deviceId: zod.z.number() }), zod.z.array(NavigationActionEntrySchema)),
47669
- /**
47670
- * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
47671
- * unsupported action ids are rejected by the provider.
47672
- */
47673
- runAction: method(zod.z.object({
47674
- deviceId: zod.z.number(),
47675
- actionId: NavigationActionIdSchema
47676
- }), zod.z.void(), { kind: "mutation" }),
47677
- /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
47678
- playSound: method(zod.z.object({
47679
- deviceId: zod.z.number(),
47680
- soundId: zod.z.number().int()
47681
- }), zod.z.void(), { kind: "mutation" }),
47682
- /**
47683
- * Turn the on-camera fill light on / off (the `OpenFullLight` control —
47684
- * works anytime, no active stream required).
47685
- */
47686
- setLightOn: method(zod.z.object({
47687
- deviceId: zod.z.number(),
47688
- on: zod.z.boolean()
47689
- }), zod.z.void(), { kind: "mutation" }),
47690
- /**
47691
- * Set the fill-light mode (auto vs manual). `manual` optionally carries the
47692
- * initial `level`. The auto/manual + level control is a CAMERA-service
47693
- * action that generally needs an active camera stream/monitor session — the
47694
- * UI shows the manual level slider ONLY when `mode === 'manual'`.
47695
- */
47696
- setLightMode: method(zod.z.object({
47697
- deviceId: zod.z.number(),
47698
- mode: NavigationLightModeSchema,
47699
- level: zod.z.number().min(40).max(100).optional()
47700
- }), zod.z.void(), { kind: "mutation" }),
47701
- /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
47702
- setLightLevel: method(zod.z.object({
47703
- deviceId: zod.z.number(),
47704
- level: zod.z.number().min(40).max(100)
47705
- }), zod.z.void(), { kind: "mutation" }),
47706
- /**
47707
- * Per-device FEATURE-FLAG report for the general primitives — drives which
47708
- * controls the UI shows (the per-entry flags for the dictionary come back on
47709
- * `listActions`).
47710
- */
47711
- getFeatures: method(zod.z.object({ deviceId: zod.z.number() }), NavigationFeaturesSchema)
47712
- },
47713
- events: { onStatusChanged: { data: zod.z.object({
47714
- deviceId: zod.z.number(),
47715
- status: NavigationStatusSchema
47716
- }) } },
47717
- status: {
47718
- schema: NavigationStatusSchema,
47719
- kind: "push"
47720
- },
47721
- /**
47722
- * Runtime-state slice mirrored by the kernel. The navigation panel watches it
47723
- * for live mode / follow / flash changes.
47724
- */
47725
- runtimeState: NavigationRuntimeStateSchema,
47726
- /**
47727
- * Runtime-state durability: **session** — like `vacuum-control`, a restored
47728
- * `mode: cleaning` / `following: true` is a robot that is not actually doing
47729
- * that. The live handle re-publishes on connect.
47730
- *
47731
- * See `RuntimeStateDurability`. Enforced by
47732
- * `scripts/check-runtime-state-durability.ts`.
47733
- */
47734
- durability: "session"
47735
- };
47736
47888
  var rebootCapability = {
47737
47889
  name: "reboot",
47738
47890
  scope: "device",
@@ -51257,6 +51409,7 @@ var require_dist_jZb_HKI5 = __commonJS({
51257
51409
  storageCapability,
51258
51410
  storageEvictableCapability,
51259
51411
  storageMigrationCapability,
51412
+ storageOccupancyCapability,
51260
51413
  storageProviderCapability,
51261
51414
  streamBrokerCapability,
51262
51415
  streamCatalogCapability,
@@ -56229,6 +56382,12 @@ var require_dist_jZb_HKI5 = __commonJS({
56229
56382
  addonId: null,
56230
56383
  access: "view"
56231
56384
  },
56385
+ "storage.listDrainProgress": {
56386
+ capName: "storage",
56387
+ capScope: "system",
56388
+ addonId: null,
56389
+ access: "view"
56390
+ },
56232
56391
  "storage.listLocationDeclarations": {
56233
56392
  capName: "storage",
56234
56393
  capScope: "system",
@@ -56373,6 +56532,12 @@ var require_dist_jZb_HKI5 = __commonJS({
56373
56532
  addonId: null,
56374
56533
  access: "view"
56375
56534
  },
56535
+ "storageOccupancy.getOccupancy": {
56536
+ capName: "storage-occupancy",
56537
+ capScope: "system",
56538
+ addonId: null,
56539
+ access: "view"
56540
+ },
56376
56541
  "storageProvider.abortUpload": {
56377
56542
  capName: "storage-provider",
56378
56543
  capScope: "system",
@@ -60186,6 +60351,12 @@ var require_dist_jZb_HKI5 = __commonJS({
60186
60351
  return StorageCleanupJobSchema;
60187
60352
  }
60188
60353
  });
60354
+ Object.defineProperty(exports, "StorageLocationModeSchema", {
60355
+ enumerable: true,
60356
+ get: function() {
60357
+ return StorageLocationModeSchema;
60358
+ }
60359
+ });
60189
60360
  Object.defineProperty(exports, "StorageLocationTypeSchema", {
60190
60361
  enumerable: true,
60191
60362
  get: function() {
@@ -60396,6 +60567,12 @@ var require_dist_jZb_HKI5 = __commonJS({
60396
60567
  return evaluateSensorEdge;
60397
60568
  }
60398
60569
  });
60570
+ Object.defineProperty(exports, "evictionPolicyOfLocation", {
60571
+ enumerable: true,
60572
+ get: function() {
60573
+ return evictionPolicyOfLocation;
60574
+ }
60575
+ });
60399
60576
  Object.defineProperty(exports, "expandCapMethods", {
60400
60577
  enumerable: true,
60401
60578
  get: function() {
@@ -60420,6 +60597,12 @@ var require_dist_jZb_HKI5 = __commonJS({
60420
60597
  return foldSnapshotByFunction;
60421
60598
  }
60422
60599
  });
60600
+ Object.defineProperty(exports, "gbToBytes", {
60601
+ enumerable: true,
60602
+ get: function() {
60603
+ return gbToBytes;
60604
+ }
60605
+ });
60423
60606
  Object.defineProperty(exports, "hydrateSchema", {
60424
60607
  enumerable: true,
60425
60608
  get: function() {
@@ -60480,6 +60663,12 @@ var require_dist_jZb_HKI5 = __commonJS({
60480
60663
  return kebabToCamel;
60481
60664
  }
60482
60665
  });
60666
+ Object.defineProperty(exports, "legacyModeOf", {
60667
+ enumerable: true,
60668
+ get: function() {
60669
+ return legacyModeOf;
60670
+ }
60671
+ });
60483
60672
  Object.defineProperty(exports, "lifecycleJobSchema", {
60484
60673
  enumerable: true,
60485
60674
  get: function() {
@@ -60510,12 +60699,30 @@ var require_dist_jZb_HKI5 = __commonJS({
60510
60699
  return looseSchema;
60511
60700
  }
60512
60701
  });
60702
+ Object.defineProperty(exports, "mayReadLocation", {
60703
+ enumerable: true,
60704
+ get: function() {
60705
+ return mayReadLocation;
60706
+ }
60707
+ });
60708
+ Object.defineProperty(exports, "mayWriteToLocation", {
60709
+ enumerable: true,
60710
+ get: function() {
60711
+ return mayWriteToLocation;
60712
+ }
60713
+ });
60513
60714
  Object.defineProperty(exports, "metricsProviderCapability", {
60514
60715
  enumerable: true,
60515
60716
  get: function() {
60516
60717
  return metricsProviderCapability;
60517
60718
  }
60518
60719
  });
60720
+ Object.defineProperty(exports, "modeMayWrite", {
60721
+ enumerable: true,
60722
+ get: function() {
60723
+ return modeMayWrite;
60724
+ }
60725
+ });
60519
60726
  Object.defineProperty(exports, "nodePin", {
60520
60727
  enumerable: true,
60521
60728
  get: function() {
@@ -60594,6 +60801,12 @@ var require_dist_jZb_HKI5 = __commonJS({
60594
60801
  return resolveContainerPrimaryChild;
60595
60802
  }
60596
60803
  });
60804
+ Object.defineProperty(exports, "resolveLocationMode", {
60805
+ enumerable: true,
60806
+ get: function() {
60807
+ return resolveLocationMode;
60808
+ }
60809
+ });
60597
60810
  Object.defineProperty(exports, "resolveMethodAuth", {
60598
60811
  enumerable: true,
60599
60812
  get: function() {
@@ -60660,6 +60873,12 @@ var require_dist_jZb_HKI5 = __commonJS({
60660
60873
  return storageMigrationCapability;
60661
60874
  }
60662
60875
  });
60876
+ Object.defineProperty(exports, "storageOccupancyCapability", {
60877
+ enumerable: true,
60878
+ get: function() {
60879
+ return storageOccupancyCapability;
60880
+ }
60881
+ });
60663
60882
  Object.defineProperty(exports, "storageProviderCapability", {
60664
60883
  enumerable: true,
60665
60884
  get: function() {
@@ -60690,6 +60909,12 @@ var require_dist_jZb_HKI5 = __commonJS({
60690
60909
  return vectorStoreCapability;
60691
60910
  }
60692
60911
  });
60912
+ Object.defineProperty(exports, "withLocationMode", {
60913
+ enumerable: true,
60914
+ get: function() {
60915
+ return withLocationMode;
60916
+ }
60917
+ });
60693
60918
  }
60694
60919
  });
60695
60920
 
@@ -60702,7 +60927,7 @@ var require_alerts_addon = __commonJS({
60702
60927
  [Symbol.toStringTag]: { value: "Module" }
60703
60928
  });
60704
60929
  require_chunk_Cek0wNdY();
60705
- var require_dist10 = require_dist_jZb_HKI5();
60930
+ var require_dist10 = require_dist_BFvWv_bz();
60706
60931
  function selectExpired(alerts, cutoffMs) {
60707
60932
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
60708
60933
  }
@@ -61521,7 +61746,7 @@ var require_console_logging = __commonJS({
61521
61746
  [Symbol.toStringTag]: { value: "Module" }
61522
61747
  });
61523
61748
  require_chunk_Cek0wNdY();
61524
- var require_dist10 = require_dist_jZb_HKI5();
61749
+ var require_dist10 = require_dist_BFvWv_bz();
61525
61750
  var require_formatter = require_formatter_DqAKDlvN();
61526
61751
  var LEVEL_RANK = {
61527
61752
  debug: 0,
@@ -61615,7 +61840,7 @@ var require_core_blocks_addon = __commonJS({
61615
61840
  "use strict";
61616
61841
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
61617
61842
  var require_chunk = require_chunk_Cek0wNdY();
61618
- var require_dist10 = require_dist_jZb_HKI5();
61843
+ var require_dist10 = require_dist_BFvWv_bz();
61619
61844
  var node_crypto = __require("crypto");
61620
61845
  var node_fs_promises = __require("fs/promises");
61621
61846
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -62512,11 +62737,11 @@ var require_core_blocks = __commonJS({
62512
62737
  }
62513
62738
  });
62514
62739
 
62515
- // ../system/dist/retired-settings-keys-Dxv9S_lO.js
62516
- var require_retired_settings_keys_Dxv9S_lO = __commonJS({
62517
- "../system/dist/retired-settings-keys-Dxv9S_lO.js"(exports) {
62740
+ // ../system/dist/retired-settings-keys-Dd7K3K3r.js
62741
+ var require_retired_settings_keys_Dd7K3K3r = __commonJS({
62742
+ "../system/dist/retired-settings-keys-Dd7K3K3r.js"(exports) {
62518
62743
  "use strict";
62519
- var require_dist10 = require_dist_jZb_HKI5();
62744
+ var require_dist10 = require_dist_BFvWv_bz();
62520
62745
  function settingsStoreIsAuthoritativeHere(env) {
62521
62746
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
62522
62747
  return raw === "" || raw === "hub";
@@ -63769,281 +63994,420 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
63769
63994
  hash
63770
63995
  } });
63771
63996
  }
63772
- var CLOCK_TICKS_PER_SEC = 100;
63773
- var PAGE_BYTES = 4096;
63774
- var NO_COST_CLAIM = { release: () => void 0 };
63775
- var nodeProcStatReader = {
63776
- readStat: (pid) => (0, node_fs_promises.readFile)(`/proc/${pid}/stat`, "utf8"),
63777
- readStatm: (pid) => (0, node_fs_promises.readFile)(`/proc/${pid}/statm`, "utf8")
63778
- };
63779
- function parseProcCpuSeconds(line) {
63780
- const close = line.lastIndexOf(")");
63781
- if (close < 0) return null;
63782
- const rest = line.slice(close + 1).trim().split(/\s+/);
63783
- const utime = Number(rest[11]);
63784
- const stime = Number(rest[12]);
63785
- if (!Number.isFinite(utime) || !Number.isFinite(stime)) return null;
63786
- return (utime + stime) / CLOCK_TICKS_PER_SEC;
63787
- }
63788
- function parseProcRssBytes(line) {
63789
- const fields = line.trim().split(/\s+/);
63790
- const residentPages = Number(fields[1]);
63791
- if (!Number.isFinite(residentPages)) return null;
63792
- return residentPages * PAGE_BYTES;
63793
- }
63794
- async function readProcessCost(pid, reader = nodeProcStatReader) {
63795
- let cpuSeconds = null;
63796
- let rssBytes = null;
63797
- try {
63798
- cpuSeconds = parseProcCpuSeconds(await reader.readStat(pid));
63799
- } catch {
63800
- cpuSeconds = null;
63997
+ var DEFAULT_FIRST_UNIT_TIMEOUT_MS = 12e3;
63998
+ var FRAGMENT_LOG_EVERY = 30;
63999
+ var Fmp4FragmentChild = class {
64000
+ deps;
64001
+ args;
64002
+ child = null;
64003
+ splitter = new require_canonical_hash.Fmp4BoxSplitter();
64004
+ stopped = false;
64005
+ unitsOut = 0;
64006
+ activeHwAccel = null;
64007
+ constructor(deps, args) {
64008
+ this.deps = deps;
64009
+ this.args = args;
63801
64010
  }
63802
- try {
63803
- rssBytes = parseProcRssBytes(await reader.readStatm(pid));
63804
- } catch {
63805
- rssBytes = null;
64011
+ /** Spawn, and resolve once the INIT segment has been cut out of stdout. */
64012
+ async start() {
64013
+ const requested = this.args.invocation.decodeHwAccel;
64014
+ this.activeHwAccel = requested;
64015
+ try {
64016
+ await this.spawnAttempt(requested);
64017
+ return;
64018
+ } catch (err) {
64019
+ if (this.stopped) throw err;
64020
+ if (requested === null || require_canonical_hash.isSoftwareDecode(requested)) throw err;
64021
+ this.deps.logger.warn("fmp4 fragment child: hardware decode produced NO fragment \u2014 retrying in SOFTWARE", {
64022
+ tags: { deviceId: this.args.deviceId },
64023
+ meta: {
64024
+ sourceId: this.args.sourceId,
64025
+ decodeHwAccel: requested,
64026
+ error: require_err_msg.errMsg(err)
64027
+ }
64028
+ });
64029
+ this.killChild();
64030
+ this.splitter = new require_canonical_hash.Fmp4BoxSplitter();
64031
+ this.activeHwAccel = null;
64032
+ await this.spawnAttempt(null);
64033
+ }
63806
64034
  }
63807
- return {
63808
- ...cpuSeconds === null ? {} : { cpuSeconds },
63809
- ...rssBytes === null ? {} : { rssBytes }
63810
- };
63811
- }
63812
- var ChildCostRegistry = class {
63813
- reader;
63814
- now;
63815
- claims = /* @__PURE__ */ new Map();
63816
- constructor(reader = nodeProcStatReader, now = Date.now) {
63817
- this.reader = reader;
63818
- this.now = now;
64035
+ /** The backend the child ACTUALLY ran with — `null` for software. */
64036
+ activeDecodeHwAccel() {
64037
+ const value = this.activeHwAccel;
64038
+ return value === null || value === "none" || value === "copy" ? null : value;
63819
64039
  }
63820
- /**
63821
- * Record one child. The returned handle is the only way to remove it.
63822
- *
63823
- * A claim with no pid is dropped rather than stored: it could carry no
63824
- * measurement, and an entry with a camera and no numbers reads on a chart as
63825
- * a camera that cost nothing.
63826
- */
63827
- claim(input) {
63828
- const pid = input.pid;
63829
- if (pid === void 0 || !Number.isInteger(pid) || pid <= 0) return NO_COST_CLAIM;
63830
- const key = /* @__PURE__ */ Symbol("child-cost-claim");
63831
- this.claims.set(key, {
63832
- role: input.role,
63833
- deviceId: input.deviceId,
63834
- unit: input.unit,
63835
- attribution: input.attribution ?? "measured",
63836
- pid,
63837
- startedAtMs: this.now()
63838
- });
63839
- return { release: () => {
63840
- this.claims.delete(key);
63841
- } };
64040
+ /** Kill ffmpeg and end the plane. Idempotent. */
64041
+ async stop() {
64042
+ if (this.stopped) return;
64043
+ this.stopped = true;
64044
+ this.killChild();
64045
+ this.args.plane.end("the fragment child stopped");
63842
64046
  }
63843
- /** Live claims, in claim order. Diagnostics and tests; not a contribution. */
63844
- list() {
63845
- return [...this.claims.values()].map((c) => ({
63846
- role: c.role,
63847
- deviceId: c.deviceId,
63848
- unit: c.unit,
63849
- attribution: c.attribution,
63850
- pid: c.pid
63851
- }));
64047
+ spawnAttempt(decodeHwAccel) {
64048
+ const args = require_canonical_hash.buildFfmpegArgs({
64049
+ ...this.args.invocation,
64050
+ decodeHwAccel,
64051
+ sink: {
64052
+ kind: "stdout",
64053
+ container: "mp4",
64054
+ fragmentMs: this.args.fragmentMs
64055
+ }
64056
+ });
64057
+ this.deps.logger.info("fmp4 fragment child: spawning ffmpeg", {
64058
+ tags: { deviceId: this.args.deviceId },
64059
+ meta: {
64060
+ sourceId: this.args.sourceId,
64061
+ fragmentMs: this.args.fragmentMs,
64062
+ decodeHwAccel: decodeHwAccel ?? "software",
64063
+ argv: args.join(" ")
64064
+ }
64065
+ });
64066
+ return new Promise((resolve, reject) => {
64067
+ const child = this.deps.spawnFn(this.deps.ffmpegBinaryPath, args, { stdio: [
64068
+ "ignore",
64069
+ "pipe",
64070
+ "pipe"
64071
+ ] });
64072
+ this.child = child;
64073
+ let settled = false;
64074
+ let failed = false;
64075
+ const isCurrent = () => !this.stopped && !failed;
64076
+ const timeoutMs = this.deps.firstUnitTimeoutMs ?? DEFAULT_FIRST_UNIT_TIMEOUT_MS;
64077
+ const settle = (fail) => {
64078
+ if (settled) return;
64079
+ settled = true;
64080
+ clearTimeout(timer);
64081
+ if (fail) {
64082
+ failed = true;
64083
+ reject(fail);
64084
+ } else resolve();
64085
+ };
64086
+ const timer = setTimeout(() => {
64087
+ settle(/* @__PURE__ */ new Error(`fmp4 fragment child: no fragment within ${timeoutMs}ms`));
64088
+ this.killChild();
64089
+ }, timeoutMs);
64090
+ timer.unref?.();
64091
+ child.stdout?.on("data", (chunk) => {
64092
+ for (const unit of this.splitter.push(chunk)) {
64093
+ this.unitsOut += 1;
64094
+ this.args.plane.publish(unit);
64095
+ if (unit.kind === "init") {
64096
+ this.deps.logger.info("fmp4 fragment child: INIT segment cut", {
64097
+ tags: { deviceId: this.args.deviceId },
64098
+ meta: {
64099
+ sourceId: this.args.sourceId,
64100
+ bytes: unit.data.length
64101
+ }
64102
+ });
64103
+ settle();
64104
+ } else if (this.unitsOut % FRAGMENT_LOG_EVERY === 0) this.deps.logger.info("fmp4 fragment child: fragments still flowing", {
64105
+ tags: { deviceId: this.args.deviceId },
64106
+ meta: {
64107
+ sourceId: this.args.sourceId,
64108
+ unitsOut: this.unitsOut,
64109
+ bytes: unit.data.length,
64110
+ subscribers: this.args.plane.subscriberCount
64111
+ }
64112
+ });
64113
+ }
64114
+ const fault = this.splitter.fault;
64115
+ if (fault !== null) this.onFault(fault, settled, isCurrent(), settle);
64116
+ });
64117
+ child.stderr?.setEncoding("utf8");
64118
+ child.stderr?.on("data", (line) => {
64119
+ this.deps.logger.debug("fmp4 fragment child ffmpeg", {
64120
+ tags: { deviceId: this.args.deviceId },
64121
+ meta: {
64122
+ sourceId: this.args.sourceId,
64123
+ line: line.trim()
64124
+ }
64125
+ });
64126
+ });
64127
+ child.once("error", (err) => {
64128
+ if (!settled) {
64129
+ settle(err);
64130
+ return;
64131
+ }
64132
+ if (!isCurrent()) return;
64133
+ this.args.plane.end("the fragment child errored");
64134
+ this.deps.onChildExit?.(err);
64135
+ });
64136
+ child.once("exit", (code, signal) => {
64137
+ if (!settled) {
64138
+ settle(/* @__PURE__ */ new Error(`fmp4 fragment child: ffmpeg exited before any fragment (code=${code} signal=${signal})`));
64139
+ return;
64140
+ }
64141
+ if (!isCurrent()) return;
64142
+ const error = /* @__PURE__ */ new Error(`fmp4 fragment child: ffmpeg exited while live (code=${code} signal=${signal})`);
64143
+ this.deps.logger.warn("fmp4 fragment child: ffmpeg exited while live", {
64144
+ tags: { deviceId: this.args.deviceId },
64145
+ meta: {
64146
+ sourceId: this.args.sourceId,
64147
+ code,
64148
+ signal,
64149
+ unitsOut: this.unitsOut
64150
+ }
64151
+ });
64152
+ this.args.plane.end("the fragment child exited");
64153
+ this.deps.onChildExit?.(error);
64154
+ });
64155
+ });
63852
64156
  }
63853
64157
  /**
63854
- * This addon's contribution: one entry per live claim, with whatever the OS
63855
- * will tell us about that child right now.
63856
- *
63857
- * A child that has exited between the claim and this read contributes an
63858
- * entry with no numbers rather than no entry — the unit exists, the addon
63859
- * believes it is running, and hiding it would make a dying writer look like
63860
- * a writer that was never started.
64158
+ * The byte stream stopped being splittable. Not recoverable — the splitter
64159
+ * cannot resynchronise mid-box — so the child is a corpse and every consumer
64160
+ * has to be told, loudly, with the reason.
63861
64161
  */
63862
- async contributions() {
63863
- const out = [];
63864
- for (const claim of this.claims.values()) out.push({
63865
- role: claim.role,
63866
- deviceId: claim.deviceId,
63867
- attribution: claim.attribution,
63868
- unit: claim.unit,
63869
- pid: claim.pid,
63870
- startedAtMs: claim.startedAtMs,
63871
- ...await readProcessCost(claim.pid, this.reader)
64162
+ onFault(reason, wasLive, current, settle) {
64163
+ const error = /* @__PURE__ */ new Error(`fmp4 fragment child: ${reason}`);
64164
+ this.deps.logger.error("fmp4 fragment child: the ffmpeg output stopped parsing as fMP4", {
64165
+ tags: { deviceId: this.args.deviceId },
64166
+ meta: {
64167
+ sourceId: this.args.sourceId,
64168
+ unitsOut: this.unitsOut,
64169
+ interstitial: this.splitter.discardedInterstitialTypes,
64170
+ reason
64171
+ }
63872
64172
  });
63873
- return out;
64173
+ this.killChild();
64174
+ settle(error);
64175
+ if (wasLive && current) {
64176
+ this.args.plane.end("the fragment child produced unsplittable output");
64177
+ this.deps.onChildExit?.(error);
64178
+ }
63874
64179
  }
63875
- };
63876
- var STORAGE_LOCATION_TYPES = [
63877
- "data",
63878
- "media",
63879
- "recordings",
63880
- "recordings-high",
63881
- "recordings-low",
63882
- "recordings-clips",
63883
- "event-images",
63884
- "models",
63885
- "addons-data",
63886
- "cache",
63887
- "logs",
63888
- "backups"
63889
- ];
63890
- var DEFAULT_LOCATION_SUBDIRS = {
63891
- data: "db",
63892
- media: "media",
63893
- recordings: "recordings",
63894
- "recordings-high": "recordings-high",
63895
- "recordings-low": "recordings-low",
63896
- "recordings-clips": "recordings-clips",
63897
- "event-images": "event-images",
63898
- models: "models",
63899
- "addons-data": "addons-data",
63900
- cache: "/tmp/camstack-cache",
63901
- logs: "logs",
63902
- backups: "backups"
63903
- };
63904
- var FilesystemStorageProvider = class {
63905
- id = "local";
63906
- name = "Local Filesystem";
63907
- supportedLocations = [...STORAGE_LOCATION_TYPES];
63908
- rootPath;
63909
- locationPaths;
63910
- constructor(rootPath, overrides) {
63911
- this.rootPath = node_path.resolve(rootPath);
63912
- this.locationPaths = /* @__PURE__ */ new Map();
63913
- for (const loc of STORAGE_LOCATION_TYPES) {
63914
- const override = overrides?.[loc];
63915
- if (override) this.locationPaths.set(loc, node_path.resolve(override));
63916
- else {
63917
- const subdir = DEFAULT_LOCATION_SUBDIRS[loc] ?? loc;
63918
- this.locationPaths.set(loc, node_path.isAbsolute(subdir) ? subdir : node_path.join(this.rootPath, subdir));
63919
- }
64180
+ killChild() {
64181
+ const child = this.child;
64182
+ this.child = null;
64183
+ if (child && !child.killed) try {
64184
+ child.kill("SIGTERM");
64185
+ } catch (err) {
64186
+ this.deps.logger.warn("fmp4 fragment child: kill error", {
64187
+ tags: { deviceId: this.args.deviceId },
64188
+ meta: {
64189
+ sourceId: this.args.sourceId,
64190
+ error: require_err_msg.errMsg(err)
64191
+ }
64192
+ });
63920
64193
  }
63921
- for (const [loc, override] of Object.entries(overrides ?? {})) if (typeof override === "string" && !this.locationPaths.has(loc)) this.locationPaths.set(loc, node_path.resolve(override));
63922
64194
  }
63923
- async resolve({ location, relativePath }) {
63924
- const base = this.locationPaths.get(location) ?? node_path.join(this.rootPath, location);
63925
- return node_path.join(base, relativePath);
64195
+ };
64196
+ var DEFAULT_QUEUE_CAPACITY = 4;
64197
+ var Fmp4FragmentPlane = class {
64198
+ logger;
64199
+ prebuffer;
64200
+ now;
64201
+ subscriptions = /* @__PURE__ */ new Map();
64202
+ /** The last init unit seen, handed to every later subscriber. */
64203
+ retainedInit = null;
64204
+ ended = false;
64205
+ /** Oldest first. Empty unless {@link Fmp4PrebufferOptions} was supplied. */
64206
+ ring = [];
64207
+ ringBytes = 0;
64208
+ constructor(logger, prebuffer, now = Date.now) {
64209
+ this.logger = logger;
64210
+ this.prebuffer = prebuffer;
64211
+ this.now = now;
63926
64212
  }
63927
- async write({ location, relativePath, data }) {
63928
- const filePath = await this.resolve({
63929
- location,
63930
- relativePath
63931
- });
63932
- await node_fs.promises.mkdir(node_path.dirname(filePath), { recursive: true });
63933
- if (Buffer.isBuffer(data)) await node_fs.promises.writeFile(filePath, data);
63934
- else {
63935
- const writeStream = node_fs.createWriteStream(filePath);
63936
- await new Promise((resolve, reject) => {
63937
- data.pipe(writeStream);
63938
- writeStream.on("finish", resolve);
63939
- writeStream.on("error", reject);
63940
- });
64213
+ get subscriberCount() {
64214
+ return this.subscriptions.size;
64215
+ }
64216
+ /** True once {@link end} has been called — no further units are accepted. */
64217
+ get isEnded() {
64218
+ return this.ended;
64219
+ }
64220
+ /** What the prebuffer ring holds right now. All zeroes when disabled. */
64221
+ prebufferStats() {
64222
+ const oldest = this.ring[0];
64223
+ return {
64224
+ fragments: this.ring.length,
64225
+ bytes: this.ringBytes,
64226
+ spanMs: oldest === void 0 ? 0 : this.now() - oldest.arrivedAt
64227
+ };
64228
+ }
64229
+ subscribe(input) {
64230
+ const replay = input.withPrebuffer === true ? this.trimmedRing() : [];
64231
+ const requested = Math.max(1, input.queueCapacity ?? DEFAULT_QUEUE_CAPACITY);
64232
+ const sub = {
64233
+ id: `fmp4-${(0, node_crypto.randomUUID)()}`,
64234
+ tag: input.tag,
64235
+ subscribedAt: this.now(),
64236
+ capacity: requested + replay.length,
64237
+ queue: [],
64238
+ delivered: 0,
64239
+ closedReason: null,
64240
+ wake: null,
64241
+ iterating: false
64242
+ };
64243
+ this.subscriptions.set(sub.id, sub);
64244
+ if (this.retainedInit !== null) this.enqueue(sub, this.retainedInit);
64245
+ for (const retained of replay) this.enqueue(sub, retained.unit);
64246
+ if (this.ended) this.closeSubscription(sub, "ended");
64247
+ this.logger?.info("fmp4 plane: subscribed", { meta: {
64248
+ subscriptionId: sub.id,
64249
+ tag: sub.tag,
64250
+ hasRetainedInit: this.retainedInit !== null,
64251
+ prebufferFragments: replay.length,
64252
+ prebufferBytes: replay.reduce((n, r) => n + r.unit.data.length, 0)
64253
+ } });
64254
+ return this.facade(sub);
64255
+ }
64256
+ /**
64257
+ * Fan one splitter unit out. An `init` REPLACES the retained one — ffmpeg
64258
+ * emits exactly one per child, and a second means the child was respawned, in
64259
+ * which case the old one describes a stream that no longer exists.
64260
+ */
64261
+ publish(unit) {
64262
+ if (this.ended) return;
64263
+ if (unit.kind === "init") {
64264
+ this.retainedInit = unit;
64265
+ this.ring.length = 0;
64266
+ this.ringBytes = 0;
64267
+ } else this.retain(unit);
64268
+ for (const sub of this.subscriptions.values()) {
64269
+ if (sub.closedReason !== null) continue;
64270
+ this.enqueue(sub, unit);
63941
64271
  }
63942
64272
  }
63943
- async read({ location, relativePath }) {
63944
- return node_fs.promises.readFile(await this.resolve({
63945
- location,
63946
- relativePath
64273
+ /**
64274
+ * The producer stopped. Every subscriber drains what it holds; its final
64275
+ * packet carries `isLast`, and its generator then completes.
64276
+ */
64277
+ end(reason = "producer ended") {
64278
+ if (this.ended) return;
64279
+ this.ended = true;
64280
+ this.logger?.info("fmp4 plane: ended", { meta: {
64281
+ reason,
64282
+ subscribers: this.subscriptions.size
64283
+ } });
64284
+ for (const sub of this.subscriptions.values()) if (sub.closedReason === null) this.closeSubscription(sub, "ended");
64285
+ }
64286
+ listSubscribers() {
64287
+ return [...this.subscriptions.values()].map((s) => ({
64288
+ tag: s.tag,
64289
+ subscribedAt: s.subscribedAt,
64290
+ delivered: s.delivered,
64291
+ closedReason: s.closedReason
63947
64292
  }));
63948
64293
  }
63949
- async exists({ location, relativePath }) {
63950
- try {
63951
- await node_fs.promises.access(await this.resolve({
63952
- location,
63953
- relativePath
63954
- }));
63955
- return true;
63956
- } catch {
63957
- return false;
63958
- }
64294
+ /** End and forget everything. Idempotent. */
64295
+ dispose() {
64296
+ this.end("disposed");
64297
+ this.subscriptions.clear();
64298
+ this.retainedInit = null;
64299
+ this.ring.length = 0;
64300
+ this.ringBytes = 0;
63959
64301
  }
63960
- async list({ location, prefix }) {
63961
- const base = this.locationPaths.get(location);
63962
- if (!base) return [];
63963
- const dir = prefix ? node_path.join(base, prefix) : base;
63964
- try {
63965
- return (await node_fs.promises.readdir(dir, { withFileTypes: true })).map((e) => prefix ? `${prefix}/${e.name}` : e.name);
63966
- } catch {
63967
- return [];
64302
+ /**
64303
+ * Add one fragment to the ring and evict from the front until BOTH bounds
64304
+ * hold. Eviction is oldest-first, which is the one place in this file where
64305
+ * dropping is correct: the ring is context, not stream — nobody is mid-decode
64306
+ * on it, and a subscriber only ever receives a contiguous tail of it.
64307
+ */
64308
+ retain(unit) {
64309
+ const prebuffer = this.prebuffer;
64310
+ if (prebuffer === void 0) return;
64311
+ const arrivedAt = this.now();
64312
+ this.ring.push({
64313
+ unit,
64314
+ arrivedAt
64315
+ });
64316
+ this.ringBytes += unit.data.length;
64317
+ const cutoff = arrivedAt - prebuffer.windowMs;
64318
+ while (this.ring.length > 0) {
64319
+ const oldest = this.ring[0];
64320
+ if (oldest === void 0) break;
64321
+ const tooOld = oldest.arrivedAt < cutoff;
64322
+ const tooBig = this.ringBytes > prebuffer.maxBytes;
64323
+ if (!tooOld && !tooBig || this.ring.length === 1) break;
64324
+ this.ring.shift();
64325
+ this.ringBytes -= oldest.unit.data.length;
63968
64326
  }
63969
64327
  }
63970
- async delete({ location, relativePath }) {
63971
- const filePath = await this.resolve({
63972
- location,
63973
- relativePath
63974
- });
63975
- await node_fs.promises.rm(filePath, { force: true });
64328
+ /**
64329
+ * The ring as a subscriber should receive it — window applied AT SUBSCRIBE
64330
+ * time, not only at publish time. A camera that went quiet keeps its last
64331
+ * fragment in the ring indefinitely (see the never-evict-the-newest rule),
64332
+ * and replaying a 40-second-old fragment as "prebuffer" would put stale video
64333
+ * at the head of a clip iOS presents as the moment of the event.
64334
+ */
64335
+ trimmedRing() {
64336
+ const prebuffer = this.prebuffer;
64337
+ if (prebuffer === void 0) return [];
64338
+ const cutoff = this.now() - prebuffer.windowMs;
64339
+ return this.ring.filter((r) => r.arrivedAt >= cutoff);
63976
64340
  }
63977
- async getAvailableSpace({ location }) {
63978
- const base = this.locationPaths.get(location);
63979
- if (!base) return null;
63980
- try {
63981
- let target = base;
63982
- while (!node_fs.existsSync(target)) {
63983
- const parent = node_path.dirname(target);
63984
- if (!parent || parent === target) return null;
63985
- target = parent;
63986
- }
63987
- const stats = await node_fs.promises.statfs(target);
63988
- return stats.bavail * stats.bsize;
63989
- } catch {
63990
- return null;
64341
+ enqueue(sub, unit) {
64342
+ if (sub.queue.length >= sub.capacity) {
64343
+ this.logger?.warn("fmp4 plane: subscriber fell behind \u2014 CLOSING it rather than gapping it", { meta: {
64344
+ subscriptionId: sub.id,
64345
+ tag: sub.tag,
64346
+ capacity: sub.capacity,
64347
+ delivered: sub.delivered
64348
+ } });
64349
+ this.closeSubscription(sub, "slow-consumer");
64350
+ return;
63991
64351
  }
64352
+ sub.queue.push({
64353
+ kind: unit.kind,
64354
+ data: unit.data,
64355
+ sequence: unit.sequence,
64356
+ isLast: false
64357
+ });
64358
+ this.wake(sub);
63992
64359
  }
63993
- /** Get the resolved path for a location type (addon-declared ids fall back
63994
- * to `<rootPath>/<id>`). */
63995
- getLocationPath(location) {
63996
- return this.locationPaths.get(location) ?? node_path.join(this.rootPath, location);
64360
+ closeSubscription(sub, reason) {
64361
+ if (sub.closedReason !== null) return;
64362
+ sub.closedReason = reason;
64363
+ if (reason === "slow-consumer") sub.queue.length = 0;
64364
+ this.wake(sub);
63997
64365
  }
63998
- /** Get the root path */
63999
- getRootPath() {
64000
- return this.rootPath;
64366
+ wake(sub) {
64367
+ const resume = sub.wake;
64368
+ sub.wake = null;
64369
+ resume?.();
64001
64370
  }
64002
- };
64003
- function signExpiringUrl(secret, id, expMs) {
64004
- return (0, node_crypto.createHmac)("sha256", secret).update(`${id}:${String(expMs)}`).digest("hex");
64005
- }
64006
- function verifyExpiringUrl(input) {
64007
- const { secret, id, exp, sig, nowMs } = input;
64008
- if (exp === void 0 || sig === void 0) return false;
64009
- const expMs = typeof exp === "number" ? exp : Number(exp);
64010
- if (!Number.isFinite(expMs)) return false;
64011
- if (expMs <= nowMs) return false;
64012
- const expected = signExpiringUrl(secret, id, expMs);
64013
- const a = Buffer.from(expected, "utf8");
64014
- const b = Buffer.from(sig, "utf8");
64015
- if (a.length !== b.length) return false;
64016
- return (0, node_crypto.timingSafeEqual)(a, b);
64017
- }
64018
- function canonicalDeviceFingerprint(shape) {
64019
- const features = [...new Set(shape.features)].toSorted();
64020
- return require_canonical_hash.canonicalHash({
64021
- deviceType: shape.deviceType,
64022
- features
64023
- });
64024
- }
64025
- function diffExportTargets(desired, lastAdvertised) {
64026
- const toAdd = [];
64027
- const toUpdate = [];
64028
- const desiredIds = /* @__PURE__ */ new Set();
64029
- for (const entry of desired) {
64030
- desiredIds.add(entry.deviceId);
64031
- const previous = lastAdvertised.get(entry.deviceId);
64032
- if (previous === void 0) toAdd.push(entry);
64033
- else if (previous.fingerprint !== entry.fingerprint) toUpdate.push(entry);
64371
+ facade(sub) {
64372
+ const plane = this;
64373
+ return {
64374
+ id: sub.id,
64375
+ tag: sub.tag,
64376
+ get delivered() {
64377
+ return sub.delivered;
64378
+ },
64379
+ get closedReason() {
64380
+ return sub.closedReason;
64381
+ },
64382
+ packets: () => plane.iterate(sub),
64383
+ release: () => {
64384
+ plane.closeSubscription(sub, "released");
64385
+ plane.subscriptions.delete(sub.id);
64386
+ }
64387
+ };
64034
64388
  }
64035
- const toRemove = [];
64036
- for (const deviceId of lastAdvertised.keys()) if (!desiredIds.has(deviceId)) toRemove.push(deviceId);
64037
- return {
64038
- toAdd,
64039
- toUpdate,
64040
- toRemove
64041
- };
64042
- }
64043
- function resolveExportFingerprint(input) {
64044
- if (input.ready) return input.fresh;
64045
- return input.persisted ?? input.fresh;
64046
- }
64389
+ async *iterate(sub) {
64390
+ if (sub.iterating) throw new Error(`fmp4 plane: subscription ${sub.tag} is already being consumed \u2014 take a second subscription`);
64391
+ sub.iterating = true;
64392
+ for (; ; ) {
64393
+ const next = sub.queue.shift();
64394
+ if (next === void 0) {
64395
+ if (sub.closedReason !== null) return;
64396
+ await new Promise((resolve) => {
64397
+ sub.wake = resolve;
64398
+ });
64399
+ continue;
64400
+ }
64401
+ const isLast = sub.closedReason === "ended" && sub.queue.length === 0;
64402
+ sub.delivered += 1;
64403
+ yield {
64404
+ ...next,
64405
+ isLast
64406
+ };
64407
+ if (isLast) return;
64408
+ }
64409
+ }
64410
+ };
64047
64411
  var DEFAULT_FIRST_DATA_TIMEOUT_MS = 8e3;
64048
64412
  var DEFAULT_RESTART_DELAY_MS = 1e3;
64049
64413
  var DEFAULT_STABLE_RUN_MS = 3e4;
@@ -64276,420 +64640,300 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
64276
64640
  if (typeof grace === "object" && grace !== null && "unref" in grace) grace.unref();
64277
64641
  }
64278
64642
  };
64279
- var DEFAULT_QUEUE_CAPACITY = 4;
64280
- var Fmp4FragmentPlane = class {
64281
- logger;
64282
- prebuffer;
64283
- now;
64284
- subscriptions = /* @__PURE__ */ new Map();
64285
- /** The last init unit seen, handed to every later subscriber. */
64286
- retainedInit = null;
64287
- ended = false;
64288
- /** Oldest first. Empty unless {@link Fmp4PrebufferOptions} was supplied. */
64289
- ring = [];
64290
- ringBytes = 0;
64291
- constructor(logger, prebuffer, now = Date.now) {
64292
- this.logger = logger;
64293
- this.prebuffer = prebuffer;
64294
- this.now = now;
64295
- }
64296
- get subscriberCount() {
64297
- return this.subscriptions.size;
64298
- }
64299
- /** True once {@link end} has been called — no further units are accepted. */
64300
- get isEnded() {
64301
- return this.ended;
64302
- }
64303
- /** What the prebuffer ring holds right now. All zeroes when disabled. */
64304
- prebufferStats() {
64305
- const oldest = this.ring[0];
64306
- return {
64307
- fragments: this.ring.length,
64308
- bytes: this.ringBytes,
64309
- spanMs: oldest === void 0 ? 0 : this.now() - oldest.arrivedAt
64310
- };
64643
+ var CLOCK_TICKS_PER_SEC = 100;
64644
+ var PAGE_BYTES = 4096;
64645
+ var NO_COST_CLAIM = { release: () => void 0 };
64646
+ var nodeProcStatReader = {
64647
+ readStat: (pid) => (0, node_fs_promises.readFile)(`/proc/${pid}/stat`, "utf8"),
64648
+ readStatm: (pid) => (0, node_fs_promises.readFile)(`/proc/${pid}/statm`, "utf8")
64649
+ };
64650
+ function parseProcCpuSeconds(line) {
64651
+ const close = line.lastIndexOf(")");
64652
+ if (close < 0) return null;
64653
+ const rest = line.slice(close + 1).trim().split(/\s+/);
64654
+ const utime = Number(rest[11]);
64655
+ const stime = Number(rest[12]);
64656
+ if (!Number.isFinite(utime) || !Number.isFinite(stime)) return null;
64657
+ return (utime + stime) / CLOCK_TICKS_PER_SEC;
64658
+ }
64659
+ function parseProcRssBytes(line) {
64660
+ const fields = line.trim().split(/\s+/);
64661
+ const residentPages = Number(fields[1]);
64662
+ if (!Number.isFinite(residentPages)) return null;
64663
+ return residentPages * PAGE_BYTES;
64664
+ }
64665
+ async function readProcessCost(pid, reader = nodeProcStatReader) {
64666
+ let cpuSeconds = null;
64667
+ let rssBytes = null;
64668
+ try {
64669
+ cpuSeconds = parseProcCpuSeconds(await reader.readStat(pid));
64670
+ } catch {
64671
+ cpuSeconds = null;
64311
64672
  }
64312
- subscribe(input) {
64313
- const replay = input.withPrebuffer === true ? this.trimmedRing() : [];
64314
- const requested = Math.max(1, input.queueCapacity ?? DEFAULT_QUEUE_CAPACITY);
64315
- const sub = {
64316
- id: `fmp4-${(0, node_crypto.randomUUID)()}`,
64317
- tag: input.tag,
64318
- subscribedAt: this.now(),
64319
- capacity: requested + replay.length,
64320
- queue: [],
64321
- delivered: 0,
64322
- closedReason: null,
64323
- wake: null,
64324
- iterating: false
64325
- };
64326
- this.subscriptions.set(sub.id, sub);
64327
- if (this.retainedInit !== null) this.enqueue(sub, this.retainedInit);
64328
- for (const retained of replay) this.enqueue(sub, retained.unit);
64329
- if (this.ended) this.closeSubscription(sub, "ended");
64330
- this.logger?.info("fmp4 plane: subscribed", { meta: {
64331
- subscriptionId: sub.id,
64332
- tag: sub.tag,
64333
- hasRetainedInit: this.retainedInit !== null,
64334
- prebufferFragments: replay.length,
64335
- prebufferBytes: replay.reduce((n, r) => n + r.unit.data.length, 0)
64336
- } });
64337
- return this.facade(sub);
64673
+ try {
64674
+ rssBytes = parseProcRssBytes(await reader.readStatm(pid));
64675
+ } catch {
64676
+ rssBytes = null;
64338
64677
  }
64339
- /**
64340
- * Fan one splitter unit out. An `init` REPLACES the retained one — ffmpeg
64341
- * emits exactly one per child, and a second means the child was respawned, in
64342
- * which case the old one describes a stream that no longer exists.
64343
- */
64344
- publish(unit) {
64345
- if (this.ended) return;
64346
- if (unit.kind === "init") {
64347
- this.retainedInit = unit;
64348
- this.ring.length = 0;
64349
- this.ringBytes = 0;
64350
- } else this.retain(unit);
64351
- for (const sub of this.subscriptions.values()) {
64352
- if (sub.closedReason !== null) continue;
64353
- this.enqueue(sub, unit);
64354
- }
64678
+ return {
64679
+ ...cpuSeconds === null ? {} : { cpuSeconds },
64680
+ ...rssBytes === null ? {} : { rssBytes }
64681
+ };
64682
+ }
64683
+ var ChildCostRegistry = class {
64684
+ reader;
64685
+ now;
64686
+ claims = /* @__PURE__ */ new Map();
64687
+ constructor(reader = nodeProcStatReader, now = Date.now) {
64688
+ this.reader = reader;
64689
+ this.now = now;
64355
64690
  }
64356
64691
  /**
64357
- * The producer stopped. Every subscriber drains what it holds; its final
64358
- * packet carries `isLast`, and its generator then completes.
64692
+ * Record one child. The returned handle is the only way to remove it.
64693
+ *
64694
+ * A claim with no pid is dropped rather than stored: it could carry no
64695
+ * measurement, and an entry with a camera and no numbers reads on a chart as
64696
+ * a camera that cost nothing.
64359
64697
  */
64360
- end(reason = "producer ended") {
64361
- if (this.ended) return;
64362
- this.ended = true;
64363
- this.logger?.info("fmp4 plane: ended", { meta: {
64364
- reason,
64365
- subscribers: this.subscriptions.size
64366
- } });
64367
- for (const sub of this.subscriptions.values()) if (sub.closedReason === null) this.closeSubscription(sub, "ended");
64698
+ claim(input) {
64699
+ const pid = input.pid;
64700
+ if (pid === void 0 || !Number.isInteger(pid) || pid <= 0) return NO_COST_CLAIM;
64701
+ const key = /* @__PURE__ */ Symbol("child-cost-claim");
64702
+ this.claims.set(key, {
64703
+ role: input.role,
64704
+ deviceId: input.deviceId,
64705
+ unit: input.unit,
64706
+ attribution: input.attribution ?? "measured",
64707
+ pid,
64708
+ startedAtMs: this.now()
64709
+ });
64710
+ return { release: () => {
64711
+ this.claims.delete(key);
64712
+ } };
64368
64713
  }
64369
- listSubscribers() {
64370
- return [...this.subscriptions.values()].map((s) => ({
64371
- tag: s.tag,
64372
- subscribedAt: s.subscribedAt,
64373
- delivered: s.delivered,
64374
- closedReason: s.closedReason
64714
+ /** Live claims, in claim order. Diagnostics and tests; not a contribution. */
64715
+ list() {
64716
+ return [...this.claims.values()].map((c) => ({
64717
+ role: c.role,
64718
+ deviceId: c.deviceId,
64719
+ unit: c.unit,
64720
+ attribution: c.attribution,
64721
+ pid: c.pid
64375
64722
  }));
64376
64723
  }
64377
- /** End and forget everything. Idempotent. */
64378
- dispose() {
64379
- this.end("disposed");
64380
- this.subscriptions.clear();
64381
- this.retainedInit = null;
64382
- this.ring.length = 0;
64383
- this.ringBytes = 0;
64384
- }
64385
64724
  /**
64386
- * Add one fragment to the ring and evict from the front until BOTH bounds
64387
- * hold. Eviction is oldest-first, which is the one place in this file where
64388
- * dropping is correct: the ring is context, not stream — nobody is mid-decode
64389
- * on it, and a subscriber only ever receives a contiguous tail of it.
64725
+ * This addon's contribution: one entry per live claim, with whatever the OS
64726
+ * will tell us about that child right now.
64727
+ *
64728
+ * A child that has exited between the claim and this read contributes an
64729
+ * entry with no numbers rather than no entry — the unit exists, the addon
64730
+ * believes it is running, and hiding it would make a dying writer look like
64731
+ * a writer that was never started.
64390
64732
  */
64391
- retain(unit) {
64392
- const prebuffer = this.prebuffer;
64393
- if (prebuffer === void 0) return;
64394
- const arrivedAt = this.now();
64395
- this.ring.push({
64396
- unit,
64397
- arrivedAt
64733
+ async contributions() {
64734
+ const out = [];
64735
+ for (const claim of this.claims.values()) out.push({
64736
+ role: claim.role,
64737
+ deviceId: claim.deviceId,
64738
+ attribution: claim.attribution,
64739
+ unit: claim.unit,
64740
+ pid: claim.pid,
64741
+ startedAtMs: claim.startedAtMs,
64742
+ ...await readProcessCost(claim.pid, this.reader)
64398
64743
  });
64399
- this.ringBytes += unit.data.length;
64400
- const cutoff = arrivedAt - prebuffer.windowMs;
64401
- while (this.ring.length > 0) {
64402
- const oldest = this.ring[0];
64403
- if (oldest === void 0) break;
64404
- const tooOld = oldest.arrivedAt < cutoff;
64405
- const tooBig = this.ringBytes > prebuffer.maxBytes;
64406
- if (!tooOld && !tooBig || this.ring.length === 1) break;
64407
- this.ring.shift();
64408
- this.ringBytes -= oldest.unit.data.length;
64409
- }
64410
- }
64411
- /**
64412
- * The ring as a subscriber should receive it — window applied AT SUBSCRIBE
64413
- * time, not only at publish time. A camera that went quiet keeps its last
64414
- * fragment in the ring indefinitely (see the never-evict-the-newest rule),
64415
- * and replaying a 40-second-old fragment as "prebuffer" would put stale video
64416
- * at the head of a clip iOS presents as the moment of the event.
64417
- */
64418
- trimmedRing() {
64419
- const prebuffer = this.prebuffer;
64420
- if (prebuffer === void 0) return [];
64421
- const cutoff = this.now() - prebuffer.windowMs;
64422
- return this.ring.filter((r) => r.arrivedAt >= cutoff);
64744
+ return out;
64423
64745
  }
64424
- enqueue(sub, unit) {
64425
- if (sub.queue.length >= sub.capacity) {
64426
- this.logger?.warn("fmp4 plane: subscriber fell behind \u2014 CLOSING it rather than gapping it", { meta: {
64427
- subscriptionId: sub.id,
64428
- tag: sub.tag,
64429
- capacity: sub.capacity,
64430
- delivered: sub.delivered
64431
- } });
64432
- this.closeSubscription(sub, "slow-consumer");
64433
- return;
64746
+ };
64747
+ var STORAGE_LOCATION_TYPES = [
64748
+ "data",
64749
+ "media",
64750
+ "recordings",
64751
+ "recordings-high",
64752
+ "recordings-low",
64753
+ "recordings-clips",
64754
+ "event-images",
64755
+ "models",
64756
+ "addons-data",
64757
+ "cache",
64758
+ "logs",
64759
+ "backups"
64760
+ ];
64761
+ var DEFAULT_LOCATION_SUBDIRS = {
64762
+ data: "db",
64763
+ media: "media",
64764
+ recordings: "recordings",
64765
+ "recordings-high": "recordings-high",
64766
+ "recordings-low": "recordings-low",
64767
+ "recordings-clips": "recordings-clips",
64768
+ "event-images": "event-images",
64769
+ models: "models",
64770
+ "addons-data": "addons-data",
64771
+ cache: "/tmp/camstack-cache",
64772
+ logs: "logs",
64773
+ backups: "backups"
64774
+ };
64775
+ var FilesystemStorageProvider = class {
64776
+ id = "local";
64777
+ name = "Local Filesystem";
64778
+ supportedLocations = [...STORAGE_LOCATION_TYPES];
64779
+ rootPath;
64780
+ locationPaths;
64781
+ constructor(rootPath, overrides) {
64782
+ this.rootPath = node_path.resolve(rootPath);
64783
+ this.locationPaths = /* @__PURE__ */ new Map();
64784
+ for (const loc of STORAGE_LOCATION_TYPES) {
64785
+ const override = overrides?.[loc];
64786
+ if (override) this.locationPaths.set(loc, node_path.resolve(override));
64787
+ else {
64788
+ const subdir = DEFAULT_LOCATION_SUBDIRS[loc] ?? loc;
64789
+ this.locationPaths.set(loc, node_path.isAbsolute(subdir) ? subdir : node_path.join(this.rootPath, subdir));
64790
+ }
64434
64791
  }
64435
- sub.queue.push({
64436
- kind: unit.kind,
64437
- data: unit.data,
64438
- sequence: unit.sequence,
64439
- isLast: false
64440
- });
64441
- this.wake(sub);
64442
- }
64443
- closeSubscription(sub, reason) {
64444
- if (sub.closedReason !== null) return;
64445
- sub.closedReason = reason;
64446
- if (reason === "slow-consumer") sub.queue.length = 0;
64447
- this.wake(sub);
64448
- }
64449
- wake(sub) {
64450
- const resume = sub.wake;
64451
- sub.wake = null;
64452
- resume?.();
64792
+ for (const [loc, override] of Object.entries(overrides ?? {})) if (typeof override === "string" && !this.locationPaths.has(loc)) this.locationPaths.set(loc, node_path.resolve(override));
64453
64793
  }
64454
- facade(sub) {
64455
- const plane = this;
64456
- return {
64457
- id: sub.id,
64458
- tag: sub.tag,
64459
- get delivered() {
64460
- return sub.delivered;
64461
- },
64462
- get closedReason() {
64463
- return sub.closedReason;
64464
- },
64465
- packets: () => plane.iterate(sub),
64466
- release: () => {
64467
- plane.closeSubscription(sub, "released");
64468
- plane.subscriptions.delete(sub.id);
64469
- }
64470
- };
64794
+ async resolve({ location, relativePath }) {
64795
+ const base = this.locationPaths.get(location) ?? node_path.join(this.rootPath, location);
64796
+ return node_path.join(base, relativePath);
64471
64797
  }
64472
- async *iterate(sub) {
64473
- if (sub.iterating) throw new Error(`fmp4 plane: subscription ${sub.tag} is already being consumed \u2014 take a second subscription`);
64474
- sub.iterating = true;
64475
- for (; ; ) {
64476
- const next = sub.queue.shift();
64477
- if (next === void 0) {
64478
- if (sub.closedReason !== null) return;
64479
- await new Promise((resolve) => {
64480
- sub.wake = resolve;
64481
- });
64482
- continue;
64483
- }
64484
- const isLast = sub.closedReason === "ended" && sub.queue.length === 0;
64485
- sub.delivered += 1;
64486
- yield {
64487
- ...next,
64488
- isLast
64489
- };
64490
- if (isLast) return;
64798
+ async write({ location, relativePath, data }) {
64799
+ const filePath = await this.resolve({
64800
+ location,
64801
+ relativePath
64802
+ });
64803
+ await node_fs.promises.mkdir(node_path.dirname(filePath), { recursive: true });
64804
+ if (Buffer.isBuffer(data)) await node_fs.promises.writeFile(filePath, data);
64805
+ else {
64806
+ const writeStream = node_fs.createWriteStream(filePath);
64807
+ await new Promise((resolve, reject) => {
64808
+ data.pipe(writeStream);
64809
+ writeStream.on("finish", resolve);
64810
+ writeStream.on("error", reject);
64811
+ });
64491
64812
  }
64492
64813
  }
64493
- };
64494
- var DEFAULT_FIRST_UNIT_TIMEOUT_MS = 12e3;
64495
- var FRAGMENT_LOG_EVERY = 30;
64496
- var Fmp4FragmentChild = class {
64497
- deps;
64498
- args;
64499
- child = null;
64500
- splitter = new require_canonical_hash.Fmp4BoxSplitter();
64501
- stopped = false;
64502
- unitsOut = 0;
64503
- activeHwAccel = null;
64504
- constructor(deps, args) {
64505
- this.deps = deps;
64506
- this.args = args;
64814
+ async read({ location, relativePath }) {
64815
+ return node_fs.promises.readFile(await this.resolve({
64816
+ location,
64817
+ relativePath
64818
+ }));
64507
64819
  }
64508
- /** Spawn, and resolve once the INIT segment has been cut out of stdout. */
64509
- async start() {
64510
- const requested = this.args.invocation.decodeHwAccel;
64511
- this.activeHwAccel = requested;
64820
+ async exists({ location, relativePath }) {
64512
64821
  try {
64513
- await this.spawnAttempt(requested);
64514
- return;
64515
- } catch (err) {
64516
- if (this.stopped) throw err;
64517
- if (requested === null || require_canonical_hash.isSoftwareDecode(requested)) throw err;
64518
- this.deps.logger.warn("fmp4 fragment child: hardware decode produced NO fragment \u2014 retrying in SOFTWARE", {
64519
- tags: { deviceId: this.args.deviceId },
64520
- meta: {
64521
- sourceId: this.args.sourceId,
64522
- decodeHwAccel: requested,
64523
- error: require_err_msg.errMsg(err)
64524
- }
64525
- });
64526
- this.killChild();
64527
- this.splitter = new require_canonical_hash.Fmp4BoxSplitter();
64528
- this.activeHwAccel = null;
64529
- await this.spawnAttempt(null);
64822
+ await node_fs.promises.access(await this.resolve({
64823
+ location,
64824
+ relativePath
64825
+ }));
64826
+ return true;
64827
+ } catch {
64828
+ return false;
64530
64829
  }
64531
64830
  }
64532
- /** The backend the child ACTUALLY ran with — `null` for software. */
64533
- activeDecodeHwAccel() {
64534
- const value = this.activeHwAccel;
64535
- return value === null || value === "none" || value === "copy" ? null : value;
64536
- }
64537
- /** Kill ffmpeg and end the plane. Idempotent. */
64538
- async stop() {
64539
- if (this.stopped) return;
64540
- this.stopped = true;
64541
- this.killChild();
64542
- this.args.plane.end("the fragment child stopped");
64831
+ async list({ location, prefix }) {
64832
+ const base = this.locationPaths.get(location);
64833
+ if (!base) return [];
64834
+ const dir = prefix ? node_path.join(base, prefix) : base;
64835
+ try {
64836
+ return (await node_fs.promises.readdir(dir, { withFileTypes: true })).map((e) => prefix ? `${prefix}/${e.name}` : e.name);
64837
+ } catch {
64838
+ return [];
64839
+ }
64543
64840
  }
64544
- spawnAttempt(decodeHwAccel) {
64545
- const args = require_canonical_hash.buildFfmpegArgs({
64546
- ...this.args.invocation,
64547
- decodeHwAccel,
64548
- sink: {
64549
- kind: "stdout",
64550
- container: "mp4",
64551
- fragmentMs: this.args.fragmentMs
64552
- }
64553
- });
64554
- this.deps.logger.info("fmp4 fragment child: spawning ffmpeg", {
64555
- tags: { deviceId: this.args.deviceId },
64556
- meta: {
64557
- sourceId: this.args.sourceId,
64558
- fragmentMs: this.args.fragmentMs,
64559
- decodeHwAccel: decodeHwAccel ?? "software",
64560
- argv: args.join(" ")
64561
- }
64562
- });
64563
- return new Promise((resolve, reject) => {
64564
- const child = this.deps.spawnFn(this.deps.ffmpegBinaryPath, args, { stdio: [
64565
- "ignore",
64566
- "pipe",
64567
- "pipe"
64568
- ] });
64569
- this.child = child;
64570
- let settled = false;
64571
- let failed = false;
64572
- const isCurrent = () => !this.stopped && !failed;
64573
- const timeoutMs = this.deps.firstUnitTimeoutMs ?? DEFAULT_FIRST_UNIT_TIMEOUT_MS;
64574
- const settle = (fail) => {
64575
- if (settled) return;
64576
- settled = true;
64577
- clearTimeout(timer);
64578
- if (fail) {
64579
- failed = true;
64580
- reject(fail);
64581
- } else resolve();
64582
- };
64583
- const timer = setTimeout(() => {
64584
- settle(/* @__PURE__ */ new Error(`fmp4 fragment child: no fragment within ${timeoutMs}ms`));
64585
- this.killChild();
64586
- }, timeoutMs);
64587
- timer.unref?.();
64588
- child.stdout?.on("data", (chunk) => {
64589
- for (const unit of this.splitter.push(chunk)) {
64590
- this.unitsOut += 1;
64591
- this.args.plane.publish(unit);
64592
- if (unit.kind === "init") {
64593
- this.deps.logger.info("fmp4 fragment child: INIT segment cut", {
64594
- tags: { deviceId: this.args.deviceId },
64595
- meta: {
64596
- sourceId: this.args.sourceId,
64597
- bytes: unit.data.length
64598
- }
64599
- });
64600
- settle();
64601
- } else if (this.unitsOut % FRAGMENT_LOG_EVERY === 0) this.deps.logger.info("fmp4 fragment child: fragments still flowing", {
64602
- tags: { deviceId: this.args.deviceId },
64603
- meta: {
64604
- sourceId: this.args.sourceId,
64605
- unitsOut: this.unitsOut,
64606
- bytes: unit.data.length,
64607
- subscribers: this.args.plane.subscriberCount
64608
- }
64609
- });
64610
- }
64611
- const fault = this.splitter.fault;
64612
- if (fault !== null) this.onFault(fault, settled, isCurrent(), settle);
64613
- });
64614
- child.stderr?.setEncoding("utf8");
64615
- child.stderr?.on("data", (line) => {
64616
- this.deps.logger.debug("fmp4 fragment child ffmpeg", {
64617
- tags: { deviceId: this.args.deviceId },
64618
- meta: {
64619
- sourceId: this.args.sourceId,
64620
- line: line.trim()
64621
- }
64622
- });
64623
- });
64624
- child.once("error", (err) => {
64625
- if (!settled) {
64626
- settle(err);
64627
- return;
64628
- }
64629
- if (!isCurrent()) return;
64630
- this.args.plane.end("the fragment child errored");
64631
- this.deps.onChildExit?.(err);
64632
- });
64633
- child.once("exit", (code, signal) => {
64634
- if (!settled) {
64635
- settle(/* @__PURE__ */ new Error(`fmp4 fragment child: ffmpeg exited before any fragment (code=${code} signal=${signal})`));
64636
- return;
64637
- }
64638
- if (!isCurrent()) return;
64639
- const error = /* @__PURE__ */ new Error(`fmp4 fragment child: ffmpeg exited while live (code=${code} signal=${signal})`);
64640
- this.deps.logger.warn("fmp4 fragment child: ffmpeg exited while live", {
64641
- tags: { deviceId: this.args.deviceId },
64642
- meta: {
64643
- sourceId: this.args.sourceId,
64644
- code,
64645
- signal,
64646
- unitsOut: this.unitsOut
64647
- }
64648
- });
64649
- this.args.plane.end("the fragment child exited");
64650
- this.deps.onChildExit?.(error);
64651
- });
64841
+ async delete({ location, relativePath }) {
64842
+ const filePath = await this.resolve({
64843
+ location,
64844
+ relativePath
64652
64845
  });
64846
+ await node_fs.promises.rm(filePath, { force: true });
64653
64847
  }
64654
- /**
64655
- * The byte stream stopped being splittable. Not recoverable — the splitter
64656
- * cannot resynchronise mid-box — so the child is a corpse and every consumer
64657
- * has to be told, loudly, with the reason.
64658
- */
64659
- onFault(reason, wasLive, current, settle) {
64660
- const error = /* @__PURE__ */ new Error(`fmp4 fragment child: ${reason}`);
64661
- this.deps.logger.error("fmp4 fragment child: the ffmpeg output stopped parsing as fMP4", {
64662
- tags: { deviceId: this.args.deviceId },
64663
- meta: {
64664
- sourceId: this.args.sourceId,
64665
- unitsOut: this.unitsOut,
64666
- interstitial: this.splitter.discardedInterstitialTypes,
64667
- reason
64848
+ async getAvailableSpace({ location }) {
64849
+ const base = this.locationPaths.get(location);
64850
+ if (!base) return null;
64851
+ try {
64852
+ let target = base;
64853
+ while (!node_fs.existsSync(target)) {
64854
+ const parent = node_path.dirname(target);
64855
+ if (!parent || parent === target) return null;
64856
+ target = parent;
64668
64857
  }
64669
- });
64670
- this.killChild();
64671
- settle(error);
64672
- if (wasLive && current) {
64673
- this.args.plane.end("the fragment child produced unsplittable output");
64674
- this.deps.onChildExit?.(error);
64858
+ const stats = await node_fs.promises.statfs(target);
64859
+ return stats.bavail * stats.bsize;
64860
+ } catch {
64861
+ return null;
64675
64862
  }
64676
64863
  }
64677
- killChild() {
64678
- const child = this.child;
64679
- this.child = null;
64680
- if (child && !child.killed) try {
64681
- child.kill("SIGTERM");
64682
- } catch (err) {
64683
- this.deps.logger.warn("fmp4 fragment child: kill error", {
64684
- tags: { deviceId: this.args.deviceId },
64685
- meta: {
64686
- sourceId: this.args.sourceId,
64687
- error: require_err_msg.errMsg(err)
64688
- }
64689
- });
64690
- }
64864
+ /** Get the resolved path for a location type (addon-declared ids fall back
64865
+ * to `<rootPath>/<id>`). */
64866
+ getLocationPath(location) {
64867
+ return this.locationPaths.get(location) ?? node_path.join(this.rootPath, location);
64868
+ }
64869
+ /** Get the root path */
64870
+ getRootPath() {
64871
+ return this.rootPath;
64691
64872
  }
64692
64873
  };
64874
+ function physicalRootOf(root, realpath = node_fs.realpathSync) {
64875
+ const resolved = node_path.default.resolve(root);
64876
+ try {
64877
+ return {
64878
+ key: realpath(resolved),
64879
+ resolved: true
64880
+ };
64881
+ } catch {
64882
+ return {
64883
+ key: resolved,
64884
+ resolved: false
64885
+ };
64886
+ }
64887
+ }
64888
+ function containsOrEquals(outer, inner) {
64889
+ if (outer === inner) return true;
64890
+ const relative = node_path.default.relative(outer, inner);
64891
+ return relative !== "" && !relative.startsWith("..") && !node_path.default.isAbsolute(relative);
64892
+ }
64893
+ function signExpiringUrl(secret, id, expMs) {
64894
+ return (0, node_crypto.createHmac)("sha256", secret).update(`${id}:${String(expMs)}`).digest("hex");
64895
+ }
64896
+ function verifyExpiringUrl(input) {
64897
+ const { secret, id, exp, sig, nowMs } = input;
64898
+ if (exp === void 0 || sig === void 0) return false;
64899
+ const expMs = typeof exp === "number" ? exp : Number(exp);
64900
+ if (!Number.isFinite(expMs)) return false;
64901
+ if (expMs <= nowMs) return false;
64902
+ const expected = signExpiringUrl(secret, id, expMs);
64903
+ const a = Buffer.from(expected, "utf8");
64904
+ const b = Buffer.from(sig, "utf8");
64905
+ if (a.length !== b.length) return false;
64906
+ return (0, node_crypto.timingSafeEqual)(a, b);
64907
+ }
64908
+ function canonicalDeviceFingerprint(shape) {
64909
+ const features = [...new Set(shape.features)].toSorted();
64910
+ return require_canonical_hash.canonicalHash({
64911
+ deviceType: shape.deviceType,
64912
+ features
64913
+ });
64914
+ }
64915
+ function diffExportTargets(desired, lastAdvertised) {
64916
+ const toAdd = [];
64917
+ const toUpdate = [];
64918
+ const desiredIds = /* @__PURE__ */ new Set();
64919
+ for (const entry of desired) {
64920
+ desiredIds.add(entry.deviceId);
64921
+ const previous = lastAdvertised.get(entry.deviceId);
64922
+ if (previous === void 0) toAdd.push(entry);
64923
+ else if (previous.fingerprint !== entry.fingerprint) toUpdate.push(entry);
64924
+ }
64925
+ const toRemove = [];
64926
+ for (const deviceId of lastAdvertised.keys()) if (!desiredIds.has(deviceId)) toRemove.push(deviceId);
64927
+ return {
64928
+ toAdd,
64929
+ toUpdate,
64930
+ toRemove
64931
+ };
64932
+ }
64933
+ function resolveExportFingerprint(input) {
64934
+ if (input.ready) return input.fresh;
64935
+ return input.persisted ?? input.fresh;
64936
+ }
64693
64937
  exports.ChildCostRegistry = ChildCostRegistry;
64694
64938
  exports.FfmpegProcess = FfmpegProcess;
64695
64939
  exports.FilesystemStorageProvider = FilesystemStorageProvider;
@@ -64700,6 +64944,7 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
64700
64944
  exports.buildBinaryPath = buildBinaryPath;
64701
64945
  exports.canonicalDeviceFingerprint = canonicalDeviceFingerprint;
64702
64946
  exports.canonicalHash = require_canonical_hash.canonicalHash;
64947
+ exports.containsOrEquals = containsOrEquals;
64703
64948
  exports.diffExportTargets = diffExportTargets;
64704
64949
  exports.downloadBinary = downloadBinary;
64705
64950
  exports.ensureBinary = ensureBinary;
@@ -64714,6 +64959,7 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
64714
64959
  exports.nodeProcStatReader = nodeProcStatReader;
64715
64960
  exports.parseProcCpuSeconds = parseProcCpuSeconds;
64716
64961
  exports.parseProcRssBytes = parseProcRssBytes;
64962
+ exports.physicalRootOf = physicalRootOf;
64717
64963
  exports.readProcessCost = readProcessCost;
64718
64964
  exports.resolveExportFingerprint = resolveExportFingerprint;
64719
64965
  exports.signExpiringUrl = signExpiringUrl;
@@ -64730,8 +64976,8 @@ var require_device_manager_addon = __commonJS({
64730
64976
  [Symbol.toStringTag]: { value: "Module" }
64731
64977
  });
64732
64978
  require_chunk_Cek0wNdY();
64733
- var require_dist10 = require_dist_jZb_HKI5();
64734
- var require_retired_settings_keys = require_retired_settings_keys_Dxv9S_lO();
64979
+ var require_dist10 = require_dist_BFvWv_bz();
64980
+ var require_retired_settings_keys = require_retired_settings_keys_Dd7K3K3r();
64735
64981
  var node_crypto = __require("crypto");
64736
64982
  var _camstack_types_node = require_node();
64737
64983
  var JOB_HISTORY = 20;
@@ -70356,7 +70602,7 @@ var require_hub_forwarder = __commonJS({
70356
70602
  [Symbol.toStringTag]: { value: "Module" }
70357
70603
  });
70358
70604
  require_chunk_Cek0wNdY();
70359
- var require_dist10 = require_dist_jZb_HKI5();
70605
+ var require_dist10 = require_dist_BFvWv_bz();
70360
70606
  var require_formatter = require_formatter_DqAKDlvN();
70361
70607
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
70362
70608
  var HubForwarderDestination = class {
@@ -70493,7 +70739,7 @@ var require_liveness_monitor_addon = __commonJS({
70493
70739
  "use strict";
70494
70740
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
70495
70741
  require_chunk_Cek0wNdY();
70496
- var require_dist10 = require_dist_jZb_HKI5();
70742
+ var require_dist10 = require_dist_BFvWv_bz();
70497
70743
  var NO_DEVICES = "liveness:no-devices";
70498
70744
  var ALL_OFFLINE = "liveness:all-devices-offline";
70499
70745
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -70689,7 +70935,7 @@ var require_local_auth_addon = __commonJS({
70689
70935
  [Symbol.toStringTag]: { value: "Module" }
70690
70936
  });
70691
70937
  var require_chunk = require_chunk_Cek0wNdY();
70692
- var require_dist10 = require_dist_jZb_HKI5();
70938
+ var require_dist10 = require_dist_BFvWv_bz();
70693
70939
  var node_crypto = __require("crypto");
70694
70940
  node_crypto = require_chunk.__toESM(node_crypto);
70695
70941
  var crypto$1 = __require("crypto");
@@ -78502,7 +78748,7 @@ var require_loki_logging = __commonJS({
78502
78748
  [Symbol.toStringTag]: { value: "Module" }
78503
78749
  });
78504
78750
  require_chunk_Cek0wNdY();
78505
- var require_dist10 = require_dist_jZb_HKI5();
78751
+ var require_dist10 = require_dist_BFvWv_bz();
78506
78752
  function sanitizeLabelName(raw) {
78507
78753
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
78508
78754
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -79067,7 +79313,7 @@ var require_native_metrics_addon = __commonJS({
79067
79313
  [Symbol.toStringTag]: { value: "Module" }
79068
79314
  });
79069
79315
  var require_chunk = require_chunk_Cek0wNdY();
79070
- var require_dist10 = require_dist_jZb_HKI5();
79316
+ var require_dist10 = require_dist_BFvWv_bz();
79071
79317
  var node_fs_promises = __require("fs/promises");
79072
79318
  var node_child_process = __require("child_process");
79073
79319
  var node_util = __require("util");
@@ -81651,7 +81897,7 @@ var require_filesystem_storage_addon = __commonJS({
81651
81897
  [Symbol.toStringTag]: { value: "Module" }
81652
81898
  });
81653
81899
  var require_chunk = require_chunk_Cek0wNdY();
81654
- var require_dist10 = require_dist_jZb_HKI5();
81900
+ var require_dist10 = require_dist_BFvWv_bz();
81655
81901
  var node_crypto = __require("crypto");
81656
81902
  var node_fs_promises = __require("fs/promises");
81657
81903
  var node_path = __require("path");
@@ -82767,8 +83013,8 @@ var require_sqlite_settings_addon = __commonJS({
82767
83013
  [Symbol.toStringTag]: { value: "Module" }
82768
83014
  });
82769
83015
  var require_chunk = require_chunk_Cek0wNdY();
82770
- var require_dist10 = require_dist_jZb_HKI5();
82771
- var require_retired_settings_keys = require_retired_settings_keys_Dxv9S_lO();
83016
+ var require_dist10 = require_dist_BFvWv_bz();
83017
+ var require_retired_settings_keys = require_retired_settings_keys_Dd7K3K3r();
82772
83018
  var node_crypto = __require("crypto");
82773
83019
  var node_fs = __require("fs");
82774
83020
  var node_module = __require("module");
@@ -85258,10 +85504,11 @@ var require_storage_orchestrator_addon = __commonJS({
85258
85504
  [Symbol.toStringTag]: { value: "Module" }
85259
85505
  });
85260
85506
  var require_chunk = require_chunk_Cek0wNdY();
85261
- var require_dist10 = require_dist_jZb_HKI5();
85507
+ var require_dist10 = require_dist_BFvWv_bz();
85262
85508
  var require_hub_cap_forward = require_hub_cap_forward_DmHNjbB0();
85263
85509
  var zod = require_zod();
85264
85510
  var node_crypto = __require("crypto");
85511
+ var _camstack_types_node = require_node();
85265
85512
  var node_fs_promises = __require("fs/promises");
85266
85513
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
85267
85514
  var node_path = __require("path");
@@ -85269,6 +85516,7 @@ var require_storage_orchestrator_addon = __commonJS({
85269
85516
  var STORAGE_LOCATIONS = "storage_locations";
85270
85517
  var NODE_ID_CONFIG_KEY = "__nodeId";
85271
85518
  var ENABLED_CONFIG_KEY = "__enabled";
85519
+ var MODE_CONFIG_KEY = "__mode";
85272
85520
  var STORAGE_LOCATIONS_COLUMNS = [
85273
85521
  {
85274
85522
  name: "id",
@@ -85399,6 +85647,7 @@ var require_storage_orchestrator_addon = __commonJS({
85399
85647
  const storedConfig = { ...loc.config };
85400
85648
  if (loc.nodeId !== void 0) storedConfig[NODE_ID_CONFIG_KEY] = loc.nodeId;
85401
85649
  if (loc.enabled !== void 0) storedConfig[ENABLED_CONFIG_KEY] = loc.enabled;
85650
+ if (loc.mode !== void 0) storedConfig[MODE_CONFIG_KEY] = loc.mode;
85402
85651
  return {
85403
85652
  id: loc.id,
85404
85653
  type: loc.type,
@@ -85423,9 +85672,12 @@ var require_storage_orchestrator_addon = __commonJS({
85423
85672
  const nodeId = typeof nodeIdRaw === "string" && nodeIdRaw.length > 0 ? nodeIdRaw : void 0;
85424
85673
  const enabledRaw = storedConfig[ENABLED_CONFIG_KEY];
85425
85674
  const enabled = typeof enabledRaw === "boolean" ? enabledRaw : void 0;
85675
+ const parsedMode = require_dist10.StorageLocationModeSchema.safeParse(storedConfig[MODE_CONFIG_KEY]);
85676
+ const mode = parsedMode.success ? parsedMode.data : void 0;
85426
85677
  const config = { ...storedConfig };
85427
85678
  delete config[NODE_ID_CONFIG_KEY];
85428
85679
  delete config[ENABLED_CONFIG_KEY];
85680
+ delete config[MODE_CONFIG_KEY];
85429
85681
  return {
85430
85682
  id,
85431
85683
  type,
@@ -85434,6 +85686,7 @@ var require_storage_orchestrator_addon = __commonJS({
85434
85686
  config,
85435
85687
  ...nodeId !== void 0 ? { nodeId } : {},
85436
85688
  ...enabled !== void 0 ? { enabled } : {},
85689
+ ...mode !== void 0 ? { mode } : {},
85437
85690
  ...require_dist10.LegacyStorageLocationDefaultSchema.parse({ isDefault: row["is_default"] === 1 || row["is_default"] === true }),
85438
85691
  isSystem: row["is_system"] === 1 || row["is_system"] === true,
85439
85692
  createdAt: Number(row["created_at"] ?? 0),
@@ -85764,7 +86017,8 @@ var require_storage_orchestrator_addon = __commonJS({
85764
86017
  const remaining = residue.filter((entry) => entry.storageClass === storageClass);
85765
86018
  if (remaining.length === 0) throw new Error(`"${storageClass}" has nothing left outside its write set \u2014 there is nothing to drain. To CHANGE where it writes, use storageMigration.start with a new destination.`);
85766
86019
  for (const entry of remaining) {
85767
- if (this.deps.locations.getLocationById(entry.toLocationId)?.enabled === false) throw new Error(`The "${storageClass}" drain destination "${entry.toLocationId}" is DISABLED \u2014 a disabled location takes no writes, and a drain is the largest write there is. Enable it on the Storage screen first.`);
86020
+ const target = this.deps.locations.getLocationById(entry.toLocationId);
86021
+ if (target !== void 0 && !require_dist10.mayWriteToLocation(target)) throw new Error(`The "${storageClass}" drain destination "${entry.toLocationId}" is not a write target (mode: ${require_dist10.resolveLocationMode(target)}) \u2014 it takes no writes, and a drain is the largest write there is. Set it active on the Storage screen first.`);
85768
86022
  moves.push({
85769
86023
  storageClass,
85770
86024
  fromLocationId: entry.fromLocationId,
@@ -86097,12 +86351,9 @@ var require_storage_orchestrator_addon = __commonJS({
86097
86351
  for (const move of job.moves) {
86098
86352
  if (move.freezeSource !== true) continue;
86099
86353
  const loc = this.deps.locations.getLocationById(move.fromLocationId);
86100
- if (!loc || loc.enabled === false) continue;
86354
+ if (!loc || !require_dist10.mayWriteToLocation(loc)) continue;
86101
86355
  const { createdAt: _c, updatedAt: _u, capacity: _cap, ...rest } = loc;
86102
- this.deps.locations.upsertLocation({
86103
- ...rest,
86104
- enabled: false
86105
- });
86356
+ this.deps.locations.upsertLocation(require_dist10.withLocationMode(rest, "readonly"));
86106
86357
  }
86107
86358
  }
86108
86359
  /**
@@ -86267,6 +86518,126 @@ var require_storage_orchestrator_addon = __commonJS({
86267
86518
  declareCollection: async (input) => (await engine()).declareCollection(input)
86268
86519
  };
86269
86520
  }
86521
+ var STORAGE_DRAIN_STATE = "storage_drain_state";
86522
+ var STORAGE_DRAIN_STATE_COLUMNS = [
86523
+ {
86524
+ name: "location_id",
86525
+ type: "TEXT",
86526
+ primaryKey: true
86527
+ },
86528
+ {
86529
+ name: "started_at_ms",
86530
+ type: "INTEGER",
86531
+ notNull: true
86532
+ },
86533
+ {
86534
+ name: "start_bytes",
86535
+ type: "INTEGER",
86536
+ notNull: true
86537
+ },
86538
+ {
86539
+ name: "cap_bytes",
86540
+ type: "INTEGER",
86541
+ notNull: true
86542
+ },
86543
+ {
86544
+ name: "last_peer_bytes",
86545
+ type: "INTEGER"
86546
+ },
86547
+ {
86548
+ name: "last_sweep_ms",
86549
+ type: "INTEGER",
86550
+ notNull: true
86551
+ },
86552
+ {
86553
+ name: "observed_growth_bytes",
86554
+ type: "INTEGER",
86555
+ notNull: true,
86556
+ defaultValue: 0
86557
+ },
86558
+ {
86559
+ name: "observed_elapsed_ms",
86560
+ type: "INTEGER",
86561
+ notNull: true,
86562
+ defaultValue: 0
86563
+ }
86564
+ ];
86565
+ function num(raw, fallback) {
86566
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : fallback;
86567
+ }
86568
+ function mapRow(row) {
86569
+ const locationId = String(row["location_id"] ?? "");
86570
+ if (!locationId) return null;
86571
+ const lastPeerRaw = row["last_peer_bytes"];
86572
+ return {
86573
+ locationId,
86574
+ startedAtMs: num(row["started_at_ms"], 0),
86575
+ startBytes: num(row["start_bytes"], 0),
86576
+ capBytes: num(row["cap_bytes"], 0),
86577
+ lastPeerBytes: typeof lastPeerRaw === "number" && Number.isFinite(lastPeerRaw) ? lastPeerRaw : null,
86578
+ lastSweepMs: num(row["last_sweep_ms"], 0),
86579
+ observedGrowthBytes: num(row["observed_growth_bytes"], 0),
86580
+ observedElapsedMs: num(row["observed_elapsed_ms"], 0)
86581
+ };
86582
+ }
86583
+ function mapState(state) {
86584
+ return {
86585
+ location_id: state.locationId,
86586
+ started_at_ms: state.startedAtMs,
86587
+ start_bytes: state.startBytes,
86588
+ cap_bytes: state.capBytes,
86589
+ last_peer_bytes: state.lastPeerBytes,
86590
+ last_sweep_ms: state.lastSweepMs,
86591
+ observed_growth_bytes: state.observedGrowthBytes,
86592
+ observed_elapsed_ms: state.observedElapsedMs
86593
+ };
86594
+ }
86595
+ var SqliteDrainStateStore = class {
86596
+ store;
86597
+ declared = false;
86598
+ constructor(store) {
86599
+ this.store = store;
86600
+ }
86601
+ async ensureDeclared() {
86602
+ if (this.declared) return;
86603
+ await this.store.declareCollection({
86604
+ collection: STORAGE_DRAIN_STATE,
86605
+ columns: STORAGE_DRAIN_STATE_COLUMNS
86606
+ });
86607
+ this.declared = true;
86608
+ }
86609
+ async load() {
86610
+ await this.ensureDeclared();
86611
+ const rows = await this.store.query({ collection: STORAGE_DRAIN_STATE });
86612
+ const out = [];
86613
+ for (const row of rows) {
86614
+ const mapped = mapRow(row.data);
86615
+ if (mapped) out.push(mapped);
86616
+ }
86617
+ return out;
86618
+ }
86619
+ async save(state) {
86620
+ await this.ensureDeclared();
86621
+ await this.store.deleteWhere({
86622
+ collection: STORAGE_DRAIN_STATE,
86623
+ filter: { where: { location_id: state.locationId } }
86624
+ });
86625
+ await this.store.insert({
86626
+ collection: STORAGE_DRAIN_STATE,
86627
+ record: {
86628
+ id: state.locationId,
86629
+ data: mapState(state)
86630
+ }
86631
+ });
86632
+ }
86633
+ async clear(locationId) {
86634
+ await this.ensureDeclared();
86635
+ await this.store.deleteWhere({
86636
+ collection: STORAGE_DRAIN_STATE,
86637
+ filter: { where: { location_id: locationId } }
86638
+ });
86639
+ }
86640
+ };
86270
86641
  function nodeOwnsItsDataStore(env) {
86271
86642
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
86272
86643
  return raw === "" || raw === "hub";
@@ -86346,6 +86717,101 @@ var require_storage_orchestrator_addon = __commonJS({
86346
86717
  }
86347
86718
  return "empty";
86348
86719
  }
86720
+ var DEFAULT_STALE_AFTER_MS = 6e4;
86721
+ var LocationOccupancySlice = class {
86722
+ deps;
86723
+ slice = /* @__PURE__ */ new Map();
86724
+ refreshing = null;
86725
+ lastRefreshMs = 0;
86726
+ constructor(deps) {
86727
+ this.deps = deps;
86728
+ }
86729
+ /**
86730
+ * What the last completed refresh saw for this location, or `undefined` when
86731
+ * nothing has reported for it. There is deliberately no fallback: unknown is
86732
+ * the truth about a location nobody owns bytes on.
86733
+ */
86734
+ ownedFor(locationId) {
86735
+ return this.slice.get(locationId);
86736
+ }
86737
+ /**
86738
+ * Serve-and-revalidate. Called by every read: it returns immediately, and
86739
+ * kicks a background refresh when the slice is older than the window. A read
86740
+ * never awaits, and never joins a refresh already in flight (D224).
86741
+ */
86742
+ serveAndRevalidate() {
86743
+ if (this.refreshing !== null) return;
86744
+ const staleAfter = this.deps.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
86745
+ if (this.lastRefreshMs !== 0 && this.deps.now() - this.lastRefreshMs < staleAfter) return;
86746
+ this.refresh();
86747
+ }
86748
+ /**
86749
+ * Ask every provider once and replace the slice.
86750
+ *
86751
+ * A provider that throws contributes NOTHING and is logged — it does not
86752
+ * contribute a zero, and it does not take the other providers' answers down
86753
+ * with it. A location that ends up with no contribution at all simply stays
86754
+ * out of the slice, which is what makes it read back as unknown.
86755
+ */
86756
+ async refresh() {
86757
+ if (this.refreshing !== null) return this.refreshing;
86758
+ this.refreshing = this.runRefresh().finally(() => {
86759
+ this.refreshing = null;
86760
+ });
86761
+ return this.refreshing;
86762
+ }
86763
+ async runRefresh() {
86764
+ const locationIds = [...this.deps.locationIds()];
86765
+ if (locationIds.length === 0) {
86766
+ this.slice = /* @__PURE__ */ new Map();
86767
+ this.lastRefreshMs = this.deps.now();
86768
+ return;
86769
+ }
86770
+ const merged = /* @__PURE__ */ new Map();
86771
+ for (const provider of this.deps.getProviders()) {
86772
+ let reports;
86773
+ try {
86774
+ reports = await provider.getOccupancy({ locationIds });
86775
+ } catch (err) {
86776
+ this.deps.logger.warn("storage-orchestrator: a storage-occupancy provider failed \u2014 its bytes are MISSING from this refresh, not counted as zero", { meta: { error: err instanceof Error ? err.message : String(err) } });
86777
+ continue;
86778
+ }
86779
+ for (const report of reports) {
86780
+ const previous = merged.get(report.locationId);
86781
+ merged.set(report.locationId, {
86782
+ bytes: (previous?.bytes ?? 0) + report.ownedBytes,
86783
+ measuredAtMs: previous === void 0 ? report.measuredAtMs : Math.min(previous.measuredAtMs, report.measuredAtMs)
86784
+ });
86785
+ }
86786
+ }
86787
+ this.slice = merged;
86788
+ this.lastRefreshMs = this.deps.now();
86789
+ }
86790
+ };
86791
+ function positiveNumber(raw) {
86792
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) return null;
86793
+ return raw;
86794
+ }
86795
+ function resolveMinFreePercent(config, providerDefaultPercent, capacity, notice) {
86796
+ const minFreeGb = positiveNumber(config["minFreeGb"]);
86797
+ const minFreePercent = positiveNumber(config["minFreePercent"]);
86798
+ const fallback = minFreePercent ?? providerDefaultPercent;
86799
+ if (minFreeGb === null) return fallback;
86800
+ if (minFreePercent !== null) notice("storage-orchestrator: location declares BOTH minFreeGb and minFreePercent \u2014 minFreeGb wins and minFreePercent is ignored. Remove one.", {
86801
+ minFreeGb,
86802
+ minFreePercent
86803
+ });
86804
+ const totalBytes = capacity?.totalBytes ?? 0;
86805
+ if (totalBytes <= 0) {
86806
+ notice("storage-orchestrator: minFreeGb cannot be converted \u2014 the volume total is unknown (remote node, or an unstattable path). Falling back to the percentage guard.", {
86807
+ minFreeGb,
86808
+ fallbackPercent: fallback
86809
+ });
86810
+ return fallback;
86811
+ }
86812
+ const percent = require_dist10.gbToBytes(minFreeGb) / totalBytes * 100;
86813
+ return Math.min(100, percent);
86814
+ }
86349
86815
  var FRAMEWORK_CONFIG_KEYS = /* @__PURE__ */ new Set([
86350
86816
  "basePath",
86351
86817
  "readOnly",
@@ -86413,7 +86879,7 @@ var require_storage_orchestrator_addon = __commonJS({
86413
86879
  return job.phase === "done" || job.phase === "failed" || job.phase === "cancelled";
86414
86880
  }
86415
86881
  function footageFrozen(location) {
86416
- return location.enabled === false || location.config["readOnly"] === true;
86882
+ return require_dist10.evictionPolicyOfLocation(location) === "never";
86417
86883
  }
86418
86884
  var StorageCleanupCoordinator = class {
86419
86885
  deps;
@@ -86566,6 +87032,66 @@ var require_storage_orchestrator_addon = __commonJS({
86566
87032
  await this.deps.state.set(job);
86567
87033
  }
86568
87034
  };
87035
+ function basePathOf(row) {
87036
+ const raw = row.config["basePath"];
87037
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
87038
+ }
87039
+ function sameNode(a, b) {
87040
+ return (a.nodeId ?? "hub") === (b.nodeId ?? "hub");
87041
+ }
87042
+ function findRootCollision(next, existing, resolveRoot, storedSelf) {
87043
+ if (storedSelf !== void 0 && basePathOf(storedSelf) === basePathOf(next)) return { outcome: "clear" };
87044
+ const basePath = basePathOf(next);
87045
+ if (basePath === null) return { outcome: "clear" };
87046
+ const root = resolveRoot(basePath);
87047
+ if (!root.resolved) return {
87048
+ outcome: "unknown",
87049
+ unknown: {
87050
+ reason: "unresolvable",
87051
+ detail: `could not resolve "${basePath}" on this node`
87052
+ }
87053
+ };
87054
+ for (const other of existing) {
87055
+ if (other.id === next.id) continue;
87056
+ if (other.type !== next.type) continue;
87057
+ if (!sameNode(other, next)) continue;
87058
+ const otherPath = basePathOf(other);
87059
+ if (otherPath === null) continue;
87060
+ const otherRoot = resolveRoot(otherPath);
87061
+ if (!otherRoot.resolved) continue;
87062
+ if (root.key === otherRoot.key) return {
87063
+ outcome: "collides",
87064
+ collision: {
87065
+ otherId: other.id,
87066
+ type: next.type,
87067
+ sharedRoot: root.key,
87068
+ kind: "equal"
87069
+ }
87070
+ };
87071
+ if ((0, _camstack_types_node.containsOrEquals)(otherRoot.key, root.key)) return {
87072
+ outcome: "collides",
87073
+ collision: {
87074
+ otherId: other.id,
87075
+ type: next.type,
87076
+ sharedRoot: otherRoot.key,
87077
+ kind: "nested"
87078
+ }
87079
+ };
87080
+ if ((0, _camstack_types_node.containsOrEquals)(root.key, otherRoot.key)) return {
87081
+ outcome: "collides",
87082
+ collision: {
87083
+ otherId: other.id,
87084
+ type: next.type,
87085
+ sharedRoot: root.key,
87086
+ kind: "nested"
87087
+ }
87088
+ };
87089
+ }
87090
+ return { outcome: "clear" };
87091
+ }
87092
+ function rootCollisionMessage(id, collision) {
87093
+ return `Cannot save "${id}": it is ${collision.kind === "equal" ? `the same directory as "${collision.otherId}"` : `nested with "${collision.otherId}" (one is inside the other's tree)`}, and both are type "${collision.type}". Two locations of one type on the same disk share a class subtree, so every per-location figure double-counts and a deletion under one is a deletion under the other. Shared root: ${collision.sharedRoot}. Two locations of DIFFERENT types may share a root \u2014 that is how recordings and recordingsLow are configured by default.`;
87094
+ }
86569
87095
  var SESSION_VOCABULARY = {
86570
87096
  upload: {
86571
87097
  idLabel: "uploadId",
@@ -86691,38 +87217,41 @@ var require_storage_orchestrator_addon = __commonJS({
86691
87217
  this.migrateOntoTheWriteSet(rows);
86692
87218
  }
86693
87219
  /**
86694
- * ONE explicit migration onto the enabled write set (D383), run once per
86695
- * boot against the rows that came off disk. Idempotent: the second boot finds
87220
+ * ONE explicit migration onto the four-state model (D385), run once per boot
87221
+ * against the rows that came off disk. Idempotent: the second boot finds
86696
87222
  * nothing to do.
86697
87223
  *
86698
87224
  * Two steps, in order, and each one is a WRITE — the point is that "absent"
86699
87225
  * stops existing rather than being re-interpreted on every read forever:
86700
87226
  *
86701
- * 1. A flagless row (persisted before `enabled` existed) is stamped
86702
- * `enabled: true`. Absence already meant active, so this preserves
86703
- * behaviour exactly; it is the operator's "the initial locations are
86704
- * enabled" spelled into the store.
86705
- * 2. A type left with NO enabled row — only reachable from a hand-edited
86706
- * store or a row an older build wrote — gets one enabled back, chosen as
87227
+ * 1. A row with no `mode` is stamped with the mode its legacy flags imply
87228
+ * (`legacyModeOf`): `config.readOnly === true` → `readonly` and the
87229
+ * config key is DELETED; `enabled === false` → `readonly` (the
87230
+ * CONSERVATIVE direction — a state change must never start deleting
87231
+ * footage on its own); everything else → `active`. `withLocationMode`
87232
+ * derives `enabled` from the mode, so the two can never disagree, and
87233
+ * this subsumes the D383 pass that stamped flagless rows `enabled: true`.
87234
+ * 2. A type left with NO write target — only reachable from a hand-edited
87235
+ * store or a row an older build wrote — gets one made `active`, chosen as
86707
87236
  * the row the RETIRED default flag named, else the `:default`-slugged
86708
87237
  * row, else the lexicographically first. Loudly: a class with no write
86709
87238
  * target records nowhere, and refusing to repair it here would leave the
86710
87239
  * hub silently unable to write with no operator action pending.
86711
87240
  *
86712
- * An operator's explicit `enabled: false` is never reversed by step 1 — only
86713
- * step 2 touches it, and only when the alternative is a class that cannot
86714
- * write at all.
87241
+ * An operator's explicit `readonly`/`drain`/`disabled` is never reversed by
87242
+ * step 1 — only step 2 touches it, and only when the alternative is a class
87243
+ * that cannot write at all.
87244
+ *
87245
+ * On the live hub every location is enabled with no `readOnly`, so step 1 is
87246
+ * a no-op that stamps `active` and nothing else changes.
86715
87247
  */
86716
87248
  migrateOntoTheWriteSet(rows) {
86717
87249
  const legacyDefaultOfType = /* @__PURE__ */ new Map();
86718
87250
  for (const row of rows) if (row.isDefault === true) legacyDefaultOfType.set(row.type, row.id);
86719
87251
  const stamped = [];
86720
87252
  for (const loc of this.locations.values()) {
86721
- if (loc.enabled !== void 0) continue;
86722
- this.persistMigrated({
86723
- ...loc,
86724
- enabled: true
86725
- });
87253
+ if (loc.mode !== void 0) continue;
87254
+ this.persistMigrated(require_dist10.withLocationMode(loc, require_dist10.legacyModeOf(loc) ?? "active"));
86726
87255
  stamped.push(loc.id);
86727
87256
  }
86728
87257
  const repaired = [];
@@ -86732,13 +87261,10 @@ var require_storage_orchestrator_addon = __commonJS({
86732
87261
  const legacyDefault = legacyDefaultOfType.get(type);
86733
87262
  const pick = ofType.find((l) => l.id === legacyDefault) ?? ofType.find((l) => l.id === `${type}:default`) ?? ofType.toSorted((a, b) => a.id.localeCompare(b.id))[0];
86734
87263
  if (!pick) continue;
86735
- this.persistMigrated({
86736
- ...pick,
86737
- enabled: true
86738
- });
87264
+ this.persistMigrated(require_dist10.withLocationMode(pick, "active"));
86739
87265
  repaired.push(pick.id);
86740
87266
  }
86741
- if (stamped.length > 0 || repaired.length > 0) this.logger.info("storage-orchestrator: migrated locations onto the enabled write set", { meta: {
87267
+ if (stamped.length > 0 || repaired.length > 0) this.logger.info("storage-orchestrator: migrated locations onto the state model", { meta: {
86742
87268
  stamped,
86743
87269
  repaired
86744
87270
  } });
@@ -86754,7 +87280,7 @@ var require_storage_orchestrator_addon = __commonJS({
86754
87280
  const store = this.locationStore;
86755
87281
  if (!store) return;
86756
87282
  store.upsert(withStamp).catch((err) => {
86757
- this.logger.error("storage-orchestrator: write-set migration persist failed", { meta: {
87283
+ this.logger.error("storage-orchestrator: state-model migration persist failed", { meta: {
86758
87284
  id: withStamp.id,
86759
87285
  error: err instanceof Error ? err.message : String(err)
86760
87286
  } });
@@ -86896,12 +87422,11 @@ var require_storage_orchestrator_addon = __commonJS({
86896
87422
  for (const [id, enabled] of enabledByLocationId) {
86897
87423
  const location = nextById.get(id);
86898
87424
  if (!location) throw new Error(`Storage location "${id}" not found`);
86899
- if (location.enabled === enabled) continue;
86900
- const updated = {
87425
+ if (require_dist10.isLocationEnabled(location) === enabled) continue;
87426
+ const updated = require_dist10.withLocationMode({
86901
87427
  ...location,
86902
- enabled,
86903
87428
  updatedAt: now
86904
- };
87429
+ }, enabled ? "active" : "readonly");
86905
87430
  nextById.set(id, updated);
86906
87431
  touched.set(id, updated);
86907
87432
  }
@@ -87006,22 +87531,19 @@ var require_storage_orchestrator_addon = __commonJS({
87006
87531
  ...input,
87007
87532
  nodeId: "hub"
87008
87533
  };
87009
- input = {
87010
- ...input,
87011
- ...resolveEnabled(input, existing, this.hasAnyLocationOfType(input.type))
87012
- };
87013
- const next = {
87534
+ this.refuseSameTypeRootCollision(input, existing);
87535
+ const next = require_dist10.withLocationMode({
87014
87536
  ...input,
87015
87537
  createdAt: existing?.createdAt ?? now,
87016
87538
  updatedAt: now
87017
- };
87539
+ }, resolveUpsertMode(input, existing, this.hasAnyLocationOfType(input.type)));
87018
87540
  refuseLastWriteTargetDisabled(existing, next, this.locations.values());
87019
87541
  this.locations.set(next.id, next);
87020
87542
  this.logger.debug("storage-orchestrator: upsertLocation", { meta: {
87021
87543
  id: next.id,
87022
87544
  type: next.type,
87023
87545
  providerId: next.providerId,
87024
- enabled: next.enabled
87546
+ mode: next.mode
87025
87547
  } });
87026
87548
  if (this.locationStore) {
87027
87549
  const store = this.locationStore;
@@ -87050,8 +87572,8 @@ var require_storage_orchestrator_addon = __commonJS({
87050
87572
  if (!loc) throw new Error(`Storage location "${id}" not found`);
87051
87573
  const ofType = [...this.locations.values()].filter((l) => l.type === loc.type);
87052
87574
  if (ofType.length <= 1) throw new Error(`Cannot delete "${id}" \u2014 it is the only location for type "${loc.type}"`);
87053
- const enabledOfType = ofType.filter((l) => l.enabled !== false);
87054
- if (loc.enabled !== false && enabledOfType.length <= 1) throw new Error(`Cannot delete "${id}" \u2014 it is the only enabled location for type "${loc.type}"`);
87575
+ const enabledOfType = ofType.filter((l) => require_dist10.mayWriteToLocation(l));
87576
+ if (require_dist10.mayWriteToLocation(loc) && enabledOfType.length <= 1) throw new Error(`Cannot delete "${id}" \u2014 it is the only enabled location for type "${loc.type}"`);
87055
87577
  const occupancy = await this.probeOccupancy(loc);
87056
87578
  if (occupancy !== "empty") {
87057
87579
  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`);
@@ -87292,29 +87814,57 @@ var require_storage_orchestrator_addon = __commonJS({
87292
87814
  basePath: input.basePath
87293
87815
  } });
87294
87816
  }
87817
+ /**
87818
+ * The same-type physical-root refusal (D389).
87819
+ *
87820
+ * An unresolvable root — another node's disk, a remote provider, a path that
87821
+ * does not exist yet — WARNS and lets the write through. Refusing on a read
87822
+ * that could not be taken blocks the operator on a doubt, and the recorder's
87823
+ * demotion still catches what slips past (D49's spirit).
87824
+ */
87825
+ refuseSameTypeRootCollision(input, existing) {
87826
+ const verdict = findRootCollision(input, this.locations.values(), this.resolveRoot, existing);
87827
+ if (verdict.outcome === "clear") return;
87828
+ if (verdict.outcome === "unknown") {
87829
+ this.logger.warn("storage-orchestrator: could not check this location for a same-type root collision \u2014 saving it anyway. A read that fails is not a refusal; the recorder demotes a same-root duplicate it later finds.", { meta: {
87830
+ id: input.id,
87831
+ type: input.type,
87832
+ ...verdict.unknown
87833
+ } });
87834
+ return;
87835
+ }
87836
+ throw new Error(rootCollisionMessage(input.id, verdict.collision));
87837
+ }
87838
+ /** Injectable so a test can drive symlinks and unresolvable paths without
87839
+ * touching a real filesystem. Production takes the shared derivation. */
87840
+ resolveRoot = (basePath) => (0, _camstack_types_node.physicalRootOf)(basePath);
87841
+ /** Test seam for {@link refuseSameTypeRootCollision}. */
87842
+ setRootResolver(resolveRoot) {
87843
+ this.resolveRoot = resolveRoot;
87844
+ }
87295
87845
  /** Internal: check whether any location of the given type exists. */
87296
87846
  hasAnyLocationOfType(type) {
87297
87847
  for (const loc of this.locations.values()) if (loc.type === type) return true;
87298
87848
  return false;
87299
87849
  }
87300
87850
  };
87301
- function resolveEnabled(input, existing, typeAlreadyHasLocation) {
87302
- const requested = input.enabled ?? existing?.enabled;
87303
- if (requested === void 0) {
87304
- if (existing) return {};
87305
- return { enabled: !typeAlreadyHasLocation };
87851
+ function resolveUpsertMode(input, existing, typeAlreadyHasLocation) {
87852
+ if (input.mode !== void 0) {
87853
+ if (input.enabled === void 0 || input.enabled === require_dist10.modeMayWrite(input.mode)) return input.mode;
87306
87854
  }
87307
- return { enabled: requested };
87855
+ if (input.enabled !== void 0) return input.enabled ? "active" : "readonly";
87856
+ if (existing) return require_dist10.resolveLocationMode(existing);
87857
+ return typeAlreadyHasLocation ? "readonly" : "active";
87308
87858
  }
87309
87859
  var OPTIONAL_WRITE_TYPES = /* @__PURE__ */ new Set(["backups"]);
87310
87860
  function refuseLastWriteTargetDisabled(existing, next, locations) {
87311
87861
  if (OPTIONAL_WRITE_TYPES.has(next.type)) return;
87312
- if (next.enabled !== false) return;
87862
+ if (require_dist10.mayWriteToLocation(next)) return;
87313
87863
  if (existing === void 0) return;
87314
- if (existing.enabled === false) return;
87864
+ if (!require_dist10.mayWriteToLocation(existing)) return;
87315
87865
  for (const loc of locations) {
87316
87866
  if (loc.id === next.id || loc.type !== next.type) continue;
87317
- if (loc.enabled !== false) return;
87867
+ if (require_dist10.mayWriteToLocation(loc)) return;
87318
87868
  }
87319
87869
  throw new Error(`Cannot disable "${next.id}": it is the last enabled location of type "${next.type}". Enable another location of this type first.`);
87320
87870
  }
@@ -87328,10 +87878,70 @@ var require_storage_orchestrator_addon = __commonJS({
87328
87878
  throw new Error(`Refusing the write-set change: it would leave type "${type}" with no enabled location. Enable one of ${ofType.map((loc) => loc.id).toSorted().join(", ")} first.`);
87329
87879
  }
87330
87880
  }
87881
+ function beginDrain(observation) {
87882
+ return {
87883
+ locationId: observation.locationId,
87884
+ startedAtMs: observation.atMs,
87885
+ startBytes: observation.usedBytes,
87886
+ capBytes: observation.usedBytes,
87887
+ lastPeerBytes: observation.peerUsedBytes,
87888
+ lastSweepMs: observation.atMs,
87889
+ observedGrowthBytes: 0,
87890
+ observedElapsedMs: 0
87891
+ };
87892
+ }
87893
+ function stepDrain(previous, observation) {
87894
+ if (previous === null) return {
87895
+ state: beginDrain(observation),
87896
+ capBytes: null,
87897
+ drained: observation.usedBytes <= 0,
87898
+ growthBytes: null
87899
+ };
87900
+ const growthBytes = previous.lastPeerBytes !== null && observation.peerUsedBytes !== null ? observation.peerUsedBytes - previous.lastPeerBytes : null;
87901
+ const ceded = growthBytes !== null && growthBytes > 0 ? growthBytes : 0;
87902
+ const nextCap = Math.max(0, Math.min(previous.capBytes, observation.usedBytes) - ceded);
87903
+ const state = {
87904
+ ...previous,
87905
+ capBytes: nextCap,
87906
+ lastPeerBytes: observation.peerUsedBytes ?? previous.lastPeerBytes,
87907
+ lastSweepMs: observation.atMs,
87908
+ observedGrowthBytes: previous.observedGrowthBytes + ceded,
87909
+ observedElapsedMs: growthBytes === null ? previous.observedElapsedMs : previous.observedElapsedMs + Math.max(0, observation.atMs - previous.lastSweepMs)
87910
+ };
87911
+ if (observation.usedBytes <= 0) return {
87912
+ state: {
87913
+ ...state,
87914
+ capBytes: 0
87915
+ },
87916
+ capBytes: null,
87917
+ drained: true,
87918
+ growthBytes
87919
+ };
87920
+ return {
87921
+ state,
87922
+ capBytes: nextCap,
87923
+ drained: false,
87924
+ growthBytes
87925
+ };
87926
+ }
87927
+ function drainProgressOf(state, usedBytes, nowMs) {
87928
+ const bytesRemaining = Math.max(0, usedBytes);
87929
+ const rateBytesPerMs = state.observedGrowthBytes > 0 && state.observedElapsedMs > 0 ? state.observedGrowthBytes / state.observedElapsedMs : null;
87930
+ return {
87931
+ locationId: state.locationId,
87932
+ startedAtMs: state.startedAtMs,
87933
+ startBytes: state.startBytes,
87934
+ bytesRemaining,
87935
+ drained: bytesRemaining <= 0,
87936
+ estimatedEmptyAtMs: rateBytesPerMs === null || bytesRemaining <= 0 ? null : nowMs + Math.ceil(bytesRemaining / rateBytesPerMs)
87937
+ };
87938
+ }
87331
87939
  var DEFAULT_MAX_ROUNDS = 8;
87332
87940
  var DEFAULT_HYSTERESIS_PERCENT = 1;
87333
87941
  var StoragePressureManager = class {
87334
87942
  deps;
87943
+ /** Last sweep's drain progress, by location. Read-only surface for the UI. */
87944
+ drainProgress = /* @__PURE__ */ new Map();
87335
87945
  constructor(deps) {
87336
87946
  this.deps = deps;
87337
87947
  }
@@ -87373,6 +87983,73 @@ var require_storage_orchestrator_addon = __commonJS({
87373
87983
  } });
87374
87984
  }
87375
87985
  }
87986
+ await this.sweepDrains();
87987
+ }
87988
+ /**
87989
+ * The drain pass (D386). A `drain` location is excluded from nothing — it is
87990
+ * given a monotonically descending effective usage cap and handed to the SAME
87991
+ * `relieveToUsageCap` machinery every other cap uses. There is no second
87992
+ * eviction path, and no operator-facing rate.
87993
+ */
87994
+ async sweepDrains() {
87995
+ const plan = await this.deps.getDrainPlan?.() ?? [];
87996
+ const store = this.deps.drainState;
87997
+ if (store === void 0) {
87998
+ if (plan.length > 0) this.deps.logger.warn("storage drain: locations are draining but no durable state store is wired \u2014 the ratchet stays inert rather than restarting from scratch every boot", { meta: { locationIds: plan.map((e) => e.locationId) } });
87999
+ this.drainProgress = /* @__PURE__ */ new Map();
88000
+ return;
88001
+ }
88002
+ const persisted = new Map((await store.load()).map((state) => [state.locationId, state]));
88003
+ const draining = new Set(plan.map((entry) => entry.locationId));
88004
+ for (const locationId of persisted.keys()) {
88005
+ if (draining.has(locationId)) continue;
88006
+ await store.clear(locationId);
88007
+ this.drainProgress.delete(locationId);
88008
+ this.deps.logger.info("storage drain: stopped \u2014 location is no longer draining", { meta: { locationId } });
88009
+ }
88010
+ const nowMs = Date.now();
88011
+ const progress = /* @__PURE__ */ new Map();
88012
+ for (const entry of plan) try {
88013
+ const usedBytes = await this.evictableUsageOf(entry.locationId);
88014
+ const peerUsedBytes = entry.activePeerLocationIds.length === 0 ? null : await this.evictableUsageOfAll(entry.activePeerLocationIds);
88015
+ const step = stepDrain(persisted.get(entry.locationId) ?? null, {
88016
+ locationId: entry.locationId,
88017
+ usedBytes,
88018
+ peerUsedBytes,
88019
+ atMs: nowMs
88020
+ });
88021
+ await store.save(step.state);
88022
+ this.deps.logger.info("storage drain: sweep", { meta: {
88023
+ locationId: entry.locationId,
88024
+ usedBytes,
88025
+ peerUsedBytes,
88026
+ growthBytes: step.growthBytes,
88027
+ capBytes: step.capBytes,
88028
+ drained: step.drained
88029
+ } });
88030
+ if (step.capBytes !== null) await this.relieveToUsageCap(entry.locationId, step.capBytes);
88031
+ progress.set(entry.locationId, drainProgressOf(step.state, await this.evictableUsageOf(entry.locationId), nowMs));
88032
+ } catch (err) {
88033
+ this.deps.logger.warn("storage drain: sweep failed for location", { meta: {
88034
+ locationId: entry.locationId,
88035
+ error: err instanceof Error ? err.message : String(err)
88036
+ } });
88037
+ }
88038
+ this.drainProgress = progress;
88039
+ }
88040
+ /** Progress for every draining location, as of the last sweep. The read a UI
88041
+ * asks for — it never triggers work of its own. */
88042
+ drainProgressSnapshot() {
88043
+ return [...this.drainProgress.values()];
88044
+ }
88045
+ /** Evictable bytes on one location, summed across every provider. */
88046
+ async evictableUsageOf(locationId) {
88047
+ const providers = this.deps.getEvictableProviders();
88048
+ return (await Promise.all(providers.map(async (p) => (await p.getEvictableUsage({ locationId })).bytes))).reduce((sum, bytes) => sum + bytes, 0);
88049
+ }
88050
+ /** Evictable bytes across a set of locations. */
88051
+ async evictableUsageOfAll(locationIds) {
88052
+ return (await Promise.all(locationIds.map((id) => this.evictableUsageOf(id)))).reduce((sum, bytes) => sum + bytes, 0);
87376
88053
  }
87377
88054
  /** Evict oldest-first until the location's evictable usage fits `maxBytes`. */
87378
88055
  async relieveToUsageCap(locationId, maxBytes) {
@@ -87466,12 +88143,31 @@ var require_storage_orchestrator_addon = __commonJS({
87466
88143
  var STORAGE_PROVIDER_CAP_NAME = "storage-provider";
87467
88144
  var DATA_STORE_PROVIDER_CAP_NAME = "data-store-provider";
87468
88145
  var STORAGE_EVICTABLE_CAP_NAME = "storage-evictable";
88146
+ var STORAGE_OCCUPANCY_CAP_NAME = "storage-occupancy";
87469
88147
  var HUB_NODE_ID = "hub";
87470
88148
  var PRESSURE_SWEEP_INTERVAL_MS = 6e4;
87471
88149
  var CAP_PROVIDER_REGISTERED_CATEGORY = "capability:provider-registered";
87472
88150
  var StorageOrchestratorAddon = class extends require_dist10.BaseAddon {
87473
88151
  service = null;
87474
88152
  pressureManager = null;
88153
+ /**
88154
+ * How much of each location CamStack itself holds (D388).
88155
+ *
88156
+ * Built on first use rather than in a field initializer: `this.ctx` does not
88157
+ * exist until `initialize()` runs, and a field initializer touching it throws
88158
+ * in the constructor.
88159
+ */
88160
+ occupancySlice = null;
88161
+ get occupancy() {
88162
+ if (this.occupancySlice) return this.occupancySlice;
88163
+ this.occupancySlice = new LocationOccupancySlice({
88164
+ logger: this.ctx.logger,
88165
+ getProviders: () => this.ctx.kernel.capabilityRegistry?.getCollection(STORAGE_OCCUPANCY_CAP_NAME) ?? [],
88166
+ locationIds: () => this.service?.listLocations().map((loc) => loc.id) ?? [],
88167
+ now: () => Date.now()
88168
+ });
88169
+ return this.occupancySlice;
88170
+ }
87475
88171
  pressureTimer = null;
87476
88172
  pressureSweepInFlight = false;
87477
88173
  migration = null;
@@ -87530,12 +88226,15 @@ var require_storage_orchestrator_addon = __commonJS({
87530
88226
  const provider = {
87531
88227
  listLocations: async ({ type }) => {
87532
88228
  const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
88229
+ this.occupancy.serveAndRevalidate();
87533
88230
  return Promise.all(rows.map(async (loc) => ({
87534
88231
  ...this.redacted(loc),
88232
+ ...this.occupancy.ownedFor(loc.id) !== void 0 ? { owned: this.occupancy.ownedFor(loc.id) } : {},
87535
88233
  capacity: require_dist10.isLocationEnabled(loc) ? await this.localCapacityOf(loc) : null
87536
88234
  })));
87537
88235
  },
87538
88236
  listLocationDeclarations: async () => service.listDeclarations(),
88237
+ listDrainProgress: async () => this.drainProgress(),
87539
88238
  upsertLocation: async (input) => {
87540
88239
  const stored = service.getLocationById(input.id);
87541
88240
  const config = restoreRedactedSecrets(input.config, stored?.config, this.secretKeysFor(input.providerId, stored?.config ?? {}));
@@ -87543,10 +88242,13 @@ var require_storage_orchestrator_addon = __commonJS({
87543
88242
  ...input,
87544
88243
  config
87545
88244
  });
88245
+ this.announceLocationsChanged(saved.id, saved.type, "upsert");
87546
88246
  return this.redacted(saved);
87547
88247
  },
87548
88248
  deleteLocation: async ({ id, force }) => {
88249
+ const doomed = service.getLocationById(id);
87549
88250
  await service.deleteLocation(id, { force: force === true });
88251
+ this.announceLocationsChanged(id, doomed?.type ?? "", "delete");
87550
88252
  },
87551
88253
  testLocation: async ({ id }) => {
87552
88254
  const loc = service.getLocationById(id);
@@ -87807,7 +88509,17 @@ var require_storage_orchestrator_addon = __commonJS({
87807
88509
  getEvictableProviders: () => this.ctx.kernel.capabilityRegistry?.getCollection(STORAGE_EVICTABLE_CAP_NAME) ?? [],
87808
88510
  getLocationThresholds: () => this.resolveLocationThresholds(),
87809
88511
  getLocationMaxUsedBytes: () => this.resolveLocationMaxUsedBytes(),
87810
- getLocationDeviceKey: (id) => this.locationDeviceKey(id)
88512
+ getLocationDeviceKey: (id) => this.locationDeviceKey(id),
88513
+ getDrainPlan: () => Promise.resolve(this.resolveDrainPlan()),
88514
+ drainState: {
88515
+ load: async () => await this.resolveDrainStore()?.load() ?? [],
88516
+ save: async (state) => {
88517
+ await this.resolveDrainStore()?.save(state);
88518
+ },
88519
+ clear: async (locationId) => {
88520
+ await this.resolveDrainStore()?.clear(locationId);
88521
+ }
88522
+ }
87811
88523
  });
87812
88524
  this.pressureTimer = setInterval(() => {
87813
88525
  this.runPressureSweep();
@@ -87957,14 +88669,64 @@ var require_storage_orchestrator_addon = __commonJS({
87957
88669
  });
87958
88670
  const byProvider = new Map(infos.map((i) => [i.providerId, i]));
87959
88671
  for (const loc of svc.listLocations()) {
88672
+ if (!require_dist10.mayReadLocation(loc)) continue;
87960
88673
  const info = byProvider.get(loc.providerId);
87961
88674
  if (!info || !info.shouldSaveDiskSpace) continue;
87962
- const override = loc.config["minFreePercent"];
87963
- const threshold = typeof override === "number" && Number.isFinite(override) && override >= 0 ? override : info.minFreePercent ?? 0;
88675
+ const threshold = resolveMinFreePercent(loc.config, info.minFreePercent ?? 0, await this.localCapacityOf(loc), (message, meta) => this.logMinFreeRedundancyOnce(loc.id, message, meta));
87964
88676
  if (threshold > 0) out.set(loc.id, threshold);
87965
88677
  }
87966
88678
  return out;
87967
88679
  }
88680
+ /** Location ids that have already logged the `minFreeGb` / `minFreePercent`
88681
+ * redundancy. The pressure sweep runs on a timer; the notice is worth
88682
+ * exactly one line per location per process, not one per minute. */
88683
+ minFreeRedundancyLogged = /* @__PURE__ */ new Set();
88684
+ logMinFreeRedundancyOnce(locationId, message, meta) {
88685
+ if (this.minFreeRedundancyLogged.has(locationId)) return;
88686
+ this.minFreeRedundancyLogged.add(locationId);
88687
+ this.ctx.logger.warn(message, { meta: {
88688
+ locationId,
88689
+ ...meta
88690
+ } });
88691
+ }
88692
+ /**
88693
+ * The draining locations and the ACTIVE peers of their class whose growth
88694
+ * paces them (D386). Rebuilt every sweep from the live mode of every
88695
+ * location, so this is also what STOPS a drain: a location the operator
88696
+ * switched back simply stops appearing.
88697
+ */
88698
+ drainStore = null;
88699
+ /** The drain ratchet's durable home, once a data-store engine exists. Same
88700
+ * late-wiring window as the location table — on an agent it is never. */
88701
+ resolveDrainStore() {
88702
+ if (this.drainStore) return this.drainStore;
88703
+ const engine = this.ctx.kernel.capabilityRegistry?.getCollection(DATA_STORE_PROVIDER_CAP_NAME)[0];
88704
+ if (!engine) return null;
88705
+ this.drainStore = new SqliteDrainStateStore(engine);
88706
+ return this.drainStore;
88707
+ }
88708
+ drainPlanRefusalLogged = false;
88709
+ resolveDrainPlan() {
88710
+ const svc = this.service;
88711
+ if (!svc) return [];
88712
+ if (this.resolveDrainStore() === null) {
88713
+ const draining = svc.listLocations().filter((l) => require_dist10.evictionPolicyOfLocation(l) === "drain");
88714
+ if (draining.length > 0 && !this.drainPlanRefusalLogged) {
88715
+ this.drainPlanRefusalLogged = true;
88716
+ this.ctx.logger.warn("storage-orchestrator: REFUSING to drain \u2014 this node has no data-store engine to persist the drain cap, and a cap recomputed at every boot would delete on a pace nobody chose", { meta: { locationIds: draining.map((l) => l.id) } });
88717
+ }
88718
+ return [];
88719
+ }
88720
+ const locations = svc.listLocations();
88721
+ return locations.filter((loc) => require_dist10.evictionPolicyOfLocation(loc) === "drain").map((loc) => ({
88722
+ locationId: loc.id,
88723
+ activePeerLocationIds: locations.filter((peer) => peer.type === loc.type && peer.id !== loc.id && require_dist10.mayWriteToLocation(peer)).map((peer) => peer.id)
88724
+ }));
88725
+ }
88726
+ /** Drain progress for every draining location, as of the last sweep. */
88727
+ drainProgress() {
88728
+ return this.pressureManager?.drainProgressSnapshot() ?? [];
88729
+ }
87968
88730
  /**
87969
88731
  * What the provider's own schema declared secret, or `undefined` when this
87970
88732
  * node has never heard from that provider. The distinction is the whole
@@ -87977,6 +88739,48 @@ var require_storage_orchestrator_addon = __commonJS({
87977
88739
  secretKeysFor(providerId, config) {
87978
88740
  return secretKeysForConfig(config, this.declaredSecretKeys(providerId));
87979
88741
  }
88742
+ /**
88743
+ * Ring the doorbell: the SET of storage locations changed (D387).
88744
+ *
88745
+ * A consumer that resolves locations at boot had no way to learn that the
88746
+ * operator added, edited or removed one — the recorder kept writing to the
88747
+ * disk set it resolved hours ago, and an operator who added a second
88748
+ * recordings disk in the admin UI got nothing until something unrelated
88749
+ * happened to re-resolve.
88750
+ *
88751
+ * Telemetry, and deliberately so (D8/D11): it carries no row, it is allowed
88752
+ * to be dropped, and every consumer re-reads `listLocations` for itself and
88753
+ * converges on its own periodic path anyway. A bulk edit rings it once per
88754
+ * row — coalescing is the consumer's job, not this one's.
88755
+ *
88756
+ * Never throws into the mutation that produced it: the location IS saved,
88757
+ * and a bus that will not take a telemetry event must not turn that into a
88758
+ * failed upsert.
88759
+ */
88760
+ announceLocationsChanged(locationId, locationType, reason) {
88761
+ try {
88762
+ this.ctx.eventBus.emit({
88763
+ id: (0, node_crypto.randomUUID)(),
88764
+ timestamp: /* @__PURE__ */ new Date(),
88765
+ source: {
88766
+ type: "addon",
88767
+ id: this.ctx.id
88768
+ },
88769
+ category: require_dist10.EventCategory.StorageLocationsChanged,
88770
+ data: {
88771
+ locationId,
88772
+ locationType,
88773
+ reason
88774
+ }
88775
+ });
88776
+ } catch (err) {
88777
+ this.ctx.logger.warn("storage-orchestrator: could not announce the location change \u2014 consumers will converge on their own periodic path", { meta: {
88778
+ locationId,
88779
+ reason,
88780
+ error: err instanceof Error ? err.message : String(err)
88781
+ } });
88782
+ }
88783
+ }
87980
88784
  /** A location with its provider's declared secrets replaced by the sentinel. */
87981
88785
  redacted(location) {
87982
88786
  return redactLocation(location, this.secretKeysFor(location.providerId, location.config));
@@ -88035,8 +88839,9 @@ var require_storage_orchestrator_addon = __commonJS({
88035
88839
  const svc = this.service;
88036
88840
  if (!svc) return out;
88037
88841
  for (const loc of svc.listLocations()) {
88842
+ if (!require_dist10.mayReadLocation(loc)) continue;
88038
88843
  const raw = loc.config["maxUsedGb"];
88039
- if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) out.set(loc.id, Math.floor(raw * 1024 ** 3));
88844
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) out.set(loc.id, require_dist10.gbToBytes(raw));
88040
88845
  }
88041
88846
  return out;
88042
88847
  }
@@ -88255,7 +89060,7 @@ var require_system_config_addon = __commonJS({
88255
89060
  [Symbol.toStringTag]: { value: "Module" }
88256
89061
  });
88257
89062
  require_chunk_Cek0wNdY();
88258
- var require_dist10 = require_dist_jZb_HKI5();
89063
+ var require_dist10 = require_dist_BFvWv_bz();
88259
89064
  var SECTION_TITLES = {
88260
89065
  server: "Server",
88261
89066
  auth: "Authentication"
@@ -106316,7 +107121,7 @@ var require_winston_logging = __commonJS({
106316
107121
  [Symbol.toStringTag]: { value: "Module" }
106317
107122
  });
106318
107123
  var require_chunk = require_chunk_Cek0wNdY();
106319
- var require_dist10 = require_dist_jZb_HKI5();
107124
+ var require_dist10 = require_dist_BFvWv_bz();
106320
107125
  var require_formatter = require_formatter_DqAKDlvN();
106321
107126
  var node_path = __require("path");
106322
107127
  node_path = require_chunk.__toESM(node_path);
@@ -108092,9 +108897,9 @@ ${(0, node_fs.readFileSync)(ca.caCertPath, "utf-8").trimEnd()}
108092
108897
  }
108093
108898
  });
108094
108899
 
108095
- // ../types/dist/event-category-b8kBSOTT.js
108096
- var require_event_category_b8kBSOTT = __commonJS({
108097
- "../types/dist/event-category-b8kBSOTT.js"(exports) {
108900
+ // ../types/dist/event-category-BVo6ta6_.js
108901
+ var require_event_category_BVo6ta6 = __commonJS({
108902
+ "../types/dist/event-category-BVo6ta6_.js"(exports) {
108098
108903
  "use strict";
108099
108904
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
108100
108905
  EventCategory2["SystemBoot"] = "system.boot";
@@ -108142,6 +108947,7 @@ var require_event_category_b8kBSOTT = __commonJS({
108142
108947
  EventCategory2["ProcessCrashed"] = "process.crashed";
108143
108948
  EventCategory2["ProcessRestartScheduled"] = "process.restart_scheduled";
108144
108949
  EventCategory2["ProcessRestarted"] = "process.restarted";
108950
+ EventCategory2["StorageLocationsChanged"] = "storage.locations-changed";
108145
108951
  EventCategory2["RecordingStarted"] = "recording.started";
108146
108952
  EventCategory2["RecordingStopped"] = "recording.stopped";
108147
108953
  EventCategory2["RecordingError"] = "recording.error";
@@ -108260,11 +109066,11 @@ var require_event_category_b8kBSOTT = __commonJS({
108260
109066
  }
108261
109067
  });
108262
109068
 
108263
- // ../types/dist/sleep-DDIFZGbc.js
108264
- var require_sleep_DDIFZGbc = __commonJS({
108265
- "../types/dist/sleep-DDIFZGbc.js"(exports) {
109069
+ // ../types/dist/sleep-BZtO-eFY.js
109070
+ var require_sleep_BZtO_eFY = __commonJS({
109071
+ "../types/dist/sleep-BZtO-eFY.js"(exports) {
108266
109072
  "use strict";
108267
- var require_event_category = require_event_category_b8kBSOTT();
109073
+ var require_event_category = require_event_category_BVo6ta6();
108268
109074
  var zod = require_zod();
108269
109075
  var WELL_KNOWN_TABS = [
108270
109076
  {
@@ -111993,8 +112799,8 @@ var require_addon = __commonJS({
111993
112799
  "../types/dist/addon.js"(exports) {
111994
112800
  "use strict";
111995
112801
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
111996
- var require_event_category = require_event_category_b8kBSOTT();
111997
- var require_sleep = require_sleep_DDIFZGbc();
112802
+ var require_event_category = require_event_category_BVo6ta6();
112803
+ var require_sleep = require_sleep_BZtO_eFY();
111998
112804
  var require_err_msg = require_err_msg_COpsHMw2();
111999
112805
  var CAP_INPUT_DEFAULTS = Object.freeze({
112000
112806
  "addons": { "getLogs": { "limit": 100 } },
@@ -112270,6 +113076,7 @@ var require_addon = __commonJS({
112270
113076
  "listTargetKinds",
112271
113077
  "listTargets"
112272
113078
  ],
113079
+ "storage-occupancy": ["getOccupancy"],
112273
113080
  "storage-provider": ["list"],
112274
113081
  "turn-provider": ["getTurnServers"],
112275
113082
  "user-passkeys": ["listPasskeys"]
@@ -130924,7 +131731,7 @@ var require_dist3 = __commonJS({
130924
131731
  "use strict";
130925
131732
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
130926
131733
  var require_chunk = require_chunk_Cek0wNdY();
130927
- var require_dist10 = require_dist_jZb_HKI5();
131734
+ var require_dist10 = require_dist_BFvWv_bz();
130928
131735
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
130929
131736
  require_alerts();
130930
131737
  var require_formatter = require_formatter_DqAKDlvN();
@@ -212020,7 +212827,7 @@ var require_enums = __commonJS({
212020
212827
  "../types/dist/enums.js"(exports) {
212021
212828
  "use strict";
212022
212829
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
212023
- var require_event_category = require_event_category_b8kBSOTT();
212830
+ var require_event_category = require_event_category_BVo6ta6();
212024
212831
  var EventSourceType = /* @__PURE__ */ (function(EventSourceType2) {
212025
212832
  EventSourceType2["Addon"] = "addon";
212026
212833
  EventSourceType2["Core"] = "core";
@@ -212040,8 +212847,8 @@ var require_dist4 = __commonJS({
212040
212847
  "../types/dist/index.js"(exports) {
212041
212848
  "use strict";
212042
212849
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
212043
- var require_event_category = require_event_category_b8kBSOTT();
212044
- var require_sleep = require_sleep_DDIFZGbc();
212850
+ var require_event_category = require_event_category_BVo6ta6();
212851
+ var require_sleep = require_sleep_BZtO_eFY();
212045
212852
  var require_canonical_hash = require_canonical_hash_DNV8S5ET();
212046
212853
  var require_enums2 = require_enums();
212047
212854
  var require_err_msg = require_err_msg_COpsHMw2();
@@ -213621,6 +214428,71 @@ var require_dist4 = __commonJS({
213621
214428
  "clip",
213622
214429
  "custom"
213623
214430
  ];
214431
+ var STORAGE_LOCATION_MODES = [
214432
+ "active",
214433
+ "readonly",
214434
+ "drain",
214435
+ "disabled"
214436
+ ];
214437
+ var StorageLocationModeSchema = zod.z.enum(STORAGE_LOCATION_MODES);
214438
+ var StorageEvictionPolicySchema = zod.z.enum([
214439
+ "normal",
214440
+ "never",
214441
+ "drain"
214442
+ ]);
214443
+ var LEGACY_READ_ONLY_CONFIG_KEY = "readOnly";
214444
+ function modeMayWrite(mode) {
214445
+ return mode === "active";
214446
+ }
214447
+ function modeMayRead(mode) {
214448
+ return mode !== "disabled";
214449
+ }
214450
+ function evictionPolicyForMode(mode) {
214451
+ switch (mode) {
214452
+ case "active":
214453
+ return "normal";
214454
+ case "drain":
214455
+ return "drain";
214456
+ case "readonly":
214457
+ case "disabled":
214458
+ return "never";
214459
+ }
214460
+ }
214461
+ function legacyModeOf(location) {
214462
+ if (location.mode !== void 0) return null;
214463
+ if (location.config["readOnly"] === true) return "readonly";
214464
+ if (location.enabled === false) return "readonly";
214465
+ return null;
214466
+ }
214467
+ function resolveLocationMode(location) {
214468
+ return (isStorageLocationMode(location.mode) ? location.mode : void 0) ?? legacyModeOf(location) ?? "active";
214469
+ }
214470
+ function isStorageLocationMode(value) {
214471
+ return STORAGE_LOCATION_MODES.some((mode) => mode === value);
214472
+ }
214473
+ function mayWriteToLocation(location) {
214474
+ return modeMayWrite(resolveLocationMode(location));
214475
+ }
214476
+ function mayReadLocation(location) {
214477
+ return modeMayRead(resolveLocationMode(location));
214478
+ }
214479
+ function evictionPolicyOfLocation(location) {
214480
+ return evictionPolicyForMode(resolveLocationMode(location));
214481
+ }
214482
+ function strippedOfLegacyReadOnly(config) {
214483
+ if (!("readOnly" in config)) return config;
214484
+ const next = { ...config };
214485
+ delete next[LEGACY_READ_ONLY_CONFIG_KEY];
214486
+ return next;
214487
+ }
214488
+ function withLocationMode(location, mode) {
214489
+ return {
214490
+ ...location,
214491
+ mode,
214492
+ enabled: modeMayWrite(mode),
214493
+ config: strippedOfLegacyReadOnly(location.config)
214494
+ };
214495
+ }
213624
214496
  var StorageLocationTypeSchema = zod.z.string().regex(/^[a-z][a-zA-Z0-9-]*$/);
213625
214497
  var StorageLocationSchema = zod.z.object({
213626
214498
  id: zod.z.string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/),
@@ -213652,6 +214524,21 @@ var require_dist4 = __commonJS({
213652
214524
  * stops existing rather than being re-derived on every read.
213653
214525
  */
213654
214526
  enabled: zod.z.boolean().optional(),
214527
+ /**
214528
+ * THE state of this location (D385), and the only authority on what may be
214529
+ * written, read or evicted here. Interpreted in exactly one place —
214530
+ * `storage-location-mode.ts` — which also folds the legacy
214531
+ * `enabled` / `config.readOnly` pair into a mode so an old row is never
214532
+ * ambiguous.
214533
+ *
214534
+ * OPTIONAL only for the wire and for rows written before D385: absence is
214535
+ * resolved by `resolveLocationMode`, and the orchestrator stamps every
214536
+ * unstamped row ONCE at hydrate so absence stops existing rather than being
214537
+ * re-derived on every read. `enabled` survives one release as a DERIVED
214538
+ * mirror (`mode === 'active'`); `withLocationMode` is the only writer of
214539
+ * either, so the two cannot disagree.
214540
+ */
214541
+ mode: StorageLocationModeSchema.optional(),
213655
214542
  /** COMPUTED at read time by the orchestrator (statfs of the backing volume
213656
214543
  * for node-local locations it can reach) — never persisted, absent when the
213657
214544
  * volume is remote/unreachable. The single capacity truth every UI reads. */
@@ -213659,13 +214546,47 @@ var require_dist4 = __commonJS({
213659
214546
  totalBytes: zod.z.number(),
213660
214547
  availableBytes: zod.z.number()
213661
214548
  }).nullable().optional(),
214549
+ /**
214550
+ * How much of that volume CamStack ITSELF holds on this location (D388) —
214551
+ * COMPUTED at read time from the `storage-occupancy` providers' own figures,
214552
+ * never persisted, never a filesystem walk.
214553
+ *
214554
+ * **ABSENT MEANS UNKNOWN, never zero.** No provider has reported for this
214555
+ * location yet — nobody stores here, the owning addon is down, or the first
214556
+ * refresh has not completed. A UI must omit the segment rather than draw it
214557
+ * at zero, which would claim we occupy nothing (D315). It is an OBJECT and
214558
+ * not a bare number precisely so that a `?? 0` on the consuming side has to
214559
+ * be spelled out loud instead of appearing by accident.
214560
+ *
214561
+ * `measuredAtMs` is the OLDEST contributing measurement, so it is honest
214562
+ * about the whole figure rather than about its freshest part.
214563
+ */
214564
+ owned: zod.z.object({
214565
+ bytes: zod.z.number().int().nonnegative(),
214566
+ measuredAtMs: zod.z.number().int().nonnegative()
214567
+ }).optional(),
213662
214568
  createdAt: zod.z.number(),
213663
214569
  updatedAt: zod.z.number()
213664
214570
  });
213665
214571
  var LegacyStorageLocationDefaultSchema = zod.z.object({ isDefault: zod.z.boolean().optional() });
213666
214572
  function isLocationEnabled(location) {
213667
- return location.enabled !== false;
214573
+ return mayWriteToLocation({
214574
+ ...location,
214575
+ config: location.config ?? {}
214576
+ });
213668
214577
  }
214578
+ var STORAGE_BYTES_PER_GB = 1024 ** 3;
214579
+ function gbToBytes(gb) {
214580
+ return Math.floor(gb * STORAGE_BYTES_PER_GB);
214581
+ }
214582
+ var StorageDrainProgressSchema = zod.z.object({
214583
+ locationId: zod.z.string(),
214584
+ startedAtMs: zod.z.number(),
214585
+ startBytes: zod.z.number(),
214586
+ bytesRemaining: zod.z.number(),
214587
+ drained: zod.z.boolean(),
214588
+ estimatedEmptyAtMs: zod.z.number().nullable()
214589
+ });
213669
214590
  var StorageLocationRefSchema = zod.z.union([StorageLocationTypeSchema, zod.z.string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/)]);
213670
214591
  var StorageAccessSchema = zod.z.enum(["local-path", "cap-mediated"]);
213671
214592
  var STORAGE_ACCESS_FALLBACK = "local-path";
@@ -230016,6 +230937,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
230016
230937
  kind: "mutation",
230017
230938
  auth: "admin"
230018
230939
  }),
230940
+ /**
230941
+ * How far each draining location has got (D386). A pure READ of what the
230942
+ * last pressure sweep computed — it starts no work, and a location that is
230943
+ * not draining simply does not appear.
230944
+ */
230945
+ listDrainProgress: require_sleep.method(zod.z.void(), zod.z.array(StorageDrainProgressSchema).readonly()),
230019
230946
  testLocation: require_sleep.method(zod.z.object({ id: zod.z.string() }), zod.z.object({
230020
230947
  ok: zod.z.boolean(),
230021
230948
  error: zod.z.string().optional()
@@ -230145,6 +231072,37 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
230145
231072
  history: require_sleep.method(zod.z.object({}), zod.z.array(StorageMigrationJobSchema).readonly(), { auth: "admin" })
230146
231073
  }
230147
231074
  };
231075
+ var StorageOccupancyReportSchema = zod.z.object({
231076
+ locationId: zod.z.string(),
231077
+ /** Bytes this provider holds on THAT location — not its eviction domain, and
231078
+ * not net of what it is willing to delete. */
231079
+ ownedBytes: zod.z.number().int().nonnegative(),
231080
+ /** When the provider last actually measured this. The orchestrator carries it
231081
+ * through so a UI can say how old the figure is instead of implying "now". */
231082
+ measuredAtMs: zod.z.number().int().nonnegative()
231083
+ });
231084
+ var storageOccupancyCapability = {
231085
+ name: "storage-occupancy",
231086
+ scope: "system",
231087
+ mode: "collection",
231088
+ internal: true,
231089
+ methods: {
231090
+ /**
231091
+ * Occupancy for the given locations, in ONE round trip.
231092
+ *
231093
+ * A provider answers only for the locations it actually holds bytes on and
231094
+ * OMITS the rest — an omitted location is "I hold nothing measurable here",
231095
+ * which the orchestrator merges as a contribution of nothing rather than as
231096
+ * a claim that the location is empty. Only a location no provider reports
231097
+ * at all stays unknown.
231098
+ *
231099
+ * This must be CHEAP and must never walk a filesystem: it is on the admin
231100
+ * UI's `listLocations` path. The owner keeps its own figure fresh (D224) and
231101
+ * answers from what it already has.
231102
+ */
231103
+ getOccupancy: require_sleep.method(zod.z.object({ locationIds: zod.z.array(zod.z.string()).readonly() }), zod.z.array(StorageOccupancyReportSchema).readonly(), { auth: "admin" })
231104
+ }
231105
+ };
230148
231106
  var ProviderInfoSchema = zod.z.discriminatedUnion("shouldSaveDiskSpace", [zod.z.object({
230149
231107
  providerId: zod.z.string().min(1),
230150
231108
  displayName: zod.z.string().min(1),
@@ -232104,94 +233062,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
232104
233062
  * whether persisting is worth a SQLite commit. */
232105
233063
  volatileStateFields: ["lastUpdated"]
232106
233064
  };
232107
- var NETWORK_LINK_TYPES = [
232108
- "wifi",
232109
- "ethernet",
232110
- "cellular",
232111
- "unknown"
232112
- ];
232113
- var NetworkLinkStatusSchema = zod.z.object({
232114
- /** The link the device is on. `'unknown'` = not read yet, not "no link". */
232115
- type: zod.z.enum(NETWORK_LINK_TYPES),
232116
- /**
232117
- * Link quality, 0..100 inclusive, normalised by the provider from whatever
232118
- * the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
232119
- * KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
232120
- * one whose reading has not landed must not be drawn at 0 %. Consumers
232121
- * SKIP a null rather than coerce it.
232122
- */
232123
- signalPercent: zod.z.number().min(0).max(100).nullable(),
232124
- /** Raw received signal strength in dBm, when the firmware reports one. */
232125
- rssiDbm: zod.z.number().optional(),
232126
- /** Network name of a wireless link, when the firmware reports it. */
232127
- ssid: zod.z.string().optional(),
232128
- /** Ms epoch of the last observation. Lets consumers reason about freshness. */
232129
- lastUpdated: zod.z.number()
232130
- });
232131
- var NETWORK_LINK_UNKNOWN = {
232132
- type: "unknown",
232133
- signalPercent: null,
232134
- lastUpdated: 0
232135
- };
232136
- function signalPercentFromBars(bars, maxBars) {
232137
- if (bars === void 0 || !Number.isFinite(bars) || maxBars <= 0) return null;
232138
- if (bars < 0 || bars > maxBars) return null;
232139
- return Math.round(bars / maxBars * 100);
232140
- }
232141
- function signalPercentFromRssi(rssiDbm) {
232142
- if (rssiDbm === void 0 || !Number.isFinite(rssiDbm) || rssiDbm > 0) return null;
232143
- return Math.round((Math.min(-50, Math.max(-100, rssiDbm)) + 100) * 2);
232144
- }
232145
- var networkLinkCapability = {
232146
- name: "network-link",
232147
- scope: "device",
232148
- deviceNative: true,
232149
- mode: "singleton",
232150
- deviceTypes: [
232151
- require_sleep.DeviceType.Camera,
232152
- require_sleep.DeviceType.Sensor,
232153
- require_sleep.DeviceType.Button,
232154
- require_sleep.DeviceType.Switch,
232155
- require_sleep.DeviceType.Light,
232156
- require_sleep.DeviceType.Lock,
232157
- require_sleep.DeviceType.Siren
232158
- ],
232159
- methods: {},
232160
- events: {
232161
- /**
232162
- * Emitted whenever the cached status changes (a link switch, a signal
232163
- * reading that moved). Mirrored on the parent chain by the
232164
- * DeviceEventPropagator like `battery.onStatusChanged`.
232165
- */
232166
- onStatusChanged: { data: zod.z.object({
232167
- deviceId: zod.z.number(),
232168
- status: NetworkLinkStatusSchema
232169
- }) }
232170
- },
232171
- status: {
232172
- schema: NetworkLinkStatusSchema,
232173
- kind: "push",
232174
- empty: NETWORK_LINK_UNKNOWN
232175
- },
232176
- /**
232177
- * Runtime-state slice — every provider stores the same shape under
232178
- * `device.runtimeState['network-link']`, read once by the badge and the
232179
- * Home Assistant projector regardless of the driver.
232180
- */
232181
- runtimeState: NetworkLinkStatusSchema,
232182
- /**
232183
- * Runtime-state durability: **restored** — a link reading is slow to
232184
- * change and a sleeping battery camera may not report for hours; the
232185
- * restored slice is what the badge shows until the next read.
232186
- *
232187
- * See `RuntimeStateDurability`. Enforced by
232188
- * `scripts/check-runtime-state-durability.ts`.
232189
- */
232190
- durability: "restored",
232191
- /** Clock fields: written, but excluded from the compare that decides
232192
- * whether persisting is worth a SQLite commit. */
232193
- volatileStateFields: ["lastUpdated"]
232194
- };
232195
233065
  var BinaryStatusSchema = zod.z.object({
232196
233066
  on: zod.z.boolean(),
232197
233067
  /** Ms epoch of the last transition. 0 if never observed. */
@@ -235333,6 +236203,385 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
235333
236203
  * whether persisting is worth a SQLite commit. */
235334
236204
  volatileStateFields: ["lastFetchedAt"]
235335
236205
  };
236206
+ var NavigationMoveCommandSchema = zod.z.object({
236207
+ pan: zod.z.number().min(-1).max(1).optional(),
236208
+ tilt: zod.z.number().min(-1).max(1).optional(),
236209
+ speed: zod.z.number().min(0).max(1).optional()
236210
+ });
236211
+ var NavigationActionIdSchema = zod.z.enum([
236212
+ "goHome",
236213
+ "locate",
236214
+ "spotClean",
236215
+ "findPet",
236216
+ "personFollow",
236217
+ "stop",
236218
+ "startClean",
236219
+ "pauseClean",
236220
+ "dockWash",
236221
+ "autoEmpty",
236222
+ "flashOn",
236223
+ "flashOff"
236224
+ ]);
236225
+ var NavigationEntryKindSchema = zod.z.enum(["action", "sound"]);
236226
+ var NavigationActionEntrySchema = zod.z.object({
236227
+ id: zod.z.string(),
236228
+ kind: NavigationEntryKindSchema,
236229
+ label: zod.z.string(),
236230
+ icon: zod.z.string(),
236231
+ /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
236232
+ soundId: zod.z.number().int().optional(),
236233
+ /** Per-device feature flag — render this entry only when true. */
236234
+ enabled: zod.z.boolean()
236235
+ });
236236
+ var NAVIGATION_ACTION_CATALOG = [
236237
+ {
236238
+ id: "startClean",
236239
+ kind: "action",
236240
+ label: "Start cleaning",
236241
+ icon: "play",
236242
+ enabled: true
236243
+ },
236244
+ {
236245
+ id: "pauseClean",
236246
+ kind: "action",
236247
+ label: "Pause cleaning",
236248
+ icon: "pause",
236249
+ enabled: true
236250
+ },
236251
+ {
236252
+ id: "stop",
236253
+ kind: "action",
236254
+ label: "Stop",
236255
+ icon: "hand",
236256
+ enabled: true
236257
+ },
236258
+ {
236259
+ id: "goHome",
236260
+ kind: "action",
236261
+ label: "Return to dock",
236262
+ icon: "home",
236263
+ enabled: true
236264
+ },
236265
+ {
236266
+ id: "locate",
236267
+ kind: "action",
236268
+ label: "Locate",
236269
+ icon: "bell",
236270
+ enabled: true
236271
+ },
236272
+ {
236273
+ id: "spotClean",
236274
+ kind: "action",
236275
+ label: "Spot clean here",
236276
+ icon: "crosshair",
236277
+ enabled: true
236278
+ },
236279
+ {
236280
+ id: "findPet",
236281
+ kind: "action",
236282
+ label: "Find pet",
236283
+ icon: "paw-print",
236284
+ enabled: true
236285
+ },
236286
+ {
236287
+ id: "personFollow",
236288
+ kind: "action",
236289
+ label: "Follow me",
236290
+ icon: "footprints",
236291
+ enabled: true
236292
+ },
236293
+ {
236294
+ id: "dockWash",
236295
+ kind: "action",
236296
+ label: "Wash mop",
236297
+ icon: "droplets",
236298
+ enabled: true
236299
+ },
236300
+ {
236301
+ id: "autoEmpty",
236302
+ kind: "action",
236303
+ label: "Empty dustbin",
236304
+ icon: "trash-2",
236305
+ enabled: true
236306
+ },
236307
+ {
236308
+ id: "flashOn",
236309
+ kind: "action",
236310
+ label: "Flash on",
236311
+ icon: "flashlight",
236312
+ enabled: true
236313
+ },
236314
+ {
236315
+ id: "flashOff",
236316
+ kind: "action",
236317
+ label: "Flash off",
236318
+ icon: "flashlight-off",
236319
+ enabled: true
236320
+ },
236321
+ {
236322
+ id: "sound:meow",
236323
+ kind: "sound",
236324
+ label: "Meow",
236325
+ icon: "cat",
236326
+ soundId: 684,
236327
+ enabled: true
236328
+ },
236329
+ {
236330
+ id: "sound:bark",
236331
+ kind: "sound",
236332
+ label: "Woof",
236333
+ icon: "dog",
236334
+ soundId: 685,
236335
+ enabled: true
236336
+ },
236337
+ {
236338
+ id: "sound:footsteps",
236339
+ kind: "sound",
236340
+ label: "Footsteps",
236341
+ icon: "footprints",
236342
+ soundId: 686,
236343
+ enabled: true
236344
+ },
236345
+ {
236346
+ id: "sound:purring",
236347
+ kind: "sound",
236348
+ label: "Purring",
236349
+ icon: "cat",
236350
+ soundId: 687,
236351
+ enabled: true
236352
+ },
236353
+ {
236354
+ id: "sound:tickTock",
236355
+ kind: "sound",
236356
+ label: "Tick-tock",
236357
+ icon: "clock",
236358
+ soundId: 688,
236359
+ enabled: true
236360
+ }
236361
+ ];
236362
+ var NavigationPointSchema = zod.z.object({
236363
+ x: zod.z.number(),
236364
+ y: zod.z.number()
236365
+ });
236366
+ var NavigationFeaturesSchema = zod.z.object({
236367
+ move: zod.z.boolean(),
236368
+ stop: zod.z.boolean(),
236369
+ goToPoint: zod.z.boolean(),
236370
+ runAction: zod.z.boolean(),
236371
+ playSound: zod.z.boolean(),
236372
+ light: zod.z.boolean(),
236373
+ lightMode: zod.z.boolean()
236374
+ });
236375
+ var NavigationLightModeSchema = zod.z.enum(["auto", "manual"]);
236376
+ var NAVIGATION_LIGHT_LEVEL_MIN = 40;
236377
+ var NAVIGATION_LIGHT_LEVEL_MAX = 100;
236378
+ var NavigationModeSchema = zod.z.enum([
236379
+ "idle",
236380
+ "cleaning",
236381
+ "spot",
236382
+ "following",
236383
+ "goto",
236384
+ "returning",
236385
+ "paused",
236386
+ "unknown"
236387
+ ]);
236388
+ var NavigationStatusSchema = zod.z.object({
236389
+ mode: NavigationModeSchema,
236390
+ following: zod.z.boolean(),
236391
+ flash: zod.z.boolean(),
236392
+ lightMode: NavigationLightModeSchema,
236393
+ lightLevel: zod.z.number().min(40).max(100),
236394
+ /** Ms epoch when the slice was last updated. */
236395
+ lastChangedAt: zod.z.number()
236396
+ });
236397
+ var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: zod.z.number() });
236398
+ var navigationCapability = {
236399
+ name: "navigation",
236400
+ scope: "device",
236401
+ deviceNative: true,
236402
+ mode: "singleton",
236403
+ deviceTypes: [require_sleep.DeviceType.Camera],
236404
+ deviceConfig: { ui: {
236405
+ kind: "widget",
236406
+ widgetId: "host/navigation-panel",
236407
+ tab: "navigation",
236408
+ topTab: true,
236409
+ label: "Navigation",
236410
+ order: 0
236411
+ } },
236412
+ methods: {
236413
+ /**
236414
+ * Momentary drive nudge (the robot moves). `protected` — mirrors
236415
+ * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
236416
+ * path) works for any authenticated user, not admin-only. The UI sends
236417
+ * these at ~1 Hz while a control is held; the provider forwards each one to
236418
+ * a single drive write WITHOUT debouncing.
236419
+ */
236420
+ move: require_sleep.method(NavigationMoveCommandSchema.extend({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
236421
+ /** Halt all motion immediately (zero drive vector). */
236422
+ stop: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
236423
+ /** Send the robot to a point on its live map. */
236424
+ goToPoint: require_sleep.method(NavigationPointSchema.extend({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
236425
+ /**
236426
+ * Enumerate the discrete controls THIS device supports (data-driven UI +
236427
+ * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
236428
+ */
236429
+ listActions: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), zod.z.array(NavigationActionEntrySchema)),
236430
+ /**
236431
+ * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
236432
+ * unsupported action ids are rejected by the provider.
236433
+ */
236434
+ runAction: require_sleep.method(zod.z.object({
236435
+ deviceId: zod.z.number(),
236436
+ actionId: NavigationActionIdSchema
236437
+ }), zod.z.void(), { kind: "mutation" }),
236438
+ /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
236439
+ playSound: require_sleep.method(zod.z.object({
236440
+ deviceId: zod.z.number(),
236441
+ soundId: zod.z.number().int()
236442
+ }), zod.z.void(), { kind: "mutation" }),
236443
+ /**
236444
+ * Turn the on-camera fill light on / off (the `OpenFullLight` control —
236445
+ * works anytime, no active stream required).
236446
+ */
236447
+ setLightOn: require_sleep.method(zod.z.object({
236448
+ deviceId: zod.z.number(),
236449
+ on: zod.z.boolean()
236450
+ }), zod.z.void(), { kind: "mutation" }),
236451
+ /**
236452
+ * Set the fill-light mode (auto vs manual). `manual` optionally carries the
236453
+ * initial `level`. The auto/manual + level control is a CAMERA-service
236454
+ * action that generally needs an active camera stream/monitor session — the
236455
+ * UI shows the manual level slider ONLY when `mode === 'manual'`.
236456
+ */
236457
+ setLightMode: require_sleep.method(zod.z.object({
236458
+ deviceId: zod.z.number(),
236459
+ mode: NavigationLightModeSchema,
236460
+ level: zod.z.number().min(40).max(100).optional()
236461
+ }), zod.z.void(), { kind: "mutation" }),
236462
+ /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
236463
+ setLightLevel: require_sleep.method(zod.z.object({
236464
+ deviceId: zod.z.number(),
236465
+ level: zod.z.number().min(40).max(100)
236466
+ }), zod.z.void(), { kind: "mutation" }),
236467
+ /**
236468
+ * Per-device FEATURE-FLAG report for the general primitives — drives which
236469
+ * controls the UI shows (the per-entry flags for the dictionary come back on
236470
+ * `listActions`).
236471
+ */
236472
+ getFeatures: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), NavigationFeaturesSchema)
236473
+ },
236474
+ events: { onStatusChanged: { data: zod.z.object({
236475
+ deviceId: zod.z.number(),
236476
+ status: NavigationStatusSchema
236477
+ }) } },
236478
+ status: {
236479
+ schema: NavigationStatusSchema,
236480
+ kind: "push"
236481
+ },
236482
+ /**
236483
+ * Runtime-state slice mirrored by the kernel. The navigation panel watches it
236484
+ * for live mode / follow / flash changes.
236485
+ */
236486
+ runtimeState: NavigationRuntimeStateSchema,
236487
+ /**
236488
+ * Runtime-state durability: **session** — like `vacuum-control`, a restored
236489
+ * `mode: cleaning` / `following: true` is a robot that is not actually doing
236490
+ * that. The live handle re-publishes on connect.
236491
+ *
236492
+ * See `RuntimeStateDurability`. Enforced by
236493
+ * `scripts/check-runtime-state-durability.ts`.
236494
+ */
236495
+ durability: "session"
236496
+ };
236497
+ var NETWORK_LINK_TYPES = [
236498
+ "wifi",
236499
+ "ethernet",
236500
+ "cellular",
236501
+ "unknown"
236502
+ ];
236503
+ var NetworkLinkStatusSchema = zod.z.object({
236504
+ /** The link the device is on. `'unknown'` = not read yet, not "no link". */
236505
+ type: zod.z.enum(NETWORK_LINK_TYPES),
236506
+ /**
236507
+ * Link quality, 0..100 inclusive, normalised by the provider from whatever
236508
+ * the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
236509
+ * KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
236510
+ * one whose reading has not landed must not be drawn at 0 %. Consumers
236511
+ * SKIP a null rather than coerce it.
236512
+ */
236513
+ signalPercent: zod.z.number().min(0).max(100).nullable(),
236514
+ /** Raw received signal strength in dBm, when the firmware reports one. */
236515
+ rssiDbm: zod.z.number().optional(),
236516
+ /** Network name of a wireless link, when the firmware reports it. */
236517
+ ssid: zod.z.string().optional(),
236518
+ /** Ms epoch of the last observation. Lets consumers reason about freshness. */
236519
+ lastUpdated: zod.z.number()
236520
+ });
236521
+ var NETWORK_LINK_UNKNOWN = {
236522
+ type: "unknown",
236523
+ signalPercent: null,
236524
+ lastUpdated: 0
236525
+ };
236526
+ function signalPercentFromBars(bars, maxBars) {
236527
+ if (bars === void 0 || !Number.isFinite(bars) || maxBars <= 0) return null;
236528
+ if (bars < 0 || bars > maxBars) return null;
236529
+ return Math.round(bars / maxBars * 100);
236530
+ }
236531
+ function signalPercentFromRssi(rssiDbm) {
236532
+ if (rssiDbm === void 0 || !Number.isFinite(rssiDbm) || rssiDbm > 0) return null;
236533
+ return Math.round((Math.min(-50, Math.max(-100, rssiDbm)) + 100) * 2);
236534
+ }
236535
+ var networkLinkCapability = {
236536
+ name: "network-link",
236537
+ scope: "device",
236538
+ deviceNative: true,
236539
+ mode: "singleton",
236540
+ deviceTypes: [
236541
+ require_sleep.DeviceType.Camera,
236542
+ require_sleep.DeviceType.Sensor,
236543
+ require_sleep.DeviceType.Button,
236544
+ require_sleep.DeviceType.Switch,
236545
+ require_sleep.DeviceType.Light,
236546
+ require_sleep.DeviceType.Lock,
236547
+ require_sleep.DeviceType.Siren
236548
+ ],
236549
+ methods: {},
236550
+ events: {
236551
+ /**
236552
+ * Emitted whenever the cached status changes (a link switch, a signal
236553
+ * reading that moved). Mirrored on the parent chain by the
236554
+ * DeviceEventPropagator like `battery.onStatusChanged`.
236555
+ */
236556
+ onStatusChanged: { data: zod.z.object({
236557
+ deviceId: zod.z.number(),
236558
+ status: NetworkLinkStatusSchema
236559
+ }) }
236560
+ },
236561
+ status: {
236562
+ schema: NetworkLinkStatusSchema,
236563
+ kind: "push",
236564
+ empty: NETWORK_LINK_UNKNOWN
236565
+ },
236566
+ /**
236567
+ * Runtime-state slice — every provider stores the same shape under
236568
+ * `device.runtimeState['network-link']`, read once by the badge and the
236569
+ * Home Assistant projector regardless of the driver.
236570
+ */
236571
+ runtimeState: NetworkLinkStatusSchema,
236572
+ /**
236573
+ * Runtime-state durability: **restored** — a link reading is slow to
236574
+ * change and a sleeping battery camera may not report for hours; the
236575
+ * restored slice is what the badge shows until the next read.
236576
+ *
236577
+ * See `RuntimeStateDurability`. Enforced by
236578
+ * `scripts/check-runtime-state-durability.ts`.
236579
+ */
236580
+ durability: "restored",
236581
+ /** Clock fields: written, but excluded from the compare that decides
236582
+ * whether persisting is worth a SQLite commit. */
236583
+ volatileStateFields: ["lastUpdated"]
236584
+ };
235336
236585
  var StreamNetworkStatsSchema = zod.z.object({
235337
236586
  nominalBitrateKbps: zod.z.number(),
235338
236587
  observedBitrateKbps: zod.z.number(),
@@ -236904,297 +238153,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
236904
238153
  */
236905
238154
  durability: "session"
236906
238155
  };
236907
- var NavigationMoveCommandSchema = zod.z.object({
236908
- pan: zod.z.number().min(-1).max(1).optional(),
236909
- tilt: zod.z.number().min(-1).max(1).optional(),
236910
- speed: zod.z.number().min(0).max(1).optional()
236911
- });
236912
- var NavigationActionIdSchema = zod.z.enum([
236913
- "goHome",
236914
- "locate",
236915
- "spotClean",
236916
- "findPet",
236917
- "personFollow",
236918
- "stop",
236919
- "startClean",
236920
- "pauseClean",
236921
- "dockWash",
236922
- "autoEmpty",
236923
- "flashOn",
236924
- "flashOff"
236925
- ]);
236926
- var NavigationEntryKindSchema = zod.z.enum(["action", "sound"]);
236927
- var NavigationActionEntrySchema = zod.z.object({
236928
- id: zod.z.string(),
236929
- kind: NavigationEntryKindSchema,
236930
- label: zod.z.string(),
236931
- icon: zod.z.string(),
236932
- /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
236933
- soundId: zod.z.number().int().optional(),
236934
- /** Per-device feature flag — render this entry only when true. */
236935
- enabled: zod.z.boolean()
236936
- });
236937
- var NAVIGATION_ACTION_CATALOG = [
236938
- {
236939
- id: "startClean",
236940
- kind: "action",
236941
- label: "Start cleaning",
236942
- icon: "play",
236943
- enabled: true
236944
- },
236945
- {
236946
- id: "pauseClean",
236947
- kind: "action",
236948
- label: "Pause cleaning",
236949
- icon: "pause",
236950
- enabled: true
236951
- },
236952
- {
236953
- id: "stop",
236954
- kind: "action",
236955
- label: "Stop",
236956
- icon: "hand",
236957
- enabled: true
236958
- },
236959
- {
236960
- id: "goHome",
236961
- kind: "action",
236962
- label: "Return to dock",
236963
- icon: "home",
236964
- enabled: true
236965
- },
236966
- {
236967
- id: "locate",
236968
- kind: "action",
236969
- label: "Locate",
236970
- icon: "bell",
236971
- enabled: true
236972
- },
236973
- {
236974
- id: "spotClean",
236975
- kind: "action",
236976
- label: "Spot clean here",
236977
- icon: "crosshair",
236978
- enabled: true
236979
- },
236980
- {
236981
- id: "findPet",
236982
- kind: "action",
236983
- label: "Find pet",
236984
- icon: "paw-print",
236985
- enabled: true
236986
- },
236987
- {
236988
- id: "personFollow",
236989
- kind: "action",
236990
- label: "Follow me",
236991
- icon: "footprints",
236992
- enabled: true
236993
- },
236994
- {
236995
- id: "dockWash",
236996
- kind: "action",
236997
- label: "Wash mop",
236998
- icon: "droplets",
236999
- enabled: true
237000
- },
237001
- {
237002
- id: "autoEmpty",
237003
- kind: "action",
237004
- label: "Empty dustbin",
237005
- icon: "trash-2",
237006
- enabled: true
237007
- },
237008
- {
237009
- id: "flashOn",
237010
- kind: "action",
237011
- label: "Flash on",
237012
- icon: "flashlight",
237013
- enabled: true
237014
- },
237015
- {
237016
- id: "flashOff",
237017
- kind: "action",
237018
- label: "Flash off",
237019
- icon: "flashlight-off",
237020
- enabled: true
237021
- },
237022
- {
237023
- id: "sound:meow",
237024
- kind: "sound",
237025
- label: "Meow",
237026
- icon: "cat",
237027
- soundId: 684,
237028
- enabled: true
237029
- },
237030
- {
237031
- id: "sound:bark",
237032
- kind: "sound",
237033
- label: "Woof",
237034
- icon: "dog",
237035
- soundId: 685,
237036
- enabled: true
237037
- },
237038
- {
237039
- id: "sound:footsteps",
237040
- kind: "sound",
237041
- label: "Footsteps",
237042
- icon: "footprints",
237043
- soundId: 686,
237044
- enabled: true
237045
- },
237046
- {
237047
- id: "sound:purring",
237048
- kind: "sound",
237049
- label: "Purring",
237050
- icon: "cat",
237051
- soundId: 687,
237052
- enabled: true
237053
- },
237054
- {
237055
- id: "sound:tickTock",
237056
- kind: "sound",
237057
- label: "Tick-tock",
237058
- icon: "clock",
237059
- soundId: 688,
237060
- enabled: true
237061
- }
237062
- ];
237063
- var NavigationPointSchema = zod.z.object({
237064
- x: zod.z.number(),
237065
- y: zod.z.number()
237066
- });
237067
- var NavigationFeaturesSchema = zod.z.object({
237068
- move: zod.z.boolean(),
237069
- stop: zod.z.boolean(),
237070
- goToPoint: zod.z.boolean(),
237071
- runAction: zod.z.boolean(),
237072
- playSound: zod.z.boolean(),
237073
- light: zod.z.boolean(),
237074
- lightMode: zod.z.boolean()
237075
- });
237076
- var NavigationLightModeSchema = zod.z.enum(["auto", "manual"]);
237077
- var NAVIGATION_LIGHT_LEVEL_MIN = 40;
237078
- var NAVIGATION_LIGHT_LEVEL_MAX = 100;
237079
- var NavigationModeSchema = zod.z.enum([
237080
- "idle",
237081
- "cleaning",
237082
- "spot",
237083
- "following",
237084
- "goto",
237085
- "returning",
237086
- "paused",
237087
- "unknown"
237088
- ]);
237089
- var NavigationStatusSchema = zod.z.object({
237090
- mode: NavigationModeSchema,
237091
- following: zod.z.boolean(),
237092
- flash: zod.z.boolean(),
237093
- lightMode: NavigationLightModeSchema,
237094
- lightLevel: zod.z.number().min(40).max(100),
237095
- /** Ms epoch when the slice was last updated. */
237096
- lastChangedAt: zod.z.number()
237097
- });
237098
- var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: zod.z.number() });
237099
- var navigationCapability = {
237100
- name: "navigation",
237101
- scope: "device",
237102
- deviceNative: true,
237103
- mode: "singleton",
237104
- deviceTypes: [require_sleep.DeviceType.Camera],
237105
- deviceConfig: { ui: {
237106
- kind: "widget",
237107
- widgetId: "host/navigation-panel",
237108
- tab: "navigation",
237109
- topTab: true,
237110
- label: "Navigation",
237111
- order: 0
237112
- } },
237113
- methods: {
237114
- /**
237115
- * Momentary drive nudge (the robot moves). `protected` — mirrors
237116
- * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
237117
- * path) works for any authenticated user, not admin-only. The UI sends
237118
- * these at ~1 Hz while a control is held; the provider forwards each one to
237119
- * a single drive write WITHOUT debouncing.
237120
- */
237121
- move: require_sleep.method(NavigationMoveCommandSchema.extend({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
237122
- /** Halt all motion immediately (zero drive vector). */
237123
- stop: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
237124
- /** Send the robot to a point on its live map. */
237125
- goToPoint: require_sleep.method(NavigationPointSchema.extend({ deviceId: zod.z.number() }), zod.z.void(), { kind: "mutation" }),
237126
- /**
237127
- * Enumerate the discrete controls THIS device supports (data-driven UI +
237128
- * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
237129
- */
237130
- listActions: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), zod.z.array(NavigationActionEntrySchema)),
237131
- /**
237132
- * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
237133
- * unsupported action ids are rejected by the provider.
237134
- */
237135
- runAction: require_sleep.method(zod.z.object({
237136
- deviceId: zod.z.number(),
237137
- actionId: NavigationActionIdSchema
237138
- }), zod.z.void(), { kind: "mutation" }),
237139
- /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
237140
- playSound: require_sleep.method(zod.z.object({
237141
- deviceId: zod.z.number(),
237142
- soundId: zod.z.number().int()
237143
- }), zod.z.void(), { kind: "mutation" }),
237144
- /**
237145
- * Turn the on-camera fill light on / off (the `OpenFullLight` control —
237146
- * works anytime, no active stream required).
237147
- */
237148
- setLightOn: require_sleep.method(zod.z.object({
237149
- deviceId: zod.z.number(),
237150
- on: zod.z.boolean()
237151
- }), zod.z.void(), { kind: "mutation" }),
237152
- /**
237153
- * Set the fill-light mode (auto vs manual). `manual` optionally carries the
237154
- * initial `level`. The auto/manual + level control is a CAMERA-service
237155
- * action that generally needs an active camera stream/monitor session — the
237156
- * UI shows the manual level slider ONLY when `mode === 'manual'`.
237157
- */
237158
- setLightMode: require_sleep.method(zod.z.object({
237159
- deviceId: zod.z.number(),
237160
- mode: NavigationLightModeSchema,
237161
- level: zod.z.number().min(40).max(100).optional()
237162
- }), zod.z.void(), { kind: "mutation" }),
237163
- /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
237164
- setLightLevel: require_sleep.method(zod.z.object({
237165
- deviceId: zod.z.number(),
237166
- level: zod.z.number().min(40).max(100)
237167
- }), zod.z.void(), { kind: "mutation" }),
237168
- /**
237169
- * Per-device FEATURE-FLAG report for the general primitives — drives which
237170
- * controls the UI shows (the per-entry flags for the dictionary come back on
237171
- * `listActions`).
237172
- */
237173
- getFeatures: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), NavigationFeaturesSchema)
237174
- },
237175
- events: { onStatusChanged: { data: zod.z.object({
237176
- deviceId: zod.z.number(),
237177
- status: NavigationStatusSchema
237178
- }) } },
237179
- status: {
237180
- schema: NavigationStatusSchema,
237181
- kind: "push"
237182
- },
237183
- /**
237184
- * Runtime-state slice mirrored by the kernel. The navigation panel watches it
237185
- * for live mode / follow / flash changes.
237186
- */
237187
- runtimeState: NavigationRuntimeStateSchema,
237188
- /**
237189
- * Runtime-state durability: **session** — like `vacuum-control`, a restored
237190
- * `mode: cleaning` / `following: true` is a robot that is not actually doing
237191
- * that. The live handle re-publishes on connect.
237192
- *
237193
- * See `RuntimeStateDurability`. Enforced by
237194
- * `scripts/check-runtime-state-durability.ts`.
237195
- */
237196
- durability: "session"
237197
- };
237198
238156
  var rebootCapability = {
237199
238157
  name: "reboot",
237200
238158
  scope: "device",
@@ -243303,6 +244261,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
243303
244261
  storage: "storage",
243304
244262
  storageEvictable: "storage-evictable",
243305
244263
  storageMigration: "storage-migration",
244264
+ storageOccupancy: "storage-occupancy",
243306
244265
  storageProvider: "storage-provider",
243307
244266
  streamBroker: "stream-broker",
243308
244267
  streamCatalog: "stream-catalog",
@@ -243847,6 +244806,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
243847
244806
  key: "storageMigration",
243848
244807
  name: "storage-migration"
243849
244808
  },
244809
+ {
244810
+ key: "storageOccupancy",
244811
+ name: "storage-occupancy"
244812
+ },
243850
244813
  {
243851
244814
  key: "storageProvider",
243852
244815
  name: "storage-provider"
@@ -244082,6 +245045,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244082
245045
  storageCapability,
244083
245046
  storageEvictableCapability,
244084
245047
  storageMigrationCapability,
245048
+ storageOccupancyCapability,
244085
245049
  storageProviderCapability,
244086
245050
  streamBrokerCapability,
244087
245051
  streamCatalogCapability,
@@ -249054,6 +250018,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
249054
250018
  addonId: null,
249055
250019
  access: "view"
249056
250020
  },
250021
+ "storage.listDrainProgress": {
250022
+ capName: "storage",
250023
+ capScope: "system",
250024
+ addonId: null,
250025
+ access: "view"
250026
+ },
249057
250027
  "storage.listLocationDeclarations": {
249058
250028
  capName: "storage",
249059
250029
  capScope: "system",
@@ -249198,6 +250168,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
249198
250168
  addonId: null,
249199
250169
  access: "view"
249200
250170
  },
250171
+ "storageOccupancy.getOccupancy": {
250172
+ capName: "storage-occupancy",
250173
+ capScope: "system",
250174
+ addonId: null,
250175
+ access: "view"
250176
+ },
249201
250177
  "storageProvider.abortUpload": {
249202
250178
  capName: "storage-provider",
249203
250179
  capScope: "system",
@@ -250332,6 +251308,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
250332
251308
  "storage",
250333
251309
  "storage-evictable",
250334
251310
  "storage-migration",
251311
+ "storage-occupancy",
250335
251312
  "storage-provider",
250336
251313
  "stream-broker",
250337
251314
  "stream-catalog",
@@ -250476,6 +251453,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
250476
251453
  "storage",
250477
251454
  "storage-evictable",
250478
251455
  "storage-migration",
251456
+ "storage-occupancy",
250479
251457
  "storage-provider",
250480
251458
  "stream-broker",
250481
251459
  "system",
@@ -253426,6 +254404,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
253426
254404
  listLocationDeclarations: (input) => dispatch("storage", "listLocationDeclarations", "query", input),
253427
254405
  upsertLocation: (input) => dispatch("storage", "upsertLocation", "mutation", input),
253428
254406
  deleteLocation: (input) => dispatch("storage", "deleteLocation", "mutation", input),
254407
+ listDrainProgress: (input) => dispatch("storage", "listDrainProgress", "query", input),
253429
254408
  testLocation: (input) => dispatch("storage", "testLocation", "query", input),
253430
254409
  listProviders: (input) => dispatch("storage", "listProviders", "query", input),
253431
254410
  testConfig: (input) => dispatch("storage", "testConfig", "query", input)
@@ -256585,6 +257564,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
256585
257564
  exports.IntercomStatusSchema = IntercomStatusSchema;
256586
257565
  exports.KNOWN_CAP_NAMES = KNOWN_CAP_NAMES;
256587
257566
  exports.KeyEventSchema = KeyEventSchema;
257567
+ exports.LEGACY_READ_ONLY_CONFIG_KEY = LEGACY_READ_ONLY_CONFIG_KEY;
256588
257568
  exports.LOAD_CONTRIBUTION_ATTRIBUTIONS = LOAD_CONTRIBUTION_ATTRIBUTIONS;
256589
257569
  exports.LOAD_CONTRIBUTION_ROLES = LOAD_CONTRIBUTION_ROLES;
256590
257570
  exports.LOG_CHANNEL_TICK_MS = LOG_CHANNEL_TICK_MS;
@@ -257036,6 +258016,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257036
258016
  exports.SOURCE_DEVICE_TYPES = SOURCE_DEVICE_TYPES;
257037
258017
  exports.SOURCE_INFO_METADATA_KEY = SOURCE_INFO_METADATA_KEY;
257038
258018
  exports.STORAGE_ACCESS_FALLBACK = STORAGE_ACCESS_FALLBACK;
258019
+ exports.STORAGE_BYTES_PER_GB = STORAGE_BYTES_PER_GB;
258020
+ exports.STORAGE_LOCATION_MODES = STORAGE_LOCATION_MODES;
257039
258021
  exports.STREAM_PROFILE_META = STREAM_PROFILE_META;
257040
258022
  exports.STREAM_QUALITY_LABELS = STREAM_QUALITY_LABELS;
257041
258023
  exports.SUB_DETECTION_TYPES = SUB_DETECTION_TYPES;
@@ -257097,9 +258079,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257097
258079
  exports.StorageCleanupJobSchema = StorageCleanupJobSchema;
257098
258080
  exports.StorageCleanupPhaseSchema = StorageCleanupPhaseSchema;
257099
258081
  exports.StorageCleanupStatusInputSchema = StorageCleanupStatusInputSchema;
258082
+ exports.StorageDrainProgressSchema = StorageDrainProgressSchema;
257100
258083
  exports.StorageEndDownloadInputSchema = EndDownloadInputSchema;
258084
+ exports.StorageEvictionPolicySchema = StorageEvictionPolicySchema;
257101
258085
  exports.StorageFinalizeUploadInputSchema = FinalizeUploadInputSchema;
257102
258086
  exports.StorageLocationDeclarationSchema = StorageLocationDeclarationSchema;
258087
+ exports.StorageLocationModeSchema = StorageLocationModeSchema;
257103
258088
  exports.StorageLocationRefSchema = StorageLocationRefSchema;
257104
258089
  exports.StorageLocationSchema = StorageLocationSchema;
257105
258090
  exports.StorageLocationTypeSchema = StorageLocationTypeSchema;
@@ -257123,6 +258108,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257123
258108
  exports.StorageMigrationPlanSchema = StorageMigrationPlanSchema;
257124
258109
  exports.StorageMigrationResidueSchema = StorageMigrationResidueSchema;
257125
258110
  exports.StorageMigrationSourcesSchema = StorageMigrationSourcesSchema;
258111
+ exports.StorageOccupancyReportSchema = StorageOccupancyReportSchema;
257126
258112
  exports.StorageProviderInfoSchema = ProviderInfoSchema;
257127
258113
  exports.StorageReadChunkInputSchema = ReadChunkInputSchema;
257128
258114
  exports.StorageTestLocationResultSchema = TestLocationResultSchema;
@@ -257404,6 +258390,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257404
258390
  exports.event = require_sleep.event;
257405
258391
  exports.eventEmitterCapability = eventEmitterCapability;
257406
258392
  exports.eventsCapability = eventsCapability;
258393
+ exports.evictionPolicyForMode = evictionPolicyForMode;
258394
+ exports.evictionPolicyOfLocation = evictionPolicyOfLocation;
257407
258395
  exports.expandCapMethods = require_sleep.expandCapMethods;
257408
258396
  exports.extractNestedAddonId = extractNestedAddonId;
257409
258397
  exports.extractSourceInfoFromMetadata = extractSourceInfoFromMetadata;
@@ -257419,6 +258407,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257419
258407
  exports.formatForBackend = formatForBackend;
257420
258408
  exports.formatForRuntime = formatForRuntime;
257421
258409
  exports.gasCapability = gasCapability;
258410
+ exports.gbToBytes = gbToBytes;
257422
258411
  exports.generateAutomationBlock = generateAutomationBlock;
257423
258412
  exports.getAudioMacroClassIds = getAudioMacroClassIds;
257424
258413
  exports.getByPath = getByPath;
@@ -257463,12 +258452,14 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257463
258452
  exports.isSecretConfigField = isSecretConfigField;
257464
258453
  exports.isSoftwareDecode = require_canonical_hash.isSoftwareDecode;
257465
258454
  exports.isSourceCap = isSourceCap;
258455
+ exports.isStorageLocationMode = isStorageLocationMode;
257466
258456
  exports.isSystemDelivery = isSystemDelivery;
257467
258457
  exports.isVoidInput = isVoidInput;
257468
258458
  exports.jobKindSchema = jobKindSchema;
257469
258459
  exports.kebabToCamel = kebabToCamel;
257470
258460
  exports.knownValues = knownValues;
257471
258461
  exports.lawnMowerControlCapability = lawnMowerControlCapability;
258462
+ exports.legacyModeOf = legacyModeOf;
257472
258463
  exports.lifecycleJobSchema = lifecycleJobSchema;
257473
258464
  exports.lifecycleJobScopeSchema = lifecycleJobScopeSchema;
257474
258465
  exports.lifecycleJobStateSchema = lifecycleJobStateSchema;
@@ -257491,6 +258482,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257491
258482
  exports.markdownToHtmlLite = markdownToHtmlLite;
257492
258483
  exports.markdownToText = markdownToText;
257493
258484
  exports.maskUrlCredentials = maskUrlCredentials;
258485
+ exports.mayReadLocation = mayReadLocation;
258486
+ exports.mayWriteToLocation = mayWriteToLocation;
257494
258487
  exports.mediaPlayerCapability = mediaPlayerCapability;
257495
258488
  exports.mergeSourceInfo = mergeSourceInfo;
257496
258489
  exports.meshNetworkCapability = meshNetworkCapability;
@@ -257498,6 +258491,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257498
258491
  exports.methodAccessForHttpMethod = methodAccessForHttpMethod;
257499
258492
  exports.metricsProviderCapability = metricsProviderCapability;
257500
258493
  exports.migrateLegacyDeviceSignalConfig = migrateLegacyDeviceSignalConfig;
258494
+ exports.modeMayRead = modeMayRead;
258495
+ exports.modeMayWrite = modeMayWrite;
257501
258496
  exports.modelConvertCapability = modelConvertCapability;
257502
258497
  exports.modelDistributorCapability = modelDistributorCapability;
257503
258498
  exports.modelFormatForRuntime = modelFormatForRuntime;
@@ -257593,6 +258588,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257593
258588
  exports.resolveEgressDecodeHwAccel = resolveEgressDecodeHwAccel;
257594
258589
  exports.resolveFormat = resolveFormat;
257595
258590
  exports.resolveHydratedFieldValue = require_sleep.resolveHydratedFieldValue;
258591
+ exports.resolveLocationMode = resolveLocationMode;
257596
258592
  exports.resolveMethodAuth = resolveMethodAuth;
257597
258593
  exports.resolveModelFormat = resolveModelFormat;
257598
258594
  exports.resolveMutate = resolveMutate;
@@ -257639,6 +258635,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257639
258635
  exports.storageCapability = storageCapability;
257640
258636
  exports.storageEvictableCapability = storageEvictableCapability;
257641
258637
  exports.storageMigrationCapability = storageMigrationCapability;
258638
+ exports.storageOccupancyCapability = storageOccupancyCapability;
257642
258639
  exports.storageProviderCapability = storageProviderCapability;
257643
258640
  exports.streamBrokerCapability = streamBrokerCapability;
257644
258641
  exports.streamCatalogCapability = streamCatalogCapability;
@@ -257646,6 +258643,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257646
258643
  exports.streamPixels = streamPixels;
257647
258644
  exports.streamQualityLabel = streamQualityLabel;
257648
258645
  exports.stripRetiredBandPreBufferSec = stripRetiredBandPreBufferSec;
258646
+ exports.strippedOfLegacyReadOnly = strippedOfLegacyReadOnly;
257649
258647
  exports.subKindsOf = subKindsOf;
257650
258648
  exports.summarisePrivacyAudio = summarisePrivacyAudio;
257651
258649
  exports.summarizeEffectiveScope = summarizeEffectiveScope;
@@ -257696,6 +258694,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
257696
258694
  exports.wiringNodeHealthSchema = wiringNodeHealthSchema;
257697
258695
  exports.wiringProbeKindSchema = wiringProbeKindSchema;
257698
258696
  exports.wiringProbeResultSchema = wiringProbeResultSchema;
258697
+ exports.withLocationMode = withLocationMode;
257699
258698
  exports.zodEntriesToConfigUI = zodEntriesToConfigUI;
257700
258699
  exports.zoneAnalyticsCapability = zoneAnalyticsCapability;
257701
258700
  exports.zoneRulesCapability = zoneRulesCapability;