@camstack/system 1.1.52 → 1.1.54

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 (58) hide show
  1. package/dist/addon-runner.js +2 -2
  2. package/dist/addon-runner.mjs +2 -2
  3. package/dist/addon-utils.d.ts +1 -0
  4. package/dist/addon-utils.js +2 -0
  5. package/dist/addon-utils.mjs +2 -2
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  10. package/dist/builtins/alerts/alerts.addon.js +1 -1
  11. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  14. package/dist/builtins/console-logging/index.js +1 -1
  15. package/dist/builtins/console-logging/index.mjs +1 -1
  16. package/dist/builtins/device-manager/device-linked-devices.d.ts +1 -1
  17. package/dist/builtins/device-manager/device-manager.addon.js +1608 -1589
  18. package/dist/builtins/device-manager/device-manager.addon.mjs +1608 -1589
  19. package/dist/builtins/device-manager/device-store-sections.d.ts +26 -0
  20. package/dist/builtins/doorbell/doorbell-settings.d.ts +58 -0
  21. package/dist/builtins/doorbell/trigger-engine.d.ts +32 -30
  22. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +39 -22
  23. package/dist/builtins/doorbell/virtual-doorbell.addon.js +207 -191
  24. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +207 -191
  25. package/dist/builtins/hub-forwarder/index.js +1 -1
  26. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  28. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  29. package/dist/builtins/local-network/local-network.addon.js +1 -1
  30. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  33. package/dist/builtins/platform-probe/index.js +1 -1
  34. package/dist/builtins/platform-probe/index.mjs +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  37. package/dist/builtins/snapshot/index.js +1 -1
  38. package/dist/builtins/snapshot/index.mjs +1 -1
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +11 -3
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +11 -3
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  45. package/dist/builtins/system-config/system-config.addon.js +1 -1
  46. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  47. package/dist/builtins/winston-logging/index.js +1 -1
  48. package/dist/builtins/winston-logging/index.mjs +1 -1
  49. package/dist/{dist-BduBhv6N.js → dist-B5C4dVvO.js} +293 -23
  50. package/dist/{dist-B_PSvr-m.mjs → dist-DssImRC5.mjs} +293 -23
  51. package/dist/index.js +22 -5
  52. package/dist/index.mjs +22 -6
  53. package/dist/kernel/index.d.ts +1 -1
  54. package/dist/kernel/moleculer/readiness-context.d.ts +12 -8
  55. package/dist/kernel/moleculer/readiness-service.d.ts +16 -1
  56. package/dist/{manifest-python-deps-CIyvuHAB.mjs → manifest-python-deps-CWom-eqp.mjs} +35 -9
  57. package/dist/{manifest-python-deps-CPAMH5gV.js → manifest-python-deps-kkvku9yU.js} +35 -9
  58. package/package.json +2 -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-BduBhv6N.js");
