@camstack/system 1.2.21 → 1.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +17 -13
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1319 -1097
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1319 -1097
  12. package/dist/builtins/backup-orchestrator/schedule-store.d.ts +33 -0
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  19. package/dist/builtins/hub-forwarder/index.js +1 -1
  20. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  21. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  22. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  23. package/dist/builtins/local-network/local-network.addon.d.ts +16 -0
  24. package/dist/builtins/local-network/local-network.addon.js +30 -2
  25. package/dist/builtins/local-network/local-network.addon.mjs +30 -2
  26. package/dist/builtins/loki-logging/index.js +34 -12
  27. package/dist/builtins/loki-logging/index.mjs +34 -12
  28. package/dist/builtins/loki-logging/loki-logging.addon.d.ts +19 -0
  29. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  31. package/dist/builtins/platform-probe/index.js +1 -1
  32. package/dist/builtins/platform-probe/index.mjs +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  35. package/dist/builtins/snapshot/index.js +1 -1
  36. package/dist/builtins/snapshot/index.mjs +1 -1
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  38. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  39. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  41. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +7 -0
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +112 -6
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +112 -6
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +17 -1
  45. package/dist/builtins/storage-orchestrator/storage-pressure-manager.d.ts +5 -0
  46. package/dist/builtins/system-config/system-config.addon.js +1 -1
  47. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  48. package/dist/builtins/winston-logging/index.js +1 -1
  49. package/dist/builtins/winston-logging/index.mjs +1 -1
  50. package/dist/{dist-DmQ6thmF.mjs → dist-CIek6F9X.mjs} +536 -76
  51. package/dist/{dist-OQ1yU92D.js → dist-CS0pdC8l.js} +536 -76
  52. package/dist/index.js +58 -8
  53. package/dist/index.mjs +58 -8
  54. package/dist/kernel/capability-registry.d.ts +9 -0
  55. package/dist/kernel/transport/local-child-registry.d.ts +18 -0
  56. package/dist/kernel/transport/parent-unowned-call.d.ts +22 -0
  57. package/dist/logging/log-manager.d.ts +32 -1
  58. package/dist/logging/partitioned-log-buffer.d.ts +15 -3
  59. package/dist/{manifest-python-deps-B30qVAuW.mjs → manifest-python-deps-57PA4rHm.mjs} +15 -6
  60. package/dist/{manifest-python-deps-CnRW2Utv.js → manifest-python-deps-CqDPWqEE.js} +14 -5
  61. package/package.json +3 -2
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-OQ1yU92D.js");
6
+ const require_dist = require("../../dist-CS0pdC8l.js");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_chunk.__toESM(node_path);
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -14,6 +14,8 @@ var StorageOrchestratorService = class {
14
14
  logger;
15
15
  getProviders;
16
16
  locationStore;
17
+ localNodeId;
18
+ getRemoteProvider;
17
19
  locations = /* @__PURE__ */ new Map();
18
20
  /**
19
21
  * Declaration-driven cardinality source, injected by the addon after
@@ -31,10 +33,12 @@ var StorageOrchestratorService = class {
31
33
  * early-boot path that predates the resolver wiring.
32
34
  */
33
35
  nodeLocalResolver = null;
34
- constructor(logger, getProviders, locationStore = null) {
36
+ constructor(logger, getProviders, locationStore = null, localNodeId = "hub", getRemoteProvider) {
35
37
  this.logger = logger;
36
38
  this.getProviders = getProviders;
37
39
  this.locationStore = locationStore;
40
+ this.localNodeId = localNodeId;
41
+ this.getRemoteProvider = getRemoteProvider;
38
42
  }
39
43
  /** True once a persistence store is wired (constructor or {@link attachStore}). */
40
44
  hasStore() {
@@ -349,6 +353,12 @@ var StorageOrchestratorService = class {
349
353
  * the right place to fix the config.
350
354
  */
351
355
  async getProviderFor(location) {
356
+ const nodeId = location.nodeId;
357
+ if (nodeId !== void 0 && nodeId !== this.localNodeId) {
358
+ const remote = this.getRemoteProvider?.(nodeId);
359
+ if (remote) return remote;
360
+ throw new Error(`storage-provider for location "${location.id}" lives on node "${nodeId}" which is not reachable`);
361
+ }
352
362
  const providers = this.getProviders();
353
363
  for (const p of providers) if ((await p.getProviderInfo()).providerId === location.providerId) return p;
354
364
  throw new Error(`No storage-provider registered for providerId "${location.providerId}" (location "${location.id}")`);
@@ -378,8 +388,8 @@ var StorageOrchestratorService = class {
378
388
  let added = 0;
379
389
  const declById = new Map(input.declarations.map((d) => [d.id, d]));
380
390
  const rootFor = (d) => {
381
- const useMedia = d.defaultRoot === "media" && input.mediaBasePath;
382
- return node_path.join(useMedia ? input.mediaBasePath : input.basePath, d.id);
391
+ const dedicated = d.defaultRoot === "media" ? input.mediaBasePath : d.defaultRoot === "backup" ? input.backupBasePath : void 0;
392
+ return node_path.join(dedicated ?? input.basePath, d.id);
383
393
  };
384
394
  for (const d of input.declarations) {
385
395
  if (this.hasAnyLocationOfType(d.id)) continue;
@@ -634,6 +644,53 @@ var StoragePressureManager = class {
634
644
  } });
635
645
  }
636
646
  }
647
+ const caps = await this.deps.getLocationMaxUsedBytes?.() ?? /* @__PURE__ */ new Map();
648
+ for (const [locationId, maxBytes] of caps) {
649
+ if (relievedDevices.size > 0) {
650
+ const deviceKey = await this.deps.getLocationDeviceKey?.(locationId) ?? null;
651
+ if (deviceKey !== null && relievedDevices.has(deviceKey)) continue;
652
+ }
653
+ try {
654
+ await this.relieveToUsageCap(locationId, maxBytes);
655
+ } catch (err) {
656
+ this.deps.logger.warn("storage pressure: usage-cap relief failed", { meta: {
657
+ locationId,
658
+ error: err instanceof Error ? err.message : String(err)
659
+ } });
660
+ }
661
+ }
662
+ }
663
+ /** Evict oldest-first until the location's evictable usage fits `maxBytes`. */
664
+ async relieveToUsageCap(locationId, maxBytes) {
665
+ const maxRounds = this.deps.maxRounds ?? DEFAULT_MAX_ROUNDS;
666
+ for (let round = 0; round < maxRounds; round++) {
667
+ const providers = this.deps.getEvictableProviders();
668
+ const usages = await Promise.all(providers.map(async (p) => ({
669
+ p,
670
+ bytes: (await p.getEvictableUsage({ locationId })).bytes
671
+ })));
672
+ const totalUsed = usages.reduce((s, u) => s + u.bytes, 0);
673
+ const excess = totalUsed - maxBytes;
674
+ if (excess <= 0) return;
675
+ let reclaimed = 0;
676
+ for (const u of usages) {
677
+ if (u.bytes <= 0) continue;
678
+ const share = Math.max(1, Math.ceil(excess * u.bytes / totalUsed));
679
+ const res = await u.p.evict({
680
+ locationId,
681
+ targetBytes: share
682
+ });
683
+ reclaimed += res.reclaimedBytes;
684
+ }
685
+ this.deps.logger.info("storage pressure: usage-cap relief round", { meta: {
686
+ locationId,
687
+ round,
688
+ totalUsed,
689
+ maxBytes,
690
+ reclaimedBytes: reclaimed
691
+ } });
692
+ if (reclaimed <= 0) return;
693
+ }
637
694
  }
638
695
  /**
639
696
  * Relieve one location until it recovers, nothing is evictable, or a round
@@ -772,13 +829,21 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
772
829
  return reg.getCollection(STORAGE_PROVIDER_CAP_NAME);
773
830
  };
774
831
  const locationStore = this.resolveLocationStore();
775
- const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore);
832
+ const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore, this.ctx.kernel.localNodeId ?? "hub", (nodeId) => {
833
+ const proxy = this.ctx.kernel.resolveNodeCapability?.("storage-provider", nodeId);
834
+ if (!proxy) return null;
835
+ return proxy;
836
+ });
776
837
  this.service = service;
777
838
  service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
778
839
  await service.initialize();
779
840
  const provider = {
780
841
  listLocations: async ({ type }) => {
781
- return type !== void 0 ? service.listLocations({ type }) : service.listLocations();
842
+ const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
843
+ return Promise.all(rows.map(async (loc) => ({
844
+ ...loc,
845
+ capacity: await this.localCapacityOf(loc)
846
+ })));
782
847
  },
783
848
  getDefaultLocation: async ({ type }) => service.getDefaultLocation(type),
784
849
  listLocationDeclarations: async () => service.listDeclarations(),
@@ -944,6 +1009,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
944
1009
  getVolumeTotalBytes: (id) => this.locationVolumeTotalBytes(id),
945
1010
  getEvictableProviders: () => this.ctx.kernel.capabilityRegistry?.getCollection(STORAGE_EVICTABLE_CAP_NAME) ?? [],
946
1011
  getLocationThresholds: () => this.resolveLocationThresholds(),
1012
+ getLocationMaxUsedBytes: () => this.resolveLocationMaxUsedBytes(),
947
1013
  getLocationDeviceKey: (id) => this.locationDeviceKey(id)
948
1014
  });
949
1015
  this.pressureTimer = setInterval(() => {
@@ -1053,6 +1119,44 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
1053
1119
  }
1054
1120
  return out;
1055
1121
  }
1122
+ /** statfs capacity of a location's basePath, walking up to the nearest
1123
+ * existing ancestor. Null for remote-node locations or unstattable paths. */
1124
+ async localCapacityOf(loc) {
1125
+ if (loc.nodeId !== void 0 && loc.nodeId !== "hub" && loc.nodeId !== this.ctx.nodeId) return null;
1126
+ const bp = loc.config["basePath"];
1127
+ if (typeof bp !== "string" || bp.length === 0) return null;
1128
+ try {
1129
+ let target = node_path.resolve(bp);
1130
+ for (;;) try {
1131
+ await node_fs_promises.access(target);
1132
+ break;
1133
+ } catch {
1134
+ const parent = node_path.dirname(target);
1135
+ if (!parent || parent === target) return null;
1136
+ target = parent;
1137
+ }
1138
+ const st = await node_fs_promises.statfs(target);
1139
+ return {
1140
+ totalBytes: st.blocks * st.bsize,
1141
+ availableBytes: st.bavail * st.bsize
1142
+ };
1143
+ } catch {
1144
+ return null;
1145
+ }
1146
+ }
1147
+ /** Per-location cap on EVICTABLE bytes (`config.maxUsedGb`): the operator's
1148
+ * "recordings may use at most N GB in total" — enforced by the pressure
1149
+ * manager alongside the free-percent guard. */
1150
+ async resolveLocationMaxUsedBytes() {
1151
+ const out = /* @__PURE__ */ new Map();
1152
+ const svc = this.service;
1153
+ if (!svc) return out;
1154
+ for (const loc of svc.listLocations()) {
1155
+ const raw = loc.config["maxUsedGb"];
1156
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) out.set(loc.id, Math.floor(raw * 1024 ** 3));
1157
+ }
1158
+ return out;
1159
+ }
1056
1160
  locationBasePath(locationId) {
1057
1161
  const bp = (this.service?.listLocations().find((l) => l.id === locationId))?.config["basePath"];
1058
1162
  return typeof bp === "string" && bp.length > 0 ? bp : null;
@@ -1120,10 +1224,12 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
1120
1224
  this.service.setRegistry(registry);
1121
1225
  this.service.pruneUndeclaredSystemLocations();
1122
1226
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
1227
+ const backupRoot = process.env.CAMSTACK_BACKUP_ROOT?.trim();
1123
1228
  this.service.seedDefaults({
1124
1229
  providerId: "filesystem-storage",
1125
1230
  basePath: this.ctx.nodeDataDir,
1126
1231
  mediaBasePath: mediaRoot && mediaRoot.length > 0 ? mediaRoot : void 0,
1232
+ backupBasePath: backupRoot && backupRoot.length > 0 ? backupRoot : void 0,
1127
1233
  declarations: registry.list()
1128
1234
  });
1129
1235
  await this.service.backfillNodeIds(HUB_NODE_ID);
@@ -1,4 +1,4 @@
1
- import { Q as storageCapability, Tt as parseJsonObject, at as BaseAddon, d as StorageLocationTypeSchema } from "../../dist-DmQ6thmF.mjs";
1
+ import { Q as storageCapability, Tt as parseJsonObject, at as BaseAddon, d as StorageLocationTypeSchema } from "../../dist-CIek6F9X.mjs";
2
2
  import * as path$1 from "node:path";
3
3
  import * as fs from "node:fs/promises";
4
4
  import { buildStorageLocationRegistry } from "@camstack/system";
@@ -7,6 +7,8 @@ var StorageOrchestratorService = class {
7
7
  logger;
8
8
  getProviders;
9
9
  locationStore;
10
+ localNodeId;
11
+ getRemoteProvider;
10
12
  locations = /* @__PURE__ */ new Map();
11
13
  /**
12
14
  * Declaration-driven cardinality source, injected by the addon after
@@ -24,10 +26,12 @@ var StorageOrchestratorService = class {
24
26
  * early-boot path that predates the resolver wiring.
25
27
  */
26
28
  nodeLocalResolver = null;
27
- constructor(logger, getProviders, locationStore = null) {
29
+ constructor(logger, getProviders, locationStore = null, localNodeId = "hub", getRemoteProvider) {
28
30
  this.logger = logger;
29
31
  this.getProviders = getProviders;
30
32
  this.locationStore = locationStore;
33
+ this.localNodeId = localNodeId;
34
+ this.getRemoteProvider = getRemoteProvider;
31
35
  }
32
36
  /** True once a persistence store is wired (constructor or {@link attachStore}). */
33
37
  hasStore() {
@@ -342,6 +346,12 @@ var StorageOrchestratorService = class {
342
346
  * the right place to fix the config.
343
347
  */
344
348
  async getProviderFor(location) {
349
+ const nodeId = location.nodeId;
350
+ if (nodeId !== void 0 && nodeId !== this.localNodeId) {
351
+ const remote = this.getRemoteProvider?.(nodeId);
352
+ if (remote) return remote;
353
+ throw new Error(`storage-provider for location "${location.id}" lives on node "${nodeId}" which is not reachable`);
354
+ }
345
355
  const providers = this.getProviders();
346
356
  for (const p of providers) if ((await p.getProviderInfo()).providerId === location.providerId) return p;
347
357
  throw new Error(`No storage-provider registered for providerId "${location.providerId}" (location "${location.id}")`);
@@ -371,8 +381,8 @@ var StorageOrchestratorService = class {
371
381
  let added = 0;
372
382
  const declById = new Map(input.declarations.map((d) => [d.id, d]));
373
383
  const rootFor = (d) => {
374
- const useMedia = d.defaultRoot === "media" && input.mediaBasePath;
375
- return path$1.join(useMedia ? input.mediaBasePath : input.basePath, d.id);
384
+ const dedicated = d.defaultRoot === "media" ? input.mediaBasePath : d.defaultRoot === "backup" ? input.backupBasePath : void 0;
385
+ return path$1.join(dedicated ?? input.basePath, d.id);
376
386
  };
377
387
  for (const d of input.declarations) {
378
388
  if (this.hasAnyLocationOfType(d.id)) continue;
@@ -627,6 +637,53 @@ var StoragePressureManager = class {
627
637
  } });
628
638
  }
629
639
  }
640
+ const caps = await this.deps.getLocationMaxUsedBytes?.() ?? /* @__PURE__ */ new Map();
641
+ for (const [locationId, maxBytes] of caps) {
642
+ if (relievedDevices.size > 0) {
643
+ const deviceKey = await this.deps.getLocationDeviceKey?.(locationId) ?? null;
644
+ if (deviceKey !== null && relievedDevices.has(deviceKey)) continue;
645
+ }
646
+ try {
647
+ await this.relieveToUsageCap(locationId, maxBytes);
648
+ } catch (err) {
649
+ this.deps.logger.warn("storage pressure: usage-cap relief failed", { meta: {
650
+ locationId,
651
+ error: err instanceof Error ? err.message : String(err)
652
+ } });
653
+ }
654
+ }
655
+ }
656
+ /** Evict oldest-first until the location's evictable usage fits `maxBytes`. */
657
+ async relieveToUsageCap(locationId, maxBytes) {
658
+ const maxRounds = this.deps.maxRounds ?? DEFAULT_MAX_ROUNDS;
659
+ for (let round = 0; round < maxRounds; round++) {
660
+ const providers = this.deps.getEvictableProviders();
661
+ const usages = await Promise.all(providers.map(async (p) => ({
662
+ p,
663
+ bytes: (await p.getEvictableUsage({ locationId })).bytes
664
+ })));
665
+ const totalUsed = usages.reduce((s, u) => s + u.bytes, 0);
666
+ const excess = totalUsed - maxBytes;
667
+ if (excess <= 0) return;
668
+ let reclaimed = 0;
669
+ for (const u of usages) {
670
+ if (u.bytes <= 0) continue;
671
+ const share = Math.max(1, Math.ceil(excess * u.bytes / totalUsed));
672
+ const res = await u.p.evict({
673
+ locationId,
674
+ targetBytes: share
675
+ });
676
+ reclaimed += res.reclaimedBytes;
677
+ }
678
+ this.deps.logger.info("storage pressure: usage-cap relief round", { meta: {
679
+ locationId,
680
+ round,
681
+ totalUsed,
682
+ maxBytes,
683
+ reclaimedBytes: reclaimed
684
+ } });
685
+ if (reclaimed <= 0) return;
686
+ }
630
687
  }
631
688
  /**
632
689
  * Relieve one location until it recovers, nothing is evictable, or a round
@@ -765,13 +822,21 @@ var StorageOrchestratorAddon = class extends BaseAddon {
765
822
  return reg.getCollection(STORAGE_PROVIDER_CAP_NAME);
766
823
  };
767
824
  const locationStore = this.resolveLocationStore();
768
- const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore);
825
+ const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore, this.ctx.kernel.localNodeId ?? "hub", (nodeId) => {
826
+ const proxy = this.ctx.kernel.resolveNodeCapability?.("storage-provider", nodeId);
827
+ if (!proxy) return null;
828
+ return proxy;
829
+ });
769
830
  this.service = service;
770
831
  service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
771
832
  await service.initialize();
772
833
  const provider = {
773
834
  listLocations: async ({ type }) => {
774
- return type !== void 0 ? service.listLocations({ type }) : service.listLocations();
835
+ const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
836
+ return Promise.all(rows.map(async (loc) => ({
837
+ ...loc,
838
+ capacity: await this.localCapacityOf(loc)
839
+ })));
775
840
  },
776
841
  getDefaultLocation: async ({ type }) => service.getDefaultLocation(type),
777
842
  listLocationDeclarations: async () => service.listDeclarations(),
@@ -937,6 +1002,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
937
1002
  getVolumeTotalBytes: (id) => this.locationVolumeTotalBytes(id),
938
1003
  getEvictableProviders: () => this.ctx.kernel.capabilityRegistry?.getCollection(STORAGE_EVICTABLE_CAP_NAME) ?? [],
939
1004
  getLocationThresholds: () => this.resolveLocationThresholds(),
1005
+ getLocationMaxUsedBytes: () => this.resolveLocationMaxUsedBytes(),
940
1006
  getLocationDeviceKey: (id) => this.locationDeviceKey(id)
941
1007
  });
942
1008
  this.pressureTimer = setInterval(() => {
@@ -1046,6 +1112,44 @@ var StorageOrchestratorAddon = class extends BaseAddon {
1046
1112
  }
1047
1113
  return out;
1048
1114
  }
1115
+ /** statfs capacity of a location's basePath, walking up to the nearest
1116
+ * existing ancestor. Null for remote-node locations or unstattable paths. */
1117
+ async localCapacityOf(loc) {
1118
+ if (loc.nodeId !== void 0 && loc.nodeId !== "hub" && loc.nodeId !== this.ctx.nodeId) return null;
1119
+ const bp = loc.config["basePath"];
1120
+ if (typeof bp !== "string" || bp.length === 0) return null;
1121
+ try {
1122
+ let target = path$1.resolve(bp);
1123
+ for (;;) try {
1124
+ await fs.access(target);
1125
+ break;
1126
+ } catch {
1127
+ const parent = path$1.dirname(target);
1128
+ if (!parent || parent === target) return null;
1129
+ target = parent;
1130
+ }
1131
+ const st = await fs.statfs(target);
1132
+ return {
1133
+ totalBytes: st.blocks * st.bsize,
1134
+ availableBytes: st.bavail * st.bsize
1135
+ };
1136
+ } catch {
1137
+ return null;
1138
+ }
1139
+ }
1140
+ /** Per-location cap on EVICTABLE bytes (`config.maxUsedGb`): the operator's
1141
+ * "recordings may use at most N GB in total" — enforced by the pressure
1142
+ * manager alongside the free-percent guard. */
1143
+ async resolveLocationMaxUsedBytes() {
1144
+ const out = /* @__PURE__ */ new Map();
1145
+ const svc = this.service;
1146
+ if (!svc) return out;
1147
+ for (const loc of svc.listLocations()) {
1148
+ const raw = loc.config["maxUsedGb"];
1149
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) out.set(loc.id, Math.floor(raw * 1024 ** 3));
1150
+ }
1151
+ return out;
1152
+ }
1049
1153
  locationBasePath(locationId) {
1050
1154
  const bp = (this.service?.listLocations().find((l) => l.id === locationId))?.config["basePath"];
1051
1155
  return typeof bp === "string" && bp.length > 0 ? bp : null;
@@ -1113,10 +1217,12 @@ var StorageOrchestratorAddon = class extends BaseAddon {
1113
1217
  this.service.setRegistry(registry);
1114
1218
  this.service.pruneUndeclaredSystemLocations();
1115
1219
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
1220
+ const backupRoot = process.env.CAMSTACK_BACKUP_ROOT?.trim();
1116
1221
  this.service.seedDefaults({
1117
1222
  providerId: "filesystem-storage",
1118
1223
  basePath: this.ctx.nodeDataDir,
1119
1224
  mediaBasePath: mediaRoot && mediaRoot.length > 0 ? mediaRoot : void 0,
1225
+ backupBasePath: backupRoot && backupRoot.length > 0 ? backupRoot : void 0,
1120
1226
  declarations: registry.list()
1121
1227
  });
1122
1228
  await this.service.backfillNodeIds(HUB_NODE_ID);
@@ -54,6 +54,11 @@ export declare class StorageOrchestratorService {
54
54
  * the provider appears. See {@link attachStore}.
55
55
  */
56
56
  private locationStore;
57
+ /** This node's id — locations pinned to OTHER nodes dispatch remotely. */
58
+ private readonly localNodeId;
59
+ /** Cross-node provider resolver (hub wiring: a node-routed capability
60
+ * proxy over `storage-provider`). Absent → remote locations error. */
61
+ private readonly getRemoteProvider?;
57
62
  private readonly locations;
58
63
  /**
59
64
  * Declaration-driven cardinality source, injected by the addon after
@@ -85,7 +90,12 @@ export declare class StorageOrchestratorService {
85
90
  * service starts store-less and the addon calls {@link attachStore} once
86
91
  * the provider appears. See {@link attachStore}.
87
92
  */
88
- locationStore?: ILocationStore | null);
93
+ locationStore?: ILocationStore | null,
94
+ /** This node's id — locations pinned to OTHER nodes dispatch remotely. */
95
+ localNodeId?: string,
96
+ /** Cross-node provider resolver (hub wiring: a node-routed capability
97
+ * proxy over `storage-provider`). Absent → remote locations error. */
98
+ getRemoteProvider?: ((nodeId: string) => IStorageProviderImpl | null) | undefined);
89
99
  /** True once a persistence store is wired (constructor or {@link attachStore}). */
90
100
  hasStore(): boolean;
91
101
  /**
@@ -235,6 +245,12 @@ export declare class StorageOrchestratorService {
235
245
  * `basePath` like everything else.
236
246
  */
237
247
  readonly mediaBasePath?: string;
248
+ /**
249
+ * Root for declarations marked `defaultRoot: 'backup'` (the dedicated
250
+ * backup volume, `CAMSTACK_BACKUP_ROOT`, default `/backups`). Absent →
251
+ * backup declarations fall back to `basePath`.
252
+ */
253
+ readonly backupBasePath?: string;
238
254
  readonly declarations: readonly StorageLocationDeclaration[];
239
255
  }): void;
240
256
  /** Internal: check whether any location of the given type exists. */
@@ -24,6 +24,9 @@ export interface PressureManagerDeps {
24
24
  readonly getEvictableProviders: () => readonly IStorageEvictableProvider[];
25
25
  /** locationId → minFreePercent threshold; entries with `<= 0` have the guard off. */
26
26
  readonly getLocationThresholds: () => Promise<ReadonlyMap<string, number>>;
27
+ /** locationId → hard cap on EVICTABLE bytes (`config.maxUsedGb`, operator's
28
+ * "recordings may use at most N GB total"). Optional — absent = no caps. */
29
+ readonly getLocationMaxUsedBytes?: () => Promise<ReadonlyMap<string, number>>;
27
30
  /** Max relief rounds per location per sweep (backstop, default 8). */
28
31
  readonly maxRounds?: number;
29
32
  /** Extra headroom % freed beyond the threshold to avoid re-breaching immediately (default 1). */
@@ -53,6 +56,8 @@ export declare class StoragePressureManager {
53
56
  constructor(deps: PressureManagerDeps);
54
57
  /** Relieve every managed location currently under its threshold. */
55
58
  sweep(): Promise<void>;
59
+ /** Evict oldest-first until the location's evictable usage fits `maxBytes`. */
60
+ private relieveToUsageCap;
56
61
  /**
57
62
  * Relieve one location until it recovers, nothing is evictable, or a round
58
63
  * makes no progress. Returns `true` when at least one relief round actually
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-OQ1yU92D.js");
6
+ const require_dist = require("../../dist-CS0pdC8l.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { St as hydrateSchema, at as BaseAddon, it as errMsg } from "../../dist-DmQ6thmF.mjs";
1
+ import { St as hydrateSchema, at as BaseAddon, it as errMsg } from "../../dist-CIek6F9X.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-OQ1yU92D.js");
6
+ const require_dist = require("../../dist-CS0pdC8l.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { at as BaseAddon, z as logDestinationCapability } from "../../dist-DmQ6thmF.mjs";
1
+ import { at as BaseAddon, z as logDestinationCapability } from "../../dist-CIek6F9X.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";