@camstack/addon-post-analysis 1.2.63 → 1.2.65

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.
@@ -7421,8 +7421,31 @@ var AdoptionJobSchema = object({
7421
7421
  error: string().nullable()
7422
7422
  });
7423
7423
  /**
7424
- * Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
7425
- * pipeline functions an operator thinks in terms of.
7424
+ * Per-camera FUNCTION SWITCHES.
7425
+ *
7426
+ * ## The aggregate group is being withdrawn — the BADGE is not (D113)
7427
+ *
7428
+ * This file shipped as "the one coherent on/off surface over the pipeline
7429
+ * functions an operator thinks in terms of". The operator's verdict on
7430
+ * 2026-08-12 was that the coherent surface bought complexity and no clarity:
7431
+ * every function already had a settings page of its own, and a second place to
7432
+ * turn it off is a second place to look. Each switch is going back to its own
7433
+ * component's original options — detection to the detection-pipeline wrapper
7434
+ * binding, audio analysis to its own, recording to `RecordingConfig.enabled`
7435
+ * (which was always first-class; the switch was a veneer over
7436
+ * `recording.setDeviceConfig`), notifications to a notification-center
7437
+ * per-device setting, the two camera planes to their own components.
7438
+ *
7439
+ * What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
7440
+ * thing that lets a status surface say DISABLED instead of BROKEN, recomposed
7441
+ * straight from the authorities with no group in the middle. That rule was
7442
+ * never about a control panel.
7443
+ *
7444
+ * Everything else here — {@link CAMERA_SWITCH_CATALOG}, {@link CameraSwitch},
7445
+ * {@link deriveCameraSwitches}, the `pipelineOrchestrator.getCameraSwitches` /
7446
+ * `setCameraSwitch` pair — is a COMPATIBILITY surface for as long as deployed
7447
+ * viewers (v1.0.305) and the admin UI still call it. It is deleted when they
7448
+ * stop; nothing new may be built on it.
7426
7449
  *
7427
7450
  * ## This file adds no state
7428
7451
  *
@@ -7784,14 +7807,29 @@ var RecordingConfigSchema = object({
7784
7807
  scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7785
7808
  }).strict();
7786
7809
  /**
7810
+ * Derive the {@link RecordingStorageModeSchema} summary from the authoritative
7811
+ * bands: `continuous` when any band records continuously, `events` when a band
7812
+ * exists at all, else `off`. Pure.
7813
+ *
7814
+ * THE one definition: the recorder stamps `config.mode` with it on save and
7815
+ * reports it as the cap's `activeMode`, and the settings UI reads it back to
7816
+ * decide which authoring tab a config opens in. Two copies would drift, and the
7817
+ * symptom (a status dot disagreeing with what is being recorded) is exactly the
7818
+ * bug this field was added to fix.
7819
+ */
7820
+ function deriveRecordingMode(config) {
7821
+ if (!config.enabled || config.bands.length === 0) return "off";
7822
+ if (config.bands.some((band) => band.mode === "continuous")) return "continuous";
7823
+ return "events";
7824
+ }
7825
+ /**
7787
7826
  * Entity-relocation job state (storage entity-routing spec, Phase 4).
7788
7827
  *
7789
- * One shape shared by the recorder's `relocateFootage` (segments) and
7790
- * pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
7791
- * page renders both movers with one component. Jobs are in-RAM (a restart
7792
- * forgets them — re-running is safe by construction: copy-if-absent, delete
7793
- * after verify) and each completed/failed run also lands one durable ops-log
7794
- * row on the owning addon's surface.
7828
+ * One shape shared by the recorder and pipeline-analytics internal movers.
7829
+ * The public admin surface is `storage-migration`; child jobs remain in RAM
7830
+ * because copy-if-absent, verify, delete and index/row repoint are resumable.
7831
+ * Each completed/failed run also lands one durable ops-log row on its owning
7832
+ * addon surface.
7795
7833
  */
