camstack 1.2.20 → 1.2.22

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.
@@ -14522,7 +14522,7 @@ function date4(params) {
14522
14522
  // ../../node_modules/zod/v4/classic/external.js
14523
14523
  config(en_default());
14524
14524
 
14525
- // ../types/dist/sleep-CeuvKr4N.mjs
14525
+ // ../types/dist/sleep-BxjBLp3L.mjs
14526
14526
  var WELL_KNOWN_TABS = [
14527
14527
  {
14528
14528
  id: "overview",
@@ -15626,6 +15626,7 @@ var RecordingConfigSchema = external_exports.object({
15626
15626
  scrubThumbnails: ScrubThumbnailPresetSchema.optional()
15627
15627
  }).strict();
15628
15628
  var RelocateJobStateSchema = external_exports.enum([
15629
+ "queued",
15629
15630
  "running",
15630
15631
  "done",
15631
15632
  "failed",
@@ -15650,20 +15651,96 @@ var RelocateJobSchema = external_exports.object({
15650
15651
  finishedAt: external_exports.number().nullable(),
15651
15652
  error: external_exports.string().nullable()
15652
15653
  });
15654
+ var RelocateFootageClassSchema = external_exports.enum(["recordings", "recordingsLow"]);
15653
15655
  var RelocateFootageInputSchema = external_exports.object({
15654
- deviceId: external_exports.number().optional(),
15655
15656
  fromLocationId: external_exports.string(),
15656
15657
  toLocationId: external_exports.string(),
15657
15658
  entities: external_exports.array(external_exports.enum(["segments"])).optional(),
15659
+ /** Limits relocation to the logical profile class. Omit only for the
15660
+ * pre-orchestration compatibility path. */
15661
+ footageClass: RelocateFootageClassSchema.optional(),
15662
+ /** Scope the move to ONE camera. Absent = every camera on the source, which
15663
+ * is what a whole-disk drain means. The rebalance path always sets it: its
15664
+ * unit is a (camera, profile) pile, not a disk. */
15665
+ deviceId: external_exports.number().int().optional(),
15666
+ /** Scope the move to specific segment profiles (`high` / `mid` / `low`).
15667
+ * Finer than `footageClass`, which cannot separate high from mid — and the
15668
+ * placement plan assigns those two independently, so a rebalance that could
15669
+ * only say "recordings" would move footage the plan never asked to move. */
15670
+ profiles: external_exports.array(external_exports.string()).optional(),
15658
15671
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
15659
15672
  * never allowed to starve live writers. */
15660
15673
  throttleMbps: external_exports.number().min(1).max(1e3).optional()
15661
15674
  });
15675
+ var StorageMigrationLeaseInputSchema = external_exports.object({ leaseId: external_exports.string().min(1) });
15676
+ var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: external_exports.string().min(1) });
15662
15677
  var RelocateMediaInputSchema = external_exports.object({
15663
- deviceId: external_exports.number().optional(),
15664
15678
  toLocationId: external_exports.string(),
15665
15679
  throttleMbps: external_exports.number().min(1).max(1e3).optional()
15666
15680
  });
15681
+ var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: external_exports.string().min(1) });
15682
+ var StorageMigrationClassSchema = external_exports.enum([
15683
+ "recordings",
15684
+ "recordingsLow",
15685
+ "eventMedia"
15686
+ ]);
15687
+ var StorageMigrationDestinationsSchema = external_exports.object({
15688
+ recordings: external_exports.string().min(1).optional(),
15689
+ recordingsLow: external_exports.string().min(1).optional(),
15690
+ eventMedia: external_exports.string().min(1).optional()
15691
+ }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
15692
+ var StorageMigrationInputSchema = external_exports.object({
15693
+ destinations: StorageMigrationDestinationsSchema,
15694
+ throttleMbps: external_exports.number().min(1).max(1e3).optional()
15695
+ });
15696
+ var StorageMigrationPhaseSchema = external_exports.enum([
15697
+ "planning",
15698
+ "pausing",
15699
+ "moving",
15700
+ "verifying",
15701
+ "repointing",
15702
+ "refreshing",
15703
+ "resuming",
15704
+ "done",
15705
+ "failed",
15706
+ "cancelled"
15707
+ ]);
15708
+ var StorageMigrationParticipantSchema = external_exports.enum([
15709
+ "pipeline",
15710
+ "recorder",
15711
+ "analytics"
15712
+ ]);
15713
+ var StorageMigrationMoveSchema = external_exports.object({
15714
+ storageClass: StorageMigrationClassSchema,
15715
+ fromLocationId: external_exports.string(),
15716
+ toLocationId: external_exports.string(),
15717
+ moverJobId: external_exports.string().nullable(),
15718
+ state: RelocateJobStateSchema.nullable(),
15719
+ error: external_exports.string().nullable()
15720
+ });
15721
+ var StorageMigrationJobSchema = external_exports.object({
15722
+ jobId: external_exports.string(),
15723
+ phase: StorageMigrationPhaseSchema,
15724
+ destinations: StorageMigrationDestinationsSchema,
15725
+ throttleMbps: external_exports.number(),
15726
+ moves: external_exports.array(StorageMigrationMoveSchema),
15727
+ pauseLeaseId: external_exports.string().nullable(),
15728
+ pausedParticipants: external_exports.array(StorageMigrationParticipantSchema),
15729
+ repointed: external_exports.boolean(),
15730
+ cancelRequested: external_exports.boolean(),
15731
+ startedAt: external_exports.number(),
15732
+ updatedAt: external_exports.number(),
15733
+ finishedAt: external_exports.number().nullable(),
15734
+ error: external_exports.string().nullable()
15735
+ });
15736
+ var StorageMigrationPlanSchema = external_exports.object({
15737
+ destinations: StorageMigrationDestinationsSchema,
15738
+ moves: external_exports.array(external_exports.object({
15739
+ storageClass: StorageMigrationClassSchema,
15740
+ fromLocationId: external_exports.string(),
15741
+ toLocationId: external_exports.string()
15742
+ }))
15743
+ });
15667
15744
  var StorageLocationTypeSchema = external_exports.string().regex(/^[a-z][a-zA-Z0-9-]*$/);
15668
15745
  var StorageLocationSchema = external_exports.object({
15669
15746
  id: external_exports.string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/),
@@ -15681,6 +15758,21 @@ var StorageLocationSchema = external_exports.object({
15681
15758
  nodeId: external_exports.string().optional(),
15682
15759
  isDefault: external_exports.boolean().default(false),
15683
15760
  isSystem: external_exports.boolean().default(false),
15761
+ /**
15762
+ * Operator opt-in: whether consumers that BALANCE across several locations
15763
+ * of a type may write here. Recordings reads it today; event media and
15764
+ * backups are the next consumers, which is why the flag lives on the
15765
+ * location rather than in any one addon's store — nothing has to be
15766
+ * extended to add the next consumer.
15767
+ *
15768
+ * OPTIONAL, and ABSENT MEANS ACTIVE. Every location persisted before the
15769
+ * flag existed reads back with no flag and keeps working exactly as before;
15770
+ * that is the whole compat story, and it is why no migration ships with it.
15771
+ * A newly CREATED sibling is stamped `false` by the orchestrator (creating a
15772
+ * disk must not silently start writing to it); the default of a type is
15773
+ * always stamped `true`.
15774
+ */
15775
+ enabled: external_exports.boolean().optional(),
15684
15776
  /** COMPUTED at read time by the orchestrator (statfs of the backing volume
15685
15777
  * for node-local locations it can reach) — never persisted, absent when the
15686
15778
  * volume is remote/unreachable. The single capacity truth every UI reads. */
@@ -20575,9 +20667,9 @@ var filesystemBrowseCapability = {
20575
20667
  mode: "singleton",
20576
20668
  internal: true,
20577
20669
  methods: {
20578
- /** The allowed roots browsing is sandboxed to on this node. */
20670
+ /** The roots `createDir` is sandboxed to on this node. Browsing is not confined to them. */
20579
20671
  listAllowedRoots: method(external_exports.void(), external_exports.array(external_exports.string()).readonly(), { auth: "admin" }),
20580
- /** Immediate subdirectories of `path` (must be within an allowed root) + free/total bytes. */
20672
+ /** Immediate subdirectories of any absolute `path` + free/total bytes. Read-only. */
20581
20673
  browse: method(external_exports.object({ path: external_exports.string() }), BrowseResultSchema, { auth: "admin" }),
20582
20674
  /** Create a subdirectory (within an allowed root). Returns its absolute path. */
20583
20675
  createDir: method(external_exports.object({ path: external_exports.string() }), external_exports.object({ path: external_exports.string() }), {
@@ -22292,18 +22384,46 @@ var NcDeliverySchema = external_exports.enum([
22292
22384
  "system-event"
22293
22385
  ]);
22294
22386
  var NcSystemEventKindSchema = external_exports.enum([
22295
- "camera-online",
22296
- "camera-offline",
22387
+ "device-online",
22388
+ "device-offline",
22389
+ "device-disabled",
22390
+ "device-enabled",
22297
22391
  "stream-online",
22298
22392
  "stream-offline",
22299
22393
  "node-online",
22300
22394
  "node-offline",
22301
22395
  "addon-update-available",
22302
- "server-update-available"
22396
+ "server-update-available",
22397
+ "alarm-triggered",
22398
+ "alarm-armed",
22399
+ "alarm-disarmed",
22400
+ "camera-online",
22401
+ "camera-offline",
22402
+ "camera-disabled",
22403
+ "camera-enabled"
22404
+ ]);
22405
+ var NC_LEGACY_SYSTEM_EVENT_KINDS = /* @__PURE__ */ new Set([
22406
+ "camera-online",
22407
+ "camera-offline",
22408
+ "camera-disabled",
22409
+ "camera-enabled"
22303
22410
  ]);
22411
+ var NC_AUTHORABLE_SYSTEM_EVENT_KINDS = NcSystemEventKindSchema.options.filter((kind) => !NC_LEGACY_SYSTEM_EVENT_KINDS.has(kind));
22304
22412
  var NcSystemEventConditionSchema = external_exports.object({
22305
22413
  kinds: external_exports.array(NcSystemEventKindSchema).min(1),
22306
22414
  deviceIds: external_exports.array(external_exports.number().int()).min(1).optional(),
22415
+ /**
22416
+ * Narrow a `device-*` kind to these device TYPES (`DeviceType` values —
22417
+ * `camera`, `switch`, `sensor`, `container`, …). Absent = every type, which
22418
+ * is what a liveness rule means when nobody said otherwise.
22419
+ *
22420
+ * This is where "only my cameras" is expressed, and it lives on the rule for
22421
+ * one reason: the intake cannot know which devices this household cares
22422
+ * about, and a producer-side filter is one no operator can change. Fails
22423
+ * CLOSED — a subject whose device type is unknown (an id the device mirror
22424
+ * does not carry) matches no `deviceTypes` list.
22425
+ */
22426
+ deviceTypes: external_exports.array(external_exports.string().min(1)).min(1).optional(),
22307
22427
  nodeIds: external_exports.array(external_exports.string().min(1)).min(1).optional(),
22308
22428
  packageNames: external_exports.array(external_exports.string().min(1)).min(1).optional()
22309
22429
  });
@@ -22339,6 +22459,16 @@ var NcOccupancyConditionSchema = external_exports.object({
22339
22459
  count: external_exports.number().int().min(0).default(1),
22340
22460
  sustainSeconds: external_exports.number().int().min(0).max(3600).default(15)
22341
22461
  });
22462
+ var NcAudioConditionSchema = external_exports.object({
22463
+ /** Audio macro labels; absent = any sound (level-only rule). */
22464
+ labels: external_exports.array(external_exports.string().min(1)).min(1).optional(),
22465
+ /** Level floor in dBFS (negative-going, `0` = full scale); absent = any level. */
22466
+ dbThreshold: external_exports.number().min(-96).max(0).optional(),
22467
+ /** Percentage of the window's samples that must be hits (1–100). */
22468
+ hitPercent: external_exports.number().int().min(1).max(100).default(60),
22469
+ /** Length of the sampling window in seconds. */
22470
+ samplingSeconds: external_exports.number().int().min(1).max(300).default(10)
22471
+ });
22342
22472
  var NcCrossingSchema = external_exports.enum([
22343
22473
  "enter",
22344
22474
  "exit",
@@ -22537,7 +22667,33 @@ var NcConditionsSchema = external_exports.object({
22537
22667
  * threshold and holds for `sustainSeconds`. Fail-closed on missing
22538
22668
  * substrate (no snapshot / missing zone). See {@link NcOccupancyCondition}.
22539
22669
  */
22540
- occupancy: NcOccupancyConditionSchema.optional()
22670
+ occupancy: NcOccupancyConditionSchema.optional(),
22671
+ /**
22672
+ * IMMEDIATE only. Sustained-sound matcher — `hitPercent` of the samples in a
22673
+ * `samplingSeconds` window clear the optional `dbThreshold` and carry one of
22674
+ * the optional `labels`. Fail-closed on missing substrate (no audio samples,
22675
+ * a window that is not full yet, neither filter given). See
22676
+ * {@link NcAudioCondition}.
22677
+ *
22678
+ * Presence of this key is what makes a rule an AUDIO rule: the engine fires
22679
+ * it ONLY on a confirmed audio window, and a rule carrying it never fires on
22680
+ * a detection, a track or a device event (the same fail-closed pairing
22681
+ * `occupancy` has with the `device-event` trigger). That is how audio labels
22682
+ * leave `classes`: an audio rule names its sounds HERE, and the legacy path
22683
+ * (an `immediate` rule naming an `audio-*` class, one notification per
22684
+ * classified sample) stays exactly as it was for rules that already use it.
22685
+ *
22686
+ * NOT in {@link NC_CONDITION_CATALOG} yet, and that is the sequencing rule
22687
+ * rather than an oversight: the viewer mirrors the descriptor enums BY HAND
22688
+ * (`camstack/src/data/notification-center.ts`, guarded by
22689
+ * `scripts/check-viewer-condition-mirror.ts`) and its rule editor STRIPS the
22690
+ * condition fields it does not know when a rule is saved from the phone.
22691
+ * Publishing an editor for a condition the app cannot round-trip is how an
22692
+ * operator loses a rule's conditions by opening it — so the descriptor, the
22693
+ * admin widget and the viewer mirror land together (P2 + P3), and only then
22694
+ * does an audio rule become authorable.
22695
+ */
22696
+ audio: NcAudioConditionSchema.optional()
22541
22697
  });
22542
22698
  var NcRuleTargetSchema = external_exports.object({
22543
22699
  /** `notification-output` Target id. */
@@ -22614,6 +22770,44 @@ var NcThrottleSchema = external_exports.object({
22614
22770
  */
22615
22771
  granularity: NcThrottleGranularitySchema.optional()
22616
22772
  });
22773
+ var NC_CONFIRM_MIN_TIMEOUT_MS = 1e3;
22774
+ var NC_CONFIRM_MAX_TIMEOUT_MS = 2e4;
22775
+ var NC_CONFIRM_DEFAULT_TIMEOUT_MS = 8e3;
22776
+ var NcConfirmExpectSchema = external_exports.object({
22777
+ op: external_exports.enum([
22778
+ ">=",
22779
+ ">",
22780
+ "<=",
22781
+ "<",
22782
+ "=="
22783
+ ]),
22784
+ count: external_exports.number().int().min(0).max(1e3)
22785
+ });
22786
+ var NcConfirmSchema = external_exports.object({
22787
+ /** Off unless asked for. An absent `confirm` and `enabled:false` are the
22788
+ * same thing, and both mean "deliver exactly as before". */
22789
+ enabled: external_exports.boolean().default(false),
22790
+ /** Explicit vision profile; absent = the `purpose:'vision'` cluster default. */
22791
+ profileId: external_exports.string().optional(),
22792
+ /**
22793
+ * The operator's question, in his own words. Absent = a question derived
22794
+ * from the rule (its class and its expectation).
22795
+ *
22796
+ * NEVER composed with text READ OUT OF THE FRAME. The model reads OSD
22797
+ * banners, signage and plates as instructions if you let them reach the
22798
+ * prompt — proven live — so the authoritative contract stays in the system
22799
+ * turn and only rule-authored words land here.
22800
+ */
22801
+ prompt: external_exports.string().max(1e3).optional(),
22802
+ /** Fire only when the model's count satisfies this. Absent = the model's
22803
+ * own boolean verdict decides. */
22804
+ expect: NcConfirmExpectSchema.optional(),
22805
+ timeoutMs: external_exports.number().int().min(NC_CONFIRM_MIN_TIMEOUT_MS).max(NC_CONFIRM_MAX_TIMEOUT_MS).default(NC_CONFIRM_DEFAULT_TIMEOUT_MS),
22806
+ /** What a timeout / unreachable model MEANS. `fire` (default) = fail-open. */
22807
+ onTimeout: external_exports.enum(["fire", "suppress"]).default("fire"),
22808
+ /** Longest edge the judged image is downscaled to before it is sent. */
22809
+ maxImagePx: external_exports.number().int().min(64).max(2048).default(448)
22810
+ });
22617
22811
  var NcRuleInputSchema = external_exports.object({
22618
22812
  name: external_exports.string().min(1).max(200),
22619
22813
  enabled: external_exports.boolean().default(true),
@@ -22673,9 +22867,45 @@ var NcRuleInputSchema = external_exports.object({
22673
22867
  * `{ cap: 'alarm-panel', method: 'arm', args: { mode: 'away' } }`, the same
22674
22868
  * shape as every other actuation.
22675
22869
  */
22676
- actions: NcRuleActionsSchema.optional()
22870
+ actions: NcRuleActionsSchema.optional(),
22871
+ /**
22872
+ * AI CONFIRM — see {@link NcConfirmSchema}. `.optional()`, never defaulted:
22873
+ * a rule that predates the gate must keep delivering byte-for-byte as it
22874
+ * did, and absent is the only way to say that without a migration.
22875
+ */
22876
+ confirm: NcConfirmSchema.optional()
22877
+ });
22878
+ var NcRulePatchSchema = NcRuleInputSchema.partial().extend({
22879
+ disabledTargetIds: external_exports.array(external_exports.string()).optional(),
22880
+ /**
22881
+ * `.partial()` DOES NOT REMOVE A FIELD'S `.default()`.
22882
+ *
22883
+ * It makes the key optional to SUPPLY; the parse still materialises the
22884
+ * default when the key is absent. And `NcRuleStore.update` merges with
22885
+ * `{ ...existing, ...patch }`, so a materialised key OVERWRITES the stored
22886
+ * one — which made every partial edit destructive:
22887
+ *
22888
+ * nc.updateRule({ throttle }) → conditions reset to `{}`
22889
+ * nc.setRuleTargetEnabled(...) → conditions reset to `{}`
22890
+ * setEnabled(ruleId, false) → conditions reset to `{}`
22891
+ *
22892
+ * A rule scoped to one camera and one zone silently became a rule that
22893
+ * matches EVERY event on EVERY camera, and lost its `media` policy
22894
+ * (zoneCrop / gif / clip / frame) and its `priority` at the same time. Seen
22895
+ * live on 2026-08-12: a rule scoped to device 617 fired on 590 and 615
22896
+ * within a minute of a two-field patch.
22897
+ *
22898
+ * So every defaulted field is re-declared here WITHOUT its default. The
22899
+ * inner defaults still apply when the caller DOES send the key — `{}` for
22900
+ * conditions remains a real instruction ("clear them") — and only the
22901
+ * absent key is now genuinely absent.
22902
+ */
22903
+ enabled: external_exports.boolean().optional(),
22904
+ conditions: NcConditionsSchema.optional(),
22905
+ media: NcMediaPolicySchema.optional(),
22906
+ throttle: NcThrottleSchema.optional(),
22907
+ priority: external_exports.number().int().min(1).max(5).optional()
22677
22908
  });
22678
- var NcRulePatchSchema = NcRuleInputSchema.partial().extend({ disabledTargetIds: external_exports.array(external_exports.string()).optional() });
22679
22909
  var NcRuleSchema = NcRuleInputSchema.extend({
22680
22910
  id: external_exports.string(),
22681
22911
  /** userId of the admin who created the rule (server-stamped caller). */
@@ -22918,6 +23148,15 @@ var NcAlarmSettingsSchema = external_exports.object({
22918
23148
  announceTargets: external_exports.array(external_exports.string().min(1)).max(16)
22919
23149
  });
22920
23150
  var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.partial();
23151
+ var NcAlarmSkipReasonSchema = external_exports.enum([
23152
+ "muted",
23153
+ "detection-off",
23154
+ "offline"
23155
+ ]);
23156
+ var NcAlarmSkippedDeviceSchema = external_exports.object({
23157
+ deviceId: external_exports.number().int(),
23158
+ reason: NcAlarmSkipReasonSchema
23159
+ });
22921
23160
  var NcAlarmModeCoverageSchema = external_exports.object({
22922
23161
  mode: AlarmArmModeSchema,
22923
23162
  /** Enabled rules gated on `armed_<mode>`. Zero means the mode does nothing. */
@@ -22925,7 +23164,18 @@ var NcAlarmModeCoverageSchema = external_exports.object({
22925
23164
  /** At least one covering rule has no device scope, so the mode covers all. */
22926
23165
  allDevices: external_exports.boolean(),
22927
23166
  /** Ids named by the covering rules. A SUBSET when `allDevices` is true. */
22928
- deviceIds: external_exports.array(external_exports.number().int())
23167
+ deviceIds: external_exports.array(external_exports.number().int()),
23168
+ /**
23169
+ * Devices this mode NAMES but cannot actually arm, each with the switch that
23170
+ * excludes it.
23171
+ *
23172
+ * "Away armed — 12 cameras" is a promise, and a muted camera among those
23173
+ * twelve makes it false in exactly the way nobody notices until an incident.
23174
+ * Defaulted to `[]` so a coverage answer computed before this field existed
23175
+ * still parses as "nothing known to be skipped" rather than failing the whole
23176
+ * alarm tab.
23177
+ */
23178
+ skippedDevices: external_exports.array(NcAlarmSkippedDeviceSchema).default([])
22929
23179
  });
22930
23180
  var NcAlarmConfigSchema = external_exports.object({
22931
23181
  /**
@@ -24185,20 +24435,28 @@ var pipelineAnalyticsCapability = {
24185
24435
  }),
24186
24436
  /** The events ops-log rows (newest-first), optionally scoped to one camera.
24187
24437
  * Backed by a declared pipeline-analytics SQLite collection. */
24188
- /**
24189
- * Move event-media blobs onto `toLocationId` (entity-routing spec Phase
24190
- * 4): per-row copy → stamp row.locationId delete old blob. Rows already
24191
- * at the target are skipped, so a re-run resumes. Single-flight.
24192
- */
24193
- relocateMedia: method(RelocateMediaInputSchema, external_exports.object({ jobId: external_exports.string() }), {
24438
+ /** Internal migration-participant lease. It drains active MediaStore
24439
+ * writes and refuses new ones without changing analytics bindings. */
24440
+ pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, external_exports.object({ paused: external_exports.literal(true) }), {
24194
24441
  kind: "mutation",
24195
24442
  auth: "admin"
24196
24443
  }),
24197
- getMediaRelocateStatus: method(external_exports.object({}), external_exports.array(RelocateJobSchema).readonly(), {
24198
- kind: "query",
24444
+ resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, external_exports.object({ resumed: external_exports.literal(true) }), {
24445
+ kind: "mutation",
24446
+ auth: "admin"
24447
+ }),
24448
+ /** Drops cached location roots after the storage coordinator repoints a
24449
+ * default so future event-media writes use the new root. */
24450
+ refreshStorageLocationsForMigration: method(StorageMigrationLeaseInputSchema, external_exports.object({ refreshed: external_exports.literal(true) }), {
24451
+ kind: "mutation",
24452
+ auth: "admin"
24453
+ }),
24454
+ startStorageMigrationMove: method(StorageMigrationMediaMoveInputSchema, external_exports.object({ jobId: external_exports.string() }), {
24455
+ kind: "mutation",
24199
24456
  auth: "admin"
24200
24457
  }),
24201
- cancelMediaRelocate: method(external_exports.object({ jobId: external_exports.string() }), external_exports.object({ cancelled: external_exports.boolean() }), {
24458
+ getStorageMigrationMoveStatus: method(external_exports.object({ jobId: external_exports.string() }), RelocateJobSchema.nullable(), { auth: "admin" }),
24459
+ cancelStorageMigrationMove: method(external_exports.object({ jobId: external_exports.string() }), external_exports.object({ cancelled: external_exports.boolean() }), {
24202
24460
  kind: "mutation",
24203
24461
  auth: "admin"
24204
24462
  }),
@@ -26035,9 +26293,16 @@ var CameraStatusSchema = external_exports.object({
26035
26293
  audio: CameraAudioStatusSchema.nullable(),
26036
26294
  recording: CameraRecordingStatusSchema.nullable(),
26037
26295
  /**
26038
- * Per-camera function switches an OPERATOR has turned off
26296
+ * Per-camera functions an OPERATOR has turned off
26039
26297
  * ([D61](../../../../docs/decisions/adr-0067.md)).
26040
26298
  *
26299
+ * Composed from the AUTHORITIES themselves — the wrapper bindings,
26300
+ * `RecordingConfig.enabled`, the notification mute, the broker's audio
26301
+ * policy, the camera's own microphone — via `composeSwitchedOff`, not from
26302
+ * the deprecated `getCameraSwitches` group ([D113](../../../../docs/decisions/adr-0113.md)).
26303
+ * The badge outlives the control panel: the panel was a convenience, this is
26304
+ * the difference between a camera being off and a camera being dead.
26305
+ *
26041
26306
  * This is the difference between DISABLED and BROKEN. A camera whose
26042
26307
  * `detection` block reports zero fps and whose `switchedOff` contains
26043
26308
  * `'object-detection'` was switched off by a person; the same camera with an
@@ -26115,6 +26380,16 @@ var pipelineOrchestratorCapability = {
26115
26380
  nodeIdMode: "data",
26116
26381
  exposesDeviceSettings: true,
26117
26382
  methods: {
26383
+ /** Internal storage-migration participant lease. While held, dispatch
26384
+ * requests are accepted as pending but no new runner attachment starts. */
26385
+ pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, external_exports.object({ paused: external_exports.literal(true) }), {
26386
+ kind: "mutation",
26387
+ auth: "admin"
26388
+ }),
26389
+ resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, external_exports.object({ resumed: external_exports.literal(true) }), {
26390
+ kind: "mutation",
26391
+ auth: "admin"
26392
+ }),
26118
26393
  /**
26119
26394
  * Pin a camera's pipeline to a specific agent (L1 affinity).
26120
26395
  * The orchestrator re-evaluates the assignment immediately and persists
@@ -26454,6 +26729,10 @@ var pipelineOrchestratorCapability = {
26454
26729
  agentNodeId: external_exports.string().optional()
26455
26730
  }), CameraPipelineConfigSchema),
26456
26731
  /**
26732
+ * @deprecated The aggregate switch group is being withdrawn
26733
+ * ([D113](../../../../docs/decisions/adr-0113.md)). Build nothing new on
26734
+ * this pair; read the authority directly.
26735
+ *
26457
26736
  * The whole per-camera function switch group, DERIVED — never a stored
26458
26737
  * list ([D61](../../../../docs/decisions/adr-0067.md)).
26459
26738
  *
@@ -26467,9 +26746,23 @@ var pipelineOrchestratorCapability = {
26467
26746
  * `auth: 'view'` deliberately — a NON-admin must be able to see that a
26468
26747
  * camera is quiet because somebody switched it off. Only the mutation is
26469
26748
  * admin-gated.
26749
+ *
26750
+ * **Removal plan.** It stays and it KEEPS WORKING while shipped viewers
26751
+ * (v1.0.305) and the admin UI still call it — removing it now is a broken
26752
+ * app on a device nobody can redeploy from here. It is served by a thin
26753
+ * shim over the same authorities (`camera-switch-service.ts`), so the
26754
+ * behaviour of the pair is the behaviour of the authorities by
26755
+ * construction. It is deleted once every surface reaches its own
26756
+ * component's options and the last caller is gone. Nothing on this server
26757
+ * reads it: `CameraStatus.switchedOff` is composed from the authorities
26758
+ * directly via `composeSwitchedOff`.
26470
26759
  */
26471
26760
  getCameraSwitches: method(external_exports.object({ deviceId: external_exports.number() }), CameraSwitchGroupSchema),
26472
26761
  /**
26762
+ * @deprecated See {@link getCameraSwitches}. Write the authority — the
26763
+ * wrapper binding, `RecordingConfig.enabled`, the notification mute — not
26764
+ * this ([D113](../../../../docs/decisions/adr-0113.md)).
26765
+ *
26473
26766
  * Flip ONE switch, routed to its existing authority.
26474
26767
  *
26475
26768
  * Never writes a parallel map: `recording` patches `RecordingConfig.enabled`
@@ -26847,24 +27140,28 @@ var snapshotCapability = {
26847
27140
  *
26848
27141
  * `getSnapshotOverview` is cache-only by contract: it answers from whatever
26849
27142
  * the wrapper happens to hold and never captures. Under D93 the client
26850
- * versions its image URL on that answer, and an image REQUEST is what enrols
26851
- * a camera in the keep-warm loop. Both of those are satisfiable by the
26852
- * client's own image cache — `expo-image` is URL-keyed and never revalidates
26853
- * — so a URL painted in a previous session comes off disk with no network,
26854
- * no enrolment, and nothing warming. Measured on the live hub: reopening
26855
- * after two minutes idle painted 15 of 16 tiles at **168 s old** with zero
26856
- * HTTP requests, and the fleet only recovered because a later poll happened
26857
- * to observe a different identity.
27143
+ * versions its image URL on that answer, and an image REQUEST was the only
27144
+ * demand signal. Both of those are satisfiable by the client's own image
27145
+ * cache — `expo-image` is URL-keyed and never revalidates — so a URL painted
27146
+ * in a previous session comes off disk with no network, no demand, and no
27147
+ * capture. Measured on the live hub: reopening after two minutes idle
27148
+ * painted 15 of 16 tiles at **168 s old** with zero HTTP requests, and the
27149
+ * fleet only recovered because a later poll happened to observe a different
27150
+ * identity.
26858
27151
  *
26859
27152
  * ## The two properties that fix it
26860
27153
  *
26861
27154
  * **It is an RPC, so no client cache can answer it.** The demand signal
26862
- * always reaches the wrapper. This method therefore MAY create keep-warm
26863
- * subscriptions, where `getSnapshotOverview` must never (D93) — the
26864
- * distinction is not "one is newer" but that the overview poll is app-wide
26865
- * (a creating overview would warm every camera on the install) while this is
26866
- * called by a rendered surface naming the tiles it is actually painting, at
26867
- * the width it is painting them.
27155
+ * always reaches the wrapper. This method therefore CAPTURES, where
27156
+ * `getSnapshotOverview` must never (D93) — the distinction is not "one is
27157
+ * newer" but that the overview poll is app-wide (a capturing overview would
27158
+ * dial every camera on the install) while this is called by a rendered
27159
+ * surface naming the tiles it is actually painting, at the width it is
27160
+ * painting them.
27161
+ *
27162
+ * Since 2026-08-11 this is the ONLY thing that refreshes a snapshot: the
27163
+ * server-side keep-warm loop was removed (operator directive — on-demand,
27164
+ * always), so a camera nobody is looking at costs nothing at all.
26868
27165
  *
26869
27166
  * **It waits, briefly and boundedly, for the capture it triggered.** The
26870
27167
  * returned `capturedAt` is the frame the link will serve, not the frame the
@@ -27108,6 +27405,23 @@ var storageEvictableCapability = {
27108
27405
  }), EvictResultSchema, { kind: "mutation" })
27109
27406
  }
27110
27407
  };
27408
+ var storageMigrationCapability = {
27409
+ name: "storage-migration",
27410
+ scope: "system",
27411
+ mode: "singleton",
27412
+ methods: {
27413
+ plan: method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin" }),
27414
+ start: method(StorageMigrationInputSchema, external_exports.object({ jobId: external_exports.string() }), {
27415
+ kind: "mutation",
27416
+ auth: "admin"
27417
+ }),
27418
+ status: method(external_exports.object({ jobId: external_exports.string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }),
27419
+ cancel: method(external_exports.object({ jobId: external_exports.string() }), external_exports.object({ cancelled: external_exports.boolean() }), {
27420
+ kind: "mutation",
27421
+ auth: "admin"
27422
+ })
27423
+ }
27424
+ };
27111
27425
  var ProviderInfoSchema = external_exports.discriminatedUnion("shouldSaveDiskSpace", [external_exports.object({
27112
27426
  providerId: external_exports.string().min(1),
27113
27427
  displayName: external_exports.string().min(1),
@@ -27222,6 +27536,24 @@ var TerminalProfileInfoSchema = external_exports.object({
27222
27536
  label: external_exports.string(),
27223
27537
  description: external_exports.string().optional()
27224
27538
  });
27539
+ var TerminalInstanceInfoSchema = external_exports.object({
27540
+ instanceId: external_exports.string(),
27541
+ cameraStableId: external_exports.string(),
27542
+ nodeId: external_exports.string(),
27543
+ profileId: external_exports.string(),
27544
+ profileLabel: external_exports.string(),
27545
+ name: external_exports.string(),
27546
+ enabled: external_exports.boolean()
27547
+ });
27548
+ var TerminalLegacyCameraSchema = external_exports.object({
27549
+ stableId: external_exports.string(),
27550
+ nodeId: external_exports.string(),
27551
+ profileId: external_exports.string(),
27552
+ profileLabel: external_exports.string(),
27553
+ name: external_exports.string(),
27554
+ /** Only legacy monitor cameras can retain their historic stable identity. */
27555
+ adoptable: external_exports.boolean()
27556
+ });
27225
27557
  var TerminalOutputEventSchema = external_exports.discriminatedUnion("kind", [external_exports.object({
27226
27558
  seq: external_exports.number().int().positive(),
27227
27559
  kind: external_exports.literal("data"),
@@ -27245,6 +27577,37 @@ var terminalSessionCapability = {
27245
27577
  methods: {
27246
27578
  /** Pre-declared profiles the operator may open. */
27247
27579
  listProfiles: method(external_exports.void(), external_exports.array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
27580
+ /** Explicit durable Terminal instances, managed centrally on the hub. */
27581
+ listInstances: method(external_exports.void(), external_exports.array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }),
27582
+ createInstance: method(external_exports.object({
27583
+ targetNodeId: external_exports.string().min(1),
27584
+ profileId: external_exports.string().min(1),
27585
+ name: external_exports.string().trim().min(1).max(160).optional()
27586
+ }), TerminalInstanceInfoSchema, {
27587
+ kind: "mutation",
27588
+ auth: "admin"
27589
+ }),
27590
+ deleteInstance: method(external_exports.object({ instanceId: external_exports.string().min(1) }), external_exports.void(), {
27591
+ kind: "mutation",
27592
+ auth: "admin"
27593
+ }),
27594
+ setInstanceEnabled: method(external_exports.object({
27595
+ instanceId: external_exports.string().min(1),
27596
+ enabled: external_exports.boolean()
27597
+ }), TerminalInstanceInfoSchema, {
27598
+ kind: "mutation",
27599
+ auth: "admin"
27600
+ }),
27601
+ /** Existing automatic cameras are shown for explicit migration only. */
27602
+ listLegacyCameras: method(external_exports.void(), external_exports.array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }),
27603
+ /** Explicitly adopt one legacy monitor camera, retaining its stable id. */
27604
+ adoptLegacyMonitor: method(external_exports.object({
27605
+ stableId: external_exports.string().min(1),
27606
+ name: external_exports.string().trim().min(1).max(160).optional()
27607
+ }), TerminalInstanceInfoSchema, {
27608
+ kind: "mutation",
27609
+ auth: "admin"
27610
+ }),
27248
27611
  /** Live sessions currently hosted by the provider. */
27249
27612
  listSessions: method(external_exports.void(), external_exports.array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }),
27250
27613
  /**
@@ -27276,7 +27639,13 @@ var terminalSessionCapability = {
27276
27639
  pullOutput: method(external_exports.object({
27277
27640
  sessionId: external_exports.string(),
27278
27641
  afterSeq: external_exports.number().int().nonnegative(),
27279
- waitMs: external_exports.number().int().min(0).max(2e3).default(0)
27642
+ waitMs: external_exports.number().int().min(0).max(2e3).default(0),
27643
+ /**
27644
+ * Wait when a just-opened session has no output yet. Kept opt-in so a
27645
+ * browser's initial repaint remains immediate; the camera snapshot
27646
+ * relay uses it to avoid encoding a blank startup frame.
27647
+ */
27648
+ waitForOutput: external_exports.boolean().optional()
27280
27649
  }), TerminalOutputBatchSchema, {
27281
27650
  kind: "mutation",
27282
27651
  auth: "admin",
@@ -29656,6 +30025,7 @@ var FaceInfoSchema = external_exports.object({
29656
30025
  var FaceFilterEnum = external_exports.enum([
29657
30026
  "unassigned",
29658
30027
  "recognized",
30028
+ "identified",
29659
30029
  "all"
29660
30030
  ]);
29661
30031
  var MediaFileLiteSchema$1 = external_exports.object({
@@ -29695,6 +30065,8 @@ var faceGalleryCapability = {
29695
30065
  auth: "admin"
29696
30066
  }),
29697
30067
  listRecentFaces: method(external_exports.object({
30068
+ /** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
30069
+ deviceId: external_exports.number().int().optional(),
29698
30070
  limit: external_exports.number().int().positive().optional(),
29699
30071
  filter: FaceFilterEnum.optional(),
29700
30072
  /**
@@ -31771,6 +32143,10 @@ var OsdSourceSchema = external_exports.discriminatedUnion("kind", [
31771
32143
  capName: external_exports.string().min(1).max(64),
31772
32144
  /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
31773
32145
  valuePath: external_exports.string().min(1).max(64)
32146
+ }),
32147
+ external_exports.object({
32148
+ kind: external_exports.literal("latest-recognition"),
32149
+ recognition: external_exports.enum(["person", "plate"])
31774
32150
  })
31775
32151
  ]);
31776
32152
  var OsdSlotBindingSchema = external_exports.object({
@@ -31897,6 +32273,21 @@ var osdManagerCapability = {
31897
32273
  auth: "admin"
31898
32274
  }),
31899
32275
  /**
32276
+ * Replace one camera's binding configuration from another. Writable source
32277
+ * slots are mapped by ordinal onto writable target slots so different
32278
+ * provider slot ids do not make the copied configuration disappear.
32279
+ */
32280
+ copyDeviceConfiguration: method(external_exports.object({
32281
+ sourceDeviceId: external_exports.number().int(),
32282
+ targetDeviceId: external_exports.number().int()
32283
+ }), external_exports.object({
32284
+ copied: external_exports.number().int().nonnegative(),
32285
+ skipped: external_exports.number().int().nonnegative()
32286
+ }), {
32287
+ kind: "mutation",
32288
+ auth: "admin"
32289
+ }),
32290
+ /**
31900
32291
  * Render without writing. `binding` overrides the stored one so an
31901
32292
  * editor can preview an unsaved change. Mutation kind only to carry
31902
32293
  * the binding object safely; no side effects.
@@ -32808,7 +33199,19 @@ var RecordingManifestSchema = external_exports.object({
32808
33199
  });
32809
33200
  var RecordingDeviceUsageSchema = external_exports.object({
32810
33201
  deviceId: external_exports.number(),
32811
- usedBytes: external_exports.number()
33202
+ usedBytes: external_exports.number(),
33203
+ /**
33204
+ * Start of this camera's OLDEST indexed segment, across every profile and
33205
+ * location — the "Oldest footage" column in Recordings → Storage, and the
33206
+ * only honest answer to "is retention actually holding?" per camera.
33207
+ *
33208
+ * `null` = the camera has no footage. OPTIONAL because a recorder that
33209
+ * predates this field omits it entirely, and a hub whose types carry the
33210
+ * field must keep validating that older provider's payload: the framework
33211
+ * (types) and the addon ship on different trains, and the addon is usually
33212
+ * the later of the two.
33213
+ */
33214
+ oldestMs: external_exports.number().nullable().optional()
32812
33215
  });
32813
33216
  var RecordingLocationUsageSchema = external_exports.object({
32814
33217
  /** StorageLocation id; null for the legacy/degraded single-root fallback. */
@@ -32831,6 +33234,42 @@ var RecordingStorageUsageSchema = external_exports.object({
32831
33234
  devices: external_exports.array(RecordingDeviceUsageSchema),
32832
33235
  locations: external_exports.array(RecordingLocationUsageSchema)
32833
33236
  });
33237
+ var RecordingRebalanceMoveSchema = external_exports.object({
33238
+ deviceId: external_exports.number(),
33239
+ profile: external_exports.string(),
33240
+ fromLocationId: external_exports.string(),
33241
+ toLocationId: external_exports.string(),
33242
+ bytes: external_exports.number(),
33243
+ files: external_exports.number().int()
33244
+ });
33245
+ var RecordingRebalanceSkipReasonSchema = external_exports.enum([
33246
+ "unassigned",
33247
+ "target-not-writable",
33248
+ "below-threshold",
33249
+ "no-headroom"
33250
+ ]);
33251
+ var RecordingRebalanceSkipSchema = external_exports.object({
33252
+ deviceId: external_exports.number(),
33253
+ profile: external_exports.string(),
33254
+ fromLocationId: external_exports.string(),
33255
+ /** The location the plan wants; null when the camera has no assignment. */
33256
+ toLocationId: external_exports.string().nullable(),
33257
+ bytes: external_exports.number(),
33258
+ reason: RecordingRebalanceSkipReasonSchema
33259
+ });
33260
+ var RecordingRebalancePlanSchema = external_exports.object({
33261
+ moves: external_exports.array(RecordingRebalanceMoveSchema),
33262
+ skipped: external_exports.array(RecordingRebalanceSkipSchema),
33263
+ bytesToMove: external_exports.number(),
33264
+ /** Relocate job ids enqueued. Always empty for the plan (dry-run) call. */
33265
+ jobIds: external_exports.array(external_exports.string())
33266
+ });
33267
+ var RecordingRebalanceInputSchema = external_exports.object({
33268
+ /** Copy throttle in MB/s (default 40) — a rebalance is a background chore. */
33269
+ throttleMbps: external_exports.number().min(1).max(1e3).optional(),
33270
+ /** Ignore piles smaller than this (default 1 GB). */
33271
+ minMoveGb: external_exports.number().min(0).optional()
33272
+ });
32834
33273
  var LocateSegmentResultSchema = external_exports.discriminatedUnion("kind", [external_exports.object({
32835
33274
  kind: external_exports.literal("segment"),
32836
33275
  startMs: external_exports.number(),
@@ -32981,12 +33420,6 @@ var recordingCapability = {
32981
33420
  auth: "admin"
32982
33421
  }),
32983
33422
  /**
32984
- * Move footage (recorded segments) from one recordings location to
32985
- * another — the drain workflow's mover (entity-routing spec Phase 4).
32986
- * Throttled copy → size-verify → delete source → index refresh; resumable
32987
- * by construction (copy-if-absent). Single-flight: one job at a time.
32988
- */
32989
- /**
32990
33423
  * Render a short GIF from recorded footage around `aroundMs` (low profile,
32991
33424
  * palette-free ffmpeg gif). Synchronous — the window is a few seconds of
32992
33425
  * `low`, rendering in ~1-2s. Built for notification attachments: the
@@ -33038,25 +33471,83 @@ var recordingCapability = {
33038
33471
  kind: "mutation",
33039
33472
  auth: "admin"
33040
33473
  }),
33474
+ /** Internal migration-participant lease. Blocks new recorder attaches and
33475
+ * waits for writers to exit while their watchers index final segments. */
33476
+ pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, external_exports.object({ paused: external_exports.literal(true) }), {
33477
+ kind: "mutation",
33478
+ auth: "admin"
33479
+ }),
33480
+ resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, external_exports.object({ resumed: external_exports.literal(true) }), {
33481
+ kind: "mutation",
33482
+ auth: "admin"
33483
+ }),
33484
+ /** Re-resolve location roots after the coordinator changes defaults. */
33485
+ refreshStorageLocationsForMigration: method(StorageMigrationLeaseInputSchema, external_exports.object({ refreshed: external_exports.literal(true) }), {
33486
+ kind: "mutation",
33487
+ auth: "admin"
33488
+ }),
33489
+ /** Internal mover, callable only by the storage-migration coordinator. */
33490
+ startStorageMigrationMove: method(StorageMigrationFootageMoveInputSchema, external_exports.object({ jobId: external_exports.string() }), {
33491
+ kind: "mutation",
33492
+ auth: "admin"
33493
+ }),
33494
+ getStorageMigrationMoveStatus: method(external_exports.object({ jobId: external_exports.string() }), RelocateJobSchema.nullable(), { auth: "admin" }),
33495
+ cancelStorageMigrationMove: method(external_exports.object({ jobId: external_exports.string() }), external_exports.object({ cancelled: external_exports.boolean() }), {
33496
+ kind: "mutation",
33497
+ auth: "admin"
33498
+ }),
33499
+ /**
33500
+ * Move footage between locations — the OPERATOR's mover, scoped to one
33501
+ * camera (and optionally to specific profiles) rather than a whole disk.
33502
+ * Queued FIFO behind the single-flight engine, so arming several is safe.
33503
+ */
33041
33504
  relocateFootage: method(RelocateFootageInputSchema, external_exports.object({ jobId: external_exports.string() }), {
33042
33505
  kind: "mutation",
33043
33506
  auth: "admin"
33044
33507
  }),
33045
- /** Every known relocate job (in-RAM, newest-first). */
33046
- getRelocateStatus: method(external_exports.object({}), external_exports.array(RelocateJobSchema).readonly(), {
33508
+ /** Every relocate job this recorder knows about, newest first (in RAM: the
33509
+ * move is resumable, so a lost list costs nothing but the display). */
33510
+ listRelocateJobs: method(external_exports.object({}), external_exports.array(RelocateJobSchema).readonly(), {
33511
+ kind: "query",
33512
+ auth: "admin"
33513
+ }),
33514
+ /** Cancel a running or queued relocate job. A queued job never runs. */
33515
+ cancelRelocateJob: method(external_exports.object({ jobId: external_exports.string() }), external_exports.object({ cancelled: external_exports.boolean() }), {
33516
+ kind: "mutation",
33517
+ auth: "admin"
33518
+ }),
33519
+ /** What a rebalance WOULD move, and what it would refuse. Moves nothing. */
33520
+ planStorageRebalance: method(RecordingRebalanceInputSchema, RecordingRebalancePlanSchema, {
33047
33521
  kind: "query",
33048
33522
  auth: "admin"
33049
33523
  }),
33050
- cancelRelocate: method(external_exports.object({ jobId: external_exports.string() }), external_exports.object({ cancelled: external_exports.boolean() }), {
33524
+ /** Arm the rebalance: enqueue one relocate job per planned move. */
33525
+ startStorageRebalance: method(RecordingRebalanceInputSchema, RecordingRebalancePlanSchema, {
33051
33526
  kind: "mutation",
33052
33527
  auth: "admin"
33053
33528
  })
33054
33529
  }
33055
33530
  };
33056
33531
  var ExportSpeedSchema = external_exports.number().min(0.25).max(32);
33532
+ var ExportDenseRangeSchema = external_exports.object({
33533
+ fromSec: external_exports.number().nonnegative(),
33534
+ toSec: external_exports.number().nonnegative()
33535
+ }).refine((r) => r.toSec > r.fromSec, { message: "dense range must have toSec > fromSec" });
33536
+ var ExportDenseSchema = external_exports.object({
33537
+ everyMs: external_exports.number().int().positive(),
33538
+ ranges: external_exports.array(ExportDenseRangeSchema).min(1).max(200)
33539
+ });
33057
33540
  var ExportTimelapseSchema = external_exports.object({
33058
33541
  everyMs: external_exports.number().int().positive(),
33059
- outputFps: external_exports.number().int().min(1).max(60).optional()
33542
+ outputFps: external_exports.number().int().min(1).max(60).optional(),
33543
+ /** Optional second, FASTER rate over the intervals that matter. */
33544
+ dense: ExportDenseSchema.optional()
33545
+ }).superRefine((v, ctx) => {
33546
+ if (v.dense !== void 0 && v.dense.everyMs >= v.everyMs) ctx.addIssue({
33547
+ code: external_exports.ZodIssueCode.custom,
33548
+ message: "dense.everyMs must be strictly smaller than the base everyMs",
33549
+ path: ["dense", "everyMs"]
33550
+ });
33060
33551
  });
33061
33552
  var ExportOptionsSchema = external_exports.object({
33062
33553
  speed: ExportSpeedSchema.optional(),
@@ -33106,8 +33597,16 @@ var ExportDownloadSchema = external_exports.object({
33106
33597
  url: external_exports.string(),
33107
33598
  endpoints: external_exports.array(external_exports.string())
33108
33599
  });
33600
+ var RECORDING_EXPORT_MAX_READ_BYTES = 50 * 1024 * 1024;
33601
+ var ExportBytesSchema = external_exports.object({
33602
+ base64: external_exports.string(),
33603
+ contentType: external_exports.string(),
33604
+ /** Suggested filename, extension included. */
33605
+ name: external_exports.string(),
33606
+ bytes: external_exports.number().int().nonnegative()
33607
+ });
33109
33608
  var recordingExportCapability = {
33110
- name: "recordingExport",
33609
+ name: "recording-export",
33111
33610
  scope: "system",
33112
33611
  mode: "singleton",
33113
33612
  methods: {
@@ -33145,6 +33644,27 @@ var recordingExportCapability = {
33145
33644
  getDownloadUrl: method(external_exports.object({ exportId: external_exports.string() }), ExportDownloadSchema, {
33146
33645
  kind: "query",
33147
33646
  auth: "protected"
33647
+ }),
33648
+ /**
33649
+ * The finished file's BYTES, base64, for a caller that must republish them
33650
+ * somewhere a session-less fetcher can reach.
33651
+ *
33652
+ * `getDownloadUrl` is the right answer for a human: the download route is
33653
+ * served `access: 'authenticated'`, which a browser satisfies and a
33654
+ * notifier BACKEND does not. It answers a RELATIVE path, so it is not even
33655
+ * a URL an outside fetcher could try. This method exists for the one case
33656
+ * that needs the other thing — a scheduled timelapse whose video has to
33657
+ * become a public attachment on the notification artifact plane.
33658
+ *
33659
+ * Deliberately narrow: `ready` only (a queued, rendering, failed, expired
33660
+ * or deleted export has no file, and answering "0 bytes" for one is how a
33661
+ * caller ships an empty attachment), still inside its lifetime, and under
33662
+ * {@link RECORDING_EXPORT_MAX_READ_BYTES}. Every refusal throws with the
33663
+ * reason — none of them is silent.
33664
+ */
33665
+ readExportBytes: method(external_exports.object({ exportId: external_exports.string() }), ExportBytesSchema, {
33666
+ kind: "query",
33667
+ auth: "protected"
33148
33668
  })
33149
33669
  }
33150
33670
  };
@@ -37396,6 +37916,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
37396
37916
  addonId: null,
37397
37917
  access: "delete"
37398
37918
  },
37919
+ "osdManager.copyDeviceConfiguration": {
37920
+ capName: "osd-manager",
37921
+ capScope: "system",
37922
+ addonId: null,
37923
+ access: "create"
37924
+ },
37399
37925
  "osdManager.getConditionSupport": {
37400
37926
  capName: "osd-manager",
37401
37927
  capScope: "system",
@@ -37492,7 +38018,7 @@ var METHOD_ACCESS_MAP = Object.freeze({
37492
38018
  addonId: null,
37493
38019
  access: "create"
37494
38020
  },
37495
- "pipelineAnalytics.cancelMediaRelocate": {
38021
+ "pipelineAnalytics.cancelStorageMigrationMove": {
37496
38022
  capName: "pipeline-analytics",
37497
38023
  capScope: "device",
37498
38024
  addonId: null,
@@ -37564,12 +38090,6 @@ var METHOD_ACCESS_MAP = Object.freeze({
37564
38090
  addonId: null,
37565
38091
  access: "view"
37566
38092
  },
37567
- "pipelineAnalytics.getMediaRelocateStatus": {
37568
- capName: "pipeline-analytics",
37569
- capScope: "device",
37570
- addonId: null,
37571
- access: "view"
37572
- },
37573
38093
  "pipelineAnalytics.getMotionEvents": {
37574
38094
  capName: "pipeline-analytics",
37575
38095
  capScope: "device",
@@ -37606,6 +38126,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
37606
38126
  addonId: null,
37607
38127
  access: "view"
37608
38128
  },
38129
+ "pipelineAnalytics.getStorageMigrationMoveStatus": {
38130
+ capName: "pipeline-analytics",
38131
+ capScope: "device",
38132
+ addonId: null,
38133
+ access: "view"
38134
+ },
37609
38135
  "pipelineAnalytics.getTrack": {
37610
38136
  capName: "pipeline-analytics",
37611
38137
  capScope: "device",
@@ -37684,6 +38210,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
37684
38210
  addonId: null,
37685
38211
  access: "view"
37686
38212
  },
38213
+ "pipelineAnalytics.pauseForStorageMigration": {
38214
+ capName: "pipeline-analytics",
38215
+ capScope: "device",
38216
+ addonId: null,
38217
+ access: "create"
38218
+ },
37687
38219
  "pipelineAnalytics.proposeRetrainAnnotations": {
37688
38220
  capName: "pipeline-analytics",
37689
38221
  capScope: "device",
@@ -37714,7 +38246,7 @@ var METHOD_ACCESS_MAP = Object.freeze({
37714
38246
  addonId: null,
37715
38247
  access: "create"
37716
38248
  },
37717
- "pipelineAnalytics.relocateMedia": {
38249
+ "pipelineAnalytics.refreshStorageLocationsForMigration": {
37718
38250
  capName: "pipeline-analytics",
37719
38251
  capScope: "device",
37720
38252
  addonId: null,
@@ -37726,6 +38258,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
37726
38258
  addonId: null,
37727
38259
  access: "create"
37728
38260
  },
38261
+ "pipelineAnalytics.resumeForStorageMigration": {
38262
+ capName: "pipeline-analytics",
38263
+ capScope: "device",
38264
+ addonId: null,
38265
+ access: "create"
38266
+ },
37729
38267
  "pipelineAnalytics.saveRetrainAnnotations": {
37730
38268
  capName: "pipeline-analytics",
37731
38269
  capScope: "device",
@@ -37750,6 +38288,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
37750
38288
  addonId: null,
37751
38289
  access: "create"
37752
38290
  },
38291
+ "pipelineAnalytics.startStorageMigrationMove": {
38292
+ capName: "pipeline-analytics",
38293
+ capScope: "device",
38294
+ addonId: null,
38295
+ access: "create"
38296
+ },
37753
38297
  "pipelineAnalytics.wipeAllAnalytics": {
37754
38298
  capName: "pipeline-analytics",
37755
38299
  capScope: "device",
@@ -38116,6 +38660,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
38116
38660
  addonId: null,
38117
38661
  access: "view"
38118
38662
  },
38663
+ "pipelineOrchestrator.pauseForStorageMigration": {
38664
+ capName: "pipeline-orchestrator",
38665
+ capScope: "system",
38666
+ addonId: null,
38667
+ access: "create"
38668
+ },
38119
38669
  "pipelineOrchestrator.rebalance": {
38120
38670
  capName: "pipeline-orchestrator",
38121
38671
  capScope: "system",
@@ -38140,6 +38690,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
38140
38690
  addonId: null,
38141
38691
  access: "view"
38142
38692
  },
38693
+ "pipelineOrchestrator.resumeForStorageMigration": {
38694
+ capName: "pipeline-orchestrator",
38695
+ capScope: "system",
38696
+ addonId: null,
38697
+ access: "create"
38698
+ },
38143
38699
  "pipelineOrchestrator.saveTemplate": {
38144
38700
  capName: "pipeline-orchestrator",
38145
38701
  capScope: "system",
@@ -38536,7 +39092,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
38536
39092
  addonId: null,
38537
39093
  access: "create"
38538
39094
  },
38539
- "recording.cancelRelocate": {
39095
+ "recording.cancelRelocateJob": {
39096
+ capName: "recording",
39097
+ capScope: "system",
39098
+ addonId: null,
39099
+ access: "create"
39100
+ },
39101
+ "recording.cancelStorageMigrationMove": {
38540
39102
  capName: "recording",
38541
39103
  capScope: "system",
38542
39104
  addonId: null,
@@ -38572,7 +39134,7 @@ var METHOD_ACCESS_MAP = Object.freeze({
38572
39134
  addonId: null,
38573
39135
  access: "view"
38574
39136
  },
38575
- "recording.getRelocateStatus": {
39137
+ "recording.getStorageMigrationMoveStatus": {
38576
39138
  capName: "recording",
38577
39139
  capScope: "system",
38578
39140
  addonId: null,
@@ -38590,12 +39152,30 @@ var METHOD_ACCESS_MAP = Object.freeze({
38590
39152
  addonId: null,
38591
39153
  access: "view"
38592
39154
  },
39155
+ "recording.listRelocateJobs": {
39156
+ capName: "recording",
39157
+ capScope: "system",
39158
+ addonId: null,
39159
+ access: "view"
39160
+ },
38593
39161
  "recording.locateSegment": {
38594
39162
  capName: "recording",
38595
39163
  capScope: "system",
38596
39164
  addonId: null,
38597
39165
  access: "view"
38598
39166
  },
39167
+ "recording.pauseForStorageMigration": {
39168
+ capName: "recording",
39169
+ capScope: "system",
39170
+ addonId: null,
39171
+ access: "create"
39172
+ },
39173
+ "recording.planStorageRebalance": {
39174
+ capName: "recording",
39175
+ capScope: "system",
39176
+ addonId: null,
39177
+ access: "view"
39178
+ },
38599
39179
  "recording.pruneFootage": {
38600
39180
  capName: "recording",
38601
39181
  capScope: "system",
@@ -38614,6 +39194,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
38614
39194
  addonId: null,
38615
39195
  access: "view"
38616
39196
  },
39197
+ "recording.refreshStorageLocationsForMigration": {
39198
+ capName: "recording",
39199
+ capScope: "system",
39200
+ addonId: null,
39201
+ access: "create"
39202
+ },
38617
39203
  "recording.relocateFootage": {
38618
39204
  capName: "recording",
38619
39205
  capScope: "system",
@@ -38638,44 +39224,68 @@ var METHOD_ACCESS_MAP = Object.freeze({
38638
39224
  addonId: null,
38639
39225
  access: "create"
38640
39226
  },
39227
+ "recording.resumeForStorageMigration": {
39228
+ capName: "recording",
39229
+ capScope: "system",
39230
+ addonId: null,
39231
+ access: "create"
39232
+ },
38641
39233
  "recording.setDeviceConfig": {
38642
39234
  capName: "recording",
38643
39235
  capScope: "system",
38644
39236
  addonId: null,
38645
39237
  access: "create"
38646
39238
  },
39239
+ "recording.startStorageMigrationMove": {
39240
+ capName: "recording",
39241
+ capScope: "system",
39242
+ addonId: null,
39243
+ access: "create"
39244
+ },
39245
+ "recording.startStorageRebalance": {
39246
+ capName: "recording",
39247
+ capScope: "system",
39248
+ addonId: null,
39249
+ access: "create"
39250
+ },
38647
39251
  "recordingExport.cancelExport": {
38648
- capName: "recordingExport",
39252
+ capName: "recording-export",
38649
39253
  capScope: "system",
38650
39254
  addonId: null,
38651
39255
  access: "create"
38652
39256
  },
38653
39257
  "recordingExport.createExport": {
38654
- capName: "recordingExport",
39258
+ capName: "recording-export",
38655
39259
  capScope: "system",
38656
39260
  addonId: null,
38657
39261
  access: "create"
38658
39262
  },
38659
39263
  "recordingExport.deleteExport": {
38660
- capName: "recordingExport",
39264
+ capName: "recording-export",
38661
39265
  capScope: "system",
38662
39266
  addonId: null,
38663
39267
  access: "delete"
38664
39268
  },
38665
39269
  "recordingExport.getDownloadUrl": {
38666
- capName: "recordingExport",
39270
+ capName: "recording-export",
38667
39271
  capScope: "system",
38668
39272
  addonId: null,
38669
39273
  access: "view"
38670
39274
  },
38671
39275
  "recordingExport.getExport": {
38672
- capName: "recordingExport",
39276
+ capName: "recording-export",
38673
39277
  capScope: "system",
38674
39278
  addonId: null,
38675
39279
  access: "view"
38676
39280
  },
38677
39281
  "recordingExport.listExports": {
38678
- capName: "recordingExport",
39282
+ capName: "recording-export",
39283
+ capScope: "system",
39284
+ addonId: null,
39285
+ access: "view"
39286
+ },
39287
+ "recordingExport.readExportBytes": {
39288
+ capName: "recording-export",
38679
39289
  capScope: "system",
38680
39290
  addonId: null,
38681
39291
  access: "view"
@@ -39034,6 +39644,30 @@ var METHOD_ACCESS_MAP = Object.freeze({
39034
39644
  addonId: null,
39035
39645
  access: "view"
39036
39646
  },
39647
+ "storageMigration.cancel": {
39648
+ capName: "storage-migration",
39649
+ capScope: "system",
39650
+ addonId: null,
39651
+ access: "create"
39652
+ },
39653
+ "storageMigration.plan": {
39654
+ capName: "storage-migration",
39655
+ capScope: "system",
39656
+ addonId: null,
39657
+ access: "view"
39658
+ },
39659
+ "storageMigration.start": {
39660
+ capName: "storage-migration",
39661
+ capScope: "system",
39662
+ addonId: null,
39663
+ access: "create"
39664
+ },
39665
+ "storageMigration.status": {
39666
+ capName: "storage-migration",
39667
+ capScope: "system",
39668
+ addonId: null,
39669
+ access: "view"
39670
+ },
39037
39671
  "storageProvider.abortUpload": {
39038
39672
  capName: "storage-provider",
39039
39673
  capScope: "system",
@@ -39412,12 +40046,42 @@ var METHOD_ACCESS_MAP = Object.freeze({
39412
40046
  addonId: null,
39413
40047
  access: "create"
39414
40048
  },
40049
+ "terminalSession.adoptLegacyMonitor": {
40050
+ capName: "terminal-session",
40051
+ capScope: "system",
40052
+ addonId: null,
40053
+ access: "create"
40054
+ },
39415
40055
  "terminalSession.close": {
39416
40056
  capName: "terminal-session",
39417
40057
  capScope: "system",
39418
40058
  addonId: null,
39419
40059
  access: "create"
39420
40060
  },
40061
+ "terminalSession.createInstance": {
40062
+ capName: "terminal-session",
40063
+ capScope: "system",
40064
+ addonId: null,
40065
+ access: "create"
40066
+ },
40067
+ "terminalSession.deleteInstance": {
40068
+ capName: "terminal-session",
40069
+ capScope: "system",
40070
+ addonId: null,
40071
+ access: "delete"
40072
+ },
40073
+ "terminalSession.listInstances": {
40074
+ capName: "terminal-session",
40075
+ capScope: "system",
40076
+ addonId: null,
40077
+ access: "view"
40078
+ },
40079
+ "terminalSession.listLegacyCameras": {
40080
+ capName: "terminal-session",
40081
+ capScope: "system",
40082
+ addonId: null,
40083
+ access: "view"
40084
+ },
39421
40085
  "terminalSession.listProfiles": {
39422
40086
  capName: "terminal-session",
39423
40087
  capScope: "system",
@@ -39448,6 +40112,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
39448
40112
  addonId: null,
39449
40113
  access: "create"
39450
40114
  },
40115
+ "terminalSession.setInstanceEnabled": {
40116
+ capName: "terminal-session",
40117
+ capScope: "system",
40118
+ addonId: null,
40119
+ access: "create"
40120
+ },
39451
40121
  "terminalSession.writeInput": {
39452
40122
  capName: "terminal-session",
39453
40123
  capScope: "system",
@@ -39968,6 +40638,12 @@ var CadenceSecField = external_exports.number().int().min(2).max(3600);
39968
40638
  var FramerateField = external_exports.number().int().min(1).max(60);
39969
40639
  var TargetsField = external_exports.array(NcRuleTargetSchema).min(1);
39970
40640
  var PriorityField = external_exports.number().int().min(1).max(5);
40641
+ var TIMELAPSE_DENSE_FLOOR_SEC = 0.1;
40642
+ var DenseCadenceSecField = external_exports.number().min(TIMELAPSE_DENSE_FLOOR_SEC).max(3600);
40643
+ var MinDwellSecField = external_exports.number().min(0).max(60);
40644
+ var PreviewTextField = external_exports.string().max(200);
40645
+ var PreviewModeField = external_exports.enum(["image", "gif"]);
40646
+ var ReportClassesField = external_exports.array(external_exports.string().min(1).max(40)).max(20);
39971
40647
  var TimelapseRuleInputSchema = external_exports.object({
39972
40648
  name: NameField,
39973
40649
  enabled: external_exports.boolean().default(true),
@@ -39983,9 +40659,30 @@ var TimelapseRuleInputSchema = external_exports.object({
39983
40659
  cadenceSec: CadenceSecField.default(15),
39984
40660
  /** Output frames per second of the assembled mp4 (predecessor parity). */
39985
40661
  framerate: FramerateField.default(10),
40662
+ /**
40663
+ * Explicit dense cadence — see {@link DenseCadenceSecField}. Absent ⇒ derived
40664
+ * as `max(1s, cadenceSec / 30)`, which is what every rule written before this
40665
+ * field gets.
40666
+ */
40667
+ denseCadenceSec: DenseCadenceSecField.optional(),
40668
+ /** Output-seconds guarantee per detection range — see {@link MinDwellSecField}. */
40669
+ minDwellSec: MinDwellSecField.optional(),
39986
40670
  /** `notification-output` targets the finished video/thumbnail is sent to. */
39987
40671
  targets: TargetsField,
39988
40672
  template: TimelapseTemplateSchema.optional(),
40673
+ /**
40674
+ * Caption on the notification's PREVIEW FRAME — see {@link PreviewTextField}
40675
+ * and {@link DEFAULT_TIMELAPSE_PREVIEW_TEXT}.
40676
+ *
40677
+ * Deliberately NOT part of {@link TimelapseTemplateSchema}: that object is
40678
+ * the notification's title/body, and clearing it (`template: null`) must not
40679
+ * silently clear the caption too.
40680
+ */
40681
+ previewText: PreviewTextField.optional(),
40682
+ /** Still or animation — see {@link PreviewModeField}. */
40683
+ previewMode: PreviewModeField.default("image"),
40684
+ /** Classes the notification counts — see {@link ReportClassesField}. */
40685
+ reportClasses: ReportClassesField.optional(),
39989
40686
  /** Canonical notification priority ordinal (1..5); per-target overridable. */
39990
40687
  priority: PriorityField.default(3)
39991
40688
  });
@@ -39996,8 +40693,13 @@ var TimelapseRulePatchSchema = external_exports.object({
39996
40693
  schedule: NcScheduleSchema.optional(),
39997
40694
  cadenceSec: CadenceSecField.optional(),
39998
40695
  framerate: FramerateField.optional(),
40696
+ denseCadenceSec: DenseCadenceSecField.optional(),
40697
+ minDwellSec: MinDwellSecField.optional(),
39999
40698
  targets: TargetsField.optional(),
40000
40699
  template: TimelapseTemplateSchema.nullable().optional(),
40700
+ previewText: PreviewTextField.optional(),
40701
+ previewMode: PreviewModeField.optional(),
40702
+ reportClasses: ReportClassesField.optional(),
40001
40703
  priority: PriorityField.optional()
40002
40704
  });
40003
40705
  var TimelapseRuleSchema = TimelapseRuleInputSchema.extend({
@@ -40009,10 +40711,28 @@ var TimelapseRuleSchema = TimelapseRuleInputSchema.extend({
40009
40711
  */
40010
40712
  ownerUserId: external_exports.string().optional(),
40011
40713
  /**
40012
- * Epoch-ms of the last successful generation the 1-hour re-generation
40013
- * guard's durable state (predecessor parity). Absent = never generated.
40714
+ * Epoch-ms of the NEWEST successful generation across every camera of this
40715
+ * rule. What a UI shows, and the compatibility floor for
40716
+ * {@link readTimelapseGeneratedAt}. Absent = never generated.
40014
40717
  */
40015
40718
  lastGeneratedAt: external_exports.number().optional(),
40719
+ /**
40720
+ * PER-CAMERA generation state, keyed by `String(deviceId)` — the
40721
+ * re-generation guard's real durable state.
40722
+ *
40723
+ * One rule covers several cameras and each renders its own video, so a rule
40724
+ * -wide stamp is wrong in the direction that DESTROYS work: camera A
40725
+ * succeeding at 06:05 tells camera B, whose render failed, that it is
40726
+ * already done — and B's night is gone for good, because the window will not
40727
+ * come back.
40728
+ *
40729
+ * ADDITIVE, so the migration is free: a row written before this field simply
40730
+ * has no map, and {@link readTimelapseGeneratedAt} falls back to
40731
+ * {@link TimelapseRuleSchema.shape.lastGeneratedAt}. Reading an old row as
40732
+ * "never generated" would re-render and re-notify every camera of every rule
40733
+ * once, on the deploy that shipped the map.
40734
+ */
40735
+ generatedByDevice: external_exports.record(external_exports.string(), external_exports.number()).optional(),
40016
40736
  /** userId of the caller who created the rule (server-stamped). */
40017
40737
  createdBy: external_exports.string(),
40018
40738
  createdAt: external_exports.number(),