@camstack/addon-osd-manager 0.1.3 → 0.1.5

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.
package/dist/index.js CHANGED
@@ -7626,12 +7626,11 @@ var RecordingConfigSchema = object({
7626
7626
  /**
7627
7627
  * Entity-relocation job state (storage entity-routing spec, Phase 4).
7628
7628
  *
7629
- * One shape shared by the recorder's `relocateFootage` (segments) and
7630
- * pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
7631
- * page renders both movers with one component. Jobs are in-RAM (a restart
7632
- * forgets them re-running is safe by construction: copy-if-absent, delete
7633
- * after verify) and each completed/failed run also lands one durable ops-log
7634
- * row on the owning addon's surface.
7629
+ * One shape shared by the recorder and pipeline-analytics internal movers.
7630
+ * The public admin surface is `storage-migration`; child jobs remain in RAM
7631
+ * because copy-if-absent, verify, delete and index/row repoint are resumable.
7632
+ * Each completed/failed run also lands one durable ops-log row on its owning
7633
+ * addon surface.
7635
7634
  */
7636
7635
  var RelocateJobStateSchema = _enum([
7637
7636
  "running",
@@ -7658,19 +7657,100 @@ var RelocateJobSchema = object({
7658
7657
  finishedAt: number().nullable(),
7659
7658
  error: string().nullable()
7660
7659
  });
7660
+ /** Profile-derived footage selection used only by the migration coordinator:
7661
+ * `recordings` owns high+mid; `recordingsLow` owns low. */
7662
+ var RelocateFootageClassSchema = _enum(["recordings", "recordingsLow"]);
7661
7663
  var RelocateFootageInputSchema = object({
7662
- deviceId: number().optional(),
7663
7664
  fromLocationId: string(),
7664
7665
  toLocationId: string(),
7665
7666
  entities: array(_enum(["segments"])).optional(),
7667
+ /** Limits relocation to the logical profile class. Omit only for the
7668
+ * pre-orchestration compatibility path. */
7669
+ footageClass: RelocateFootageClassSchema.optional(),
7666
7670
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7667
7671
  * never allowed to starve live writers. */
7668
7672
  throttleMbps: number().min(1).max(1e3).optional()
7669
7673
  });
7670
- var RelocateMediaInputSchema = object({
7671
- deviceId: number().optional(),
7674
+ /** Internal, lease-scoped participant operation. It is intentionally separate
7675
+ * from persistent recording settings: a migration never changes
7676
+ * `RecordingConfig.enabled` or camera wrapper bindings. */
7677
+ var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
7678
+ var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
7679
+ var StorageMigrationMediaMoveInputSchema = object({
7672
7680
  toLocationId: string(),
7673
7681
  throttleMbps: number().min(1).max(1e3).optional()
7682
+ }).extend({ leaseId: string().min(1) });
7683
+ /** The independently selectable logical storage classes. `recordings`
7684
+ * encompasses the high and mid segment profiles; `recordingsLow` is low
7685
+ * segments; `eventMedia` is post-analysis blobs. */
7686
+ var StorageMigrationClassSchema = _enum([
7687
+ "recordings",
7688
+ "recordingsLow",
7689
+ "eventMedia"
7690
+ ]);
7691
+ /** A destination is always an existing, fully-qualified location id. The
7692
+ * migration API intentionally never changes a source location's `basePath`:
7693
+ * callers create a new `<type>:<slug>` location, then select it here. */
7694
+ var StorageMigrationDestinationsSchema = object({
7695
+ recordings: string().min(1).optional(),
7696
+ recordingsLow: string().min(1).optional(),
7697
+ eventMedia: string().min(1).optional()
7698
+ }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
7699
+ /** Shared input for planning and starting an orchestrated storage migration. */
7700
+ var StorageMigrationInputSchema = object({
7701
+ destinations: StorageMigrationDestinationsSchema,
7702
+ throttleMbps: number().min(1).max(1e3).optional()
7703
+ });
7704
+ /** The durable coordinator state machine. The only phase that changes default
7705
+ * locations is `repointing`, after every selected mover has completed and been
7706
+ * verified. */
7707
+ var StorageMigrationPhaseSchema = _enum([
7708
+ "planning",
7709
+ "pausing",
7710
+ "moving",
7711
+ "verifying",
7712
+ "repointing",
7713
+ "refreshing",
7714
+ "resuming",
7715
+ "done",
7716
+ "failed",
7717
+ "cancelled"
7718
+ ]);
7719
+ var StorageMigrationParticipantSchema = _enum([
7720
+ "pipeline",
7721
+ "recorder",
7722
+ "analytics"
7723
+ ]);
7724
+ var StorageMigrationMoveSchema = object({
7725
+ storageClass: StorageMigrationClassSchema,
7726
+ fromLocationId: string(),
7727
+ toLocationId: string(),
7728
+ moverJobId: string().nullable(),
7729
+ state: RelocateJobStateSchema.nullable(),
7730
+ error: string().nullable()
7731
+ });
7732
+ var StorageMigrationJobSchema = object({
7733
+ jobId: string(),
7734
+ phase: StorageMigrationPhaseSchema,
7735
+ destinations: StorageMigrationDestinationsSchema,
7736
+ throttleMbps: number(),
7737
+ moves: array(StorageMigrationMoveSchema),
7738
+ pauseLeaseId: string().nullable(),
7739
+ pausedParticipants: array(StorageMigrationParticipantSchema),
7740
+ repointed: boolean(),
7741
+ cancelRequested: boolean(),
7742
+ startedAt: number(),
7743
+ updatedAt: number(),
7744
+ finishedAt: number().nullable(),
7745
+ error: string().nullable()
7746
+ });
7747
+ var StorageMigrationPlanSchema = object({
7748
+ destinations: StorageMigrationDestinationsSchema,
7749
+ moves: array(object({
7750
+ storageClass: StorageMigrationClassSchema,
7751
+ fromLocationId: string(),
7752
+ toLocationId: string()
7753
+ }))
7674
7754
  });
7675
7755
  /**
7676
7756
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -18440,20 +18520,28 @@ var pipelineAnalyticsCapability = {
18440
18520
  }),
18441
18521
  /** The events ops-log rows (newest-first), optionally scoped to one camera.
18442
18522
  * Backed by a declared pipeline-analytics SQLite collection. */
18443
- /**
18444
- * Move event-media blobs onto `toLocationId` (entity-routing spec Phase
18445
- * 4): per-row copy stamp row.locationId → delete old blob. Rows already
18446
- * at the target are skipped, so a re-run resumes. Single-flight.
18447
- */
18448
- relocateMedia: method(RelocateMediaInputSchema, object({ jobId: string() }), {
18523
+ /** Internal migration-participant lease. It drains active MediaStore
18524
+ * writes and refuses new ones without changing analytics bindings. */
18525
+ pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
18449
18526
  kind: "mutation",
18450
18527
  auth: "admin"
18451
18528
  }),
18452
- getMediaRelocateStatus: method(object({}), array(RelocateJobSchema).readonly(), {
18453
- kind: "query",
18529
+ resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
18530
+ kind: "mutation",
18531
+ auth: "admin"
18532
+ }),
18533
+ /** Drops cached location roots after the storage coordinator repoints a
18534
+ * default so future event-media writes use the new root. */
18535
+ refreshStorageLocationsForMigration: method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
18536
+ kind: "mutation",
18454
18537
  auth: "admin"
18455
18538
  }),
18456
- cancelMediaRelocate: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
18539
+ startStorageMigrationMove: method(StorageMigrationMediaMoveInputSchema, object({ jobId: string() }), {
18540
+ kind: "mutation",
18541
+ auth: "admin"
18542
+ }),
18543
+ getStorageMigrationMoveStatus: method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }),
18544
+ cancelStorageMigrationMove: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
18457
18545
  kind: "mutation",
18458
18546
  auth: "admin"
18459
18547
  }),
@@ -20502,6 +20590,16 @@ var pipelineOrchestratorCapability = {
20502
20590
  nodeIdMode: "data",
20503
20591
  exposesDeviceSettings: true,
20504
20592
  methods: {
20593
+ /** Internal storage-migration participant lease. While held, dispatch
20594
+ * requests are accepted as pending but no new runner attachment starts. */
20595
+ pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
20596
+ kind: "mutation",
20597
+ auth: "admin"
20598
+ }),
20599
+ resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
20600
+ kind: "mutation",
20601
+ auth: "admin"
20602
+ }),
20505
20603
  /**
20506
20604
  * Pin a camera's pipeline to a specific agent (L1 affinity).
20507
20605
  * The orchestrator re-evaluates the assignment immediately and persists
@@ -21643,6 +21741,28 @@ var storageEvictableCapability = {
21643
21741
  }), EvictResultSchema, { kind: "mutation" })
21644
21742
  }
21645
21743
  };
21744
+ /**
21745
+ * The single operator-facing storage migration surface. It coordinates the
21746
+ * pipeline, recorder and post-analysis participants; callers never mutate a
21747
+ * camera recording config or invoke an individual mover directly.
21748
+ */
21749
+ var storageMigrationCapability = {
21750
+ name: "storage-migration",
21751
+ scope: "system",
21752
+ mode: "singleton",
21753
+ methods: {
21754
+ plan: method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin" }),
21755
+ start: method(StorageMigrationInputSchema, object({ jobId: string() }), {
21756
+ kind: "mutation",
21757
+ auth: "admin"
21758
+ }),
21759
+ status: method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }),
21760
+ cancel: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21761
+ kind: "mutation",
21762
+ auth: "admin"
21763
+ })
21764
+ }
21765
+ };
21646
21766
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
21647
21767
  providerId: string().min(1),
