@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.mjs CHANGED
@@ -7622,12 +7622,11 @@ var RecordingConfigSchema = object({
7622
7622
  /**
7623
7623
  * Entity-relocation job state (storage entity-routing spec, Phase 4).
7624
7624
  *
7625
- * One shape shared by the recorder's `relocateFootage` (segments) and
7626
- * pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
7627
- * page renders both movers with one component. Jobs are in-RAM (a restart
7628
- * forgets them re-running is safe by construction: copy-if-absent, delete
7629
- * after verify) and each completed/failed run also lands one durable ops-log
7630
- * row on the owning addon's surface.
7625
+ * One shape shared by the recorder and pipeline-analytics internal movers.
7626
+ * The public admin surface is `storage-migration`; child jobs remain in RAM
7627
+ * because copy-if-absent, verify, delete and index/row repoint are resumable.
7628
+ * Each completed/failed run also lands one durable ops-log row on its owning
7629
+ * addon surface.
7631
7630
  */
7632
7631
  var RelocateJobStateSchema = _enum([
7633
7632
  "running",
@@ -7654,19 +7653,100 @@ var RelocateJobSchema = object({
7654
7653
  finishedAt: number().nullable(),
7655
7654
  error: string().nullable()
7656
7655
  });
7656
+ /** Profile-derived footage selection used only by the migration coordinator:
7657
+ * `recordings` owns high+mid; `recordingsLow` owns low. */
7658
+ var RelocateFootageClassSchema = _enum(["recordings", "recordingsLow"]);
7657
7659
  var RelocateFootageInputSchema = object({
7658
- deviceId: number().optional(),
7659
7660
  fromLocationId: string(),
7660
7661
  toLocationId: string(),
7661
7662
  entities: array(_enum(["segments"])).optional(),
7663
+ /** Limits relocation to the logical profile class. Omit only for the
7664
+ * pre-orchestration compatibility path. */
7665
+ footageClass: RelocateFootageClassSchema.optional(),
7662
7666
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7663
7667
  * never allowed to starve live writers. */
7664
7668
  throttleMbps: number().min(1).max(1e3).optional()
7665
7669
  });
7666
- var RelocateMediaInputSchema = object({
7667
- deviceId: number().optional(),
7670
+ /** Internal, lease-scoped participant operation. It is intentionally separate
7671
+ * from persistent recording settings: a migration never changes
7672
+ * `RecordingConfig.enabled` or camera wrapper bindings. */
7673
+ var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
7674
+ var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
7675
+ var StorageMigrationMediaMoveInputSchema = object({
7668
7676
  toLocationId: string(),
7669
7677
  throttleMbps: number().min(1).max(1e3).optional()
7678
+ }).extend({ leaseId: string().min(1) });
7679
+ /** The independently selectable logical storage classes. `recordings`
7680
+ * encompasses the high and mid segment profiles; `recordingsLow` is low
7681
+ * segments; `eventMedia` is post-analysis blobs. */
7682
+ var StorageMigrationClassSchema = _enum([
7683
+ "recordings",
7684
+ "recordingsLow",
7685
+ "eventMedia"
7686
+ ]);
7687
+ /** A destination is always an existing, fully-qualified location id. The
7688
+ * migration API intentionally never changes a source location's `basePath`:
7689
+ * callers create a new `<type>:<slug>` location, then select it here. */
7690
+ var StorageMigrationDestinationsSchema = object({
7691
+ recordings: string().min(1).optional(),
7692
+ recordingsLow: string().min(1).optional(),
7693
+ eventMedia: string().min(1).optional()
7694
+ }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
7695
+ /** Shared input for planning and starting an orchestrated storage migration. */
7696
+ var StorageMigrationInputSchema = object({
7697
+ destinations: StorageMigrationDestinationsSchema,
7698
+ throttleMbps: number().min(1).max(1e3).optional()
7699
+ });
7700
+ /** The durable coordinator state machine. The only phase that changes default
7701
+ * locations is `repointing`, after every selected mover has completed and been
7702
+ * verified. */
7703
+ var StorageMigrationPhaseSchema = _enum([
7704
+ "planning",
7705
+ "pausing",
7706
+ "moving",
7707
+ "verifying",
7708
+ "repointing",
7709
+ "refreshing",
7710
+ "resuming",
7711
+ "done",
7712
+ "failed",
7713
+ "cancelled"
7714
+ ]);
7715
+ var StorageMigrationParticipantSchema = _enum([
7716
+ "pipeline",
7717
+ "recorder",
7718
+ "analytics"
7719
+ ]);
7720
+ var StorageMigrationMoveSchema = object({
7721
+ storageClass: StorageMigrationClassSchema,
7722
+ fromLocationId: string(),
7723
+ toLocationId: string(),
7724
+ moverJobId: string().nullable(),
7725
+ state: RelocateJobStateSchema.nullable(),
7726
+ error: string().nullable()
7727
+ });
7728
+ var StorageMigrationJobSchema = object({
7729
+ jobId: string(),
7730
+ phase: StorageMigrationPhaseSchema,
7731
+ destinations: StorageMigrationDestinationsSchema,
7732
+ throttleMbps: number(),
7733
+ moves: array(StorageMigrationMoveSchema),
7734
+ pauseLeaseId: string().nullable(),
7735
+ pausedParticipants: array(StorageMigrationParticipantSchema),
7736
+ repointed: boolean(),
7737
+ cancelRequested: boolean(),
7738
+ startedAt: number(),
7739
+ updatedAt: number(),
7740
+ finishedAt: number().nullable(),
7741
+ error: string().nullable()
7742
+ });
7743
+ var StorageMigrationPlanSchema = object({
7744
+ destinations: StorageMigrationDestinationsSchema,
7745
+ moves: array(object({
7746
+ storageClass: StorageMigrationClassSchema,
7747
+ fromLocationId: string(),
7748
+ toLocationId: string()
7749
+ }))
7670
7750
  });
7671
7751
  /**
7672
7752
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -18436,20 +18516,28 @@ var pipelineAnalyticsCapability = {
18436
18516
  }),
18437
18517
  /** The events ops-log rows (newest-first), optionally scoped to one camera.
18438
18518
  * Backed by a declared pipeline-analytics SQLite collection. */
18439
- /**
18440
- * Move event-media blobs onto `toLocationId` (entity-routing spec Phase
18441
- * 4): per-row copy stamp row.locationId → delete old blob. Rows already
18442
- * at the target are skipped, so a re-run resumes. Single-flight.
18443
- */
18444
- relocateMedia: method(RelocateMediaInputSchema, object({ jobId: string() }), {
18519
+ /** Internal migration-participant lease. It drains active MediaStore
18520
+ * writes and refuses new ones without changing analytics bindings. */
18521
+ pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
18445
18522
  kind: "mutation",
18446
18523
  auth: "admin"
18447
18524
  }),
18448
- getMediaRelocateStatus: method(object({}), array(RelocateJobSchema).readonly(), {
18449
- kind: "query",
18525
+ resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
18526
+ kind: "mutation",
18527
+ auth: "admin"
18528
+ }),
18529
+ /** Drops cached location roots after the storage coordinator repoints a
18530
+ * default so future event-media writes use the new root. */
18531
+ refreshStorageLocationsForMigration: method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
18532
+ kind: "mutation",
18450
18533
  auth: "admin"
18451
18534
  }),
18452
- cancelMediaRelocate: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
18535
+ startStorageMigrationMove: method(StorageMigrationMediaMoveInputSchema, object({ jobId: string() }), {
18536
+ kind: "mutation",
18537
+ auth: "admin"
18538
+ }),
18539
+ getStorageMigrationMoveStatus: method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }),
18540
+ cancelStorageMigrationMove: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
18453
18541
  kind: "mutation",
18454
18542
  auth: "admin"
18455
18543
  }),
@@ -20498,6 +20586,16 @@ var pipelineOrchestratorCapability = {
20498
20586
  nodeIdMode: "data",
20499
20587
  exposesDeviceSettings: true,
20500
20588
  methods: {
20589
+ /** Internal storage-migration participant lease. While held, dispatch
20590
+ * requests are accepted as pending but no new runner attachment starts. */
20591
+ pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
20592
+ kind: "mutation",
20593
+ auth: "admin"
20594
+ }),
20595
+ resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
20596
+ kind: "mutation",
20597
+ auth: "admin"
20598
+ }),
20501
20599
  /**
20502
20600
  * Pin a camera's pipeline to a specific agent (L1 affinity).
20503
20601
  * The orchestrator re-evaluates the assignment immediately and persists
@@ -21639,6 +21737,28 @@ var storageEvictableCapability = {
21639
21737
  }), EvictResultSchema, { kind: "mutation" })
21640
21738
  }
21641
21739
  };
21740
+ /**
21741
+ * The single operator-facing storage migration surface. It coordinates the
21742
+ * pipeline, recorder and post-analysis participants; callers never mutate a
21743
+ * camera recording config or invoke an individual mover directly.
21744
+ */
21745
+ var storageMigrationCapability = {
21746
+ name: "storage-migration",
21747
+ scope: "system",
21748
+ mode: "singleton",
21749
+ methods: {
21750
+ plan: method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin" }),
21751
+ start: method(StorageMigrationInputSchema, object({ jobId: string() }), {
21752
+ kind: "mutation",
21753
+ auth: "admin"
21754
+ }),
21755
+ status: method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }),
21756
+ cancel: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21757
+ kind: "mutation",
21758
+ auth: "admin"
21759
+ })
21760
+ }
21761
+ };
21642
21762
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
21643
21763
  providerId: string().min(1),