7796
7834
  var RelocateJobStateSchema = _enum([
7797
7835
  "running",
@@ -7818,19 +7856,100 @@ var RelocateJobSchema = object({
7818
7856
  finishedAt: number().nullable(),
7819
7857
  error: string().nullable()
7820
7858
  });
7859
+ /** Profile-derived footage selection used only by the migration coordinator:
7860
+ * `recordings` owns high+mid; `recordingsLow` owns low. */
7861
+ var RelocateFootageClassSchema = _enum(["recordings", "recordingsLow"]);
7821
7862
  var RelocateFootageInputSchema = object({
7822
- deviceId: number().optional(),
7823
7863
  fromLocationId: string(),
7824
7864
  toLocationId: string(),
7825
7865
  entities: array(_enum(["segments"])).optional(),
7866
+ /** Limits relocation to the logical profile class. Omit only for the
7867
+ * pre-orchestration compatibility path. */
7868
+ footageClass: RelocateFootageClassSchema.optional(),
7826
7869
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7827
7870
  * never allowed to starve live writers. */
7828
7871
  throttleMbps: number().min(1).max(1e3).optional()
7829
7872
  });
7830
- var RelocateMediaInputSchema = object({
7831
- deviceId: number().optional(),
7873
+ /** Internal, lease-scoped participant operation. It is intentionally separate
7874
+ * from persistent recording settings: a migration never changes
7875
+ * `RecordingConfig.enabled` or camera wrapper bindings. */
7876
+ var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
7877
+ var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
7878
+ var StorageMigrationMediaMoveInputSchema = object({
7832
7879
  toLocationId: string(),
7833
7880
  throttleMbps: number().min(1).max(1e3).optional()
7881
+ }).extend({ leaseId: string().min(1) });
7882
+ /** The independently selectable logical storage classes. `recordings`
7883
+ * encompasses the high and mid segment profiles; `recordingsLow` is low
7884
+ * segments; `eventMedia` is post-analysis blobs. */
7885
+ var StorageMigrationClassSchema = _enum([
7886
+ "recordings",
7887
+ "recordingsLow",
7888
+ "eventMedia"
7889
+ ]);
7890
+ /** A destination is always an existing, fully-qualified location id. The
7891
+ * migration API intentionally never changes a source location's `basePath`:
7892
+ * callers create a new `<type>:<slug>` location, then select it here. */
7893
+ var StorageMigrationDestinationsSchema = object({
7894
+ recordings: string().min(1).optional(),
7895
+ recordingsLow: string().min(1).optional(),
7896
+ eventMedia: string().min(1).optional()
7897
+ }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
7898
+ /** Shared input for planning and starting an orchestrated storage migration. */
7899
+ var StorageMigrationInputSchema = object({
7900
+ destinations: StorageMigrationDestinationsSchema,
7901
+ throttleMbps: number().min(1).max(1e3).optional()
7902
+ });
7903
+ /** The durable coordinator state machine. The only phase that changes default
7904
+ * locations is `repointing`, after every selected mover has completed and been
7905
+ * verified. */
7906
+ var StorageMigrationPhaseSchema = _enum([
7907
+ "planning",
7908
+ "pausing",
7909
+ "moving",
7910
+ "verifying",
7911
+ "repointing",
7912
+ "refreshing",
7913
+ "resuming",
7914
+ "done",
7915
+ "failed",
7916
+ "cancelled"
7917
+ ]);
7918
+ var StorageMigrationParticipantSchema = _enum([
7919
+ "pipeline",
7920
+ "recorder",
7921
+ "analytics"
7922
+ ]);
7923
+ var StorageMigrationMoveSchema = object({
7924
+ storageClass: StorageMigrationClassSchema,
7925
+ fromLocationId: string(),
7926
+ toLocationId: string(),
7927
+ moverJobId: string().nullable(),
7928
+ state: RelocateJobStateSchema.nullable(),
7929
+ error: string().nullable()
7930
+ });
7931
+ var StorageMigrationJobSchema = object({
7932
+ jobId: string(),
7933
+ phase: StorageMigrationPhaseSchema,
7934
+ destinations: StorageMigrationDestinationsSchema,
7935
+ throttleMbps: number(),
7936
+ moves: array(StorageMigrationMoveSchema),
7937
+ pauseLeaseId: string().nullable(),
7938
+ pausedParticipants: array(StorageMigrationParticipantSchema),
7939
+ repointed: boolean(),
7940
+ cancelRequested: boolean(),
7941
+ startedAt: number(),
7942
+ updatedAt: number(),
7943
+ finishedAt: number().nullable(),
7944
+ error: string().nullable()
7945
+ });
7946
+ var StorageMigrationPlanSchema = object({
7947
+ destinations: StorageMigrationDestinationsSchema,
7948
+ moves: array(object({
7949
+ storageClass: StorageMigrationClassSchema,
7950
+ fromLocationId: string(),
7951
+ toLocationId: string()
7952
+ }))
7834
7953
  });
7835
7954
  /**
7836
7955
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -17149,20 +17268,28 @@ var pipelineAnalyticsCapability = {
17149
17268
  }),
17150
17269
  /** The events ops-log rows (newest-first), optionally scoped to one camera.
17151
17270
  * Backed by a declared pipeline-analytics SQLite collection. */
17152
- /**
17153
- * Move event-media blobs onto `toLocationId` (entity-routing spec Phase
17154
- * 4): per-row copy → stamp row.locationId → delete old blob. Rows already
17155
- * at the target are skipped, so a re-run resumes. Single-flight.
17156
- */
17157
- relocateMedia: method(RelocateMediaInputSchema, object({ jobId: string() }), {
17271
+ /** Internal migration-participant lease. It drains active MediaStore
17272
+ * writes and refuses new ones without changing analytics bindings. */
17273
+ pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
17158
17274
  kind: "mutation",
17159
17275
  auth: "admin"
17160
17276
  }),
17161
- getMediaRelocateStatus: method(object({}), array(RelocateJobSchema).readonly(), {
17162
- kind: "query",
17277
+ resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
17278
+ kind: "mutation",
17163
17279
  auth: "admin"
17164
17280
  }),
17165
- cancelMediaRelocate: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
17281
+ /** Drops cached location roots after the storage coordinator repoints a
17282
+ * default so future event-media writes use the new root. */
17283
+ refreshStorageLocationsForMigration: method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
17284
+ kind: "mutation",
17285
+ auth: "admin"
17286
+ }),
17287
+ startStorageMigrationMove: method(StorageMigrationMediaMoveInputSchema, object({ jobId: string() }), {
17288
+ kind: "mutation",
17289
+ auth: "admin"
17290
+ }),
17291
+ getStorageMigrationMoveStatus: method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }),
17292
+ cancelStorageMigrationMove: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
17166
17293
  kind: "mutation",
17167
17294
  auth: "admin"
17168
17295
  }),