21648
21768
  displayName: string().min(1),
@@ -21791,6 +21911,28 @@ var TerminalProfileInfoSchema = object({
21791
21911
  label: string(),
21792
21912
  description: string().optional()
21793
21913
  });
21914
+ /**
21915
+ * A durable operator-created Terminal instance. Profiles are templates; only
21916
+ * an instance declares a camera.
21917
+ */
21918
+ var TerminalInstanceInfoSchema = object({
21919
+ instanceId: string(),
21920
+ cameraStableId: string(),
21921
+ nodeId: string(),
21922
+ profileId: string(),
21923
+ profileLabel: string(),
21924
+ name: string(),
21925
+ enabled: boolean()
21926
+ });
21927
+ var TerminalLegacyCameraSchema = object({
21928
+ stableId: string(),
21929
+ nodeId: string(),
21930
+ profileId: string(),
21931
+ profileLabel: string(),
21932
+ name: string(),
21933
+ /** Only legacy monitor cameras can retain their historic stable identity. */
21934
+ adoptable: boolean()
21935
+ });
21794
21936
  var TerminalOutputEventSchema = discriminatedUnion("kind", [object({
21795
21937
  seq: number().int().positive(),
21796
21938
  kind: literal("data"),
@@ -21810,10 +21952,9 @@ var TerminalOutputBatchSchema = object({
21810
21952
  /**
21811
21953
  * terminal-session — singleton system capability for interactive TTY sessions.
21812
21954
  *
21813
- * Phase 1 (this cap): open/resize/close/list a pty running an allowlisted
21814
- * profile, streamed to xterm.js over the data plane. Phase 2 (streaming a
21815
- * session as a camera) is a separate device-provider concern and does not
21816
- * change this contract.
21955
+ * Owns both live PTY lifecycle and durable Terminal instance management.
21956
+ * Profiles are allowlisted templates; an explicit instance is the only path
21957
+ * that declares a camera.
21817
21958
  */
21818
21959
  var terminalSessionCapability = {
21819
21960
  name: "terminal-session",
@@ -21822,6 +21963,37 @@ var terminalSessionCapability = {
21822
21963
  methods: {
21823
21964
  /** Pre-declared profiles the operator may open. */
21824
21965
  listProfiles: method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
21966
+ /** Explicit durable Terminal instances, managed centrally on the hub. */
21967
+ listInstances: method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }),
21968
+ createInstance: method(object({
21969
+ targetNodeId: string().min(1),
21970
+ profileId: string().min(1),
21971
+ name: string().trim().min(1).max(160).optional()
21972
+ }), TerminalInstanceInfoSchema, {
21973
+ kind: "mutation",
21974
+ auth: "admin"
21975
+ }),
21976
+ deleteInstance: method(object({ instanceId: string().min(1) }), _void(), {
21977
+ kind: "mutation",
21978
+ auth: "admin"
21979
+ }),
21980
+ setInstanceEnabled: method(object({
21981
+ instanceId: string().min(1),
21982
+ enabled: boolean()
21983
+ }), TerminalInstanceInfoSchema, {
21984
+ kind: "mutation",
21985
+ auth: "admin"
21986
+ }),
21987
+ /** Existing automatic cameras are shown for explicit migration only. */
21988
+ listLegacyCameras: method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }),
21989
+ /** Explicitly adopt one legacy monitor camera, retaining its stable id. */
21990
+ adoptLegacyMonitor: method(object({
21991
+ stableId: string().min(1),
21992
+ name: string().trim().min(1).max(160).optional()
21993
+ }), TerminalInstanceInfoSchema, {
21994
+ kind: "mutation",
21995
+ auth: "admin"
21996
+ }),
21825
21997
  /** Live sessions currently hosted by the provider. */
21826
21998
  listSessions: method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }),
21827
21999
  /**
@@ -21853,7 +22025,13 @@ var terminalSessionCapability = {
21853
22025
  pullOutput: method(object({
21854
22026
  sessionId: string(),
21855
22027
  afterSeq: number().int().nonnegative(),
21856
- waitMs: number().int().min(0).max(2e3).default(0)
22028
+ waitMs: number().int().min(0).max(2e3).default(0),
22029
+ /**
22030
+ * Wait when a just-opened session has no output yet. Kept opt-in so a
22031
+ * browser's initial repaint remains immediate; the camera snapshot
22032
+ * relay uses it to avoid encoding a blank startup frame.
22033
+ */
22034
+ waitForOutput: boolean().optional()
21857
22035
  }), TerminalOutputBatchSchema, {
21858
22036
  kind: "mutation",
21859
22037
  auth: "admin",
@@ -24802,6 +24980,7 @@ var FaceInfoSchema = object({
24802
24980
  var FaceFilterEnum = _enum([
24803
24981
  "unassigned",
24804
24982
  "recognized",
24983
+ "identified",
24805
24984
  "all"
24806
24985
  ]);
24807
24986
  var MediaFileLiteSchema$1 = object({
@@ -24841,6 +25020,8 @@ var faceGalleryCapability = {
24841
25020
  auth: "admin"
24842
25021
  }),
24843
25022
  listRecentFaces: method(object({
25023
+ /** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
25024
+ deviceId: number().int().optional(),
24844
25025
  limit: number().int().positive().optional(),
24845
25026
  filter: FaceFilterEnum.optional(),
24846
25027
  /**
@@ -27384,6 +27565,10 @@ var OsdSourceSchema = discriminatedUnion("kind", [
27384
27565
  capName: string().min(1).max(64),
27385
27566
  /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
27386
27567
  valuePath: string().min(1).max(64)
27568
+ }),
27569
+ object({
27570
+ kind: literal("latest-recognition"),
27571
+ recognition: _enum(["person", "plate"])
27387
27572
  })
27388
27573
  ]);
27389
27574
  var OsdSlotBindingSchema = object({
@@ -27522,6 +27707,21 @@ var osdManagerCapability = {
27522
27707
  auth: "admin"
27523
27708
  }),
27524
27709
  /**
27710
+ * Replace one camera's binding configuration from another. Writable source
27711
+ * slots are mapped by ordinal onto writable target slots so different
27712
+ * provider slot ids do not make the copied configuration disappear.
27713
+ */
27714
+ copyDeviceConfiguration: method(object({
27715
+ sourceDeviceId: number().int(),
27716
+ targetDeviceId: number().int()
27717
+ }), object({
27718
+ copied: number().int().nonnegative(),
27719
+ skipped: number().int().nonnegative()
27720
+ }), {
27721
+ kind: "mutation",
27722
+ auth: "admin"
27723
+ }),
27724
+ /**
27525
27725
  * Render without writing. `binding` overrides the stored one so an
27526
27726
  * editor can preview an unsaved change. Mutation kind only to carry
27527
27727
  * the binding object safely; no side effects.
@@ -28759,12 +28959,6 @@ var recordingCapability = {
28759
28959
  auth: "admin"
28760
28960
  }),
28761
28961
  /**
28762
- * Move footage (recorded segments) from one recordings location to
28763
- * another — the drain workflow's mover (entity-routing spec Phase 4).
28764
- * Throttled copy → size-verify → delete source → index refresh; resumable
28765
- * by construction (copy-if-absent). Single-flight: one job at a time.
28766
- */
28767
- /**
28768
28962
  * Render a short GIF from recorded footage around `aroundMs` (low profile,
28769
28963
  * palette-free ffmpeg gif). Synchronous — the window is a few seconds of
28770
28964
  * `low`, rendering in ~1-2s. Built for notification attachments: the
@@ -28816,16 +29010,28 @@ var recordingCapability = {
28816
29010
  kind: "mutation",
28817
29011
  auth: "admin"
28818
29012
  }),
28819
- relocateFootage: method(RelocateFootageInputSchema, object({ jobId: string() }), {
29013
+ /** Internal migration-participant lease. Blocks new recorder attaches and
29014
+ * waits for writers to exit while their watchers index final segments. */
29015
+ pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
28820
29016
  kind: "mutation",
28821
29017
  auth: "admin"
28822
29018
  }),
28823
- /** Every known relocate job (in-RAM, newest-first). */
28824
- getRelocateStatus: method(object({}), array(RelocateJobSchema).readonly(), {
28825
- kind: "query",
29019
+ resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
29020
+ kind: "mutation",
29021
+ auth: "admin"
29022
+ }),
29023
+ /** Re-resolve location roots after the coordinator changes defaults. */
29024
+ refreshStorageLocationsForMigration: method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
29025
+ kind: "mutation",
28826
29026
  auth: "admin"
28827
29027
  }),
28828
- cancelRelocate: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
29028
+ /** Internal mover, callable only by the storage-migration coordinator. */
29029
+ startStorageMigrationMove: method(StorageMigrationFootageMoveInputSchema, object({ jobId: string() }), {
29030
+ kind: "mutation",
29031
+ auth: "admin"
29032
+ }),
29033
+ getStorageMigrationMoveStatus: method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }),
29034
+ cancelStorageMigrationMove: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28829
29035
  kind: "mutation",
28830
29036
  auth: "admin"
28831
29037
  })