21644
21764
  displayName: string().min(1),
@@ -21787,6 +21907,28 @@ var TerminalProfileInfoSchema = object({
21787
21907
  label: string(),
21788
21908
  description: string().optional()
21789
21909
  });
21910
+ /**
21911
+ * A durable operator-created Terminal instance. Profiles are templates; only
21912
+ * an instance declares a camera.
21913
+ */
21914
+ var TerminalInstanceInfoSchema = object({
21915
+ instanceId: string(),
21916
+ cameraStableId: string(),
21917
+ nodeId: string(),
21918
+ profileId: string(),
21919
+ profileLabel: string(),
21920
+ name: string(),
21921
+ enabled: boolean()
21922
+ });
21923
+ var TerminalLegacyCameraSchema = object({
21924
+ stableId: string(),
21925
+ nodeId: string(),
21926
+ profileId: string(),
21927
+ profileLabel: string(),
21928
+ name: string(),
21929
+ /** Only legacy monitor cameras can retain their historic stable identity. */
21930
+ adoptable: boolean()
21931
+ });
21790
21932
  var TerminalOutputEventSchema = discriminatedUnion("kind", [object({
21791
21933
  seq: number().int().positive(),
21792
21934
  kind: literal("data"),
@@ -21806,10 +21948,9 @@ var TerminalOutputBatchSchema = object({
21806
21948
  /**
21807
21949
  * terminal-session — singleton system capability for interactive TTY sessions.
21808
21950
  *
21809
- * Phase 1 (this cap): open/resize/close/list a pty running an allowlisted
21810
- * profile, streamed to xterm.js over the data plane. Phase 2 (streaming a
21811
- * session as a camera) is a separate device-provider concern and does not
21812
- * change this contract.
21951
+ * Owns both live PTY lifecycle and durable Terminal instance management.
21952
+ * Profiles are allowlisted templates; an explicit instance is the only path
21953
+ * that declares a camera.
21813
21954
  */
21814
21955
  var terminalSessionCapability = {
21815
21956
  name: "terminal-session",
@@ -21818,6 +21959,37 @@ var terminalSessionCapability = {
21818
21959
  methods: {
21819
21960
  /** Pre-declared profiles the operator may open. */
21820
21961
  listProfiles: method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
21962
+ /** Explicit durable Terminal instances, managed centrally on the hub. */
21963
+ listInstances: method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }),
21964
+ createInstance: method(object({
21965
+ targetNodeId: string().min(1),
21966
+ profileId: string().min(1),
21967
+ name: string().trim().min(1).max(160).optional()
21968
+ }), TerminalInstanceInfoSchema, {
21969
+ kind: "mutation",
21970
+ auth: "admin"
21971
+ }),
21972
+ deleteInstance: method(object({ instanceId: string().min(1) }), _void(), {
21973
+ kind: "mutation",
21974
+ auth: "admin"
21975
+ }),
21976
+ setInstanceEnabled: method(object({
21977
+ instanceId: string().min(1),
21978
+ enabled: boolean()
21979
+ }), TerminalInstanceInfoSchema, {
21980
+ kind: "mutation",
21981
+ auth: "admin"
21982
+ }),
21983
+ /** Existing automatic cameras are shown for explicit migration only. */
21984
+ listLegacyCameras: method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }),
21985
+ /** Explicitly adopt one legacy monitor camera, retaining its stable id. */
21986
+ adoptLegacyMonitor: method(object({
21987
+ stableId: string().min(1),
21988
+ name: string().trim().min(1).max(160).optional()
21989
+ }), TerminalInstanceInfoSchema, {
21990
+ kind: "mutation",
21991
+ auth: "admin"
21992
+ }),
21821
21993
  /** Live sessions currently hosted by the provider. */
21822
21994
  listSessions: method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }),
21823
21995
  /**
@@ -21849,7 +22021,13 @@ var terminalSessionCapability = {
21849
22021
  pullOutput: method(object({
21850
22022
  sessionId: string(),
21851
22023
  afterSeq: number().int().nonnegative(),
21852
- waitMs: number().int().min(0).max(2e3).default(0)
22024
+ waitMs: number().int().min(0).max(2e3).default(0),
22025
+ /**
22026
+ * Wait when a just-opened session has no output yet. Kept opt-in so a
22027
+ * browser's initial repaint remains immediate; the camera snapshot
22028
+ * relay uses it to avoid encoding a blank startup frame.
22029
+ */
22030
+ waitForOutput: boolean().optional()
21853
22031
  }), TerminalOutputBatchSchema, {
21854
22032
  kind: "mutation",
21855
22033
  auth: "admin",
@@ -24798,6 +24976,7 @@ var FaceInfoSchema = object({
24798
24976
  var FaceFilterEnum = _enum([
24799
24977
  "unassigned",
24800
24978
  "recognized",
24979
+ "identified",
24801
24980
  "all"
24802
24981
  ]);
24803
24982
  var MediaFileLiteSchema$1 = object({
@@ -24837,6 +25016,8 @@ var faceGalleryCapability = {
24837
25016
  auth: "admin"
24838
25017
  }),
24839
25018
  listRecentFaces: method(object({
25019
+ /** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
25020
+ deviceId: number().int().optional(),
24840
25021
  limit: number().int().positive().optional(),
24841
25022
  filter: FaceFilterEnum.optional(),
24842
25023
  /**
@@ -27380,6 +27561,10 @@ var OsdSourceSchema = discriminatedUnion("kind", [
27380
27561
  capName: string().min(1).max(64),
27381
27562
  /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
27382
27563
  valuePath: string().min(1).max(64)
27564
+ }),
27565
+ object({
27566
+ kind: literal("latest-recognition"),
27567
+ recognition: _enum(["person", "plate"])
27383
27568
  })
27384
27569
  ]);
27385
27570
  var OsdSlotBindingSchema = object({
@@ -27518,6 +27703,21 @@ var osdManagerCapability = {
27518
27703
  auth: "admin"
27519
27704
  }),
27520
27705
  /**
27706
+ * Replace one camera's binding configuration from another. Writable source
27707
+ * slots are mapped by ordinal onto writable target slots so different
27708
+ * provider slot ids do not make the copied configuration disappear.
27709
+ */
27710
+ copyDeviceConfiguration: method(object({
27711
+ sourceDeviceId: number().int(),
27712
+ targetDeviceId: number().int()
27713
+ }), object({
27714
+ copied: number().int().nonnegative(),
27715
+ skipped: number().int().nonnegative()
27716
+ }), {
27717
+ kind: "mutation",
27718
+ auth: "admin"
27719
+ }),
27720
+ /**
27521
27721
  * Render without writing. `binding` overrides the stored one so an
27522
27722
  * editor can preview an unsaved change. Mutation kind only to carry
27523
27723
  * the binding object safely; no side effects.
@@ -28755,12 +28955,6 @@ var recordingCapability = {
28755
28955
  auth: "admin"
28756
28956
  }),
28757
28957
  /**
28758
- * Move footage (recorded segments) from one recordings location to
28759
- * another — the drain workflow's mover (entity-routing spec Phase 4).
28760
- * Throttled copy → size-verify → delete source → index refresh; resumable
28761
- * by construction (copy-if-absent). Single-flight: one job at a time.
28762
- */
28763
- /**
28764
28958
  * Render a short GIF from recorded footage around `aroundMs` (low profile,
28765
28959
  * palette-free ffmpeg gif). Synchronous — the window is a few seconds of
28766
28960
  * `low`, rendering in ~1-2s. Built for notification attachments: the
@@ -28812,16 +29006,28 @@ var recordingCapability = {
28812
29006
  kind: "mutation",
28813
29007
  auth: "admin"
28814
29008
  }),
28815
- relocateFootage: method(RelocateFootageInputSchema, object({ jobId: string() }), {
29009
+ /** Internal migration-participant lease. Blocks new recorder attaches and
29010
+ * waits for writers to exit while their watchers index final segments. */
29011
+ pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
28816
29012
  kind: "mutation",
28817
29013
  auth: "admin"
28818
29014
  }),
28819
- /** Every known relocate job (in-RAM, newest-first). */
28820
- getRelocateStatus: method(object({}), array(RelocateJobSchema).readonly(), {
28821
- kind: "query",
29015
+ resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
29016
+ kind: "mutation",
29017
+ auth: "admin"
29018
+ }),
29019
+ /** Re-resolve location roots after the coordinator changes defaults. */
29020
+ refreshStorageLocationsForMigration: method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
29021
+ kind: "mutation",
28822
29022
  auth: "admin"
28823
29023
  }),
28824
- cancelRelocate: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
29024
+ /** Internal mover, callable only by the storage-migration coordinator. */
29025
+ startStorageMigrationMove: method(StorageMigrationFootageMoveInputSchema, object({ jobId: string() }), {
29026
+ kind: "mutation",
29027
+ auth: "admin"
29028
+ }),
29029
+ getStorageMigrationMoveStatus: method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }),
29030
+ cancelStorageMigrationMove: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28825
29031
  kind: "mutation",
28826
29032
  auth: "admin"
28827
29033
  })