6
+ const require_dist = require("../../dist-B5C4dVvO.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { O as filesystemBrowseCapability, Q as storageProviderCapability, it as BaseAddon } from "../../dist-B_PSvr-m.mjs";
1
+ import { O as filesystemBrowseCapability, Q as storageProviderCapability, it as BaseAddon } from "../../dist-DssImRC5.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import * as path$1 from "node:path";
4
4
  import { basename, dirname, join, resolve, sep } from "node:path";
@@ -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-BduBhv6N.js");
6
+ const require_dist = require("../../dist-B5C4dVvO.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let better_sqlite3 = require("better-sqlite3");
9
9
  better_sqlite3 = require_chunk.__toESM(better_sqlite3);
@@ -510,10 +510,18 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
510
510
  if (existingCols.length > 0) {
511
511
  const existingNames = new Set(existingCols.map((c) => c.name));
512
512
  const declaredNames = new Set(schema.columns.map((c) => c.name));
513
- const missingDeclared = schema.columns.some((c) => !existingNames.has(c.name));
514
- if (existingCols.length <= 2 && existingNames.has("data") && !declaredNames.has("data") || missingDeclared) try {
513
+ const missingCols = schema.columns.filter((c) => !existingNames.has(c.name));
514
+ const isLegacyKv = existingCols.length <= 2 && existingNames.has("data") && !declaredNames.has("data");
515
+ const addableInPlace = (col) => !col.primaryKey && !col.unique && (!col.notNull || col.defaultValue !== void 0);
516
+ if (isLegacyKv || missingCols.some((c) => !addableInPlace(c))) try {
515
517
  this.getDb().exec(`DROP TABLE "${table}"`);
516
518
  } catch {}
519
+ else if (missingCols.length > 0) for (const col of missingCols) {
520
+ const parts = [`"${col.name}" ${col.type}`];
521
+ if (col.notNull) parts.push("NOT NULL");
522
+ if (col.defaultValue !== void 0) parts.push(`DEFAULT ${typeof col.defaultValue === "string" ? `'${col.defaultValue}'` : col.defaultValue === null ? "NULL" : String(col.defaultValue)}`);
523
+ this.getDb().exec(`ALTER TABLE "${table}" ADD COLUMN ${parts.join(" ")}`);
524
+ }
517
525
  }
518
526
  this.getDb().exec(`CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`);
519
527
  if (schema.indexes) for (const idx of schema.indexes) {
@@ -1,4 +1,4 @@
1
- import { Y as settingsStoreCapability, it as BaseAddon, mt as asJsonObject, rt as errMsg, s as RUNTIME_DEFAULTS, wt as parseJsonUnknown } from "../../dist-B_PSvr-m.mjs";
1
+ import { Y as settingsStoreCapability, it as BaseAddon, mt as asJsonObject, rt as errMsg, s as RUNTIME_DEFAULTS, wt as parseJsonUnknown } from "../../dist-DssImRC5.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import Database from "better-sqlite3";
4
4
  //#region src/builtins/sqlite-storage/sqlite-settings-backend.ts
@@ -504,10 +504,18 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
504
504
  if (existingCols.length > 0) {
505
505
  const existingNames = new Set(existingCols.map((c) => c.name));
506
506
  const declaredNames = new Set(schema.columns.map((c) => c.name));
507
- const missingDeclared = schema.columns.some((c) => !existingNames.has(c.name));
508
- if (existingCols.length <= 2 && existingNames.has("data") && !declaredNames.has("data") || missingDeclared) try {
507
+ const missingCols = schema.columns.filter((c) => !existingNames.has(c.name));
508
+ const isLegacyKv = existingCols.length <= 2 && existingNames.has("data") && !declaredNames.has("data");
509
+ const addableInPlace = (col) => !col.primaryKey && !col.unique && (!col.notNull || col.defaultValue !== void 0);
510
+ if (isLegacyKv || missingCols.some((c) => !addableInPlace(c))) try {
509
511
  this.getDb().exec(`DROP TABLE "${table}"`);
510
512
  } catch {}
513
+ else if (missingCols.length > 0) for (const col of missingCols) {
514
+ const parts = [`"${col.name}" ${col.type}`];
515
+ if (col.notNull) parts.push("NOT NULL");
516
+ if (col.defaultValue !== void 0) parts.push(`DEFAULT ${typeof col.defaultValue === "string" ? `'${col.defaultValue}'` : col.defaultValue === null ? "NULL" : String(col.defaultValue)}`);
517
+ this.getDb().exec(`ALTER TABLE "${table}" ADD COLUMN ${parts.join(" ")}`);
518
+ }
511
519
  }
512
520
  this.getDb().exec(`CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`);
513
521
  if (schema.indexes) for (const idx of schema.indexes) {
@@ -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-BduBhv6N.js");
6
+ const require_dist = require("../../dist-B5C4dVvO.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");
@@ -1,4 +1,4 @@
1
- import { Ct as parseJsonObject, Z as storageCapability, it as BaseAddon, u as StorageLocationTypeSchema } from "../../dist-B_PSvr-m.mjs";
1
+ import { Ct as parseJsonObject, Z as storageCapability, it as BaseAddon, u as StorageLocationTypeSchema } from "../../dist-DssImRC5.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";
@@ -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-BduBhv6N.js");
6
+ const require_dist = require("../../dist-B5C4dVvO.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 { it as BaseAddon, rt as errMsg, xt as hydrateSchema } from "../../dist-B_PSvr-m.mjs";
1
+ import { it as BaseAddon, rt as errMsg, xt as hydrateSchema } from "../../dist-DssImRC5.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-BduBhv6N.js");
6
+ const require_dist = require("../../dist-B5C4dVvO.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 { L as logDestinationCapability, it as BaseAddon } from "../../dist-B_PSvr-m.mjs";
1
+ import { L as logDestinationCapability, it as BaseAddon } from "../../dist-DssImRC5.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";
@@ -1,5 +1,5 @@
1
1
  let zod = require("zod");
2
- //#region ../types/dist/event-category-H4AVePnn.mjs
2
+ //#region ../types/dist/event-category-D4HJq7Mw.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -152,6 +152,11 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
152
152
  /** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
153
153
  * thumb is a scrub gap the recorder's keyframe backfill covers. */
154
154
  EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
155
+ /** Export render progress (0–100). Telemetry (D8): a lost tick is a stale
156
+ * progress bar the client reconciles via `recordingExport.getExport`. */
157
+ EventCategory["RecordingExportProgress"] = "recording.export.progress";
158
+ EventCategory["RecordingExportCompleted"] = "recording.export.completed";
159
+ EventCategory["RecordingExportFailed"] = "recording.export.failed";
155
160
  EventCategory["DetectionEvent"] = "detection.event";
156
161
  EventCategory["SessionTrackNew"] = "session.track.new";
157
162
  EventCategory["SessionTrackExpired"] = "session.track.expired";
@@ -441,6 +446,25 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
441
446
  */
442
447
  EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
443
448
  /**
449
+ * Fired by `addon-post-analysis` when a package-drop is confirmed inside
450
+ * a package zone — a newly-appeared stationary object of a package class
451
+ * that cleared the class / zone / dwell / size gates. Payload:
452
+ * `PipelineAnalyticsPackageDeliveredPayload` carrying `{ deviceId,
453
+ * entryId, className, zoneIds, keyFrameMediaKey?, bbox, timestamp }`.
454
+ * Telemetry (D8): the durable record is the `package-events` store row;
455
+ * this bus topic drives notifier rules + live UI. See
456
+ * docs/superpowers/specs/2026-07-17-package-zones-design.md §5.1.
457
+ */
458
+ EventCategory["PipelineAnalyticsPackageDelivered"] = "pipeline-analytics.package-delivered";
459
+ /**
460
+ * Fired by `addon-post-analysis` when a previously-delivered package
461
+ * leaves its zone (the stationary entry departed — moved or swept).
462
+ * Payload: `PipelineAnalyticsPackagePickedUpPayload` carrying
463
+ * `{ deviceId, entryId, deliveredEventId, className, timestamp }`.
464
+ * Telemetry (D8). See package-zones-design §5.2.
465
+ */
466
+ EventCategory["PipelineAnalyticsPackagePickedUp"] = "pipeline-analytics.package-picked-up";
467
+ /**
444
468
  * Fired by `addon-post-analysis` whenever a gallery face row changes:
445
469
  * `kind:'buffered'` a new detected face was persisted, `'assigned'` /
446
470
  * `'unassigned'` its identity link changed, `'deleted'` the row was
@@ -1802,6 +1826,19 @@ function scopeKey(scope) {
1802
1826
  case "device": return `device:${scope.deviceId}`;
1803
1827
  }
1804
1828
  }
1829
+ /**
1830
+ * Producer transition order within ONE generation: `starting → ready →
1831
+ * down`. Used by `hydrate` to accept only forward moves for
1832
+ * same-generation snapshot records (a snapshot can be ahead of a lost
1833
+ * delta, never behind it, within one producer generation).
1834
+ */
1835
+ function stateRank(state) {
1836
+ switch (state) {
1837
+ case "starting": return 0;
1838
+ case "ready": return 1;
1839
+ case "down": return 2;
1840
+ }
1841
+ }
1805
1842
  function scopesEqual(a, b) {
1806
1843
  if (a.type !== b.type) return false;
1807
1844
  if (a.type === "global" || b.type === "global") return true;
@@ -1852,43 +1889,70 @@ var ReadinessRegistry = class {
1852
1889
  return Array.from(this.snapshot.values());
1853
1890
  }
1854
1891
  /**
1855
- * Hydrate the snapshot from an authoritative source. Entries already
1856
- * present locally are skipped live deltas (received via the event
1857
- * bus subscription) always take precedence over the snapshot. For
1858
- * each newly hydrated entry, a one-shot transition is dispatched to
1859
- * matching subscriptions so pending `awaitReady` callers unblock
1860
- * without having to wait for a fresh event.
1892
+ * Hydrate the snapshot from an authoritative source a RECONCILE, not
1893
+ * an add-only merge (D8: readiness events are telemetry and may be
1894
+ * lost; the on-reconnect/periodic snapshot re-pull is the repair path,
1895
+ * so it MUST be able to advance a stale record the 2026-07-17
1896
+ * "still awaiting platform-probe forever" wedge was this method
1897
+ * skipping every already-seen key).
1898
+ *
1899
+ * - **Unseen keys** are added (epoch 1 — mirrors the value the
1900
+ * consumer would have derived from the first observed `ready`).
1901
+ * - **Existing keys** are UPDATED when the authoritative record
1902
+ * carries a different generation (producer restart / synthetic-down
1903
+ * superseded — epoch bumps on a new-generation `ready`, mirroring
1904
+ * `ingest`), or a FORWARD state move within the same generation
1905
+ * (`starting → ready → down`; within one producer generation a
1906
+ * snapshot can only be ahead of a lost delta, never behind it).
1907
+ * - **Never a same-generation regress** — a snapshot pulled just
1908
+ * before a live delta landed must not un-ready a record (the next
1909
+ * reconcile round converges it anyway).
1910
+ * - **Never a record this process is authoritative for** — node-scoped
1911
+ * records for our own nodeId, and device/global records we emitted,
1912
+ * keep local truth (an authority's stale copy of OUR record must
1913
+ * not regress us).
1861
1914
  *
1862
- * Local `epoch` is reset to 1 per entry — consumer-side epoch is
1863
- * derived from observed generation transitions, so this mirrors the
1864
- * value that would have been assigned had the consumer observed the
1865
- * first `ready` event directly.
1915
+ * Every applied entry dispatches a one-shot transition to matching
1916
+ * subscriptions so pending `awaitReady` callers unblock without having
1917
+ * to wait for a fresh event.
1866
1918
  */
1867
1919
  hydrate(records) {
1868
1920
  const now = this.now();
1869
1921
  for (const record of records) {
1870
1922
  const key = readinessKey(record.capName, record.scope);
1871
- if (this.snapshot.has(key)) continue;
1923
+ const prev = this.snapshot.get(key) ?? null;
1924
+ let epoch;
1925
+ let durationInPrevState;
1926
+ if (prev !== null) {
1927
+ if (this.isLocallyAuthoritative(prev)) continue;
1928
+ const sameGeneration = prev.generation === record.generation;
1929
+ if (sameGeneration && stateRank(record.state) <= stateRank(prev.state)) continue;
1930
+ epoch = !sameGeneration && record.state === "ready" ? prev.epoch + 1 : prev.epoch;
1931
+ durationInPrevState = Math.max(0, now - prev.lastChange);
1932
+ } else {
1933
+ epoch = 1;
1934
+ durationInPrevState = 0;
1935
+ }
1872
1936
  const hydrated = {
1873
1937
  capName: record.capName,
1874
1938
  scope: record.scope,
1875
1939
  state: record.state,
1876
1940
  generation: record.generation,
1877
- epoch: 1,
1941
+ epoch,
1878
1942
  lastChange: now,
1879
1943
  sourceNodeId: record.sourceNodeId
1880
1944
  };
1881
1945
  this.snapshot.set(key, hydrated);
1882
- if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → ${record.state} (hydrated, gen=${record.generation.slice(0, 6)})`);
1946
+ if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → ${record.state} (hydrated${prev !== null ? " update" : ""}, gen=${record.generation.slice(0, 6)})`);
1883
1947
  const transition = {
1884
1948
  capName: record.capName,
1885
1949
  scope: record.scope,
1886
1950
  state: record.state,
1887
- epoch: 1,
1951
+ epoch,
1888
1952
  generation: record.generation,
1889
1953
  sourceNodeId: "hydrated",
1890
1954
  ts: now,
1891
- durationInPrevState: 0
1955
+ durationInPrevState
1892
1956
  };
1893
1957
  for (const sub of this.subscriptions) {
1894
1958
  if (sub.capName !== record.capName) continue;
@@ -1901,6 +1965,17 @@ var ReadinessRegistry = class {
1901
1965
  }
1902
1966
  }
1903
1967
  }
1968
+ /**
1969
+ * True when THIS process is the origin authority for `record` — a
1970
+ * snapshot pulled from elsewhere must never overwrite it:
1971
+ * - node-scoped records whose `scope.nodeId` is our own node id
1972
+ * (this process — or a child bridged onto our bus — emits them);
1973
+ * - device/global-scoped records whose latest transition WE emitted.
1974
+ */
1975
+ isLocallyAuthoritative(record) {
1976
+ if (record.scope.type === "node") return record.scope.nodeId === this.sourceNodeId;
1977
+ return record.sourceNodeId === this.sourceNodeId;
1978
+ }
1904
1979
  /** Shallow copy of the full snapshot — mainly for diagnostics/tests. */
1905
1980
  getAll() {
1906
1981
  return new Map(this.snapshot);
@@ -3204,6 +3279,24 @@ var RecordingRetentionSchema = zod.z.object({
3204
3279
  maxSizeGb: zod.z.number().min(0).optional()
3205
3280
  });
3206
3281
  /**
3282
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
3283
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
3284
+ * previews at. Five graduated steps; absent on a config = `standard` (the
3285
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
3286
+ *
3287
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
3288
+ * Each window's index sidecar carries its own tile dims, so a camera whose
3289
+ * preset changed over time renders every historical window at the dims it was
3290
+ * written with.
3291
+ */
3292
+ var ScrubThumbnailPresetSchema = zod.z.enum([
3293
+ "minimal",
3294
+ "low",
3295
+ "standard",
3296
+ "high",
3297
+ "max"
3298
+ ]);
3299
+ /**
3207
3300
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
3208
3301
  *
3209
3302
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -3240,7 +3333,14 @@ var RecordingConfigSchema = zod.z.object({
3240
3333
  * derived into bands once via `migrateConfigToBands`.
3241
3334
  */
3242
3335
  bands: zod.z.array(RecordingBandSchema).optional(),
3243
- retention: RecordingRetentionSchema.optional()
3336
+ retention: RecordingRetentionSchema.optional(),
3337
+ /**
3338
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
3339
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
3340
+ * windows only — existing sheets are immutable, and each window's index
3341
+ * carries its own tile dims so mixed-preset history renders correctly.
3342
+ */
3343
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
3244
3344
  });
3245
3345
  /**
3246
3346
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -11080,7 +11180,11 @@ zod.z.array(ZoneRuleSchema).readonly();
11080
11180
  * Extend the enum here when a new gating consumer comes online (audio
11081
11181
  * gating, alert filtering, …) — no other surface needs to change.
11082
11182
  */
11083
- var ZoneRuleStageEnum = zod.z.enum(["motion", "detection"]);
11183
+ var ZoneRuleStageEnum = zod.z.enum([
11184
+ "motion",
11185
+ "detection",
11186
+ "package"
11187
+ ]);
11084
11188
  /**
11085
11189
  * Zone rules capability — per-camera CRUD over the {@link ZoneRule}
11086
11190
  * arrays that decide how each pipeline stage uses the polygon zones.
@@ -11124,16 +11228,24 @@ var zoneRulesCapability = {
11124
11228
  })
11125
11229
  },
11126
11230
  /**
11127
- * Runtime-state slice — both stages mirrored together so consumers
11231
+ * Runtime-state slice — every stage mirrored together so consumers
11128
11232
  * see one reactive handle (`device.state.zoneRules.value`) instead
11129
- * of two. Bulk-replace mutations on either stage write the full
11130
- * `{motion, detection}` shape, so subscribers always get the
11233
+ * of one per stage. Bulk-replace mutations on any stage write the full
11234
+ * `{motion, detection, package}` shape, so subscribers always get the
11131
11235
  * complete current set. Consumers that only care about one stage
11132
11236
  * just read the matching property.
11237
+ *
11238
+ * `package` backs the package-drop detector — a package zone is a
11239
+ * `ZoneRule` on the `'package'` stage referencing drawn polygons
11240
+ * (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
11241
+ * The orchestrator provider that writes this stage lands in a later
11242
+ * slice; until then the mirror carries only `{motion, detection}` and
11243
+ * consumers read `package` as absent (treat as `[]`).
11133
11244
  */
11134
11245
  runtimeState: zod.z.object({
11135
11246
  motion: zod.z.array(ZoneRuleSchema).readonly(),
11136
- detection: zod.z.array(ZoneRuleSchema).readonly()
11247
+ detection: zod.z.array(ZoneRuleSchema).readonly(),
11248
+ package: zod.z.array(ZoneRuleSchema).readonly()
11137
11249
  })
11138
11250
  };
11139
11251
  var ProviderStatusSchema = zod.z.object({
@@ -15699,6 +15811,7 @@ var EventKindIconSchema = zod.z.enum([
15699
15811
  "smoke",
15700
15812
  "water",
15701
15813
  "button",
15814
+ "package",
15702
15815
  "generic"
15703
15816
  ]);
15704
15817
  var EventKindCategorySchema = zod.z.enum([
@@ -15706,7 +15819,8 @@ var EventKindCategorySchema = zod.z.enum([
15706
15819
  "audio",
15707
15820
  "detection",
15708
15821
  "sensor",
15709
- "custom"
15822
+ "custom",
15823
+ "package"
15710
15824
  ]);
15711
15825
  var EventKindDescriptorSchema = zod.z.object({
15712
15826
  /** Stable kind id (e.g. 'motion', 'person', 'contact'). */
@@ -20843,6 +20957,125 @@ var recordingCapability = {
20843
20957
  }
20844
20958
  };
20845
20959
  /**
20960
+ * `recordingExport` cap — render a footage time range into a single downloadable
20961
+ * MP4 (regular / accelerated / decelerated / timelapse, ± audio), kept for a
20962
+ * bounded lifetime with a durable history, auto-expiry, and optional
20963
+ * delete-after-download.
20964
+ *
20965
+ * Like `recording` this is a `scope:'system', mode:'singleton'` cap: the
20966
+ * recorder self-gates so exactly ONE node (the designated `recordingNodeId`,
20967
+ * default `hub`) registers it. Device-scoped methods carry `deviceId` in their
20968
+ * input and dispatch to that single provider; the render runs on the node that
20969
+ * owns the footage (no cross-node segment transfer). Download rides the
20970
+ * framework addon data-plane. State persists in a DurableState blob (NOT
20971
+ * SQLite); history rows survive file deletion for audit.
20972
+ */
20973
+ /** Playback-speed multiplier for the render (1 = realtime). */
20974
+ var ExportSpeedSchema = zod.z.number().min(.25).max(32);
20975
+ /** Timelapse cadence — sample one source frame per `everyMs`, output at `outputFps`. */
20976
+ var ExportTimelapseSchema = zod.z.object({
20977
+ everyMs: zod.z.number().int().positive(),
20978
+ outputFps: zod.z.number().int().min(1).max(60).optional()
20979
+ });
20980
+ /**
20981
+ * Render options. `speed` and `timelapse` are mutually exclusive. `includeAudio`
20982
+ * is honoured only for a realtime-ish speed (0.5–2×); timelapse is always
20983
+ * silent. `maxLifeMs` bounds how long the finished file is kept;
20984
+ * `deleteAfterDownload` removes it shortly after the first complete download.
20985
+ */
20986
+ var ExportOptionsSchema = zod.z.object({
20987
+ speed: ExportSpeedSchema.optional(),
20988
+ timelapse: ExportTimelapseSchema.optional(),
20989
+ includeAudio: zod.z.boolean(),
20990
+ maxLifeMs: zod.z.number().int().positive(),
20991
+ deleteAfterDownload: zod.z.boolean(),
20992
+ title: zod.z.string().max(200).optional()
20993
+ }).superRefine((v, ctx) => {
20994
+ if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
20995
+ code: zod.z.ZodIssueCode.custom,
20996
+ message: "speed and timelapse are mutually exclusive",
20997
+ path: ["timelapse"]
20998
+ });
20999
+ });
21000
+ var ExportStateSchema = zod.z.enum([
21001
+ "queued",
21002
+ "rendering",
21003
+ "ready",
21004
+ "failed",
21005
+ "expired",
21006
+ "deleted"
21007
+ ]);
21008
+ /** One export job / history row. */
21009
+ var ExportRecordSchema = zod.z.object({
21010
+ id: zod.z.string(),
21011
+ deviceId: zod.z.number(),
21012
+ profile: zod.z.string(),
21013
+ fromMs: zod.z.number(),
21014
+ toMs: zod.z.number(),
21015
+ options: ExportOptionsSchema,
21016
+ state: ExportStateSchema,
21017
+ /** 0–100 while rendering; null otherwise. */
21018
+ progressPct: zod.z.number().nullable(),
21019
+ /** File size once ready; null before. */
21020
+ fileBytes: zod.z.number().nullable(),
21021
+ expiresAt: zod.z.number(),
21022
+ deleteAfterDownload: zod.z.boolean(),
21023
+ /** Epoch of the first complete download; null until then. */
21024
+ downloadedAt: zod.z.number().nullable(),
21025
+ createdAt: zod.z.number(),
21026
+ /** User id/name that requested the export. */
21027
+ createdBy: zod.z.string(),
21028
+ /** Failure reason when state is 'failed'; null otherwise. */
21029
+ error: zod.z.string().nullable()
21030
+ });
21031
+ /** Candidate download URLs (LAN first, then operator extra hosts). */
21032
+ var ExportDownloadSchema = zod.z.object({
21033
+ url: zod.z.string(),
21034
+ endpoints: zod.z.array(zod.z.string())
21035
+ });
21036
+ var recordingExportCapability = {
21037
+ name: "recordingExport",
21038
+ scope: "system",
21039
+ mode: "singleton",
21040
+ methods: {
21041
+ /** Queue a render of `[fromMs,toMs)` for `deviceId`/`profile`. Fails fast
21042
+ * when no footage covers the range. Returns the queued record. */
21043
+ createExport: method(zod.z.object({
21044
+ deviceId: zod.z.number(),
21045
+ profile: zod.z.string(),
21046
+ fromMs: zod.z.number(),
21047
+ toMs: zod.z.number(),
21048
+ options: ExportOptionsSchema
21049
+ }), ExportRecordSchema, {
21050
+ kind: "mutation",
21051
+ auth: "protected"
21052
+ }),
21053
+ /** All export rows (history included), optionally scoped to one device. */
21054
+ listExports: method(zod.z.object({ deviceId: zod.z.number().optional() }), zod.z.array(ExportRecordSchema), {
21055
+ kind: "query",
21056
+ auth: "protected"
21057
+ }),
21058
+ getExport: method(zod.z.object({ exportId: zod.z.string() }), ExportRecordSchema, {
21059
+ kind: "query",
21060
+ auth: "protected"
21061
+ }),
21062
+ /** Abort a queued/rendering export; the partial file is removed. */
21063
+ cancelExport: method(zod.z.object({ exportId: zod.z.string() }), ExportRecordSchema, {
21064
+ kind: "mutation",
21065
+ auth: "protected"
21066
+ }),
21067
+ /** Remove the file but keep the history row (state → 'deleted'). */
21068
+ deleteExport: method(zod.z.object({ exportId: zod.z.string() }), ExportRecordSchema, {
21069
+ kind: "mutation",
21070
+ auth: "protected"
21071
+ }),
21072
+ getDownloadUrl: method(zod.z.object({ exportId: zod.z.string() }), ExportDownloadSchema, {
21073
+ kind: "query",
21074
+ auth: "protected"
21075
+ })
21076
+ }
21077
+ };
21078
+ /**
20846
21079
  * One publishable camera stream as its OWNING PROVIDER describes it — the same
20847
21080
  * payload the (legacy) push `streamBroker.publishCameraStream` carried, minus
20848
21081
  * `deviceId`.
@@ -21604,6 +21837,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
21604
21837
  ptzAutotrackCapability,
21605
21838
  rebootCapability,
21606
21839
  recordingCapability,
21840
+ recordingExportCapability,
21607
21841
  sceneMonitorCapability,
21608
21842
  scriptRunnerCapability,
21609
21843
  serverManagementCapability,
@@ -25327,6 +25561,42 @@ var METHOD_ACCESS_MAP = Object.freeze({
25327
25561
  addonId: null,
25328
25562
  access: "create"
25329
25563
  },
25564
+ "recordingExport.cancelExport": {
25565
+ capName: "recordingExport",
25566
+ capScope: "system",
25567
+ addonId: null,
25568
+ access: "create"
25569
+ },
25570
+ "recordingExport.createExport": {
25571
+ capName: "recordingExport",
25572
+ capScope: "system",
25573
+ addonId: null,
25574
+ access: "create"
25575
+ },
25576
+ "recordingExport.deleteExport": {
25577
+ capName: "recordingExport",
25578
+ capScope: "system",
25579
+ addonId: null,
25580
+ access: "delete"
25581
+ },
25582
+ "recordingExport.getDownloadUrl": {
25583
+ capName: "recordingExport",
25584
+ capScope: "system",
25585
+ addonId: null,
25586
+ access: "view"
25587
+ },
25588
+ "recordingExport.getExport": {
25589
+ capName: "recordingExport",
25590
+ capScope: "system",
25591
+ addonId: null,
25592
+ access: "view"
25593
+ },
25594
+ "recordingExport.listExports": {
25595
+ capName: "recordingExport",
25596
+ capScope: "system",
25597
+ addonId: null,
25598
+ access: "view"
25599
+ },
25330
25600
  "sceneMonitor.captureReference": {
25331
25601
  capName: "scene-monitor",
25332
25602
  capScope: "device",