@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.
@@ -7390,8 +7390,31 @@ var AdoptionJobSchema = object({
7390
7390
  error: string().nullable()
7391
7391
  });
7392
7392
  /**
7393
- * Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
7394
- * pipeline functions an operator thinks in terms of.
7393
+ * Per-camera FUNCTION SWITCHES.
7394
+ *
7395
+ * ## The aggregate group is being withdrawn — the BADGE is not (D113)
7396
+ *
7397
+ * This file shipped as "the one coherent on/off surface over the pipeline
7398
+ * functions an operator thinks in terms of". The operator's verdict on
7399
+ * 2026-08-12 was that the coherent surface bought complexity and no clarity:
7400
+ * every function already had a settings page of its own, and a second place to
7401
+ * turn it off is a second place to look. Each switch is going back to its own
7402
+ * component's original options — detection to the detection-pipeline wrapper
7403
+ * binding, audio analysis to its own, recording to `RecordingConfig.enabled`
7404
+ * (which was always first-class; the switch was a veneer over
7405
+ * `recording.setDeviceConfig`), notifications to a notification-center
7406
+ * per-device setting, the two camera planes to their own components.
7407
+ *
7408
+ * What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
7409
+ * thing that lets a status surface say DISABLED instead of BROKEN, recomposed
7410
+ * straight from the authorities with no group in the middle. That rule was
7411
+ * never about a control panel.
7412
+ *
7413
+ * Everything else here — {@link CAMERA_SWITCH_CATALOG}, {@link CameraSwitch},
7414
+ * {@link deriveCameraSwitches}, the `pipelineOrchestrator.getCameraSwitches` /
7415
+ * `setCameraSwitch` pair — is a COMPATIBILITY surface for as long as deployed
7416
+ * viewers (v1.0.305) and the admin UI still call it. It is deleted when they
7417
+ * stop; nothing new may be built on it.
7395
7418
  *
7396
7419
  * ## This file adds no state
7397
7420
  *
@@ -7753,14 +7776,29 @@ var RecordingConfigSchema = object({
7753
7776
  scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7754
7777
  }).strict();
7755
7778
  /**
7779
+ * Derive the {@link RecordingStorageModeSchema} summary from the authoritative
7780
+ * bands: `continuous` when any band records continuously, `events` when a band
7781
+ * exists at all, else `off`. Pure.
7782
+ *
7783
+ * THE one definition: the recorder stamps `config.mode` with it on save and
7784
+ * reports it as the cap's `activeMode`, and the settings UI reads it back to
7785
+ * decide which authoring tab a config opens in. Two copies would drift, and the
7786
+ * symptom (a status dot disagreeing with what is being recorded) is exactly the
7787
+ * bug this field was added to fix.
7788
+ */
7789
+ function deriveRecordingMode(config) {
7790
+ if (!config.enabled || config.bands.length === 0) return "off";
7791
+ if (config.bands.some((band) => band.mode === "continuous")) return "continuous";
7792
+ return "events";
7793
+ }
7794
+ /**
7756
7795
  * Entity-relocation job state (storage entity-routing spec, Phase 4).
7757
7796
  *
7758
- * One shape shared by the recorder's `relocateFootage` (segments) and
7759
- * pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
7760
- * page renders both movers with one component. Jobs are in-RAM (a restart
7761
- * forgets them — re-running is safe by construction: copy-if-absent, delete
7762
- * after verify) and each completed/failed run also lands one durable ops-log
7763
- * row on the owning addon's surface.
7797
+ * One shape shared by the recorder and pipeline-analytics internal movers.
7798
+ * The public admin surface is `storage-migration`; child jobs remain in RAM
7799
+ * because copy-if-absent, verify, delete and index/row repoint are resumable.
7800
+ * Each completed/failed run also lands one durable ops-log row on its owning
7801
+ * addon surface.
7764
7802
  */