@@ -30780,6 +30986,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
30780
30986
  ssoBridgeCapability,
30781
30987
  storageCapability,
30782
30988
  storageEvictableCapability,
30989
+ storageMigrationCapability,
30783
30990
  storageProviderCapability,
30784
30991
  streamBrokerCapability,
30785
30992
  streamCatalogCapability,
@@ -33706,6 +33913,12 @@ Object.freeze({
33706
33913
  addonId: null,
33707
33914
  access: "delete"
33708
33915
  },
33916
+ "osdManager.copyDeviceConfiguration": {
33917
+ capName: "osd-manager",
33918
+ capScope: "system",
33919
+ addonId: null,
33920
+ access: "create"
33921
+ },
33709
33922
  "osdManager.getConditionSupport": {
33710
33923
  capName: "osd-manager",
33711
33924
  capScope: "system",
@@ -33802,7 +34015,7 @@ Object.freeze({
33802
34015
  addonId: null,
33803
34016
  access: "create"
33804
34017
  },
33805
- "pipelineAnalytics.cancelMediaRelocate": {
34018
+ "pipelineAnalytics.cancelStorageMigrationMove": {
33806
34019
  capName: "pipeline-analytics",
33807
34020
  capScope: "device",
33808
34021
  addonId: null,
@@ -33874,12 +34087,6 @@ Object.freeze({
33874
34087
  addonId: null,
33875
34088
  access: "view"
33876
34089
  },
33877
- "pipelineAnalytics.getMediaRelocateStatus": {
33878
- capName: "pipeline-analytics",
33879
- capScope: "device",
33880
- addonId: null,
33881
- access: "view"
33882
- },
33883
34090
  "pipelineAnalytics.getMotionEvents": {
33884
34091
  capName: "pipeline-analytics",
33885
34092
  capScope: "device",
@@ -33916,6 +34123,12 @@ Object.freeze({
33916
34123
  addonId: null,
33917
34124
  access: "view"
33918
34125
  },
34126
+ "pipelineAnalytics.getStorageMigrationMoveStatus": {
34127
+ capName: "pipeline-analytics",
34128
+ capScope: "device",
34129
+ addonId: null,
34130
+ access: "view"
34131
+ },
33919
34132
  "pipelineAnalytics.getTrack": {
33920
34133
  capName: "pipeline-analytics",
33921
34134
  capScope: "device",
@@ -33994,6 +34207,12 @@ Object.freeze({
33994
34207
  addonId: null,
33995
34208
  access: "view"
33996
34209
  },
34210
+ "pipelineAnalytics.pauseForStorageMigration": {
34211
+ capName: "pipeline-analytics",
34212
+ capScope: "device",
34213
+ addonId: null,
34214
+ access: "create"
34215
+ },
33997
34216
  "pipelineAnalytics.proposeRetrainAnnotations": {
33998
34217
  capName: "pipeline-analytics",
33999
34218
  capScope: "device",
@@ -34024,7 +34243,7 @@ Object.freeze({
34024
34243
  addonId: null,
34025
34244
  access: "create"
34026
34245
  },
34027
- "pipelineAnalytics.relocateMedia": {
34246
+ "pipelineAnalytics.refreshStorageLocationsForMigration": {
34028
34247
  capName: "pipeline-analytics",
34029
34248
  capScope: "device",
34030
34249
  addonId: null,
@@ -34036,6 +34255,12 @@ Object.freeze({
34036
34255
  addonId: null,
34037
34256
  access: "create"
34038
34257
  },
34258
+ "pipelineAnalytics.resumeForStorageMigration": {
34259
+ capName: "pipeline-analytics",
34260
+ capScope: "device",
34261
+ addonId: null,
34262
+ access: "create"
34263
+ },
34039
34264
  "pipelineAnalytics.saveRetrainAnnotations": {
34040
34265
  capName: "pipeline-analytics",
34041
34266
  capScope: "device",
@@ -34060,6 +34285,12 @@ Object.freeze({
34060
34285
  addonId: null,
34061
34286
  access: "create"
34062
34287
  },
34288
+ "pipelineAnalytics.startStorageMigrationMove": {
34289
+ capName: "pipeline-analytics",
34290
+ capScope: "device",
34291
+ addonId: null,
34292
+ access: "create"
34293
+ },
34063
34294
  "pipelineAnalytics.wipeAllAnalytics": {
34064
34295
  capName: "pipeline-analytics",
34065
34296
  capScope: "device",
@@ -34426,6 +34657,12 @@ Object.freeze({
34426
34657
  addonId: null,
34427
34658
  access: "view"
34428
34659
  },
34660
+ "pipelineOrchestrator.pauseForStorageMigration": {
34661
+ capName: "pipeline-orchestrator",
34662
+ capScope: "system",
34663
+ addonId: null,
34664
+ access: "create"
34665
+ },
34429
34666
  "pipelineOrchestrator.rebalance": {
34430
34667
  capName: "pipeline-orchestrator",
34431
34668
  capScope: "system",
@@ -34450,6 +34687,12 @@ Object.freeze({
34450
34687
  addonId: null,
34451
34688
  access: "view"
34452
34689
  },
34690
+ "pipelineOrchestrator.resumeForStorageMigration": {
34691
+ capName: "pipeline-orchestrator",
34692
+ capScope: "system",
34693
+ addonId: null,
34694
+ access: "create"
34695
+ },
34453
34696
  "pipelineOrchestrator.saveTemplate": {
34454
34697
  capName: "pipeline-orchestrator",
34455
34698
  capScope: "system",
@@ -34846,7 +35089,7 @@ Object.freeze({
34846
35089
  addonId: null,
34847
35090
  access: "create"
34848
35091
  },
34849
- "recording.cancelRelocate": {
35092
+ "recording.cancelStorageMigrationMove": {
34850
35093
  capName: "recording",
34851
35094
  capScope: "system",
34852
35095
  addonId: null,
@@ -34882,7 +35125,7 @@ Object.freeze({
34882
35125
  addonId: null,
34883
35126
  access: "view"
34884
35127
  },
34885
- "recording.getRelocateStatus": {
35128
+ "recording.getStorageMigrationMoveStatus": {
34886
35129
  capName: "recording",
34887
35130
  capScope: "system",
34888
35131
  addonId: null,
@@ -34906,6 +35149,12 @@ Object.freeze({
34906
35149
  addonId: null,
34907
35150
  access: "view"
34908
35151
  },
35152
+ "recording.pauseForStorageMigration": {
35153
+ capName: "recording",
35154
+ capScope: "system",
35155
+ addonId: null,
35156
+ access: "create"
35157
+ },
34909
35158
  "recording.pruneFootage": {
34910
35159
  capName: "recording",
34911
35160
  capScope: "system",
@@ -34924,7 +35173,7 @@ Object.freeze({
34924
35173
  addonId: null,
34925
35174
  access: "view"
34926
35175
  },
34927
- "recording.relocateFootage": {
35176
+ "recording.refreshStorageLocationsForMigration": {
34928
35177
  capName: "recording",
34929
35178
  capScope: "system",
34930
35179
  addonId: null,
@@ -34948,12 +35197,24 @@ Object.freeze({
34948
35197
  addonId: null,
34949
35198
  access: "create"
34950
35199
  },
35200
+ "recording.resumeForStorageMigration": {
35201
+ capName: "recording",
35202
+ capScope: "system",
35203
+ addonId: null,
35204
+ access: "create"
35205
+ },
34951
35206
  "recording.setDeviceConfig": {
34952
35207
  capName: "recording",
34953
35208
  capScope: "system",
34954
35209
  addonId: null,
34955
35210
  access: "create"
34956
35211
  },
35212
+ "recording.startStorageMigrationMove": {
35213
+ capName: "recording",
35214
+ capScope: "system",
35215
+ addonId: null,
35216
+ access: "create"
35217
+ },
34957
35218
  "recordingExport.cancelExport": {
34958
35219
  capName: "recordingExport",
34959
35220
  capScope: "system",
@@ -35344,6 +35605,30 @@ Object.freeze({
35344
35605
  addonId: null,
35345
35606
  access: "view"
35346
35607
  },
35608
+ "storageMigration.cancel": {
35609
+ capName: "storage-migration",
35610
+ capScope: "system",
35611
+ addonId: null,
35612
+ access: "create"
35613
+ },
35614
+ "storageMigration.plan": {
35615
+ capName: "storage-migration",
35616
+ capScope: "system",
35617
+ addonId: null,
35618
+ access: "view"
35619
+ },
35620
+ "storageMigration.start": {
35621
+ capName: "storage-migration",
35622
+ capScope: "system",
35623
+ addonId: null,
35624
+ access: "create"
35625
+ },
35626
+ "storageMigration.status": {
35627
+ capName: "storage-migration",
35628
+ capScope: "system",
35629
+ addonId: null,
35630
+ access: "view"
35631
+ },
35347
35632
  "storageProvider.abortUpload": {
35348
35633
  capName: "storage-provider",
35349
35634
  capScope: "system",
@@ -35722,12 +36007,42 @@ Object.freeze({
35722
36007
  addonId: null,
35723
36008
  access: "create"
35724
36009
  },
36010
+ "terminalSession.adoptLegacyMonitor": {
36011
+ capName: "terminal-session",
36012
+ capScope: "system",
36013
+ addonId: null,
36014
+ access: "create"
36015
+ },
35725
36016
  "terminalSession.close": {
35726
36017
  capName: "terminal-session",
35727
36018
  capScope: "system",
35728
36019
  addonId: null,
35729
36020
  access: "create"
35730
36021
  },
36022
+ "terminalSession.createInstance": {
36023
+ capName: "terminal-session",
36024
+ capScope: "system",
36025
+ addonId: null,
36026
+ access: "create"
36027
+ },
36028
+ "terminalSession.deleteInstance": {
36029
+ capName: "terminal-session",
36030
+ capScope: "system",
36031
+ addonId: null,
36032
+ access: "delete"
36033
+ },
36034
+ "terminalSession.listInstances": {
36035
+ capName: "terminal-session",
36036
+ capScope: "system",
36037
+ addonId: null,
36038
+ access: "view"
36039
+ },
36040
+ "terminalSession.listLegacyCameras": {
36041
+ capName: "terminal-session",
36042
+ capScope: "system",
36043
+ addonId: null,
36044
+ access: "view"
36045
+ },
35731
36046
  "terminalSession.listProfiles": {
35732
36047
  capName: "terminal-session",
35733
36048
  capScope: "system",
@@ -35758,6 +36073,12 @@ Object.freeze({
35758
36073
  addonId: null,
35759
36074
  access: "create"
35760
36075
  },
36076
+ "terminalSession.setInstanceEnabled": {
36077
+ capName: "terminal-session",
36078
+ capScope: "system",
36079
+ addonId: null,
36080
+ access: "create"
36081
+ },
35761
36082
  "terminalSession.writeInput": {
35762
36083
  capName: "terminal-session",
35763
36084
  capScope: "system",
@@ -36589,7 +36910,8 @@ var OsdBindingsStore = class {
36589
36910
  * Widening it means giving `evaluateGate` the facts to answer it — a
36590
36911
  * live detection subject would unlock `classes`, `zones`, `minConfidence`.
36591
36912
  */
36592
- var OSD_SUPPORTED_CONDITIONS = ["devices", "deviceState"];
36913
+ var OSD_SUPPORTED_CONDITIONS = ["deviceState"];
36914
+ var OSD_RUNTIME_CONDITIONS = ["devices", ...OSD_SUPPORTED_CONDITIONS];
36593
36915
  var OPEN = { open: true };
36594
36916
  /**
36595
36917
  * Which condition keys are set on `conditions` but outside the supported
@@ -36597,7 +36919,7 @@ var OPEN = { open: true };
36597
36919
  */
36598
36920
  function unsupportedConditionKeys(conditions) {
36599
36921
  if (conditions === void 0) return [];
36600
- const supported = new Set(OSD_SUPPORTED_CONDITIONS);
36922
+ const supported = new Set(OSD_RUNTIME_CONDITIONS);
36601
36923
  const present = [];
36602
36924
  for (const [key, value] of Object.entries(conditions)) {
36603
36925
  if (value === void 0) continue;
@@ -36840,6 +37162,10 @@ function resolveSource(binding, cameraDeviceId, facts) {
36840
37162
  const source = binding.source;
36841
37163
  if (source.kind === "static") return { value: { raw: source.text } };
36842
37164
  if (source.kind === "clock") return { value: { raw: formatClock(facts.atMs, source.pattern, source.timezone) } };
37165
+ if (source.kind === "latest-recognition") {
37166
+ const value = facts.recognitionByDevice.get(cameraDeviceId)?.[source.recognition];
37167
+ return value === void 0 ? { reason: `no recognized ${source.recognition} has been persisted for device ${cameraDeviceId}` } : { value: { raw: value } };
37168
+ }
36843
37169
  const deviceId = source.deviceId ?? cameraDeviceId;
36844
37170
  const deviceState = facts.stateByDevice.get(deviceId);
36845
37171
  if (deviceState === void 0) return { reason: `no state mirrored for device ${deviceId}` };
@@ -36963,6 +37289,10 @@ var OsdManager = class {
36963
37289
  lastWritten = /* @__PURE__ */ new Map();
36964
37290
  /** Cameras with at least one binding — the loop's work list. */
36965
37291
  boundDevices = /* @__PURE__ */ new Set();
37292
+ recognitions = /* @__PURE__ */ new Map();
37293
+ recognitionHydration = /* @__PURE__ */ new Map();
37294
+ recognitionGeneration = /* @__PURE__ */ new Map();
37295
+ recognitionInvalidatedAt = /* @__PURE__ */ new Map();
36966
37296
  timer = null;
36967
37297
  constructor(deps) {
36968
37298
  this.deps = deps;
@@ -36990,6 +37320,36 @@ var OsdManager = class {
36990
37320
  this.lastWritten.clear();
36991
37321
  }
36992
37322
  /**
37323
+ * Keep recognition overlays event-fresh. The persisted-track hydration below
37324
+ * remains the restart/cold-start authority; lifecycle events only advance it.
37325
+ */
37326
+ onTrackLifecycle(payload) {
37327
+ let changed = false;
37328
+ if (payload.label !== void 0 && (payload.bestClassName === "person" || payload.classes.includes("person"))) changed = this.mergeRecognition(payload.deviceId, "person", payload.label, payload.lastSeen, payload.firstSeen) || changed;
37329
+ if (payload.plateText !== void 0) changed = this.mergeRecognition(payload.deviceId, "plate", payload.plateText, payload.lastSeen, payload.firstSeen) || changed;
37330
+ if (!changed || !this.boundDevices.has(payload.deviceId)) return;
37331
+ this.renderDeviceInternal(payload.deviceId, null).catch((err) => {
37332
+ this.logger.warn("osd-manager: recognition-triggered render failed", {
37333
+ tags: { deviceId: payload.deviceId },
37334
+ meta: { error: String(err) }
37335
+ });
37336
+ });
37337
+ }
37338
+ /** Gallery rows are authoritative for corrections, assignments and deletion. */
37339
+ onRecognitionGalleryChanged(deviceId) {
37340
+ this.recognitionGeneration.set(deviceId, (this.recognitionGeneration.get(deviceId) ?? 0) + 1);
37341
+ this.recognitionInvalidatedAt.set(deviceId, this.now());
37342
+ this.recognitions.delete(deviceId);
37343
+ this.recognitionHydration.delete(deviceId);
37344
+ if (!this.boundDevices.has(deviceId)) return;
37345
+ this.renderDeviceInternal(deviceId, null).catch((err) => {
37346
+ this.logger.warn("osd-manager: gallery-triggered render failed", {
37347
+ tags: { deviceId },
37348
+ meta: { error: String(err) }
37349
+ });
37350
+ });
37351
+ }
37352
+ /**
36993
37353
  * Reconcile the work list against the store rather than trusting an event
36994
37354
  * to have arrived (D8). Cheap: one settings read per camera, on the tick
36995
37355
  * cadence, not per frame.
@@ -37083,6 +37443,54 @@ var OsdManager = class {
37083
37443
  });
37084
37444
  return { success: true };
37085
37445
  };
37446
+ copyDeviceConfiguration = async ({ sourceDeviceId, targetDeviceId }) => {
37447
+ if (sourceDeviceId === targetDeviceId) throw new Error("osd-manager: source and target camera must be different");
37448
+ const [sourceStatus, targetStatus, sourceBindings] = await Promise.all([
37449
+ this.deps.readOsdStatus(sourceDeviceId),
37450
+ this.deps.readOsdStatus(targetDeviceId),
37451
+ this.readBindings(sourceDeviceId)
37452
+ ]);
37453
+ if (sourceStatus === null) throw new Error(`osd-manager: source device ${sourceDeviceId} exposes no overlay slots`);
37454
+ if (targetStatus === null) throw new Error(`osd-manager: target device ${targetDeviceId} exposes no overlay slots`);
37455
+ const sourceSlots = sourceStatus.overlays.filter((slot) => slot.readOnly !== true && sourceBindings[slot.id] !== void 0);
37456
+ if (sourceSlots.length === 0) throw new Error(`osd-manager: source device ${sourceDeviceId} has no configured writable slots`);
37457
+ const targetSlots = targetStatus.overlays.filter((slot) => slot.readOnly !== true);
37458
+ const copiedBindings = {};
37459
+ const copyCount = Math.min(sourceSlots.length, targetSlots.length);
37460
+ for (let index = 0; index < copyCount; index += 1) {
37461
+ const sourceSlot = sourceSlots[index];
37462
+ const targetSlot = targetSlots[index];
37463
+ if (sourceSlot === void 0 || targetSlot === void 0) continue;
37464
+ const binding = sourceBindings[sourceSlot.id];
37465
+ if (binding === void 0) continue;
37466
+ const conditions = binding.conditions;
37467
+ const normalizedConditions = conditions === void 0 ? void 0 : (() => {
37468
+ const { devices: _cameraScope, ...rest } = conditions;
37469
+ return Object.keys(rest).length === 0 ? void 0 : rest;
37470
+ })();
37471
+ copiedBindings[targetSlot.id] = {
37472
+ ...binding,
37473
+ conditions: normalizedConditions
37474
+ };
37475
+ }
37476
+ await this.deps.bindings(targetDeviceId).set(copiedBindings);
37477
+ for (const key of [...this.lastWritten.keys()]) if (key.startsWith(`${targetDeviceId}:`)) this.lastWritten.delete(key);
37478
+ if (Object.keys(copiedBindings).length > 0) this.boundDevices.add(targetDeviceId);
37479
+ else this.boundDevices.delete(targetDeviceId);
37480
+ await this.renderDeviceInternal(targetDeviceId, null);
37481
+ this.logger.info("osd-manager: camera configuration copied", {
37482
+ tags: { deviceId: targetDeviceId },
37483
+ meta: {
37484
+ sourceDeviceId,
37485
+ copied: Object.keys(copiedBindings).length,
37486
+ skipped: Math.max(0, sourceSlots.length - targetSlots.length)
37487
+ }
37488
+ });
37489
+ return {
37490
+ copied: Object.keys(copiedBindings).length,
37491
+ skipped: Math.max(0, sourceSlots.length - targetSlots.length)
37492
+ };
37493
+ };
37086
37494
  previewSlot = async ({ deviceId, slotId, binding }) => {
37087
37495
  if (binding !== void 0) assertSupportedConditions(binding.conditions);
37088
37496
  const effective = binding ?? (await this.readBindings(deviceId))[slotId] ?? null;
@@ -37157,6 +37565,7 @@ var OsdManager = class {
37157
37565
  */
37158
37566
  async collectFacts(cameraDeviceId, bindings) {
37159
37567
  const wanted = new Set([cameraDeviceId]);
37568
+ const needsRecognition = Object.values(bindings).some((binding) => binding.source.kind === "latest-recognition");
37160
37569
  for (const binding of Object.values(bindings)) {
37161
37570
  if (binding.source.kind === "device-state" && binding.source.deviceId !== void 0) wanted.add(binding.source.deviceId);
37162
37571
  const gated = binding.conditions?.deviceState?.deviceId;
@@ -37175,12 +37584,59 @@ var OsdManager = class {
37175
37584
  meta: { error: String(err) }
37176
37585
  });
37177
37586
  }
37587
+ if (needsRecognition) await this.hydrateRecognitions(cameraDeviceId);
37588
+ const recognitionByDevice = /* @__PURE__ */ new Map();
37589
+ const known = this.recognitions.get(cameraDeviceId);
37590
+ if (known !== void 0) recognitionByDevice.set(cameraDeviceId, {
37591
+ ...known.person !== void 0 ? { person: known.person.text } : {},
37592
+ ...known.plate !== void 0 ? { plate: known.plate.text } : {}
37593
+ });
37178
37594
  return {
37179
37595
  atMs: this.now(),
37180
37596
  stateByDevice,
37597
+ recognitionByDevice,
37181
37598
  deviceState: (id) => gateState.get(id)
37182
37599
  };
37183
37600
  }
37601
+ async hydrateRecognitions(deviceId) {
37602
+ const existing = this.recognitionHydration.get(deviceId);
37603
+ if (existing !== void 0) return existing;
37604
+ const generation = this.recognitionGeneration.get(deviceId) ?? 0;
37605
+ const hydration = (async () => {
37606
+ try {
37607
+ const latest = await this.deps.readLatestRecognitions(deviceId);
37608
+ if ((this.recognitionGeneration.get(deviceId) ?? 0) !== generation) return;
37609
+ if (latest.person !== void 0) this.mergeRecognition(deviceId, "person", latest.person.text, latest.person.atMs);
37610
+ if (latest.plate !== void 0) this.mergeRecognition(deviceId, "plate", latest.plate.text, latest.plate.atMs);
37611
+ } catch (err) {
37612
+ this.logger.warn("osd-manager: recognition history hydration failed", {
37613
+ tags: { deviceId },
37614
+ meta: { error: String(err) }
37615
+ });
37616
+ this.recognitionHydration.delete(deviceId);
37617
+ }
37618
+ })();
37619
+ this.recognitionHydration.set(deviceId, hydration);
37620
+ await hydration;
37621
+ }
37622
+ mergeRecognition(deviceId, kind, text, atMs, trackFirstSeen) {
37623
+ const normalized = text.trim();
37624
+ if (normalized === "") return false;
37625
+ const invalidatedAt = this.recognitionInvalidatedAt.get(deviceId);
37626
+ if (trackFirstSeen !== void 0 && invalidatedAt !== void 0 && trackFirstSeen <= invalidatedAt) return false;
37627
+ const current = this.recognitions.get(deviceId) ?? {};
37628
+ const previous = current[kind];
37629
+ if (previous !== void 0 && previous.atMs > atMs) return false;
37630
+ if (previous?.atMs === atMs && previous.text === normalized) return false;
37631
+ this.recognitions.set(deviceId, {
37632
+ ...current,
37633
+ [kind]: {
37634
+ text: normalized,
37635
+ atMs
37636
+ }
37637
+ });
37638
+ return true;
37639
+ }
37184
37640
  async readBindings(deviceId) {
37185
37641
  try {
37186
37642
  return await this.deps.bindings(deviceId).get();
@@ -37240,7 +37696,8 @@ var OsdManagerAddon = class extends BaseAddon {
37240
37696
  icon: "monitor",
37241
37697
  path: "/addon/osd-manager",
37242
37698
  remoteName: "addon_osd_manager_page",
37243
- bundle: "remoteEntry.js"
37699
+ bundle: "remoteEntry.js",
37700
+ section: "detection"
37244
37701
  }];
37245
37702
  constructor() {
37246
37703
  super({ ...DEFAULTS });
@@ -37288,9 +37745,41 @@ var OsdManagerAddon = class extends BaseAddon {
37288
37745
  const bindings = await ctx.api.deviceManager.getBindings.query({ deviceId });
37289
37746
  return [...new Set(bindings.entries.map((e) => e.capName))];
37290
37747
  },
37748
+ readLatestRecognitions: async (deviceId) => {
37749
+ const [faces, plates] = await Promise.all([ctx.api.faceGallery.listRecentFaces.query({
37750
+ deviceId,
37751
+ limit: 1,
37752
+ filter: "identified",
37753
+ includeCrops: false
37754
+ }), ctx.api.plateGallery.listPlates.query({
37755
+ deviceId,
37756
+ limit: 1
37757
+ })]);
37758
+ const face = faces[0];
37759
+ const plate = plates[0];
37760
+ return {
37761
+ ...face?.identityName !== void 0 ? { person: {
37762
+ text: face.identityName,
37763
+ atMs: face.timestamp
37764
+ } } : {},
37765
+ ...plate !== void 0 ? { plate: {
37766
+ text: plate.text,
37767
+ atMs: plate.timestamp
37768
+ } } : {}
37769
+ };
37770
+ },
37291
37771
  bindings: (deviceId) => store.forDevice(deviceId)
37292
37772
  });
37293
37773
  this.manager = manager;
37774
+ this.subscribe({ category: EventCategory.PipelineAnalyticsTrackLifecycle }, (event) => {
37775
+ manager.onTrackLifecycle(event.data);
37776
+ });
37777
+ this.subscribe({ category: EventCategory.PipelineAnalyticsFaceGalleryChanged }, (event) => {
37778
+ manager.onRecognitionGalleryChanged(event.data.deviceId);
37779
+ });
37780
+ this.subscribe({ category: EventCategory.PipelineAnalyticsPlateGalleryChanged }, (event) => {
37781
+ manager.onRecognitionGalleryChanged(event.data.deviceId);
37782
+ });
37294
37783
  await manager.start(Math.max(1, this.config.tickSeconds) * 1e3);
37295
37784
  const pagesProvider = {
37296
37785
  id: this.id,