@@ -18825,9 +18952,16 @@ var CameraStatusSchema = object({
18825
18952
  audio: CameraAudioStatusSchema.nullable(),
18826
18953
  recording: CameraRecordingStatusSchema.nullable(),
18827
18954
  /**
18828
- * Per-camera function switches an OPERATOR has turned off
18955
+ * Per-camera functions an OPERATOR has turned off
18829
18956
  * ([D61](../../../../docs/decisions/adr-0067.md)).
18830
18957
  *
18958
+ * Composed from the AUTHORITIES themselves — the wrapper bindings,
18959
+ * `RecordingConfig.enabled`, the notification mute, the broker's audio
18960
+ * policy, the camera's own microphone — via `composeSwitchedOff`, not from
18961
+ * the deprecated `getCameraSwitches` group ([D113](../../../../docs/decisions/adr-0113.md)).
18962
+ * The badge outlives the control panel: the panel was a convenience, this is
18963
+ * the difference between a camera being off and a camera being dead.
18964
+ *
18831
18965
  * This is the difference between DISABLED and BROKEN. A camera whose
18832
18966
  * `detection` block reports zero fps and whose `switchedOff` contains
18833
18967
  * `'object-detection'` was switched off by a person; the same camera with an
@@ -18898,7 +19032,13 @@ var NodeInferenceDevicesSchema = object({
18898
19032
  reachable: boolean(),
18899
19033
  devices: array(NodeInferenceDeviceSchema).readonly()
18900
19034
  });
18901
- method(object({
19035
+ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19036
+ kind: "mutation",
19037
+ auth: "admin"
19038
+ }), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
19039
+ kind: "mutation",
19040
+ auth: "admin"
19041
+ }), method(object({
18902
19042
  deviceId: number(),
18903
19043
  agentNodeId: string()
18904
19044
  }), object({ success: literal(true) }), {
@@ -19602,6 +19742,13 @@ method(object({ locationId: string() }), EvictableUsageSchema), method(object({
19602
19742
  locationId: string(),
19603
19743
  targetBytes: number().int().positive()
19604
19744
  }), EvictResultSchema, { kind: "mutation" });
19745
+ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin" }), method(StorageMigrationInputSchema, object({ jobId: string() }), {
19746
+ kind: "mutation",
19747
+ auth: "admin"
19748
+ }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
19749
+ kind: "mutation",
19750
+ auth: "admin"
19751
+ });
19605
19752
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
19606
19753
  providerId: string().min(1),
19607
19754
  displayName: string().min(1),
@@ -19705,6 +19852,28 @@ var TerminalProfileInfoSchema = object({
19705
19852
  label: string(),
19706
19853
  description: string().optional()
19707
19854
  });
19855
+ /**
19856
+ * A durable operator-created Terminal instance. Profiles are templates; only
19857
+ * an instance declares a camera.
19858
+ */
19859
+ var TerminalInstanceInfoSchema = object({
19860
+ instanceId: string(),
19861
+ cameraStableId: string(),
19862
+ nodeId: string(),
19863
+ profileId: string(),
19864
+ profileLabel: string(),
19865
+ name: string(),
19866
+ enabled: boolean()
19867
+ });
19868
+ var TerminalLegacyCameraSchema = object({
19869
+ stableId: string(),
19870
+ nodeId: string(),
19871
+ profileId: string(),
19872
+ profileLabel: string(),
19873
+ name: string(),
19874
+ /** Only legacy monitor cameras can retain their historic stable identity. */
19875
+ adoptable: boolean()
19876
+ });
19708
19877
  var TerminalOutputEventSchema = discriminatedUnion("kind", [object({
19709
19878
  seq: number().int().positive(),
19710
19879
  kind: literal("data"),
@@ -19721,7 +19890,29 @@ var TerminalOutputBatchSchema = object({
19721
19890
  snapshot: string().optional(),
19722
19891
  events: array(TerminalOutputEventSchema).readonly()
19723
19892
  });
19724
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19893
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19894
+ targetNodeId: string().min(1),
19895
+ profileId: string().min(1),
19896
+ name: string().trim().min(1).max(160).optional()
19897
+ }), TerminalInstanceInfoSchema, {
19898
+ kind: "mutation",
19899
+ auth: "admin"
19900
+ }), method(object({ instanceId: string().min(1) }), _void(), {
19901
+ kind: "mutation",
19902
+ auth: "admin"
19903
+ }), method(object({
19904
+ instanceId: string().min(1),
19905
+ enabled: boolean()
19906
+ }), TerminalInstanceInfoSchema, {
19907
+ kind: "mutation",
19908
+ auth: "admin"
19909
+ }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
19910
+ stableId: string().min(1),
19911
+ name: string().trim().min(1).max(160).optional()
19912
+ }), TerminalInstanceInfoSchema, {
19913
+ kind: "mutation",
19914
+ auth: "admin"
19915
+ }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19725
19916
  profileId: string(),
19726
19917
  cols: number().int().positive(),
19727
19918
  rows: number().int().positive()
@@ -19738,7 +19929,13 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19738
19929
  }), method(object({
19739
19930
  sessionId: string(),
19740
19931
  afterSeq: number().int().nonnegative(),
19741
- waitMs: number().int().min(0).max(2e3).default(0)
19932
+ waitMs: number().int().min(0).max(2e3).default(0),
19933
+ /**
19934
+ * Wait when a just-opened session has no output yet. Kept opt-in so a
19935
+ * browser's initial repaint remains immediate; the camera snapshot
19936
+ * relay uses it to avoid encoding a blank startup frame.
19937
+ */
19938
+ waitForOutput: boolean().optional()
19742
19939
  }), TerminalOutputBatchSchema, {
19743
19940
  kind: "mutation",
19744
19941
  auth: "admin",
@@ -25785,13 +25982,19 @@ method(object({
25785
25982
  }), {
25786
25983
  kind: "mutation",
25787
25984
  auth: "admin"
25788
- }), method(RelocateFootageInputSchema, object({ jobId: string() }), {
25985
+ }), method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
25789
25986
  kind: "mutation",
25790
25987
  auth: "admin"
25791
- }), method(object({}), array(RelocateJobSchema).readonly(), {
25792
- kind: "query",
25988
+ }), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
25989
+ kind: "mutation",
25990
+ auth: "admin"
25991
+ }), method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
25992
+ kind: "mutation",
25793
25993
  auth: "admin"