7765
7803
  var RelocateJobStateSchema = _enum([
7766
7804
  "running",
@@ -7787,19 +7825,100 @@ var RelocateJobSchema = object({
7787
7825
  finishedAt: number().nullable(),
7788
7826
  error: string().nullable()
7789
7827
  });
7828
+ /** Profile-derived footage selection used only by the migration coordinator:
7829
+ * `recordings` owns high+mid; `recordingsLow` owns low. */
7830
+ var RelocateFootageClassSchema = _enum(["recordings", "recordingsLow"]);
7790
7831
  var RelocateFootageInputSchema = object({
7791
- deviceId: number().optional(),
7792
7832
  fromLocationId: string(),
7793
7833
  toLocationId: string(),
7794
7834
  entities: array(_enum(["segments"])).optional(),
7835
+ /** Limits relocation to the logical profile class. Omit only for the
7836
+ * pre-orchestration compatibility path. */
7837
+ footageClass: RelocateFootageClassSchema.optional(),
7795
7838
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7796
7839
  * never allowed to starve live writers. */
7797
7840
  throttleMbps: number().min(1).max(1e3).optional()
7798
7841
  });
7799
- var RelocateMediaInputSchema = object({
7800
- deviceId: number().optional(),
7842
+ /** Internal, lease-scoped participant operation. It is intentionally separate
7843
+ * from persistent recording settings: a migration never changes
7844
+ * `RecordingConfig.enabled` or camera wrapper bindings. */
7845
+ var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
7846
+ var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
7847
+ var StorageMigrationMediaMoveInputSchema = object({
7801
7848
  toLocationId: string(),
7802
7849
  throttleMbps: number().min(1).max(1e3).optional()
7850
+ }).extend({ leaseId: string().min(1) });
7851
+ /** The independently selectable logical storage classes. `recordings`
7852
+ * encompasses the high and mid segment profiles; `recordingsLow` is low
7853
+ * segments; `eventMedia` is post-analysis blobs. */
7854
+ var StorageMigrationClassSchema = _enum([
7855
+ "recordings",
7856
+ "recordingsLow",
7857
+ "eventMedia"
7858
+ ]);
7859
+ /** A destination is always an existing, fully-qualified location id. The
7860
+ * migration API intentionally never changes a source location's `basePath`:
7861
+ * callers create a new `<type>:<slug>` location, then select it here. */
7862
+ var StorageMigrationDestinationsSchema = object({
7863
+ recordings: string().min(1).optional(),
7864
+ recordingsLow: string().min(1).optional(),
7865
+ eventMedia: string().min(1).optional()
7866
+ }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
7867
+ /** Shared input for planning and starting an orchestrated storage migration. */
7868
+ var StorageMigrationInputSchema = object({
7869
+ destinations: StorageMigrationDestinationsSchema,
7870
+ throttleMbps: number().min(1).max(1e3).optional()
7871
+ });
7872
+ /** The durable coordinator state machine. The only phase that changes default
7873
+ * locations is `repointing`, after every selected mover has completed and been
7874
+ * verified. */
7875
+ var StorageMigrationPhaseSchema = _enum([
7876
+ "planning",
7877
+ "pausing",
7878
+ "moving",
7879
+ "verifying",
7880
+ "repointing",
7881
+ "refreshing",
7882
+ "resuming",
7883
+ "done",
7884
+ "failed",
7885
+ "cancelled"
7886
+ ]);
7887
+ var StorageMigrationParticipantSchema = _enum([
7888
+ "pipeline",
7889
+ "recorder",
7890
+ "analytics"
7891
+ ]);
7892
+ var StorageMigrationMoveSchema = object({
7893
+ storageClass: StorageMigrationClassSchema,
7894
+ fromLocationId: string(),
7895
+ toLocationId: string(),
7896
+ moverJobId: string().nullable(),
7897
+ state: RelocateJobStateSchema.nullable(),
7898
+ error: string().nullable()
7899
+ });
7900
+ var StorageMigrationJobSchema = object({
7901
+ jobId: string(),
7902
+ phase: StorageMigrationPhaseSchema,
7903
+ destinations: StorageMigrationDestinationsSchema,
7904
+ throttleMbps: number(),
7905
+ moves: array(StorageMigrationMoveSchema),
7906
+ pauseLeaseId: string().nullable(),
7907
+ pausedParticipants: array(StorageMigrationParticipantSchema),
7908
+ repointed: boolean(),
7909
+ cancelRequested: boolean(),
7910
+ startedAt: number(),
7911
+ updatedAt: number(),
7912
+ finishedAt: number().nullable(),
7913
+ error: string().nullable()
7914
+ });
7915
+ var StorageMigrationPlanSchema = object({
7916
+ destinations: StorageMigrationDestinationsSchema,
7917
+ moves: array(object({
7918
+ storageClass: StorageMigrationClassSchema,
7919
+ fromLocationId: string(),
7920
+ toLocationId: string()
7921
+ }))
7803
7922
  });
7804
7923
  /**
7805
7924
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -17118,20 +17237,28 @@ var pipelineAnalyticsCapability = {
17118
17237
  }),
17119
17238
  /** The events ops-log rows (newest-first), optionally scoped to one camera.
17120
17239
  * Backed by a declared pipeline-analytics SQLite collection. */
17121
- /**
17122
- * Move event-media blobs onto `toLocationId` (entity-routing spec Phase
17123
- * 4): per-row copy → stamp row.locationId → delete old blob. Rows already
17124
- * at the target are skipped, so a re-run resumes. Single-flight.
17125
- */
17126
- relocateMedia: method(RelocateMediaInputSchema, object({ jobId: string() }), {
17240
+ /** Internal migration-participant lease. It drains active MediaStore
17241
+ * writes and refuses new ones without changing analytics bindings. */
17242
+ pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
17127
17243
  kind: "mutation",
17128
17244
  auth: "admin"
17129
17245
  }),
17130
- getMediaRelocateStatus: method(object({}), array(RelocateJobSchema).readonly(), {
17131
- kind: "query",
17246
+ resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
17247
+ kind: "mutation",
17248
+ auth: "admin"
17249
+ }),
17250
+ /** Drops cached location roots after the storage coordinator repoints a
17251
+ * default so future event-media writes use the new root. */
17252
+ refreshStorageLocationsForMigration: method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
17253
+ kind: "mutation",
17254
+ auth: "admin"
17255
+ }),
17256
+ startStorageMigrationMove: method(StorageMigrationMediaMoveInputSchema, object({ jobId: string() }), {
17257
+ kind: "mutation",
17132
17258
  auth: "admin"
17133
17259
  }),
17134
- cancelMediaRelocate: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
17260
+ getStorageMigrationMoveStatus: method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }),
17261
+ cancelStorageMigrationMove: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
17135
17262
  kind: "mutation",
17136
17263
  auth: "admin"
17137
17264
  }),