@@ -30776,6 +30982,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
30776
30982
  ssoBridgeCapability,
30777
30983
  storageCapability,
30778
30984
  storageEvictableCapability,
30985
+ storageMigrationCapability,
30779
30986
  storageProviderCapability,
30780
30987
  streamBrokerCapability,
30781
30988
  streamCatalogCapability,
@@ -33702,6 +33909,12 @@ Object.freeze({
33702
33909
  addonId: null,
33703
33910
  access: "delete"
33704
33911
  },
33912
+ "osdManager.copyDeviceConfiguration": {
33913
+ capName: "osd-manager",
33914
+ capScope: "system",
33915
+ addonId: null,
33916
+ access: "create"
33917
+ },
33705
33918
  "osdManager.getConditionSupport": {
33706
33919
  capName: "osd-manager",
33707
33920
  capScope: "system",
@@ -33798,7 +34011,7 @@ Object.freeze({
33798
34011
  addonId: null,
33799
34012
  access: "create"
33800
34013
  },
33801
- "pipelineAnalytics.cancelMediaRelocate": {
34014
+ "pipelineAnalytics.cancelStorageMigrationMove": {
33802
34015
  capName: "pipeline-analytics",
33803
34016
  capScope: "device",
33804
34017
  addonId: null,
@@ -33870,12 +34083,6 @@ Object.freeze({
33870
34083
  addonId: null,
33871
34084
  access: "view"
33872
34085
  },
33873
- "pipelineAnalytics.getMediaRelocateStatus": {
33874
- capName: "pipeline-analytics",
33875
- capScope: "device",
33876
- addonId: null,
33877
- access: "view"
33878
- },
33879
34086
  "pipelineAnalytics.getMotionEvents": {
33880
34087
  capName: "pipeline-analytics",
33881
34088
  capScope: "device",
@@ -33912,6 +34119,12 @@ Object.freeze({
33912
34119
  addonId: null,
33913
34120
  access: "view"
33914
34121
  },
34122
+ "pipelineAnalytics.getStorageMigrationMoveStatus": {
34123
+ capName: "pipeline-analytics",
34124
+ capScope: "device",
34125
+ addonId: null,
34126
+ access: "view"
34127
+ },
33915
34128
  "pipelineAnalytics.getTrack": {
33916
34129
  capName: "pipeline-analytics",
33917
34130
  capScope: "device",
@@ -33990,6 +34203,12 @@ Object.freeze({
33990
34203
  addonId: null,
33991
34204
  access: "view"
33992
34205
  },
34206
+ "pipelineAnalytics.pauseForStorageMigration": {
34207
+ capName: "pipeline-analytics",
34208
+ capScope: "device",
34209
+ addonId: null,
34210
+ access: "create"
34211
+ },
33993
34212
  "pipelineAnalytics.proposeRetrainAnnotations": {
33994
34213
  capName: "pipeline-analytics",
33995
34214
  capScope: "device",
@@ -34020,7 +34239,7 @@ Object.freeze({
34020
34239
  addonId: null,
34021
34240
  access: "create"
34022
34241
  },
34023
- "pipelineAnalytics.relocateMedia": {
34242
+ "pipelineAnalytics.refreshStorageLocationsForMigration": {
34024
34243
  capName: "pipeline-analytics",
34025
34244
  capScope: "device",
34026
34245
  addonId: null,
@@ -34032,6 +34251,12 @@ Object.freeze({
34032
34251
  addonId: null,
34033
34252
  access: "create"
34034
34253
  },
34254
+ "pipelineAnalytics.resumeForStorageMigration": {
34255
+ capName: "pipeline-analytics",
34256
+ capScope: "device",
34257
+ addonId: null,
34258
+ access: "create"
34259
+ },
34035
34260
  "pipelineAnalytics.saveRetrainAnnotations": {
34036
34261
  capName: "pipeline-analytics",
34037
34262
  capScope: "device",
@@ -34056,6 +34281,12 @@ Object.freeze({
34056
34281
  addonId: null,
34057
34282
  access: "create"
34058
34283
  },
34284
+ "pipelineAnalytics.startStorageMigrationMove": {
34285
+ capName: "pipeline-analytics",
34286
+ capScope: "device",
34287
+ addonId: null,
34288
+ access: "create"
34289
+ },
34059
34290
  "pipelineAnalytics.wipeAllAnalytics": {
34060
34291
  capName: "pipeline-analytics",
34061
34292
  capScope: "device",
@@ -34422,6 +34653,12 @@ Object.freeze({
34422
34653
  addonId: null,
34423
34654
  access: "view"
34424
34655
  },
34656
+ "pipelineOrchestrator.pauseForStorageMigration": {
34657
+ capName: "pipeline-orchestrator",
34658
+ capScope: "system",
34659
+ addonId: null,
34660
+ access: "create"
34661
+ },
34425
34662
  "pipelineOrchestrator.rebalance": {
34426
34663
  capName: "pipeline-orchestrator",
34427
34664
  capScope: "system",
@@ -34446,6 +34683,12 @@ Object.freeze({
34446
34683
  addonId: null,
34447
34684
  access: "view"
34448
34685
  },
34686
+ "pipelineOrchestrator.resumeForStorageMigration": {
34687
+ capName: "pipeline-orchestrator",
34688
+ capScope: "system",
34689
+ addonId: null,
34690
+ access: "create"
34691
+ },
34449
34692
  "pipelineOrchestrator.saveTemplate": {
34450
34693
  capName: "pipeline-orchestrator",
34451
34694
  capScope: "system",
@@ -34842,7 +35085,7 @@ Object.freeze({
34842
35085
  addonId: null,
34843
35086
  access: "create"
34844
35087
  },
34845
- "recording.cancelRelocate": {
35088
+ "recording.cancelStorageMigrationMove": {
34846
35089
  capName: "recording",
34847
35090
  capScope: "system",
34848
35091
  addonId: null,
@@ -34878,7 +35121,7 @@ Object.freeze({
34878
35121
  addonId: null,
34879
35122
  access: "view"
34880
35123
  },
34881
- "recording.getRelocateStatus": {
35124
+ "recording.getStorageMigrationMoveStatus": {
34882
35125
  capName: "recording",
34883
35126
  capScope: "system",
34884
35127
  addonId: null,
@@ -34902,6 +35145,12 @@ Object.freeze({
34902
35145
  addonId: null,
34903
35146
  access: "view"
34904
35147
  },
35148
+ "recording.pauseForStorageMigration": {
35149
+ capName: "recording",
35150
+ capScope: "system",
35151
+ addonId: null,
35152
+ access: "create"
35153
+ },
34905
35154
  "recording.pruneFootage": {
34906
35155
  capName: "recording",
34907
35156
  capScope: "system",
@@ -34920,7 +35169,7 @@ Object.freeze({
34920
35169
  addonId: null,
34921
35170
  access: "view"
34922
35171
  },
34923
- "recording.relocateFootage": {
35172
+ "recording.refreshStorageLocationsForMigration": {
34924
35173
  capName: "recording",
34925
35174
  capScope: "system",
34926
35175
  addonId: null,
@@ -34944,12 +35193,24 @@ Object.freeze({
34944
35193
  addonId: null,
34945
35194
  access: "create"
34946
35195
  },
35196
+ "recording.resumeForStorageMigration": {
35197
+ capName: "recording",
35198
+ capScope: "system",
35199
+ addonId: null,
35200
+ access: "create"
35201
+ },
34947
35202
  "recording.setDeviceConfig": {
34948
35203
  capName: "recording",
34949
35204
  capScope: "system",
34950
35205
  addonId: null,
34951
35206
  access: "create"
34952
35207
  },
35208
+ "recording.startStorageMigrationMove": {
35209
+ capName: "recording",
35210
+ capScope: "system",
35211
+ addonId: null,
35212
+ access: "create"
35213
+ },
34953
35214
  "recordingExport.cancelExport": {
34954
35215
  capName: "recordingExport",
34955
35216
  capScope: "system",
@@ -35340,6 +35601,30 @@ Object.freeze({
35340
35601
  addonId: null,
35341
35602
  access: "view"
35342
35603
  },
35604
+ "storageMigration.cancel": {
35605
+ capName: "storage-migration",
35606
+ capScope: "system",
35607
+ addonId: null,
35608
+ access: "create"
35609
+ },
35610
+ "storageMigration.plan": {
35611
+ capName: "storage-migration",
35612
+ capScope: "system",
35613
+ addonId: null,
35614
+ access: "view"
35615
+ },
35616
+ "storageMigration.start": {
35617
+ capName: "storage-migration",
35618
+ capScope: "system",
35619
+ addonId: null,
35620
+ access: "create"
35621
+ },
35622
+ "storageMigration.status": {
35623
+ capName: "storage-migration",
35624
+ capScope: "system",
35625
+ addonId: null,
35626
+ access: "view"
35627
+ },
35343
35628
  "storageProvider.abortUpload": {
35344
35629
  capName: "storage-provider",
35345
35630
  capScope: "system",
@@ -35718,12 +36003,42 @@ Object.freeze({
35718
36003
  addonId: null,
35719
36004
  access: "create"
35720
36005
  },
36006
+ "terminalSession.adoptLegacyMonitor": {
36007
+ capName: "terminal-session",
36008
+ capScope: "system",
36009
+ addonId: null,
36010
+ access: "create"
36011
+ },
35721
36012
  "terminalSession.close": {
35722
36013
  capName: "terminal-session",
35723
36014
  capScope: "system",
35724
36015
  addonId: null,
35725
36016
  access: "create"
35726
36017
  },
36018
+ "terminalSession.createInstance": {
36019
+ capName: "terminal-session",
36020
+ capScope: "system",
36021
+ addonId: null,
36022
+ access: "create"
36023
+ },
36024
+ "terminalSession.deleteInstance": {
36025
+ capName: "terminal-session",
36026
+ capScope: "system",
36027
+ addonId: null,
36028
+ access: "delete"
36029
+ },
36030
+ "terminalSession.listInstances": {
36031
+ capName: "terminal-session",
36032
+ capScope: "system",
36033
+ addonId: null,
36034
+ access: "view"
36035
+ },
36036
+ "terminalSession.listLegacyCameras": {
36037
+ capName: "terminal-session",
36038
+ capScope: "system",
36039
+ addonId: null,
36040
+ access: "view"
36041
+ },
35727
36042
  "terminalSession.listProfiles": {
35728
36043
  capName: "terminal-session",
35729
36044
  capScope: "system",
@@ -35754,6 +36069,12 @@ Object.freeze({
35754
36069
  addonId: null,
35755
36070
  access: "create"
35756
36071
  },
36072
+ "terminalSession.setInstanceEnabled": {
36073
+ capName: "terminal-session",
36074
+ capScope: "system",
36075
+ addonId: null,
36076
+ access: "create"
36077
+ },
35757
36078
  "terminalSession.writeInput": {
35758
36079
  capName: "terminal-session",
35759
36080
  capScope: "system",
@@ -36585,7 +36906,8 @@ var OsdBindingsStore = class {
36585
36906
  * Widening it means giving `evaluateGate` the facts to answer it — a
36586
36907
  * live detection subject would unlock `classes`, `zones`, `minConfidence`.
36587
36908
  */
36588
- var OSD_SUPPORTED_CONDITIONS = ["devices", "deviceState"];
36909
+ var OSD_SUPPORTED_CONDITIONS = ["deviceState"];
36910
+ var OSD_RUNTIME_CONDITIONS = ["devices", ...OSD_SUPPORTED_CONDITIONS];
36589
36911
  var OPEN = { open: true };
36590
36912
  /**
36591
36913
  * Which condition keys are set on `conditions` but outside the supported
@@ -36593,7 +36915,7 @@ var OPEN = { open: true };
36593
36915
  */
36594
36916
  function unsupportedConditionKeys(conditions) {
36595
36917
  if (conditions === void 0) return [];
36596
- const supported = new Set(OSD_SUPPORTED_CONDITIONS);
36918
+ const supported = new Set(OSD_RUNTIME_CONDITIONS);
36597
36919
  const present = [];
36598
36920
  for (const [key, value] of Object.entries(conditions)) {
36599
36921
  if (value === void 0) continue;
@@ -36836,6 +37158,10 @@ function resolveSource(binding, cameraDeviceId, facts) {
36836
37158
  const source = binding.source;
36837
37159
  if (source.kind === "static") return { value: { raw: source.text } };
36838
37160
  if (source.kind === "clock") return { value: { raw: formatClock(facts.atMs, source.pattern, source.timezone) } };
37161
+ if (source.kind === "latest-recognition") {
37162
+ const value = facts.recognitionByDevice.get(cameraDeviceId)?.[source.recognition];
37163
+ return value === void 0 ? { reason: `no recognized ${source.recognition} has been persisted for device ${cameraDeviceId}` } : { value: { raw: value } };
37164
+ }
36839
37165
  const deviceId = source.deviceId ?? cameraDeviceId;
36840
37166
  const deviceState = facts.stateByDevice.get(deviceId);
36841
37167
  if (deviceState === void 0) return { reason: `no state mirrored for device ${deviceId}` };
@@ -36959,6 +37285,10 @@ var OsdManager = class {
36959
37285
  lastWritten = /* @__PURE__ */ new Map();
36960
37286
  /** Cameras with at least one binding — the loop's work list. */
36961
37287
  boundDevices = /* @__PURE__ */ new Set();
37288
+ recognitions = /* @__PURE__ */ new Map();
37289
+ recognitionHydration = /* @__PURE__ */ new Map();
37290
+ recognitionGeneration = /* @__PURE__ */ new Map();
37291
+ recognitionInvalidatedAt = /* @__PURE__ */ new Map();
36962
37292
  timer = null;
36963
37293
  constructor(deps) {
36964
37294
  this.deps = deps;
@@ -36986,6 +37316,36 @@ var OsdManager = class {
36986
37316
  this.lastWritten.clear();
36987
37317
  }
36988
37318
  /**
37319
+ * Keep recognition overlays event-fresh. The persisted-track hydration below
37320
+ * remains the restart/cold-start authority; lifecycle events only advance it.
37321
+ */
37322
+ onTrackLifecycle(payload) {
37323
+ let changed = false;
37324
+ 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;
37325
+ if (payload.plateText !== void 0) changed = this.mergeRecognition(payload.deviceId, "plate", payload.plateText, payload.lastSeen, payload.firstSeen) || changed;
37326
+ if (!changed || !this.boundDevices.has(payload.deviceId)) return;
37327
+ this.renderDeviceInternal(payload.deviceId, null).catch((err) => {
37328
+ this.logger.warn("osd-manager: recognition-triggered render failed", {
37329
+ tags: { deviceId: payload.deviceId },
37330
+ meta: { error: String(err) }
37331
+ });
37332
+ });
37333
+ }
37334
+ /** Gallery rows are authoritative for corrections, assignments and deletion. */
37335
+ onRecognitionGalleryChanged(deviceId) {
37336
+ this.recognitionGeneration.set(deviceId, (this.recognitionGeneration.get(deviceId) ?? 0) + 1);
37337
+ this.recognitionInvalidatedAt.set(deviceId, this.now());
37338
+ this.recognitions.delete(deviceId);
37339
+ this.recognitionHydration.delete(deviceId);
37340
+ if (!this.boundDevices.has(deviceId)) return;
37341
+ this.renderDeviceInternal(deviceId, null).catch((err) => {
37342
+ this.logger.warn("osd-manager: gallery-triggered render failed", {
37343
+ tags: { deviceId },
37344
+ meta: { error: String(err) }
37345
+ });
37346
+ });
37347
+ }
37348
+ /**
36989
37349
  * Reconcile the work list against the store rather than trusting an event
36990
37350
  * to have arrived (D8). Cheap: one settings read per camera, on the tick
36991
37351
  * cadence, not per frame.
@@ -37079,6 +37439,54 @@ var OsdManager = class {
37079
37439
  });
37080
37440
  return { success: true };
37081
37441
  };
37442
+ copyDeviceConfiguration = async ({ sourceDeviceId, targetDeviceId }) => {
37443
+ if (sourceDeviceId === targetDeviceId) throw new Error("osd-manager: source and target camera must be different");
37444
+ const [sourceStatus, targetStatus, sourceBindings] = await Promise.all([
37445
+ this.deps.readOsdStatus(sourceDeviceId),
37446
+ this.deps.readOsdStatus(targetDeviceId),
37447
+ this.readBindings(sourceDeviceId)
37448
+ ]);
37449
+ if (sourceStatus === null) throw new Error(`osd-manager: source device ${sourceDeviceId} exposes no overlay slots`);
37450
+ if (targetStatus === null) throw new Error(`osd-manager: target device ${targetDeviceId} exposes no overlay slots`);
37451
+ const sourceSlots = sourceStatus.overlays.filter((slot) => slot.readOnly !== true && sourceBindings[slot.id] !== void 0);
37452
+ if (sourceSlots.length === 0) throw new Error(`osd-manager: source device ${sourceDeviceId} has no configured writable slots`);
37453
+ const targetSlots = targetStatus.overlays.filter((slot) => slot.readOnly !== true);
37454
+ const copiedBindings = {};
37455
+ const copyCount = Math.min(sourceSlots.length, targetSlots.length);
37456
+ for (let index = 0; index < copyCount; index += 1) {
37457
+ const sourceSlot = sourceSlots[index];
37458
+ const targetSlot = targetSlots[index];
37459
+ if (sourceSlot === void 0 || targetSlot === void 0) continue;
37460
+ const binding = sourceBindings[sourceSlot.id];
37461
+ if (binding === void 0) continue;
37462
+ const conditions = binding.conditions;
37463
+ const normalizedConditions = conditions === void 0 ? void 0 : (() => {
37464
+ const { devices: _cameraScope, ...rest } = conditions;
37465
+ return Object.keys(rest).length === 0 ? void 0 : rest;
37466
+ })();
37467
+ copiedBindings[targetSlot.id] = {
37468
+ ...binding,
37469
+ conditions: normalizedConditions
37470
+ };
37471
+ }
37472
+ await this.deps.bindings(targetDeviceId).set(copiedBindings);
37473
+ for (const key of [...this.lastWritten.keys()]) if (key.startsWith(`${targetDeviceId}:`)) this.lastWritten.delete(key);
37474
+ if (Object.keys(copiedBindings).length > 0) this.boundDevices.add(targetDeviceId);
37475
+ else this.boundDevices.delete(targetDeviceId);
37476
+ await this.renderDeviceInternal(targetDeviceId, null);
37477
+ this.logger.info("osd-manager: camera configuration copied", {
37478
+ tags: { deviceId: targetDeviceId },
37479
+ meta: {
37480
+ sourceDeviceId,
37481
+ copied: Object.keys(copiedBindings).length,
37482
+ skipped: Math.max(0, sourceSlots.length - targetSlots.length)
37483
+ }
37484
+ });
37485
+ return {
37486
+ copied: Object.keys(copiedBindings).length,
37487
+ skipped: Math.max(0, sourceSlots.length - targetSlots.length)
37488
+ };
37489
+ };
37082
37490
  previewSlot = async ({ deviceId, slotId, binding }) => {
37083
37491
  if (binding !== void 0) assertSupportedConditions(binding.conditions);
37084
37492
  const effective = binding ?? (await this.readBindings(deviceId))[slotId] ?? null;
@@ -37153,6 +37561,7 @@ var OsdManager = class {
37153
37561
  */
37154
37562
  async collectFacts(cameraDeviceId, bindings) {
37155
37563
  const wanted = new Set([cameraDeviceId]);
37564
+ const needsRecognition = Object.values(bindings).some((binding) => binding.source.kind === "latest-recognition");
37156
37565
  for (const binding of Object.values(bindings)) {
37157
37566
  if (binding.source.kind === "device-state" && binding.source.deviceId !== void 0) wanted.add(binding.source.deviceId);
37158
37567
  const gated = binding.conditions?.deviceState?.deviceId;
@@ -37171,12 +37580,59 @@ var OsdManager = class {
37171
37580
  meta: { error: String(err) }
37172
37581
  });
37173
37582
  }
37583
+ if (needsRecognition) await this.hydrateRecognitions(cameraDeviceId);
37584
+ const recognitionByDevice = /* @__PURE__ */ new Map();
37585
+ const known = this.recognitions.get(cameraDeviceId);
37586
+ if (known !== void 0) recognitionByDevice.set(cameraDeviceId, {
37587
+ ...known.person !== void 0 ? { person: known.person.text } : {},
37588
+ ...known.plate !== void 0 ? { plate: known.plate.text } : {}
37589
+ });
37174
37590
  return {
37175
37591
  atMs: this.now(),
37176
37592
  stateByDevice,
37593
+ recognitionByDevice,
37177
37594
  deviceState: (id) => gateState.get(id)
37178
37595
  };
37179
37596
  }
37597
+ async hydrateRecognitions(deviceId) {
37598
+ const existing = this.recognitionHydration.get(deviceId);
37599
+ if (existing !== void 0) return existing;
37600
+ const generation = this.recognitionGeneration.get(deviceId) ?? 0;
37601
+ const hydration = (async () => {
37602
+ try {
37603
+ const latest = await this.deps.readLatestRecognitions(deviceId);
37604
+ if ((this.recognitionGeneration.get(deviceId) ?? 0) !== generation) return;
37605
+ if (latest.person !== void 0) this.mergeRecognition(deviceId, "person", latest.person.text, latest.person.atMs);
37606
+ if (latest.plate !== void 0) this.mergeRecognition(deviceId, "plate", latest.plate.text, latest.plate.atMs);
37607
+ } catch (err) {
37608
+ this.logger.warn("osd-manager: recognition history hydration failed", {
37609
+ tags: { deviceId },
37610
+ meta: { error: String(err) }
37611
+ });
37612
+ this.recognitionHydration.delete(deviceId);
37613
+ }
37614
+ })();
37615
+ this.recognitionHydration.set(deviceId, hydration);
37616
+ await hydration;
37617
+ }
37618
+ mergeRecognition(deviceId, kind, text, atMs, trackFirstSeen) {
37619
+ const normalized = text.trim();
37620
+ if (normalized === "") return false;
37621
+ const invalidatedAt = this.recognitionInvalidatedAt.get(deviceId);
37622
+ if (trackFirstSeen !== void 0 && invalidatedAt !== void 0 && trackFirstSeen <= invalidatedAt) return false;
37623
+ const current = this.recognitions.get(deviceId) ?? {};
37624
+ const previous = current[kind];
37625
+ if (previous !== void 0 && previous.atMs > atMs) return false;
37626
+ if (previous?.atMs === atMs && previous.text === normalized) return false;
37627
+ this.recognitions.set(deviceId, {
37628
+ ...current,
37629
+ [kind]: {
37630
+ text: normalized,
37631
+ atMs
37632
+ }
37633
+ });
37634
+ return true;
37635
+ }
37180
37636
  async readBindings(deviceId) {
37181
37637
  try {
37182
37638
  return await this.deps.bindings(deviceId).get();
@@ -37236,7 +37692,8 @@ var OsdManagerAddon = class extends BaseAddon {
37236
37692
  icon: "monitor",
37237
37693
  path: "/addon/osd-manager",
37238
37694
  remoteName: "addon_osd_manager_page",
37239
- bundle: "remoteEntry.js"
37695
+ bundle: "remoteEntry.js",
37696
+ section: "detection"
37240
37697
  }];
37241
37698
  constructor() {
37242
37699
  super({ ...DEFAULTS });
@@ -37284,9 +37741,41 @@ var OsdManagerAddon = class extends BaseAddon {
37284
37741
  const bindings = await ctx.api.deviceManager.getBindings.query({ deviceId });
37285
37742
  return [...new Set(bindings.entries.map((e) => e.capName))];
37286
37743
  },
37744
+ readLatestRecognitions: async (deviceId) => {
37745
+ const [faces, plates] = await Promise.all([ctx.api.faceGallery.listRecentFaces.query({
37746
+ deviceId,
37747
+ limit: 1,
37748
+ filter: "identified",
37749
+ includeCrops: false
37750
+ }), ctx.api.plateGallery.listPlates.query({
37751
+ deviceId,
37752
+ limit: 1
37753
+ })]);
37754
+ const face = faces[0];
37755
+ const plate = plates[0];
37756
+ return {
37757
+ ...face?.identityName !== void 0 ? { person: {
37758
+ text: face.identityName,
37759
+ atMs: face.timestamp
37760
+ } } : {},
37761
+ ...plate !== void 0 ? { plate: {
37762
+ text: plate.text,
37763
+ atMs: plate.timestamp
37764
+ } } : {}
37765
+ };
37766
+ },
37287
37767
  bindings: (deviceId) => store.forDevice(deviceId)
37288
37768
  });
37289
37769
  this.manager = manager;
37770
+ this.subscribe({ category: EventCategory.PipelineAnalyticsTrackLifecycle }, (event) => {
37771
+ manager.onTrackLifecycle(event.data);
37772
+ });
37773
+ this.subscribe({ category: EventCategory.PipelineAnalyticsFaceGalleryChanged }, (event) => {
37774
+ manager.onRecognitionGalleryChanged(event.data.deviceId);
37775
+ });
37776
+ this.subscribe({ category: EventCategory.PipelineAnalyticsPlateGalleryChanged }, (event) => {
37777
+ manager.onRecognitionGalleryChanged(event.data.deviceId);
37778
+ });
37290
37779
  await manager.start(Math.max(1, this.config.tickSeconds) * 1e3);
37291
37780
  const pagesProvider = {
37292
37781
  id: this.id,