25794
- }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
25994
+ }), method(StorageMigrationFootageMoveInputSchema, object({ jobId: string() }), {
25995
+ kind: "mutation",
25996
+ auth: "admin"
25997
+ }), method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
25795
25998
  kind: "mutation",
25796
25999
  auth: "admin"
25797
26000
  });
@@ -25811,10 +26014,42 @@ method(object({
25811
26014
  */
25812
26015
  /** Playback-speed multiplier for the render (1 = realtime). */
25813
26016
  var ExportSpeedSchema = number().min(.25).max(32);
26017
+ /**
26018
+ * One dense interval, in SECONDS FROM THE EXPORT'S OWN `fromMs`.
26019
+ *
26020
+ * Relative and not absolute epoch on purpose: the renderer's frame-select
26021
+ * expression sees ffmpeg's `t`, which starts at 0 for the export's source
26022
+ * playlist. Handing it absolute epochs would make every call site responsible
26023
+ * for the same subtraction, and the one that forgot would emit a filter that
26024
+ * selects nothing — silently, as a uniform timelapse.
26025
+ */
26026
+ var ExportDenseRangeSchema = object({
26027
+ fromSec: number().nonnegative(),
26028
+ toSec: number().nonnegative()
26029
+ }).refine((r) => r.toSec > r.fromSec, { message: "dense range must have toSec > fromSec" });
26030
+ /**
26031
+ * Dense-interval overlay for a timelapse: sample at `dense.everyMs` INSIDE the
26032
+ * listed ranges and at the base `everyMs` everywhere else.
26033
+ *
26034
+ * `everyMs` must be strictly smaller than the base cadence — a dense rate that
26035
+ * is not denser renders a uniform timelapse the operator believes is two-rate.
26036
+ */
26037
+ var ExportDenseSchema = object({
26038
+ everyMs: number().int().positive(),
26039
+ ranges: array(ExportDenseRangeSchema).min(1).max(200)
26040
+ });
25814
26041
  /** Timelapse cadence — sample one source frame per `everyMs`, output at `outputFps`. */
25815
26042
  var ExportTimelapseSchema = object({
25816
26043
  everyMs: number().int().positive(),
25817
- outputFps: number().int().min(1).max(60).optional()
26044
+ outputFps: number().int().min(1).max(60).optional(),
26045
+ /** Optional second, FASTER rate over the intervals that matter. */
26046
+ dense: ExportDenseSchema.optional()
26047
+ }).superRefine((v, ctx) => {
26048
+ if (v.dense !== void 0 && v.dense.everyMs >= v.everyMs) ctx.addIssue({
26049
+ code: ZodIssueCode.custom,
26050
+ message: "dense.everyMs must be strictly smaller than the base everyMs",
26051
+ path: ["dense", "everyMs"]
26052
+ });
25818
26053
  });
25819
26054
  /**
25820
26055
  * Render options. `speed` and `timelapse` are mutually exclusive. `includeAudio`
@@ -25872,6 +26107,38 @@ var ExportDownloadSchema = object({
25872
26107
  url: string(),
25873
26108
  endpoints: array(string())
25874
26109
  });
26110
+ /**
26111
+ * Hard ceiling on ONE {@link recordingExportCapability} byte read — 50 MiB.
26112
+ *
26113
+ * Two independent reasons land on the same number, which is why it is this one
26114
+ * and not a rounder guess:
26115
+ *
26116
+ * - **Nobody would accept more.** The roomiest byte cap any notifier backend
26117
+ * declares is telegram's 50 MiB, and the degrade engine DROPS an over-cap
26118
+ * attachment outright rather than degrading it to a link. Bytes above this
26119
+ * are read, encoded and moved to be thrown away at the last step.
26120
+ * - **The envelope is unary.** A base64 payload is held whole, ~1.33× its
26121
+ * size, in the provider AND in the caller — on a hub this repo has already
26122
+ * OOM'd once (D9/D18). A bounded on-demand read at human speed is the shape
26123
+ * those records permit; an unbounded one is the shape they forbid.
26124
+ *
26125
+ * Above it the provider REFUSES with a log line rather than truncating: half a
26126
+ * video is worse than a notification that says there is no attachment.
26127
+ */
26128
+ var RECORDING_EXPORT_MAX_READ_BYTES = 50 * 1024 * 1024;
26129
+ /**
26130
+ * A finished export's bytes, inline.
26131
+ *
26132
+ * `bytes` is the DECODED length — the number the caller bounds and logs
26133
+ * against, so nobody has to infer it from the base64 length.
26134
+ */
26135
+ var ExportBytesSchema = object({
26136
+ base64: string(),
26137
+ contentType: string(),
26138
+ /** Suggested filename, extension included. */
26139
+ name: string(),
26140
+ bytes: number().int().nonnegative()
26141
+ });
25875
26142
  method(object({
25876
26143
  deviceId: number(),
25877
26144
  profile: string(),
@@ -25896,6 +26163,9 @@ method(object({
25896
26163
  }), method(object({ exportId: string() }), ExportDownloadSchema, {
25897
26164
  kind: "query",
25898
26165
  auth: "protected"
26166
+ }), method(object({ exportId: string() }), ExportBytesSchema, {
26167
+ kind: "query",
26168
+ auth: "protected"
25899
26169
  });
25900
26170
  /**
25901
26171
  * scene-monitor — device-scoped reference-region state cap. An operator marks
@@ -28362,9 +28632,10 @@ var DeclaredDevices = class {
28362
28632
  }
28363
28633
  const integrationId = spec.integrationId ?? await this.ensureIntegration(spec.integrationName);
28364
28634
  const index = await this.readIndex();
28635
+ const live = await this.readLiveByStableId();
28365
28636
  const outcomes = [];
28366
28637
  for (const declaration of spec.devices) {
28367
- const outcome = await this.applyDeclaration(declaration, integrationId, index);
28638
+ const outcome = await this.applyDeclaration(declaration, integrationId, index, live);
28368
28639
  if (outcome !== null) outcomes.push(outcome);
28369
28640
  }
28370
28641
  return {
@@ -28410,6 +28681,26 @@ var DeclaredDevices = class {
28410
28681
  return new Map(rows.map((row) => [row.stableId, row]));
28411
28682
  }
28412
28683
  /**
28684
+ * Devices this kernel already has CONSTRUCTED, by stableId.
28685
+ *
28686
+ * Distinct from {@link readIndex}, and the distinction is the bug: the index
28687
+ * is persisted rows, this is live objects. A row without an object must be
28688
+ * adopted; an object must be left exactly as it is.
28689
+ *
28690
+ * Failure is non-fatal and deliberately so — an empty map degrades to the
28691
+ * previous behaviour (attempt the adopt) rather than skipping a device that
28692
+ * genuinely needs bringing up.
28693
+ */
28694
+ async readLiveByStableId() {
28695
+ try {
28696
+ const devices = await this.ports.devices.getAll();
28697
+ return new Map(devices.map((device) => [device.stableId, device]));
28698
+ } catch (err) {
28699
+ this.ports.logger.warn("could not read live devices — falling back to adopt-by-row", { meta: { error: err instanceof Error ? err.message : String(err) } });
28700
+ return /* @__PURE__ */ new Map();
28701
+ }
28702
+ }
28703
+ /**
28413
28704
  * One declaration: adopt what exists, create what does not.
28414
28705
  *
28415
28706
  * The create branch is the destructive one — it seeds `initialMeta`, and
@@ -28418,8 +28709,15 @@ var DeclaredDevices = class {
28418
28709
  * the declared name over the operator's rename. D49: that branch needs a
28419
28710
  * second read to agree.
28420
28711
  */
28421
- async applyDeclaration(declaration, integrationId, index) {
28712
+ async applyDeclaration(declaration, integrationId, index, live) {
28422
28713
  try {
28714
+ const alreadyLive = live.get(declaration.stableId);
28715
+ if (alreadyLive !== void 0) return {
28716
+ stableId: declaration.stableId,
28717
+ deviceId: alreadyLive.id,
28718
+ device: alreadyLive,
28719
+ created: false
28720
+ };
28423
28721
  let existing = index.get(declaration.stableId);
28424
28722
  if (existing === void 0) {
28425
28723
  existing = (await this.readIndex()).get(declaration.stableId);
@@ -31524,7 +31822,7 @@ Object.freeze({
31524
31822
  addonId: null,
31525
31823
  access: "create"
31526
31824
  },
31527
- "pipelineAnalytics.cancelMediaRelocate": {
31825
+ "pipelineAnalytics.cancelStorageMigrationMove": {
31528
31826
  capName: "pipeline-analytics",
31529
31827
  capScope: "device",
31530
31828
  addonId: null,
@@ -31596,12 +31894,6 @@ Object.freeze({
31596
31894
  addonId: null,
31597
31895
  access: "view"
31598
31896
  },
31599
- "pipelineAnalytics.getMediaRelocateStatus": {
31600
- capName: "pipeline-analytics",
31601
- capScope: "device",
31602
- addonId: null,
31603
- access: "view"
31604
- },
31605
31897
  "pipelineAnalytics.getMotionEvents": {
31606
31898
  capName: "pipeline-analytics",
31607
31899
  capScope: "device",
@@ -31638,6 +31930,12 @@ Object.freeze({
31638
31930
  addonId: null,
31639
31931
  access: "view"
31640
31932
  },
31933
+ "pipelineAnalytics.getStorageMigrationMoveStatus": {
31934
+ capName: "pipeline-analytics",
31935
+ capScope: "device",
31936
+ addonId: null,
31937
+ access: "view"
31938
+ },
31641
31939
  "pipelineAnalytics.getTrack": {
31642
31940
  capName: "pipeline-analytics",
31643
31941
  capScope: "device",
@@ -31716,6 +32014,12 @@ Object.freeze({
31716
32014
  addonId: null,
31717
32015
  access: "view"
31718
32016
  },
32017
+ "pipelineAnalytics.pauseForStorageMigration": {
32018
+ capName: "pipeline-analytics",
32019
+ capScope: "device",
32020
+ addonId: null,
32021
+ access: "create"
32022
+ },
31719
32023
  "pipelineAnalytics.proposeRetrainAnnotations": {
31720
32024
  capName: "pipeline-analytics",
31721
32025
  capScope: "device",
@@ -31746,7 +32050,7 @@ Object.freeze({
31746
32050
  addonId: null,
31747
32051
  access: "create"
31748
32052
  },
31749
- "pipelineAnalytics.relocateMedia": {
32053
+ "pipelineAnalytics.refreshStorageLocationsForMigration": {
31750
32054
  capName: "pipeline-analytics",
31751
32055
  capScope: "device",
31752
32056
  addonId: null,
@@ -31758,6 +32062,12 @@ Object.freeze({
31758
32062
  addonId: null,
31759
32063
  access: "create"
31760
32064
  },
32065
+ "pipelineAnalytics.resumeForStorageMigration": {
32066
+ capName: "pipeline-analytics",
32067
+ capScope: "device",
32068
+ addonId: null,
32069
+ access: "create"
32070
+ },
31761
32071
  "pipelineAnalytics.saveRetrainAnnotations": {
31762
32072
  capName: "pipeline-analytics",
31763
32073
  capScope: "device",
@@ -31782,6 +32092,12 @@ Object.freeze({
31782
32092
  addonId: null,
31783
32093
  access: "create"
31784
32094
  },
32095
+ "pipelineAnalytics.startStorageMigrationMove": {
32096
+ capName: "pipeline-analytics",
32097
+ capScope: "device",
32098
+ addonId: null,
32099
+ access: "create"
32100
+ },
31785
32101
  "pipelineAnalytics.wipeAllAnalytics": {
31786
32102
  capName: "pipeline-analytics",
31787
32103
  capScope: "device",
@@ -32148,6 +32464,12 @@ Object.freeze({
32148
32464
  addonId: null,
32149
32465
  access: "view"
32150
32466
  },
32467
+ "pipelineOrchestrator.pauseForStorageMigration": {
32468
+ capName: "pipeline-orchestrator",
32469
+ capScope: "system",
32470
+ addonId: null,
32471
+ access: "create"
32472
+ },
32151
32473
  "pipelineOrchestrator.rebalance": {
32152
32474
  capName: "pipeline-orchestrator",
32153
32475
  capScope: "system",
@@ -32172,6 +32494,12 @@ Object.freeze({
32172
32494
  addonId: null,
32173
32495
  access: "view"
32174
32496
  },
32497
+ "pipelineOrchestrator.resumeForStorageMigration": {
32498
+ capName: "pipeline-orchestrator",
32499
+ capScope: "system",
32500
+ addonId: null,
32501
+ access: "create"
32502
+ },
32175
32503
  "pipelineOrchestrator.saveTemplate": {
32176
32504
  capName: "pipeline-orchestrator",
32177
32505
  capScope: "system",
@@ -32568,7 +32896,7 @@ Object.freeze({
32568
32896
  addonId: null,
32569
32897
  access: "create"
32570
32898
  },
32571
- "recording.cancelRelocate": {
32899
+ "recording.cancelStorageMigrationMove": {
32572
32900
  capName: "recording",
32573
32901
  capScope: "system",
32574
32902
  addonId: null,
@@ -32604,7 +32932,7 @@ Object.freeze({
32604
32932
  addonId: null,
32605
32933
  access: "view"
32606
32934
  },
32607
- "recording.getRelocateStatus": {
32935
+ "recording.getStorageMigrationMoveStatus": {
32608
32936
  capName: "recording",
32609
32937
  capScope: "system",
32610
32938
  addonId: null,
@@ -32628,6 +32956,12 @@ Object.freeze({
32628
32956
  addonId: null,
32629
32957
  access: "view"
32630
32958
  },
32959
+ "recording.pauseForStorageMigration": {
32960
+ capName: "recording",
32961
+ capScope: "system",
32962
+ addonId: null,
32963
+ access: "create"
32964
+ },
32631
32965
  "recording.pruneFootage": {
32632
32966
  capName: "recording",
32633
32967
  capScope: "system",
@@ -32646,7 +32980,7 @@ Object.freeze({
32646
32980
  addonId: null,
32647
32981
  access: "view"
32648
32982
  },
32649
- "recording.relocateFootage": {
32983
+ "recording.refreshStorageLocationsForMigration": {
32650
32984
  capName: "recording",
32651
32985
  capScope: "system",
32652
32986
  addonId: null,
@@ -32670,12 +33004,24 @@ Object.freeze({
32670
33004
  addonId: null,
32671
33005
  access: "create"
32672
33006
  },
33007
+ "recording.resumeForStorageMigration": {
33008
+ capName: "recording",
33009
+ capScope: "system",
33010
+ addonId: null,
33011
+ access: "create"
33012
+ },
32673
33013
  "recording.setDeviceConfig": {
32674
33014
  capName: "recording",
32675
33015
  capScope: "system",
32676
33016
  addonId: null,
32677
33017
  access: "create"
32678
33018
  },
33019
+ "recording.startStorageMigrationMove": {
33020
+ capName: "recording",
33021
+ capScope: "system",
33022
+ addonId: null,
33023
+ access: "create"
33024
+ },
32679
33025
  "recordingExport.cancelExport": {
32680
33026
  capName: "recordingExport",
32681
33027
  capScope: "system",
@@ -32712,6 +33058,12 @@ Object.freeze({
32712
33058
  addonId: null,
32713
33059
  access: "view"
32714
33060
  },
33061
+ "recordingExport.readExportBytes": {
33062
+ capName: "recordingExport",
33063
+ capScope: "system",
33064
+ addonId: null,
33065
+ access: "view"
33066
+ },
32715
33067
  "sceneMonitor.captureReference": {
32716
33068
  capName: "scene-monitor",
32717
33069
  capScope: "device",
@@ -33066,6 +33418,30 @@ Object.freeze({
33066
33418
  addonId: null,
33067
33419
  access: "view"
33068
33420
  },
33421
+ "storageMigration.cancel": {
33422
+ capName: "storage-migration",
33423
+ capScope: "system",
33424
+ addonId: null,
33425
+ access: "create"
33426
+ },
33427
+ "storageMigration.plan": {
33428
+ capName: "storage-migration",
33429
+ capScope: "system",
33430
+ addonId: null,
33431
+ access: "view"
33432
+ },
33433
+ "storageMigration.start": {
33434
+ capName: "storage-migration",
33435
+ capScope: "system",
33436
+ addonId: null,
33437
+ access: "create"
33438
+ },
33439
+ "storageMigration.status": {
33440
+ capName: "storage-migration",
33441
+ capScope: "system",
33442
+ addonId: null,
33443
+ access: "view"
33444
+ },
33069
33445
  "storageProvider.abortUpload": {
33070
33446
  capName: "storage-provider",
33071
33447
  capScope: "system",
@@ -33444,12 +33820,42 @@ Object.freeze({
33444
33820
  addonId: null,
33445
33821
  access: "create"
33446
33822
  },
33823
+ "terminalSession.adoptLegacyMonitor": {
33824
+ capName: "terminal-session",
33825
+ capScope: "system",
33826
+ addonId: null,
33827
+ access: "create"
33828
+ },
33447
33829
  "terminalSession.close": {
33448
33830
  capName: "terminal-session",
33449
33831
  capScope: "system",
33450
33832
  addonId: null,
33451
33833
  access: "create"
33452
33834
  },
33835
+ "terminalSession.createInstance": {
33836
+ capName: "terminal-session",
33837
+ capScope: "system",
33838
+ addonId: null,
33839
+ access: "create"
33840
+ },
33841
+ "terminalSession.deleteInstance": {
33842
+ capName: "terminal-session",
33843
+ capScope: "system",
33844
+ addonId: null,
33845
+ access: "delete"
33846
+ },
33847
+ "terminalSession.listInstances": {
33848
+ capName: "terminal-session",
33849
+ capScope: "system",
33850
+ addonId: null,
33851
+ access: "view"
33852
+ },
33853
+ "terminalSession.listLegacyCameras": {
33854
+ capName: "terminal-session",
33855
+ capScope: "system",
33856
+ addonId: null,
33857
+ access: "view"
33858
+ },
33453
33859
  "terminalSession.listProfiles": {
33454
33860
  capName: "terminal-session",
33455
33861
  capScope: "system",
@@ -33480,6 +33886,12 @@ Object.freeze({
33480
33886
  addonId: null,
33481
33887
  access: "create"
33482
33888
  },
33889
+ "terminalSession.setInstanceEnabled": {
33890
+ capName: "terminal-session",
33891
+ capScope: "system",
33892
+ addonId: null,
33893
+ access: "create"
33894
+ },
33483
33895
  "terminalSession.writeInput": {
33484
33896
  capName: "terminal-session",
33485
33897
  capScope: "system",
@@ -34109,7 +34521,23 @@ var TimelapseRuleInputSchema = object({
34109
34521
  /** Canonical notification priority ordinal (1..5); per-target overridable. */
34110
34522
  priority: PriorityField.default(3)
34111
34523
  });
34112
- object({
34524
+ /**
34525
+ * Partial patch for an update — any subset of the INPUT fields, with NO
34526
+ * defaults (an absent key means "leave unchanged", never "reset to default").
34527
+ * Provenance and ownership are absent by construction: a patch can rename or
34528
+ * retune a rule, never re-own it or forge its generation state.
34529
+ *
34530
+ * CLEAR SIGNAL: `template` is the one OPTIONAL field an editor can REMOVE, so
34531
+ * it is `.nullable()` here and NOWHERE else. Wire representation:
34532
+ * - key absent → leave the template unchanged
34533
+ * - `"template": null` → CLEAR it (the persisted rule loses the key)
34534
+ * - `"template": {...}` → replace it
34535
+ * `undefined` is deliberately NOT the clear signal: it does not survive
34536
+ * `JSON.stringify`, so a viewer editor could not express "remove". The
34537
+ * PERSISTED rule never carries `null` — the store drops the key (see
34538
+ * `TimelapseStore.update`).
34539
+ */
34540
+ var TimelapseRulePatchSchema = object({
34113
34541
  name: NameField.optional(),
34114
34542
  enabled: boolean().optional(),
34115
34543
  deviceIds: DeviceIdsField.optional(),
@@ -34130,15 +34558,50 @@ var TimelapseRuleSchema = TimelapseRuleInputSchema.extend({
34130
34558
  */
34131
34559
  ownerUserId: string().optional(),
34132
34560
  /**
34133
- * Epoch-ms of the last successful generation — the 1-hour re-generation
34134
- * guard's durable state (predecessor parity). Absent = never generated.
34561
+ * Epoch-ms of the NEWEST successful generation across every camera of this
34562
+ * rule. What a UI shows, and the compatibility floor for
34563
+ * {@link readTimelapseGeneratedAt}. Absent = never generated.
34135
34564
  */
34136
34565
  lastGeneratedAt: number().optional(),
34566
+ /**
34567
+ * PER-CAMERA generation state, keyed by `String(deviceId)` — the
34568
+ * re-generation guard's real durable state.
34569
+ *
34570
+ * One rule covers several cameras and each renders its own video, so a rule
34571
+ * -wide stamp is wrong in the direction that DESTROYS work: camera A
34572
+ * succeeding at 06:05 tells camera B, whose render failed, that it is
34573
+ * already done — and B's night is gone for good, because the window will not
34574
+ * come back.
34575
+ *
34576
+ * ADDITIVE, so the migration is free: a row written before this field simply
34577
+ * has no map, and {@link readTimelapseGeneratedAt} falls back to
34578
+ * {@link TimelapseRuleSchema.shape.lastGeneratedAt}. Reading an old row as
34579
+ * "never generated" would re-render and re-notify every camera of every rule
34580
+ * once, on the deploy that shipped the map.
34581
+ */
34582
+ generatedByDevice: record(string(), number()).optional(),
34137
34583
  /** userId of the caller who created the rule (server-stamped). */
34138
34584
  createdBy: string(),
34139
34585
  createdAt: number(),
34140
34586
  updatedAt: number()
34141
34587
  });
34588
+ /**
34589
+ * The last successful generation for ONE camera of a rule, epoch-ms.
34590
+ *
34591
+ * The per-device map wins; a rule with no map falls back to the rule-wide
34592
+ * `lastGeneratedAt` (the compatible-migration path — see
34593
+ * {@link TimelapseRuleSchema}); a rule with neither returns 0, which every
34594
+ * guard reads as "never generated".
34595
+ *
34596
+ * Read through this helper and never off the field directly: the fallback is
34597
+ * the whole migration, and a call site that forgot it would re-render an
34598
+ * entire rule set once.
34599
+ */
34600
+ function readTimelapseGeneratedAt(rule, deviceId) {
34601
+ const map = rule.generatedByDevice;
34602
+ if (map !== void 0) return map[String(deviceId)] ?? 0;
34603
+ return rule.lastGeneratedAt ?? 0;
34604
+ }
34142
34605
  object({
34143
34606
  /**
34144
34607
  * Fraction of the box's own size added on EACH side before cutting.
@@ -34417,6 +34880,12 @@ Object.defineProperty(exports, "OpsLogEntrySchema", {
34417
34880
  return OpsLogEntrySchema;
34418
34881
  }
34419
34882
  });
34883
+ Object.defineProperty(exports, "RECORDING_EXPORT_MAX_READ_BYTES", {
34884
+ enumerable: true,
34885
+ get: function() {
34886
+ return RECORDING_EXPORT_MAX_READ_BYTES;
34887
+ }
34888
+ });
34420
34889
  Object.defineProperty(exports, "RetrainStatusSchema", {
34421
34890
  enumerable: true,
34422
34891
  get: function() {
@@ -34429,6 +34898,12 @@ Object.defineProperty(exports, "TimelapseRuleInputSchema", {
34429
34898
  return TimelapseRuleInputSchema;
34430
34899
  }
34431
34900
  });
34901
+ Object.defineProperty(exports, "TimelapseRulePatchSchema", {
34902
+ enumerable: true,
34903
+ get: function() {
34904
+ return TimelapseRulePatchSchema;
34905
+ }
34906
+ });
34432
34907
  Object.defineProperty(exports, "TimelapseRuleSchema", {
34433
34908
  enumerable: true,
34434
34909
  get: function() {
@@ -34519,6 +34994,12 @@ Object.defineProperty(exports, "defineCustomActions", {
34519
34994
  return defineCustomActions;
34520
34995
  }
34521
34996
  });
34997
+ Object.defineProperty(exports, "deriveRecordingMode", {
34998
+ enumerable: true,
34999
+ get: function() {
35000
+ return deriveRecordingMode;
35001
+ }
35002
+ });
34522
35003
  Object.defineProperty(exports, "embeddingEncoderCapability", {
34523
35004
  enumerable: true,
34524
35005
  get: function() {
@@ -34621,6 +35102,12 @@ Object.defineProperty(exports, "readDeviceStateFrom", {
34621
35102
  return readDeviceStateFrom;
34622
35103
  }
34623
35104
  });
35105
+ Object.defineProperty(exports, "readTimelapseGeneratedAt", {
35106
+ enumerable: true,
35107
+ get: function() {
35108
+ return readTimelapseGeneratedAt;
35109
+ }
35110
+ });
34624
35111
  Object.defineProperty(exports, "record", {
34625
35112
  enumerable: true,
34626
35113
  get: function() {