@@ -18794,9 +18921,16 @@ var CameraStatusSchema = object({
18794
18921
  audio: CameraAudioStatusSchema.nullable(),
18795
18922
  recording: CameraRecordingStatusSchema.nullable(),
18796
18923
  /**
18797
- * Per-camera function switches an OPERATOR has turned off
18924
+ * Per-camera functions an OPERATOR has turned off
18798
18925
  * ([D61](../../../../docs/decisions/adr-0067.md)).
18799
18926
  *
18927
+ * Composed from the AUTHORITIES themselves — the wrapper bindings,
18928
+ * `RecordingConfig.enabled`, the notification mute, the broker's audio
18929
+ * policy, the camera's own microphone — via `composeSwitchedOff`, not from
18930
+ * the deprecated `getCameraSwitches` group ([D113](../../../../docs/decisions/adr-0113.md)).
18931
+ * The badge outlives the control panel: the panel was a convenience, this is
18932
+ * the difference between a camera being off and a camera being dead.
18933
+ *
18800
18934
  * This is the difference between DISABLED and BROKEN. A camera whose
18801
18935
  * `detection` block reports zero fps and whose `switchedOff` contains
18802
18936
  * `'object-detection'` was switched off by a person; the same camera with an
@@ -18867,7 +19001,13 @@ var NodeInferenceDevicesSchema = object({
18867
19001
  reachable: boolean(),
18868
19002
  devices: array(NodeInferenceDeviceSchema).readonly()
18869
19003
  });
18870
- method(object({
19004
+ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19005
+ kind: "mutation",
19006
+ auth: "admin"
19007
+ }), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
19008
+ kind: "mutation",
19009
+ auth: "admin"
19010
+ }), method(object({
18871
19011
  deviceId: number(),
18872
19012
  agentNodeId: string()
18873
19013
  }), object({ success: literal(true) }), {
@@ -19571,6 +19711,13 @@ method(object({ locationId: string() }), EvictableUsageSchema), method(object({
19571
19711
  locationId: string(),
19572
19712
  targetBytes: number().int().positive()
19573
19713
  }), EvictResultSchema, { kind: "mutation" });
19714
+ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin" }), method(StorageMigrationInputSchema, object({ jobId: string() }), {
19715
+ kind: "mutation",
19716
+ auth: "admin"
19717
+ }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
19718
+ kind: "mutation",
19719
+ auth: "admin"
19720
+ });
19574
19721
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
19575
19722
  providerId: string().min(1),
19576
19723
  displayName: string().min(1),
@@ -19674,6 +19821,28 @@ var TerminalProfileInfoSchema = object({
19674
19821
  label: string(),
19675
19822
  description: string().optional()
19676
19823
  });
19824
+ /**
19825
+ * A durable operator-created Terminal instance. Profiles are templates; only
19826
+ * an instance declares a camera.
19827
+ */
19828
+ var TerminalInstanceInfoSchema = object({
19829
+ instanceId: string(),
19830
+ cameraStableId: string(),
19831
+ nodeId: string(),
19832
+ profileId: string(),
19833
+ profileLabel: string(),
19834
+ name: string(),
19835
+ enabled: boolean()
19836
+ });
19837
+ var TerminalLegacyCameraSchema = object({
19838
+ stableId: string(),
19839
+ nodeId: string(),
19840
+ profileId: string(),
19841
+ profileLabel: string(),
19842
+ name: string(),
19843
+ /** Only legacy monitor cameras can retain their historic stable identity. */
19844
+ adoptable: boolean()
19845
+ });
19677
19846
  var TerminalOutputEventSchema = discriminatedUnion("kind", [object({
19678
19847
  seq: number().int().positive(),
19679
19848
  kind: literal("data"),
@@ -19690,7 +19859,29 @@ var TerminalOutputBatchSchema = object({
19690
19859
  snapshot: string().optional(),
19691
19860
  events: array(TerminalOutputEventSchema).readonly()
19692
19861
  });
19693
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19862
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19863
+ targetNodeId: string().min(1),
19864
+ profileId: string().min(1),
19865
+ name: string().trim().min(1).max(160).optional()
19866
+ }), TerminalInstanceInfoSchema, {
19867
+ kind: "mutation",
19868
+ auth: "admin"
19869
+ }), method(object({ instanceId: string().min(1) }), _void(), {
19870
+ kind: "mutation",
19871
+ auth: "admin"
19872
+ }), method(object({
19873
+ instanceId: string().min(1),
19874
+ enabled: boolean()
19875
+ }), TerminalInstanceInfoSchema, {
19876
+ kind: "mutation",
19877
+ auth: "admin"
19878
+ }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
19879
+ stableId: string().min(1),
19880
+ name: string().trim().min(1).max(160).optional()
19881
+ }), TerminalInstanceInfoSchema, {
19882
+ kind: "mutation",
19883
+ auth: "admin"
19884
+ }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19694
19885
  profileId: string(),
19695
19886
  cols: number().int().positive(),
19696
19887
  rows: number().int().positive()
@@ -19707,7 +19898,13 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19707
19898
  }), method(object({
19708
19899
  sessionId: string(),
19709
19900
  afterSeq: number().int().nonnegative(),
19710
- waitMs: number().int().min(0).max(2e3).default(0)
19901
+ waitMs: number().int().min(0).max(2e3).default(0),
19902
+ /**
19903
+ * Wait when a just-opened session has no output yet. Kept opt-in so a
19904
+ * browser's initial repaint remains immediate; the camera snapshot
19905
+ * relay uses it to avoid encoding a blank startup frame.
19906
+ */
19907
+ waitForOutput: boolean().optional()
19711
19908
  }), TerminalOutputBatchSchema, {
19712
19909
  kind: "mutation",
19713
19910
  auth: "admin",
@@ -25754,13 +25951,19 @@ method(object({
25754
25951
  }), {
25755
25952
  kind: "mutation",
25756
25953
  auth: "admin"
25757
- }), method(RelocateFootageInputSchema, object({ jobId: string() }), {
25954
+ }), method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
25758
25955
  kind: "mutation",
25759
25956
  auth: "admin"
25760
- }), method(object({}), array(RelocateJobSchema).readonly(), {
25761
- kind: "query",
25957
+ }), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
25958
+ kind: "mutation",
25959
+ auth: "admin"
25960
+ }), method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
25961
+ kind: "mutation",
25962
+ auth: "admin"
25963
+ }), method(StorageMigrationFootageMoveInputSchema, object({ jobId: string() }), {
25964
+ kind: "mutation",
25762
25965
  auth: "admin"
25763
- }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
25966
+ }), method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
25764
25967
  kind: "mutation",
25765
25968
  auth: "admin"
25766
25969
  });
@@ -25780,10 +25983,42 @@ method(object({
25780
25983
  */
25781
25984
  /** Playback-speed multiplier for the render (1 = realtime). */
25782
25985
  var ExportSpeedSchema = number().min(.25).max(32);
25986
+ /**
25987
+ * One dense interval, in SECONDS FROM THE EXPORT'S OWN `fromMs`.
25988
+ *
25989
+ * Relative and not absolute epoch on purpose: the renderer's frame-select
25990
+ * expression sees ffmpeg's `t`, which starts at 0 for the export's source
25991
+ * playlist. Handing it absolute epochs would make every call site responsible
25992
+ * for the same subtraction, and the one that forgot would emit a filter that
25993
+ * selects nothing — silently, as a uniform timelapse.
25994
+ */
25995
+ var ExportDenseRangeSchema = object({
25996
+ fromSec: number().nonnegative(),
25997
+ toSec: number().nonnegative()
25998
+ }).refine((r) => r.toSec > r.fromSec, { message: "dense range must have toSec > fromSec" });
25999
+ /**
26000
+ * Dense-interval overlay for a timelapse: sample at `dense.everyMs` INSIDE the
26001
+ * listed ranges and at the base `everyMs` everywhere else.
26002
+ *
26003
+ * `everyMs` must be strictly smaller than the base cadence — a dense rate that
26004
+ * is not denser renders a uniform timelapse the operator believes is two-rate.
26005
+ */
26006
+ var ExportDenseSchema = object({
26007
+ everyMs: number().int().positive(),
26008
+ ranges: array(ExportDenseRangeSchema).min(1).max(200)
26009
+ });
25783
26010
  /** Timelapse cadence — sample one source frame per `everyMs`, output at `outputFps`. */
25784
26011
  var ExportTimelapseSchema = object({
25785
26012
  everyMs: number().int().positive(),
25786
- outputFps: number().int().min(1).max(60).optional()
26013
+ outputFps: number().int().min(1).max(60).optional(),
26014
+ /** Optional second, FASTER rate over the intervals that matter. */
26015
+ dense: ExportDenseSchema.optional()
26016
+ }).superRefine((v, ctx) => {
26017
+ if (v.dense !== void 0 && v.dense.everyMs >= v.everyMs) ctx.addIssue({
26018
+ code: ZodIssueCode.custom,
26019
+ message: "dense.everyMs must be strictly smaller than the base everyMs",
26020
+ path: ["dense", "everyMs"]
26021
+ });
25787
26022
  });
25788
26023
  /**
25789
26024
  * Render options. `speed` and `timelapse` are mutually exclusive. `includeAudio`
@@ -25841,6 +26076,38 @@ var ExportDownloadSchema = object({
25841
26076
  url: string(),
25842
26077
  endpoints: array(string())
25843
26078
  });
26079
+ /**
26080
+ * Hard ceiling on ONE {@link recordingExportCapability} byte read — 50 MiB.
26081
+ *
26082
+ * Two independent reasons land on the same number, which is why it is this one
26083
+ * and not a rounder guess:
26084
+ *
26085
+ * - **Nobody would accept more.** The roomiest byte cap any notifier backend
26086
+ * declares is telegram's 50 MiB, and the degrade engine DROPS an over-cap
26087
+ * attachment outright rather than degrading it to a link. Bytes above this
26088
+ * are read, encoded and moved to be thrown away at the last step.
26089
+ * - **The envelope is unary.** A base64 payload is held whole, ~1.33× its
26090
+ * size, in the provider AND in the caller — on a hub this repo has already
26091
+ * OOM'd once (D9/D18). A bounded on-demand read at human speed is the shape
26092
+ * those records permit; an unbounded one is the shape they forbid.
26093
+ *
26094
+ * Above it the provider REFUSES with a log line rather than truncating: half a
26095
+ * video is worse than a notification that says there is no attachment.
26096
+ */
26097
+ var RECORDING_EXPORT_MAX_READ_BYTES = 50 * 1024 * 1024;
26098
+ /**
26099
+ * A finished export's bytes, inline.
26100
+ *
26101
+ * `bytes` is the DECODED length — the number the caller bounds and logs
26102
+ * against, so nobody has to infer it from the base64 length.
26103
+ */
26104
+ var ExportBytesSchema = object({
26105
+ base64: string(),
26106
+ contentType: string(),
26107
+ /** Suggested filename, extension included. */
26108
+ name: string(),
26109
+ bytes: number().int().nonnegative()
26110
+ });
25844
26111
  method(object({
25845
26112
  deviceId: number(),
25846
26113
  profile: string(),
@@ -25865,6 +26132,9 @@ method(object({
25865
26132
  }), method(object({ exportId: string() }), ExportDownloadSchema, {
25866
26133
  kind: "query",
25867
26134
  auth: "protected"
26135
+ }), method(object({ exportId: string() }), ExportBytesSchema, {
26136
+ kind: "query",
26137
+ auth: "protected"
25868
26138
  });
25869
26139
  /**
25870
26140
  * scene-monitor — device-scoped reference-region state cap. An operator marks
@@ -28331,9 +28601,10 @@ var DeclaredDevices = class {
28331
28601
  }
28332
28602
  const integrationId = spec.integrationId ?? await this.ensureIntegration(spec.integrationName);
28333
28603
  const index = await this.readIndex();
28604
+ const live = await this.readLiveByStableId();
28334
28605
  const outcomes = [];
28335
28606
  for (const declaration of spec.devices) {
28336
- const outcome = await this.applyDeclaration(declaration, integrationId, index);
28607
+ const outcome = await this.applyDeclaration(declaration, integrationId, index, live);
28337
28608
  if (outcome !== null) outcomes.push(outcome);
28338
28609
  }
28339
28610
  return {
@@ -28379,6 +28650,26 @@ var DeclaredDevices = class {
28379
28650
  return new Map(rows.map((row) => [row.stableId, row]));
28380
28651
  }
28381
28652
  /**
28653
+ * Devices this kernel already has CONSTRUCTED, by stableId.
28654
+ *
28655
+ * Distinct from {@link readIndex}, and the distinction is the bug: the index
28656
+ * is persisted rows, this is live objects. A row without an object must be
28657
+ * adopted; an object must be left exactly as it is.
28658
+ *
28659
+ * Failure is non-fatal and deliberately so — an empty map degrades to the
28660
+ * previous behaviour (attempt the adopt) rather than skipping a device that
28661
+ * genuinely needs bringing up.
28662
+ */
28663
+ async readLiveByStableId() {
28664
+ try {
28665
+ const devices = await this.ports.devices.getAll();
28666
+ return new Map(devices.map((device) => [device.stableId, device]));
28667
+ } catch (err) {
28668
+ this.ports.logger.warn("could not read live devices — falling back to adopt-by-row", { meta: { error: err instanceof Error ? err.message : String(err) } });
28669
+ return /* @__PURE__ */ new Map();
28670
+ }
28671
+ }
28672
+ /**
28382
28673
  * One declaration: adopt what exists, create what does not.
28383
28674
  *
28384
28675
  * The create branch is the destructive one — it seeds `initialMeta`, and
@@ -28387,8 +28678,15 @@ var DeclaredDevices = class {
28387
28678
  * the declared name over the operator's rename. D49: that branch needs a
28388
28679
  * second read to agree.
28389
28680
  */
28390
- async applyDeclaration(declaration, integrationId, index) {
28681
+ async applyDeclaration(declaration, integrationId, index, live) {
28391
28682
  try {
28683
+ const alreadyLive = live.get(declaration.stableId);
28684
+ if (alreadyLive !== void 0) return {
28685
+ stableId: declaration.stableId,
28686
+ deviceId: alreadyLive.id,
28687
+ device: alreadyLive,
28688
+ created: false
28689
+ };
28392
28690
  let existing = index.get(declaration.stableId);
28393
28691
  if (existing === void 0) {
28394
28692
  existing = (await this.readIndex()).get(declaration.stableId);
@@ -31493,7 +31791,7 @@ Object.freeze({
31493
31791
  addonId: null,
31494
31792
  access: "create"
31495
31793
  },
31496
- "pipelineAnalytics.cancelMediaRelocate": {
31794
+ "pipelineAnalytics.cancelStorageMigrationMove": {
31497
31795
  capName: "pipeline-analytics",
31498
31796
  capScope: "device",
31499
31797
  addonId: null,
@@ -31565,12 +31863,6 @@ Object.freeze({
31565
31863
  addonId: null,
31566
31864
  access: "view"
31567
31865
  },
31568
- "pipelineAnalytics.getMediaRelocateStatus": {
31569
- capName: "pipeline-analytics",
31570
- capScope: "device",
31571
- addonId: null,
31572
- access: "view"
31573
- },
31574
31866
  "pipelineAnalytics.getMotionEvents": {
31575
31867
  capName: "pipeline-analytics",
31576
31868
  capScope: "device",
@@ -31607,6 +31899,12 @@ Object.freeze({
31607
31899
  addonId: null,
31608
31900
  access: "view"
31609
31901
  },
31902
+ "pipelineAnalytics.getStorageMigrationMoveStatus": {
31903
+ capName: "pipeline-analytics",
31904
+ capScope: "device",
31905
+ addonId: null,
31906
+ access: "view"
31907
+ },
31610
31908
  "pipelineAnalytics.getTrack": {
31611
31909
  capName: "pipeline-analytics",
31612
31910
  capScope: "device",
@@ -31685,6 +31983,12 @@ Object.freeze({
31685
31983
  addonId: null,
31686
31984
  access: "view"
31687
31985
  },
31986
+ "pipelineAnalytics.pauseForStorageMigration": {
31987
+ capName: "pipeline-analytics",
31988
+ capScope: "device",
31989
+ addonId: null,
31990
+ access: "create"
31991
+ },
31688
31992
  "pipelineAnalytics.proposeRetrainAnnotations": {
31689
31993
  capName: "pipeline-analytics",
31690
31994
  capScope: "device",
@@ -31715,7 +32019,7 @@ Object.freeze({
31715
32019
  addonId: null,
31716
32020
  access: "create"
31717
32021
  },
31718
- "pipelineAnalytics.relocateMedia": {
32022
+ "pipelineAnalytics.refreshStorageLocationsForMigration": {
31719
32023
  capName: "pipeline-analytics",
31720
32024
  capScope: "device",
31721
32025
  addonId: null,
@@ -31727,6 +32031,12 @@ Object.freeze({
31727
32031
  addonId: null,
31728
32032
  access: "create"
31729
32033
  },
32034
+ "pipelineAnalytics.resumeForStorageMigration": {
32035
+ capName: "pipeline-analytics",
32036
+ capScope: "device",
32037
+ addonId: null,
32038
+ access: "create"
32039
+ },
31730
32040
  "pipelineAnalytics.saveRetrainAnnotations": {
31731
32041
  capName: "pipeline-analytics",
31732
32042
  capScope: "device",
@@ -31751,6 +32061,12 @@ Object.freeze({
31751
32061
  addonId: null,
31752
32062
  access: "create"
31753
32063
  },
32064
+ "pipelineAnalytics.startStorageMigrationMove": {
32065
+ capName: "pipeline-analytics",
32066
+ capScope: "device",
32067
+ addonId: null,
32068
+ access: "create"
32069
+ },
31754
32070
  "pipelineAnalytics.wipeAllAnalytics": {
31755
32071
  capName: "pipeline-analytics",
31756
32072
  capScope: "device",
@@ -32117,6 +32433,12 @@ Object.freeze({
32117
32433
  addonId: null,
32118
32434
  access: "view"
32119
32435
  },
32436
+ "pipelineOrchestrator.pauseForStorageMigration": {
32437
+ capName: "pipeline-orchestrator",
32438
+ capScope: "system",
32439
+ addonId: null,
32440
+ access: "create"
32441
+ },
32120
32442
  "pipelineOrchestrator.rebalance": {
32121
32443
  capName: "pipeline-orchestrator",
32122
32444
  capScope: "system",
@@ -32141,6 +32463,12 @@ Object.freeze({
32141
32463
  addonId: null,
32142
32464
  access: "view"
32143
32465
  },
32466
+ "pipelineOrchestrator.resumeForStorageMigration": {
32467
+ capName: "pipeline-orchestrator",
32468
+ capScope: "system",
32469
+ addonId: null,
32470
+ access: "create"
32471
+ },
32144
32472
  "pipelineOrchestrator.saveTemplate": {
32145
32473
  capName: "pipeline-orchestrator",
32146
32474
  capScope: "system",
@@ -32537,7 +32865,7 @@ Object.freeze({
32537
32865
  addonId: null,
32538
32866
  access: "create"
32539
32867
  },
32540
- "recording.cancelRelocate": {
32868
+ "recording.cancelStorageMigrationMove": {
32541
32869
  capName: "recording",
32542
32870
  capScope: "system",
32543
32871
  addonId: null,
@@ -32573,7 +32901,7 @@ Object.freeze({
32573
32901
  addonId: null,
32574
32902
  access: "view"
32575
32903
  },
32576
- "recording.getRelocateStatus": {
32904
+ "recording.getStorageMigrationMoveStatus": {
32577
32905
  capName: "recording",
32578
32906
  capScope: "system",
32579
32907
  addonId: null,
@@ -32597,6 +32925,12 @@ Object.freeze({
32597
32925
  addonId: null,
32598
32926
  access: "view"
32599
32927
  },
32928
+ "recording.pauseForStorageMigration": {
32929
+ capName: "recording",
32930
+ capScope: "system",
32931
+ addonId: null,
32932
+ access: "create"
32933
+ },
32600
32934
  "recording.pruneFootage": {
32601
32935
  capName: "recording",
32602
32936
  capScope: "system",
@@ -32615,7 +32949,7 @@ Object.freeze({
32615
32949
  addonId: null,
32616
32950
  access: "view"
32617
32951
  },
32618
- "recording.relocateFootage": {
32952
+ "recording.refreshStorageLocationsForMigration": {
32619
32953
  capName: "recording",
32620
32954
  capScope: "system",
32621
32955
  addonId: null,
@@ -32639,12 +32973,24 @@ Object.freeze({
32639
32973
  addonId: null,
32640
32974
  access: "create"
32641
32975
  },
32976
+ "recording.resumeForStorageMigration": {
32977
+ capName: "recording",
32978
+ capScope: "system",
32979
+ addonId: null,
32980
+ access: "create"
32981
+ },
32642
32982
  "recording.setDeviceConfig": {
32643
32983
  capName: "recording",
32644
32984
  capScope: "system",
32645
32985
  addonId: null,
32646
32986
  access: "create"
32647
32987
  },
32988
+ "recording.startStorageMigrationMove": {
32989
+ capName: "recording",
32990
+ capScope: "system",
32991
+ addonId: null,
32992
+ access: "create"
32993
+ },
32648
32994
  "recordingExport.cancelExport": {
32649
32995
  capName: "recordingExport",
32650
32996
  capScope: "system",
@@ -32681,6 +33027,12 @@ Object.freeze({
32681
33027
  addonId: null,
32682
33028
  access: "view"
32683
33029
  },
33030
+ "recordingExport.readExportBytes": {
33031
+ capName: "recordingExport",
33032
+ capScope: "system",
33033
+ addonId: null,
33034
+ access: "view"
33035
+ },
32684
33036
  "sceneMonitor.captureReference": {
32685
33037
  capName: "scene-monitor",
32686
33038
  capScope: "device",
@@ -33035,6 +33387,30 @@ Object.freeze({
33035
33387
  addonId: null,
33036
33388
  access: "view"
33037
33389
  },
33390
+ "storageMigration.cancel": {
33391
+ capName: "storage-migration",
33392
+ capScope: "system",
33393
+ addonId: null,
33394
+ access: "create"
33395
+ },
33396
+ "storageMigration.plan": {
33397
+ capName: "storage-migration",
33398
+ capScope: "system",
33399
+ addonId: null,
33400
+ access: "view"
33401
+ },
33402
+ "storageMigration.start": {
33403
+ capName: "storage-migration",
33404
+ capScope: "system",
33405
+ addonId: null,
33406
+ access: "create"
33407
+ },
33408
+ "storageMigration.status": {
33409
+ capName: "storage-migration",
33410
+ capScope: "system",
33411
+ addonId: null,
33412
+ access: "view"
33413
+ },
33038
33414
  "storageProvider.abortUpload": {
33039
33415
  capName: "storage-provider",
33040
33416
  capScope: "system",
@@ -33413,12 +33789,42 @@ Object.freeze({
33413
33789
  addonId: null,
33414
33790
  access: "create"
33415
33791
  },
33792
+ "terminalSession.adoptLegacyMonitor": {
33793
+ capName: "terminal-session",
33794
+ capScope: "system",
33795
+ addonId: null,
33796
+ access: "create"
33797
+ },
33416
33798
  "terminalSession.close": {
33417
33799
  capName: "terminal-session",
33418
33800
  capScope: "system",
33419
33801
  addonId: null,
33420
33802
  access: "create"
33421
33803
  },
33804
+ "terminalSession.createInstance": {
33805
+ capName: "terminal-session",
33806
+ capScope: "system",
33807
+ addonId: null,
33808
+ access: "create"
33809
+ },
33810
+ "terminalSession.deleteInstance": {
33811
+ capName: "terminal-session",
33812
+ capScope: "system",
33813
+ addonId: null,
33814
+ access: "delete"
33815
+ },
33816
+ "terminalSession.listInstances": {
33817
+ capName: "terminal-session",
33818
+ capScope: "system",
33819
+ addonId: null,
33820
+ access: "view"
33821
+ },
33822
+ "terminalSession.listLegacyCameras": {
33823
+ capName: "terminal-session",
33824
+ capScope: "system",
33825
+ addonId: null,
33826
+ access: "view"
33827
+ },
33422
33828
  "terminalSession.listProfiles": {
33423
33829
  capName: "terminal-session",
33424
33830
  capScope: "system",
@@ -33449,6 +33855,12 @@ Object.freeze({
33449
33855
  addonId: null,
33450
33856
  access: "create"
33451
33857
  },
33858
+ "terminalSession.setInstanceEnabled": {
33859
+ capName: "terminal-session",
33860
+ capScope: "system",
33861
+ addonId: null,
33862
+ access: "create"
33863
+ },
33452
33864
  "terminalSession.writeInput": {
33453
33865
  capName: "terminal-session",
33454
33866
  capScope: "system",
@@ -34078,7 +34490,23 @@ var TimelapseRuleInputSchema = object({
34078
34490
  /** Canonical notification priority ordinal (1..5); per-target overridable. */
34079
34491
  priority: PriorityField.default(3)
34080
34492
  });
34081
- object({
34493
+ /**
34494
+ * Partial patch for an update — any subset of the INPUT fields, with NO
34495
+ * defaults (an absent key means "leave unchanged", never "reset to default").
34496
+ * Provenance and ownership are absent by construction: a patch can rename or
34497
+ * retune a rule, never re-own it or forge its generation state.
34498
+ *
34499
+ * CLEAR SIGNAL: `template` is the one OPTIONAL field an editor can REMOVE, so
34500
+ * it is `.nullable()` here and NOWHERE else. Wire representation:
34501
+ * - key absent → leave the template unchanged
34502
+ * - `"template": null` → CLEAR it (the persisted rule loses the key)
34503
+ * - `"template": {...}` → replace it
34504
+ * `undefined` is deliberately NOT the clear signal: it does not survive
34505
+ * `JSON.stringify`, so a viewer editor could not express "remove". The
34506
+ * PERSISTED rule never carries `null` — the store drops the key (see
34507
+ * `TimelapseStore.update`).
34508
+ */
34509
+ var TimelapseRulePatchSchema = object({
34082
34510
  name: NameField.optional(),
34083
34511
  enabled: boolean().optional(),
34084
34512
  deviceIds: DeviceIdsField.optional(),
@@ -34099,15 +34527,50 @@ var TimelapseRuleSchema = TimelapseRuleInputSchema.extend({
34099
34527
  */
34100
34528
  ownerUserId: string().optional(),
34101
34529
  /**
34102
- * Epoch-ms of the last successful generation — the 1-hour re-generation
34103
- * guard's durable state (predecessor parity). Absent = never generated.
34530
+ * Epoch-ms of the NEWEST successful generation across every camera of this
34531
+ * rule. What a UI shows, and the compatibility floor for
34532
+ * {@link readTimelapseGeneratedAt}. Absent = never generated.
34104
34533
  */
34105
34534
  lastGeneratedAt: number().optional(),
34535
+ /**
34536
+ * PER-CAMERA generation state, keyed by `String(deviceId)` — the
34537
+ * re-generation guard's real durable state.
34538
+ *
34539
+ * One rule covers several cameras and each renders its own video, so a rule
34540
+ * -wide stamp is wrong in the direction that DESTROYS work: camera A
34541
+ * succeeding at 06:05 tells camera B, whose render failed, that it is
34542
+ * already done — and B's night is gone for good, because the window will not
34543
+ * come back.
34544
+ *
34545
+ * ADDITIVE, so the migration is free: a row written before this field simply
34546
+ * has no map, and {@link readTimelapseGeneratedAt} falls back to
34547
+ * {@link TimelapseRuleSchema.shape.lastGeneratedAt}. Reading an old row as
34548
+ * "never generated" would re-render and re-notify every camera of every rule
34549
+ * once, on the deploy that shipped the map.
34550
+ */
34551
+ generatedByDevice: record(string(), number()).optional(),
34106
34552
  /** userId of the caller who created the rule (server-stamped). */
34107
34553
  createdBy: string(),
34108
34554
  createdAt: number(),
34109
34555
  updatedAt: number()
34110
34556
  });
34557
+ /**
34558
+ * The last successful generation for ONE camera of a rule, epoch-ms.
34559
+ *
34560
+ * The per-device map wins; a rule with no map falls back to the rule-wide
34561
+ * `lastGeneratedAt` (the compatible-migration path — see
34562
+ * {@link TimelapseRuleSchema}); a rule with neither returns 0, which every
34563
+ * guard reads as "never generated".
34564
+ *
34565
+ * Read through this helper and never off the field directly: the fallback is
34566
+ * the whole migration, and a call site that forgot it would re-render an
34567
+ * entire rule set once.
34568
+ */
34569
+ function readTimelapseGeneratedAt(rule, deviceId) {
34570
+ const map = rule.generatedByDevice;
34571
+ if (map !== void 0) return map[String(deviceId)] ?? 0;
34572
+ return rule.lastGeneratedAt ?? 0;
34573
+ }
34111
34574
  object({
34112
34575
  /**
34113
34576
  * Fraction of the box's own size added on EACH side before cutting.
@@ -34260,4 +34723,4 @@ function vectorDimFromBase64(encoded) {
34260
34723
  return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
34261
34724
  }
34262
34725
  //#endregion
34263
- export { array as $, embeddingEncoderCapability as A, subKindsOf as B, addonWidgetsSourceCapability as C, cosineSimilarity as D, buildEventKindDescriptor as E, kebabToCamel as F, BaseAddon as G, videoclipsCapability as H, notificationRulesCapability as I, hydrateSchema as J, DeviceType as K, pipelineAnalyticsCapability as L, faceGalleryCapability as M, hfModelUrl as N, customAction as O, isScheduleActive as P, _enum as Q, plateGalleryCapability as R, TrackSourceSchema as S, audioMetricsCapability as T, zoneAnalyticsCapability as U, vectorDimFromBase64 as V, errMsg as W, nodePin as X, isDeviceScopedCap as Y, sleep as Z, NcTaxonomySchema as _, EVENT_PAD_MS as a, string as at, TimelapseRuleInputSchema as b, NC_CONDITION_CATALOG as c, NcRuleInputSchema as d, boolean as et, NcRulePatchSchema as f, NcSnoozeSuppressedSchema as g, NcSnoozeSchema as h, EVENT_KIND_BY_CAP as i, record as it, encodeVectorBase64 as j, defineCustomActions as k, NC_TAXONOMY as l, NcSnoozeInputSchema as m, DEFAULT_EVENT_COLOR as n, number as nt, LabelAttributionSchema as o, unknown as ot, NcRuleSchema as p, createEvent as q, DeclaredDevices as r, object as rt, MACRO_LABELS as s, EventCategory as st, BaseDevice as t, literal as tt, NcConditionDescriptorSchema as u, OpsLogEntrySchema as v, alarmPanelCapability as w, TimelapseRuleSchema as x, RetrainStatusSchema as y, readDeviceStateFrom as z };
34726
+ export { isDeviceScopedCap as $, customAction as A, pipelineAnalyticsCapability as B, TimelapseRuleSchema as C, audioMetricsCapability as D, alarmPanelCapability as E, faceGalleryCapability as F, vectorDimFromBase64 as G, readDeviceStateFrom as H, hfModelUrl as I, errMsg as J, videoclipsCapability as K, isScheduleActive as L, deriveRecordingMode as M, embeddingEncoderCapability as N, buildEventKindDescriptor as O, encodeVectorBase64 as P, hydrateSchema as Q, kebabToCamel as R, TimelapseRulePatchSchema as S, addonWidgetsSourceCapability as T, readTimelapseGeneratedAt as U, plateGalleryCapability as V, subKindsOf as W, DeviceType as X, BaseAddon as Y, createEvent as Z, NcTaxonomySchema as _, EVENT_PAD_MS as a, literal as at, RetrainStatusSchema as b, NC_CONDITION_CATALOG as c, record as ct, NcRuleInputSchema as d, EventCategory as dt, nodePin as et, NcRulePatchSchema as f, NcSnoozeSuppressedSchema as g, NcSnoozeSchema as h, EVENT_KIND_BY_CAP as i, boolean as it, defineCustomActions as j, cosineSimilarity as k, NC_TAXONOMY as l, string as lt, NcSnoozeInputSchema as m, DEFAULT_EVENT_COLOR as n, _enum as nt, LabelAttributionSchema as o, number as ot, NcRuleSchema as p, zoneAnalyticsCapability as q, DeclaredDevices as r, array as rt, MACRO_LABELS as s, object as st, BaseDevice as t, sleep as tt, NcConditionDescriptorSchema as u, unknown as ut, OpsLogEntrySchema as v, TrackSourceSchema as w, TimelapseRuleInputSchema as x, RECORDING_EXPORT_MAX_READ_BYTES as y, notificationRulesCapability as z };