@camstack/addon-pipeline-orchestrator 1.2.44 → 1.2.47
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/assets/viewer/.viewer-version +1 -1
- package/assets/viewer/_expo/static/js/web/{Clipboard-ea01d8cace0464fa1c39149da612be36.js → Clipboard-7ce228c3105c6643f60262c6665333bb.js} +8 -8
- package/assets/viewer/_expo/static/js/web/{Haptics-35bc7b5bbbc3731f03b5d5fafff2ac0c.js → Haptics-a27d50d15732e0b48774f90db59c02bf.js} +3 -3
- package/assets/viewer/_expo/static/js/web/{index-5b79e9d56650c4720921fd27c27ccb33.js → index-a8dfe1019b6f61fc84bf4dba973b9571.js} +27 -26
- package/assets/viewer/_expo/static/js/web/{node-fs-7cc97cb7668ecaeabcbcfee447afb982.js → node-fs-48d2a76a1b4358828a29686e594be263.js} +1 -1
- package/assets/viewer/_expo/static/js/web/{node-path-b4104903c9511874cbbf037d17e9f78d.js → node-path-f5cd235cc4e30b6508669583b1b2c718.js} +1 -1
- package/assets/viewer/index.html +1 -1
- package/dist/_stub.js +2 -2
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-KN60YkLI.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-DVSFAO6F.mjs} +3 -3
- package/dist/{_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-REijOHWS.mjs → _virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DeEbejFq.mjs} +1 -1
- package/dist/{_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C-VsJkiZ.mjs → _virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-K6wrAHkp.mjs} +1 -1
- package/dist/{hostInit-CKQmEzIK.mjs → hostInit-D4Kq_1dM.mjs} +3 -3
- package/dist/index.js +667 -64
- package/dist/index.mjs +667 -64
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/assets/viewer/assets/node_modules/expo-router/assets/arrow_down.017bc6ba3fc25503e5eb5e53826d48a8.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/error.d1ea1496f9057eb392d5bbf3732a61b7.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/file.19eeb73b9593a38f8e9f418337fc7d10.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/forward.d8b800c443b8972542883e0b9de2bdc6.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/pkg.ab19f4cbc543357183a20571f68380a3.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/back-icon-mask.0a328cd9c1afd0afe8e3b1ec5165b1b4.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/back-icon.35ba0eaec5a4f5ed12ca16fabeae451d.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@2x.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@3x.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@4x.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/close-icon.808e1b1b9b53114ec2838071a7e6daa7.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/close-icon.808e1b1b9b53114ec2838071a7e6daa7@2x.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/close-icon.808e1b1b9b53114ec2838071a7e6daa7@3x.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/close-icon.808e1b1b9b53114ec2838071a7e6daa7@4x.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/search-icon.286d67d3f74808a60a78d3ebf1a5fb57.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/sitemap.412dd9275b6b48ad28f5e3d81bb1f626.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/unmatched.20e71bdf79e3a97bf55fd9e164041578.png +0 -0
package/dist/index.mjs
CHANGED
|
@@ -8365,12 +8365,11 @@ var RecordingConfigSchema = object({
|
|
|
8365
8365
|
/**
|
|
8366
8366
|
* Entity-relocation job state (storage entity-routing spec, Phase 4).
|
|
8367
8367
|
*
|
|
8368
|
-
* One shape shared by the recorder
|
|
8369
|
-
*
|
|
8370
|
-
*
|
|
8371
|
-
*
|
|
8372
|
-
*
|
|
8373
|
-
* row on the owning addon's surface.
|
|
8368
|
+
* One shape shared by the recorder and pipeline-analytics internal movers.
|
|
8369
|
+
* The public admin surface is `storage-migration`; child jobs remain in RAM
|
|
8370
|
+
* because copy-if-absent, verify, delete and index/row repoint are resumable.
|
|
8371
|
+
* Each completed/failed run also lands one durable ops-log row on its owning
|
|
8372
|
+
* addon surface.
|
|
8374
8373
|
*/
|
|
8375
8374
|
var RelocateJobStateSchema = _enum([
|
|
8376
8375
|
"running",
|
|
@@ -8397,19 +8396,100 @@ var RelocateJobSchema = object({
|
|
|
8397
8396
|
finishedAt: number().nullable(),
|
|
8398
8397
|
error: string().nullable()
|
|
8399
8398
|
});
|
|
8399
|
+
/** Profile-derived footage selection used only by the migration coordinator:
|
|
8400
|
+
* `recordings` owns high+mid; `recordingsLow` owns low. */
|
|
8401
|
+
var RelocateFootageClassSchema = _enum(["recordings", "recordingsLow"]);
|
|
8400
8402
|
var RelocateFootageInputSchema = object({
|
|
8401
|
-
deviceId: number().optional(),
|
|
8402
8403
|
fromLocationId: string(),
|
|
8403
8404
|
toLocationId: string(),
|
|
8404
8405
|
entities: array(_enum(["segments"])).optional(),
|
|
8406
|
+
/** Limits relocation to the logical profile class. Omit only for the
|
|
8407
|
+
* pre-orchestration compatibility path. */
|
|
8408
|
+
footageClass: RelocateFootageClassSchema.optional(),
|
|
8405
8409
|
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
8406
8410
|
* never allowed to starve live writers. */
|
|
8407
8411
|
throttleMbps: number().min(1).max(1e3).optional()
|
|
8408
8412
|
});
|
|
8409
|
-
|
|
8410
|
-
|
|
8413
|
+
/** Internal, lease-scoped participant operation. It is intentionally separate
|
|
8414
|
+
* from persistent recording settings: a migration never changes
|
|
8415
|
+
* `RecordingConfig.enabled` or camera wrapper bindings. */
|
|
8416
|
+
var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
|
|
8417
|
+
var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
|
|
8418
|
+
var StorageMigrationMediaMoveInputSchema = object({
|
|
8411
8419
|
toLocationId: string(),
|
|
8412
8420
|
throttleMbps: number().min(1).max(1e3).optional()
|
|
8421
|
+
}).extend({ leaseId: string().min(1) });
|
|
8422
|
+
/** The independently selectable logical storage classes. `recordings`
|
|
8423
|
+
* encompasses the high and mid segment profiles; `recordingsLow` is low
|
|
8424
|
+
* segments; `eventMedia` is post-analysis blobs. */
|
|
8425
|
+
var StorageMigrationClassSchema = _enum([
|
|
8426
|
+
"recordings",
|
|
8427
|
+
"recordingsLow",
|
|
8428
|
+
"eventMedia"
|
|
8429
|
+
]);
|
|
8430
|
+
/** A destination is always an existing, fully-qualified location id. The
|
|
8431
|
+
* migration API intentionally never changes a source location's `basePath`:
|
|
8432
|
+
* callers create a new `<type>:<slug>` location, then select it here. */
|
|
8433
|
+
var StorageMigrationDestinationsSchema = object({
|
|
8434
|
+
recordings: string().min(1).optional(),
|
|
8435
|
+
recordingsLow: string().min(1).optional(),
|
|
8436
|
+
eventMedia: string().min(1).optional()
|
|
8437
|
+
}).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
|
|
8438
|
+
/** Shared input for planning and starting an orchestrated storage migration. */
|
|
8439
|
+
var StorageMigrationInputSchema = object({
|
|
8440
|
+
destinations: StorageMigrationDestinationsSchema,
|
|
8441
|
+
throttleMbps: number().min(1).max(1e3).optional()
|
|
8442
|
+
});
|
|
8443
|
+
/** The durable coordinator state machine. The only phase that changes default
|
|
8444
|
+
* locations is `repointing`, after every selected mover has completed and been
|
|
8445
|
+
* verified. */
|
|
8446
|
+
var StorageMigrationPhaseSchema = _enum([
|
|
8447
|
+
"planning",
|
|
8448
|
+
"pausing",
|
|
8449
|
+
"moving",
|
|
8450
|
+
"verifying",
|
|
8451
|
+
"repointing",
|
|
8452
|
+
"refreshing",
|
|
8453
|
+
"resuming",
|
|
8454
|
+
"done",
|
|
8455
|
+
"failed",
|
|
8456
|
+
"cancelled"
|
|
8457
|
+
]);
|
|
8458
|
+
var StorageMigrationParticipantSchema = _enum([
|
|
8459
|
+
"pipeline",
|
|
8460
|
+
"recorder",
|
|
8461
|
+
"analytics"
|
|
8462
|
+
]);
|
|
8463
|
+
var StorageMigrationMoveSchema = object({
|
|
8464
|
+
storageClass: StorageMigrationClassSchema,
|
|
8465
|
+
fromLocationId: string(),
|
|
8466
|
+
toLocationId: string(),
|
|
8467
|
+
moverJobId: string().nullable(),
|
|
8468
|
+
state: RelocateJobStateSchema.nullable(),
|
|
8469
|
+
error: string().nullable()
|
|
8470
|
+
});
|
|
8471
|
+
var StorageMigrationJobSchema = object({
|
|
8472
|
+
jobId: string(),
|
|
8473
|
+
phase: StorageMigrationPhaseSchema,
|
|
8474
|
+
destinations: StorageMigrationDestinationsSchema,
|
|
8475
|
+
throttleMbps: number(),
|
|
8476
|
+
moves: array(StorageMigrationMoveSchema),
|
|
8477
|
+
pauseLeaseId: string().nullable(),
|
|
8478
|
+
pausedParticipants: array(StorageMigrationParticipantSchema),
|
|
8479
|
+
repointed: boolean(),
|
|
8480
|
+
cancelRequested: boolean(),
|
|
8481
|
+
startedAt: number(),
|
|
8482
|
+
updatedAt: number(),
|
|
8483
|
+
finishedAt: number().nullable(),
|
|
8484
|
+
error: string().nullable()
|
|
8485
|
+
});
|
|
8486
|
+
var StorageMigrationPlanSchema = object({
|
|
8487
|
+
destinations: StorageMigrationDestinationsSchema,
|
|
8488
|
+
moves: array(object({
|
|
8489
|
+
storageClass: StorageMigrationClassSchema,
|
|
8490
|
+
fromLocationId: string(),
|
|
8491
|
+
toLocationId: string()
|
|
8492
|
+
}))
|
|
8413
8493
|
});
|
|
8414
8494
|
/**
|
|
8415
8495
|
* `StorageLocationType` — an addon-declared id that identifies the *kind* of
|
|
@@ -16851,13 +16931,19 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
16851
16931
|
}), method(object({ deviceId: number() }), EventPruneCountsSchema, {
|
|
16852
16932
|
kind: "mutation",
|
|
16853
16933
|
auth: "admin"
|
|
16854
|
-
}), method(
|
|
16934
|
+
}), method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
16855
16935
|
kind: "mutation",
|
|
16856
16936
|
auth: "admin"
|
|
16857
|
-
}), method(object({
|
|
16858
|
-
kind: "
|
|
16937
|
+
}), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
|
|
16938
|
+
kind: "mutation",
|
|
16939
|
+
auth: "admin"
|
|
16940
|
+
}), method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
|
|
16941
|
+
kind: "mutation",
|
|
16942
|
+
auth: "admin"
|
|
16943
|
+
}), method(StorageMigrationMediaMoveInputSchema, object({ jobId: string() }), {
|
|
16944
|
+
kind: "mutation",
|
|
16859
16945
|
auth: "admin"
|
|
16860
|
-
}), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
16946
|
+
}), method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
16861
16947
|
kind: "mutation",
|
|
16862
16948
|
auth: "admin"
|
|
16863
16949
|
}), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
|
|
@@ -18697,6 +18783,16 @@ var pipelineOrchestratorCapability = {
|
|
|
18697
18783
|
nodeIdMode: "data",
|
|
18698
18784
|
exposesDeviceSettings: true,
|
|
18699
18785
|
methods: {
|
|
18786
|
+
/** Internal storage-migration participant lease. While held, dispatch
|
|
18787
|
+
* requests are accepted as pending but no new runner attachment starts. */
|
|
18788
|
+
pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
18789
|
+
kind: "mutation",
|
|
18790
|
+
auth: "admin"
|
|
18791
|
+
}),
|
|
18792
|
+
resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
|
|
18793
|
+
kind: "mutation",
|
|
18794
|
+
auth: "admin"
|
|
18795
|
+
}),
|
|
18700
18796
|
/**
|
|
18701
18797
|
* Pin a camera's pipeline to a specific agent (L1 affinity).
|
|
18702
18798
|
* The orchestrator re-evaluates the assignment immediately and persists
|
|
@@ -19644,6 +19740,13 @@ method(object({ locationId: string() }), EvictableUsageSchema), method(object({
|
|
|
19644
19740
|
locationId: string(),
|
|
19645
19741
|
targetBytes: number().int().positive()
|
|
19646
19742
|
}), EvictResultSchema, { kind: "mutation" });
|
|
19743
|
+
method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin" }), method(StorageMigrationInputSchema, object({ jobId: string() }), {
|
|
19744
|
+
kind: "mutation",
|
|
19745
|
+
auth: "admin"
|
|
19746
|
+
}), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
19747
|
+
kind: "mutation",
|
|
19748
|
+
auth: "admin"
|
|
19749
|
+
});
|
|
19647
19750
|
var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
19648
19751
|
providerId: string().min(1),
|
|
19649
19752
|
displayName: string().min(1),
|
|
@@ -19747,6 +19850,28 @@ var TerminalProfileInfoSchema = object({
|
|
|
19747
19850
|
label: string(),
|
|
19748
19851
|
description: string().optional()
|
|
19749
19852
|
});
|
|
19853
|
+
/**
|
|
19854
|
+
* A durable operator-created Terminal instance. Profiles are templates; only
|
|
19855
|
+
* an instance declares a camera.
|
|
19856
|
+
*/
|
|
19857
|
+
var TerminalInstanceInfoSchema = object({
|
|
19858
|
+
instanceId: string(),
|
|
19859
|
+
cameraStableId: string(),
|
|
19860
|
+
nodeId: string(),
|
|
19861
|
+
profileId: string(),
|
|
19862
|
+
profileLabel: string(),
|
|
19863
|
+
name: string(),
|
|
19864
|
+
enabled: boolean()
|
|
19865
|
+
});
|
|
19866
|
+
var TerminalLegacyCameraSchema = object({
|
|
19867
|
+
stableId: string(),
|
|
19868
|
+
nodeId: string(),
|
|
19869
|
+
profileId: string(),
|
|
19870
|
+
profileLabel: string(),
|
|
19871
|
+
name: string(),
|
|
19872
|
+
/** Only legacy monitor cameras can retain their historic stable identity. */
|
|
19873
|
+
adoptable: boolean()
|
|
19874
|
+
});
|
|
19750
19875
|
var TerminalOutputEventSchema = discriminatedUnion("kind", [object({
|
|
19751
19876
|
seq: number().int().positive(),
|
|
19752
19877
|
kind: literal("data"),
|
|
@@ -19763,7 +19888,29 @@ var TerminalOutputBatchSchema = object({
|
|
|
19763
19888
|
snapshot: string().optional(),
|
|
19764
19889
|
events: array(TerminalOutputEventSchema).readonly()
|
|
19765
19890
|
});
|
|
19766
|
-
method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(
|
|
19891
|
+
method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
|
|
19892
|
+
targetNodeId: string().min(1),
|
|
19893
|
+
profileId: string().min(1),
|
|
19894
|
+
name: string().trim().min(1).max(160).optional()
|
|
19895
|
+
}), TerminalInstanceInfoSchema, {
|
|
19896
|
+
kind: "mutation",
|
|
19897
|
+
auth: "admin"
|
|
19898
|
+
}), method(object({ instanceId: string().min(1) }), _void(), {
|
|
19899
|
+
kind: "mutation",
|
|
19900
|
+
auth: "admin"
|
|
19901
|
+
}), method(object({
|
|
19902
|
+
instanceId: string().min(1),
|
|
19903
|
+
enabled: boolean()
|
|
19904
|
+
}), TerminalInstanceInfoSchema, {
|
|
19905
|
+
kind: "mutation",
|
|
19906
|
+
auth: "admin"
|
|
19907
|
+
}), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
|
|
19908
|
+
stableId: string().min(1),
|
|
19909
|
+
name: string().trim().min(1).max(160).optional()
|
|
19910
|
+
}), TerminalInstanceInfoSchema, {
|
|
19911
|
+
kind: "mutation",
|
|
19912
|
+
auth: "admin"
|
|
19913
|
+
}), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
|
|
19767
19914
|
profileId: string(),
|
|
19768
19915
|
cols: number().int().positive(),
|
|
19769
19916
|
rows: number().int().positive()
|
|
@@ -19780,7 +19927,13 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
|
19780
19927
|
}), method(object({
|
|
19781
19928
|
sessionId: string(),
|
|
19782
19929
|
afterSeq: number().int().nonnegative(),
|
|
19783
|
-
waitMs: number().int().min(0).max(2e3).default(0)
|
|
19930
|
+
waitMs: number().int().min(0).max(2e3).default(0),
|
|
19931
|
+
/**
|
|
19932
|
+
* Wait when a just-opened session has no output yet. Kept opt-in so a
|
|
19933
|
+
* browser's initial repaint remains immediate; the camera snapshot
|
|
19934
|
+
* relay uses it to avoid encoding a blank startup frame.
|
|
19935
|
+
*/
|
|
19936
|
+
waitForOutput: boolean().optional()
|
|
19784
19937
|
}), TerminalOutputBatchSchema, {
|
|
19785
19938
|
kind: "mutation",
|
|
19786
19939
|
auth: "admin",
|
|
@@ -21721,6 +21874,7 @@ var FaceInfoSchema = object({
|
|
|
21721
21874
|
var FaceFilterEnum = _enum([
|
|
21722
21875
|
"unassigned",
|
|
21723
21876
|
"recognized",
|
|
21877
|
+
"identified",
|
|
21724
21878
|
"all"
|
|
21725
21879
|
]);
|
|
21726
21880
|
var MediaFileLiteSchema$1 = object({
|
|
@@ -21749,6 +21903,8 @@ method(_void(), array(IdentitySchema).readonly()), method(object({ name: string(
|
|
|
21749
21903
|
kind: "mutation",
|
|
21750
21904
|
auth: "admin"
|
|
21751
21905
|
}), method(object({
|
|
21906
|
+
/** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
|
|
21907
|
+
deviceId: number().int().optional(),
|
|
21752
21908
|
limit: number().int().positive().optional(),
|
|
21753
21909
|
filter: FaceFilterEnum.optional(),
|
|
21754
21910
|
/**
|
|
@@ -23514,6 +23670,10 @@ var OsdSourceSchema = discriminatedUnion("kind", [
|
|
|
23514
23670
|
capName: string().min(1).max(64),
|
|
23515
23671
|
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
23516
23672
|
valuePath: string().min(1).max(64)
|
|
23673
|
+
}),
|
|
23674
|
+
object({
|
|
23675
|
+
kind: literal("latest-recognition"),
|
|
23676
|
+
recognition: _enum(["person", "plate"])
|
|
23517
23677
|
})
|
|
23518
23678
|
]);
|
|
23519
23679
|
var OsdSlotBindingSchema = object({
|
|
@@ -23619,6 +23779,15 @@ method(object({ deviceId: number().int() }), object({
|
|
|
23619
23779
|
}), object({ success: literal(true) }), {
|
|
23620
23780
|
kind: "mutation",
|
|
23621
23781
|
auth: "admin"
|
|
23782
|
+
}), method(object({
|
|
23783
|
+
sourceDeviceId: number().int(),
|
|
23784
|
+
targetDeviceId: number().int()
|
|
23785
|
+
}), object({
|
|
23786
|
+
copied: number().int().nonnegative(),
|
|
23787
|
+
skipped: number().int().nonnegative()
|
|
23788
|
+
}), {
|
|
23789
|
+
kind: "mutation",
|
|
23790
|
+
auth: "admin"
|
|
23622
23791
|
}), method(object({
|
|
23623
23792
|
deviceId: number().int(),
|
|
23624
23793
|
slotId: string().min(1),
|
|
@@ -24504,13 +24673,19 @@ method(object({
|
|
|
24504
24673
|
}), {
|
|
24505
24674
|
kind: "mutation",
|
|
24506
24675
|
auth: "admin"
|
|
24507
|
-
}), method(
|
|
24676
|
+
}), method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
24508
24677
|
kind: "mutation",
|
|
24509
24678
|
auth: "admin"
|
|
24510
|
-
}), method(object({
|
|
24511
|
-
kind: "
|
|
24679
|
+
}), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
|
|
24680
|
+
kind: "mutation",
|
|
24681
|
+
auth: "admin"
|
|
24682
|
+
}), method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
|
|
24683
|
+
kind: "mutation",
|
|
24684
|
+
auth: "admin"
|
|
24685
|
+
}), method(StorageMigrationFootageMoveInputSchema, object({ jobId: string() }), {
|
|
24686
|
+
kind: "mutation",
|
|
24512
24687
|
auth: "admin"
|
|
24513
|
-
}), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
24688
|
+
}), method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
24514
24689
|
kind: "mutation",
|
|
24515
24690
|
auth: "admin"
|
|
24516
24691
|
});
|
|
@@ -28709,6 +28884,12 @@ Object.freeze({
|
|
|
28709
28884
|
addonId: null,
|
|
28710
28885
|
access: "delete"
|
|
28711
28886
|
},
|
|
28887
|
+
"osdManager.copyDeviceConfiguration": {
|
|
28888
|
+
capName: "osd-manager",
|
|
28889
|
+
capScope: "system",
|
|
28890
|
+
addonId: null,
|
|
28891
|
+
access: "create"
|
|
28892
|
+
},
|
|
28712
28893
|
"osdManager.getConditionSupport": {
|
|
28713
28894
|
capName: "osd-manager",
|
|
28714
28895
|
capScope: "system",
|
|
@@ -28805,7 +28986,7 @@ Object.freeze({
|
|
|
28805
28986
|
addonId: null,
|
|
28806
28987
|
access: "create"
|
|
28807
28988
|
},
|
|
28808
|
-
"pipelineAnalytics.
|
|
28989
|
+
"pipelineAnalytics.cancelStorageMigrationMove": {
|
|
28809
28990
|
capName: "pipeline-analytics",
|
|
28810
28991
|
capScope: "device",
|
|
28811
28992
|
addonId: null,
|
|
@@ -28877,12 +29058,6 @@ Object.freeze({
|
|
|
28877
29058
|
addonId: null,
|
|
28878
29059
|
access: "view"
|
|
28879
29060
|
},
|
|
28880
|
-
"pipelineAnalytics.getMediaRelocateStatus": {
|
|
28881
|
-
capName: "pipeline-analytics",
|
|
28882
|
-
capScope: "device",
|
|
28883
|
-
addonId: null,
|
|
28884
|
-
access: "view"
|
|
28885
|
-
},
|
|
28886
29061
|
"pipelineAnalytics.getMotionEvents": {
|
|
28887
29062
|
capName: "pipeline-analytics",
|
|
28888
29063
|
capScope: "device",
|
|
@@ -28919,6 +29094,12 @@ Object.freeze({
|
|
|
28919
29094
|
addonId: null,
|
|
28920
29095
|
access: "view"
|
|
28921
29096
|
},
|
|
29097
|
+
"pipelineAnalytics.getStorageMigrationMoveStatus": {
|
|
29098
|
+
capName: "pipeline-analytics",
|
|
29099
|
+
capScope: "device",
|
|
29100
|
+
addonId: null,
|
|
29101
|
+
access: "view"
|
|
29102
|
+
},
|
|
28922
29103
|
"pipelineAnalytics.getTrack": {
|
|
28923
29104
|
capName: "pipeline-analytics",
|
|
28924
29105
|
capScope: "device",
|
|
@@ -28997,6 +29178,12 @@ Object.freeze({
|
|
|
28997
29178
|
addonId: null,
|
|
28998
29179
|
access: "view"
|
|
28999
29180
|
},
|
|
29181
|
+
"pipelineAnalytics.pauseForStorageMigration": {
|
|
29182
|
+
capName: "pipeline-analytics",
|
|
29183
|
+
capScope: "device",
|
|
29184
|
+
addonId: null,
|
|
29185
|
+
access: "create"
|
|
29186
|
+
},
|
|
29000
29187
|
"pipelineAnalytics.proposeRetrainAnnotations": {
|
|
29001
29188
|
capName: "pipeline-analytics",
|
|
29002
29189
|
capScope: "device",
|
|
@@ -29027,7 +29214,7 @@ Object.freeze({
|
|
|
29027
29214
|
addonId: null,
|
|
29028
29215
|
access: "create"
|
|
29029
29216
|
},
|
|
29030
|
-
"pipelineAnalytics.
|
|
29217
|
+
"pipelineAnalytics.refreshStorageLocationsForMigration": {
|
|
29031
29218
|
capName: "pipeline-analytics",
|
|
29032
29219
|
capScope: "device",
|
|
29033
29220
|
addonId: null,
|
|
@@ -29039,6 +29226,12 @@ Object.freeze({
|
|
|
29039
29226
|
addonId: null,
|
|
29040
29227
|
access: "create"
|
|
29041
29228
|
},
|
|
29229
|
+
"pipelineAnalytics.resumeForStorageMigration": {
|
|
29230
|
+
capName: "pipeline-analytics",
|
|
29231
|
+
capScope: "device",
|
|
29232
|
+
addonId: null,
|
|
29233
|
+
access: "create"
|
|
29234
|
+
},
|
|
29042
29235
|
"pipelineAnalytics.saveRetrainAnnotations": {
|
|
29043
29236
|
capName: "pipeline-analytics",
|
|
29044
29237
|
capScope: "device",
|
|
@@ -29063,6 +29256,12 @@ Object.freeze({
|
|
|
29063
29256
|
addonId: null,
|
|
29064
29257
|
access: "create"
|
|
29065
29258
|
},
|
|
29259
|
+
"pipelineAnalytics.startStorageMigrationMove": {
|
|
29260
|
+
capName: "pipeline-analytics",
|
|
29261
|
+
capScope: "device",
|
|
29262
|
+
addonId: null,
|
|
29263
|
+
access: "create"
|
|
29264
|
+
},
|
|
29066
29265
|
"pipelineAnalytics.wipeAllAnalytics": {
|
|
29067
29266
|
capName: "pipeline-analytics",
|
|
29068
29267
|
capScope: "device",
|
|
@@ -29429,6 +29628,12 @@ Object.freeze({
|
|
|
29429
29628
|
addonId: null,
|
|
29430
29629
|
access: "view"
|
|
29431
29630
|
},
|
|
29631
|
+
"pipelineOrchestrator.pauseForStorageMigration": {
|
|
29632
|
+
capName: "pipeline-orchestrator",
|
|
29633
|
+
capScope: "system",
|
|
29634
|
+
addonId: null,
|
|
29635
|
+
access: "create"
|
|
29636
|
+
},
|
|
29432
29637
|
"pipelineOrchestrator.rebalance": {
|
|
29433
29638
|
capName: "pipeline-orchestrator",
|
|
29434
29639
|
capScope: "system",
|
|
@@ -29453,6 +29658,12 @@ Object.freeze({
|
|
|
29453
29658
|
addonId: null,
|
|
29454
29659
|
access: "view"
|
|
29455
29660
|
},
|
|
29661
|
+
"pipelineOrchestrator.resumeForStorageMigration": {
|
|
29662
|
+
capName: "pipeline-orchestrator",
|
|
29663
|
+
capScope: "system",
|
|
29664
|
+
addonId: null,
|
|
29665
|
+
access: "create"
|
|
29666
|
+
},
|
|
29456
29667
|
"pipelineOrchestrator.saveTemplate": {
|
|
29457
29668
|
capName: "pipeline-orchestrator",
|
|
29458
29669
|
capScope: "system",
|
|
@@ -29849,7 +30060,7 @@ Object.freeze({
|
|
|
29849
30060
|
addonId: null,
|
|
29850
30061
|
access: "create"
|
|
29851
30062
|
},
|
|
29852
|
-
"recording.
|
|
30063
|
+
"recording.cancelStorageMigrationMove": {
|
|
29853
30064
|
capName: "recording",
|
|
29854
30065
|
capScope: "system",
|
|
29855
30066
|
addonId: null,
|
|
@@ -29885,7 +30096,7 @@ Object.freeze({
|
|
|
29885
30096
|
addonId: null,
|
|
29886
30097
|
access: "view"
|
|
29887
30098
|
},
|
|
29888
|
-
"recording.
|
|
30099
|
+
"recording.getStorageMigrationMoveStatus": {
|
|
29889
30100
|
capName: "recording",
|
|
29890
30101
|
capScope: "system",
|
|
29891
30102
|
addonId: null,
|
|
@@ -29909,6 +30120,12 @@ Object.freeze({
|
|
|
29909
30120
|
addonId: null,
|
|
29910
30121
|
access: "view"
|
|
29911
30122
|
},
|
|
30123
|
+
"recording.pauseForStorageMigration": {
|
|
30124
|
+
capName: "recording",
|
|
30125
|
+
capScope: "system",
|
|
30126
|
+
addonId: null,
|
|
30127
|
+
access: "create"
|
|
30128
|
+
},
|
|
29912
30129
|
"recording.pruneFootage": {
|
|
29913
30130
|
capName: "recording",
|
|
29914
30131
|
capScope: "system",
|
|
@@ -29927,7 +30144,7 @@ Object.freeze({
|
|
|
29927
30144
|
addonId: null,
|
|
29928
30145
|
access: "view"
|
|
29929
30146
|
},
|
|
29930
|
-
"recording.
|
|
30147
|
+
"recording.refreshStorageLocationsForMigration": {
|
|
29931
30148
|
capName: "recording",
|
|
29932
30149
|
capScope: "system",
|
|
29933
30150
|
addonId: null,
|
|
@@ -29951,12 +30168,24 @@ Object.freeze({
|
|
|
29951
30168
|
addonId: null,
|
|
29952
30169
|
access: "create"
|
|
29953
30170
|
},
|
|
30171
|
+
"recording.resumeForStorageMigration": {
|
|
30172
|
+
capName: "recording",
|
|
30173
|
+
capScope: "system",
|
|
30174
|
+
addonId: null,
|
|
30175
|
+
access: "create"
|
|
30176
|
+
},
|
|
29954
30177
|
"recording.setDeviceConfig": {
|
|
29955
30178
|
capName: "recording",
|
|
29956
30179
|
capScope: "system",
|
|
29957
30180
|
addonId: null,
|
|
29958
30181
|
access: "create"
|
|
29959
30182
|
},
|
|
30183
|
+
"recording.startStorageMigrationMove": {
|
|
30184
|
+
capName: "recording",
|
|
30185
|
+
capScope: "system",
|
|
30186
|
+
addonId: null,
|
|
30187
|
+
access: "create"
|
|
30188
|
+
},
|
|
29960
30189
|
"recordingExport.cancelExport": {
|
|
29961
30190
|
capName: "recordingExport",
|
|
29962
30191
|
capScope: "system",
|
|
@@ -30347,6 +30576,30 @@ Object.freeze({
|
|
|
30347
30576
|
addonId: null,
|
|
30348
30577
|
access: "view"
|
|
30349
30578
|
},
|
|
30579
|
+
"storageMigration.cancel": {
|
|
30580
|
+
capName: "storage-migration",
|
|
30581
|
+
capScope: "system",
|
|
30582
|
+
addonId: null,
|
|
30583
|
+
access: "create"
|
|
30584
|
+
},
|
|
30585
|
+
"storageMigration.plan": {
|
|
30586
|
+
capName: "storage-migration",
|
|
30587
|
+
capScope: "system",
|
|
30588
|
+
addonId: null,
|
|
30589
|
+
access: "view"
|
|
30590
|
+
},
|
|
30591
|
+
"storageMigration.start": {
|
|
30592
|
+
capName: "storage-migration",
|
|
30593
|
+
capScope: "system",
|
|
30594
|
+
addonId: null,
|
|
30595
|
+
access: "create"
|
|
30596
|
+
},
|
|
30597
|
+
"storageMigration.status": {
|
|
30598
|
+
capName: "storage-migration",
|
|
30599
|
+
capScope: "system",
|
|
30600
|
+
addonId: null,
|
|
30601
|
+
access: "view"
|
|
30602
|
+
},
|
|
30350
30603
|
"storageProvider.abortUpload": {
|
|
30351
30604
|
capName: "storage-provider",
|
|
30352
30605
|
capScope: "system",
|
|
@@ -30725,12 +30978,42 @@ Object.freeze({
|
|
|
30725
30978
|
addonId: null,
|
|
30726
30979
|
access: "create"
|
|
30727
30980
|
},
|
|
30981
|
+
"terminalSession.adoptLegacyMonitor": {
|
|
30982
|
+
capName: "terminal-session",
|
|
30983
|
+
capScope: "system",
|
|
30984
|
+
addonId: null,
|
|
30985
|
+
access: "create"
|
|
30986
|
+
},
|
|
30728
30987
|
"terminalSession.close": {
|
|
30729
30988
|
capName: "terminal-session",
|
|
30730
30989
|
capScope: "system",
|
|
30731
30990
|
addonId: null,
|
|
30732
30991
|
access: "create"
|
|
30733
30992
|
},
|
|
30993
|
+
"terminalSession.createInstance": {
|
|
30994
|
+
capName: "terminal-session",
|
|
30995
|
+
capScope: "system",
|
|
30996
|
+
addonId: null,
|
|
30997
|
+
access: "create"
|
|
30998
|
+
},
|
|
30999
|
+
"terminalSession.deleteInstance": {
|
|
31000
|
+
capName: "terminal-session",
|
|
31001
|
+
capScope: "system",
|
|
31002
|
+
addonId: null,
|
|
31003
|
+
access: "delete"
|
|
31004
|
+
},
|
|
31005
|
+
"terminalSession.listInstances": {
|
|
31006
|
+
capName: "terminal-session",
|
|
31007
|
+
capScope: "system",
|
|
31008
|
+
addonId: null,
|
|
31009
|
+
access: "view"
|
|
31010
|
+
},
|
|
31011
|
+
"terminalSession.listLegacyCameras": {
|
|
31012
|
+
capName: "terminal-session",
|
|
31013
|
+
capScope: "system",
|
|
31014
|
+
addonId: null,
|
|
31015
|
+
access: "view"
|
|
31016
|
+
},
|
|
30734
31017
|
"terminalSession.listProfiles": {
|
|
30735
31018
|
capName: "terminal-session",
|
|
30736
31019
|
capScope: "system",
|
|
@@ -30761,6 +31044,12 @@ Object.freeze({
|
|
|
30761
31044
|
addonId: null,
|
|
30762
31045
|
access: "create"
|
|
30763
31046
|
},
|
|
31047
|
+
"terminalSession.setInstanceEnabled": {
|
|
31048
|
+
capName: "terminal-session",
|
|
31049
|
+
capScope: "system",
|
|
31050
|
+
addonId: null,
|
|
31051
|
+
access: "create"
|
|
31052
|
+
},
|
|
30764
31053
|
"terminalSession.writeInput": {
|
|
30765
31054
|
capName: "terminal-session",
|
|
30766
31055
|
capScope: "system",
|
|
@@ -34671,6 +34960,48 @@ var DeviceFeaturesMirror = class {
|
|
|
34671
34960
|
}
|
|
34672
34961
|
};
|
|
34673
34962
|
//#endregion
|
|
34963
|
+
//#region src/watchdog-camera.ts
|
|
34964
|
+
/**
|
|
34965
|
+
* Derive the watchdog camera descriptor from a runner config. Captures which
|
|
34966
|
+
* stages are expected to flow CONTINUOUSLY (always-on audio, analyzer motion,
|
|
34967
|
+
* always-on detection) → the camStreamId feeding each. Audio has no dedicated
|
|
34968
|
+
* stream field on the runner config, so it shares the motion stream id.
|
|
34969
|
+
*
|
|
34970
|
+
* `pipelineEnabled === false` (the `object-detection` switch, D62/ADR-0109)
|
|
34971
|
+
* arms NOTHING: `startDetection` deliberately performs no attach and opens no
|
|
34972
|
+
* decode session for such a camera, so no continuous stage can ever produce a
|
|
34973
|
+
* signal. The camera is still returned (and still registered by the caller) so
|
|
34974
|
+
* its watchdog row stays visible — a switched-off camera must read as off, not
|
|
34975
|
+
* as broken.
|
|
34976
|
+
*
|
|
34977
|
+
* `hasAudioSource` is the caller's evidence that the camera's source actually
|
|
34978
|
+
* carries an audio track (`BrokerStats.audio !== null`). It fails OPEN: an
|
|
34979
|
+
* unknown or failed read passes `true`, because the audio stage is diagnostic
|
|
34980
|
+
* and a false negative silently stops watching a working camera.
|
|
34981
|
+
*/
|
|
34982
|
+
function buildWatchdogCamera(config, deviceName, hasAudioSource) {
|
|
34983
|
+
const continuousStages = /* @__PURE__ */ new Map();
|
|
34984
|
+
if (config.pipelineEnabled === false) return {
|
|
34985
|
+
deviceId: config.deviceId,
|
|
34986
|
+
deviceName,
|
|
34987
|
+
audioMode: config.audioMode,
|
|
34988
|
+
detectionMode: config.detectionMode,
|
|
34989
|
+
motionSources: config.motionSources,
|
|
34990
|
+
continuousStages
|
|
34991
|
+
};
|
|
34992
|
+
if (config.audioMode === "always-on" && hasAudioSource) continuousStages.set("audio", config.motionStreamId);
|
|
34993
|
+
if (config.motionSources.includes("analyzer")) continuousStages.set("motion", config.motionStreamId);
|
|
34994
|
+
if (config.detectionMode === "always-on") continuousStages.set("detection", config.detectionStreamId);
|
|
34995
|
+
return {
|
|
34996
|
+
deviceId: config.deviceId,
|
|
34997
|
+
deviceName,
|
|
34998
|
+
audioMode: config.audioMode,
|
|
34999
|
+
detectionMode: config.detectionMode,
|
|
35000
|
+
motionSources: config.motionSources,
|
|
35001
|
+
continuousStages
|
|
35002
|
+
};
|
|
35003
|
+
}
|
|
35004
|
+
//#endregion
|
|
34674
35005
|
//#region src/detection-wiring-controller.ts
|
|
34675
35006
|
var DetectionWiringController = class {
|
|
34676
35007
|
deps;
|
|
@@ -34875,7 +35206,8 @@ var DetectionWiringController = class {
|
|
|
34875
35206
|
await this.stopDetection(deviceId);
|
|
34876
35207
|
}
|
|
34877
35208
|
this.activeDetections.set(deviceId, config);
|
|
34878
|
-
if (config.
|
|
35209
|
+
if (!config.pipelineEnabled) log.info("object detection switched off — no attach, no decode session", { meta: { detectionMode: config.detectionMode } });
|
|
35210
|
+
else if (config.detectionMode === "on-motion") {
|
|
34879
35211
|
if (config.motionSources.includes("analyzer")) try {
|
|
34880
35212
|
await this.deps.session.ensureMotionWatch(deviceId, config);
|
|
34881
35213
|
} catch (err) {
|
|
@@ -34966,11 +35298,44 @@ var DetectionWiringController = class {
|
|
|
34966
35298
|
this.deps.forgetDeviceZones(deviceId);
|
|
34967
35299
|
this.deps.forgetDeviceZoneRules(deviceId);
|
|
34968
35300
|
this.deps.loadShed.forgetDevice(deviceId);
|
|
34969
|
-
|
|
35301
|
+
const settings = this.deps.ctx().settings;
|
|
35302
|
+
if (!settings) return;
|
|
34970
35303
|
await this.deps.settingsStore.deleteCameraSettings(deviceId);
|
|
35304
|
+
try {
|
|
35305
|
+
await settings.clearDeviceStore(deviceId);
|
|
35306
|
+
} catch (err) {
|
|
35307
|
+
this.deps.logger.warn("failed to clear per-device orchestrator settings on removal", {
|
|
35308
|
+
tags: { deviceId },
|
|
35309
|
+
meta: { error: errMsg(err) }
|
|
35310
|
+
});
|
|
35311
|
+
}
|
|
34971
35312
|
this.deps.logger.info("Purged pipeline config for removed device", { tags: { deviceId } });
|
|
34972
35313
|
}
|
|
34973
35314
|
/**
|
|
35315
|
+
* True ONLY when the device registry positively answered "this device does
|
|
35316
|
+
* not exist" (`getDevice` → `null`, per the cap contract).
|
|
35317
|
+
*
|
|
35318
|
+
* A FAILED read answers `false` — D49: a read that fails changes nothing, and
|
|
35319
|
+
* the destructive direction here is refusing to run a live camera.
|
|
35320
|
+
* Deliberately NOT routed through {@link lookupDeviceFeatures}'s mirror: that
|
|
35321
|
+
* one treats missing-as-unknown on purpose (a provider restart passes through
|
|
35322
|
+
* exactly that state and must not re-derive a battery camera's profile), and
|
|
35323
|
+
* conflating the two would make a provider restart look like a deletion.
|
|
35324
|
+
*/
|
|
35325
|
+
async isDeviceDeleted(deviceId) {
|
|
35326
|
+
const api = this.deps.api();
|
|
35327
|
+
if (!api) return false;
|
|
35328
|
+
try {
|
|
35329
|
+
return await api.deviceManager.getDevice.query({ deviceId }) === null;
|
|
35330
|
+
} catch (err) {
|
|
35331
|
+
this.deps.logger.debug("device existence check failed — proceeding as before", {
|
|
35332
|
+
tags: { deviceId },
|
|
35333
|
+
meta: { error: errMsg(err) }
|
|
35334
|
+
});
|
|
35335
|
+
return false;
|
|
35336
|
+
}
|
|
35337
|
+
}
|
|
35338
|
+
/**
|
|
34974
35339
|
* Handle a profile-slot change: build a fresh detection config from
|
|
34975
35340
|
* per-device settings and start detection if motion is enabled.
|
|
34976
35341
|
* Fires on `camera-streams.onProfileSlotsChanged` — any publish,
|
|
@@ -34980,6 +35345,11 @@ var DetectionWiringController = class {
|
|
|
34980
35345
|
if (!this.deps.ctx()) return;
|
|
34981
35346
|
const log = this.deps.logger.withTags({ deviceId });
|
|
34982
35347
|
log.info("handleDeviceRegistered", { meta: { phase: "handleDeviceRegistered" } });
|
|
35348
|
+
if (await this.isDeviceDeleted(deviceId)) {
|
|
35349
|
+
log.warn("device no longer exists — refusing to start detection");
|
|
35350
|
+
await this.handleDeviceUnregistered(deviceId);
|
|
35351
|
+
return;
|
|
35352
|
+
}
|
|
34983
35353
|
const config = await this.buildDetectionConfig(deviceId);
|
|
34984
35354
|
if (config === TRANSIENT_SLOT_READ) {
|
|
34985
35355
|
log.warn("Profile-slot read failed transiently — keeping detection, scheduling retry");
|
|
@@ -35119,28 +35489,17 @@ var DetectionWiringController = class {
|
|
|
35119
35489
|
});
|
|
35120
35490
|
}
|
|
35121
35491
|
/**
|
|
35122
|
-
* Derive the watchdog camera descriptor from a runner config
|
|
35123
|
-
*
|
|
35124
|
-
*
|
|
35125
|
-
*
|
|
35126
|
-
* the motion stream id.
|
|
35492
|
+
* Derive the watchdog camera descriptor from a runner config — thin delegate
|
|
35493
|
+
* onto the pure `buildWatchdogCamera` (`watchdog-camera.ts`), where the
|
|
35494
|
+
* arming rules live (a switched-off camera arms nothing; the audio stage
|
|
35495
|
+
* needs evidence of an audio track).
|
|
35127
35496
|
*
|
|
35128
35497
|
* Public — `PlacementService`'s `buildWatchdogCamera` closure (wired in
|
|
35129
|
-
* `orchestrator-bootstrap.ts`) calls straight through to this method
|
|
35498
|
+
* `orchestrator-bootstrap.ts`) calls straight through to this method; the
|
|
35499
|
+
* caller resolves `hasAudioSource` once per dispatch.
|
|
35130
35500
|
*/
|
|
35131
|
-
buildWatchdogCamera(config, deviceName) {
|
|
35132
|
-
|
|
35133
|
-
if (config.audioMode === "always-on") continuousStages.set("audio", config.motionStreamId);
|
|
35134
|
-
if (config.motionSources.includes("analyzer")) continuousStages.set("motion", config.motionStreamId);
|
|
35135
|
-
if (config.detectionMode === "always-on") continuousStages.set("detection", config.detectionStreamId);
|
|
35136
|
-
return {
|
|
35137
|
-
deviceId: config.deviceId,
|
|
35138
|
-
deviceName,
|
|
35139
|
-
audioMode: config.audioMode,
|
|
35140
|
-
detectionMode: config.detectionMode,
|
|
35141
|
-
motionSources: config.motionSources,
|
|
35142
|
-
continuousStages
|
|
35143
|
-
};
|
|
35501
|
+
buildWatchdogCamera(config, deviceName, hasAudioSource) {
|
|
35502
|
+
return buildWatchdogCamera(config, deviceName, hasAudioSource);
|
|
35144
35503
|
}
|
|
35145
35504
|
/**
|
|
35146
35505
|
* Watchdog recovery callback. Invoked when a camera's continuous stage
|
|
@@ -35153,7 +35512,7 @@ var DetectionWiringController = class {
|
|
|
35153
35512
|
* Public — `PipelineWatchdog`'s `recover` closure (wired in
|
|
35154
35513
|
* `orchestrator-bootstrap.ts`) calls straight through to this method.
|
|
35155
35514
|
*/
|
|
35156
|
-
recoverPipelineStage(deviceId, stage, streamId) {
|
|
35515
|
+
recoverPipelineStage(deviceId, stage, streamId, attempt) {
|
|
35157
35516
|
if (stage === "audio") {
|
|
35158
35517
|
const cfg = this.deps.ledger.getConfig(deviceId);
|
|
35159
35518
|
const audioCfg = cfg ? {
|
|
@@ -35167,6 +35526,14 @@ var DetectionWiringController = class {
|
|
|
35167
35526
|
});
|
|
35168
35527
|
});
|
|
35169
35528
|
}
|
|
35529
|
+
this.deps.logger.debug("watchdog recovery: requesting source refresh", {
|
|
35530
|
+
tags: { deviceId },
|
|
35531
|
+
meta: {
|
|
35532
|
+
stage,
|
|
35533
|
+
camStreamId: streamId,
|
|
35534
|
+
attempt
|
|
35535
|
+
}
|
|
35536
|
+
});
|
|
35170
35537
|
this.deps.eventBus.emit({
|
|
35171
35538
|
id: `watchdog-refresh-${deviceId}-${Date.now()}`,
|
|
35172
35539
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -35569,6 +35936,64 @@ function computeDispatchGap(desired, known) {
|
|
|
35569
35936
|
return [...desired].filter((id) => !known.has(id));
|
|
35570
35937
|
}
|
|
35571
35938
|
/**
|
|
35939
|
+
* Split the desired fleet (derived from broker profile slots) against the LIVE
|
|
35940
|
+
* device registry.
|
|
35941
|
+
*
|
|
35942
|
+
* The broker keeps a deleted device's profile-slot assignments forever, so
|
|
35943
|
+
* `desiredDeviceIdsFromSlots` alone re-proposes a camera the operator already
|
|
35944
|
+
* deleted — and the reconcile's gap-fill then resurrects it. Callers dispatch
|
|
35945
|
+
* `existing` only.
|
|
35946
|
+
*
|
|
35947
|
+
* The caller MUST NOT call this with a failed/partial registry read: a read
|
|
35948
|
+
* that fails changes nothing (D49), it never prunes the fleet.
|
|
35949
|
+
*/
|
|
35950
|
+
function splitDesiredByExistence(desired, liveDeviceIds) {
|
|
35951
|
+
const existing = /* @__PURE__ */ new Set();
|
|
35952
|
+
const vanished = /* @__PURE__ */ new Set();
|
|
35953
|
+
for (const deviceId of desired) if (liveDeviceIds.has(deviceId)) existing.add(deviceId);
|
|
35954
|
+
else vanished.add(deviceId);
|
|
35955
|
+
return {
|
|
35956
|
+
existing,
|
|
35957
|
+
vanished
|
|
35958
|
+
};
|
|
35959
|
+
}
|
|
35960
|
+
/**
|
|
35961
|
+
* Decide which vanished devices are CONFIRMED gone (D49's two-disagreements
|
|
35962
|
+
* rule). Teardown destroys work — stopping detection, releasing the camera and
|
|
35963
|
+
* purging its persisted config — so a single registry read is never enough: a
|
|
35964
|
+
* device must be missing in TWO consecutive reconcile passes before it is torn
|
|
35965
|
+
* down. Anything that puts the device back (a successful listing that contains
|
|
35966
|
+
* it, or a failed read the caller declines to act on) resets the streak, so the
|
|
35967
|
+
* count only ever advances on agreeing evidence.
|
|
35968
|
+
*
|
|
35969
|
+
* Teardown fires ONCE per vanish streak (`torn-down`) — the broker's slots
|
|
35970
|
+
* outlive the device, so it stays vanished on every later pass and would
|
|
35971
|
+
* otherwise be re-purged forever.
|
|
35972
|
+
*
|
|
35973
|
+
* Pure — the caller owns the (in-memory, boot-empty) state map.
|
|
35974
|
+
*/
|
|
35975
|
+
function reconcileVanishedDevices(vanished, prior) {
|
|
35976
|
+
const tearDown = [];
|
|
35977
|
+
const nextState = /* @__PURE__ */ new Map();
|
|
35978
|
+
for (const deviceId of vanished) {
|
|
35979
|
+
const state = prior.get(deviceId);
|
|
35980
|
+
if (state === void 0) {
|
|
35981
|
+
nextState.set(deviceId, "first-pass");
|
|
35982
|
+
continue;
|
|
35983
|
+
}
|
|
35984
|
+
if (state === "first-pass") {
|
|
35985
|
+
tearDown.push(deviceId);
|
|
35986
|
+
nextState.set(deviceId, "torn-down");
|
|
35987
|
+
continue;
|
|
35988
|
+
}
|
|
35989
|
+
nextState.set(deviceId, "torn-down");
|
|
35990
|
+
}
|
|
35991
|
+
return {
|
|
35992
|
+
tearDown,
|
|
35993
|
+
nextState
|
|
35994
|
+
};
|
|
35995
|
+
}
|
|
35996
|
+
/**
|
|
35572
35997
|
* Converge the orchestrator's `assignments` map to runner truth. Runner
|
|
35573
35998
|
* attachments (`getLocalCameras`) are AUTHORITATIVE; this returns the actions
|
|
35574
35999
|
* that make the map agree with them:
|
|
@@ -35715,11 +36140,17 @@ var PlacementService = class {
|
|
|
35715
36140
|
deps;
|
|
35716
36141
|
/** True while an auto-rebalance pass is running (skip overlap). */
|
|
35717
36142
|
autoRebalanceInFlight = false;
|
|
36143
|
+
inFlightAttaches = 0;
|
|
36144
|
+
idleWaiters = [];
|
|
35718
36145
|
constructor(deps) {
|
|
35719
36146
|
this.deps = deps;
|
|
35720
36147
|
}
|
|
35721
36148
|
async dispatchCamera(runnerConfig) {
|
|
35722
36149
|
if (!this.deps.ctx()) throw new Error("PipelineOrchestrator: dispatchCamera called before initialize");
|
|
36150
|
+
if (this.deps.maintenancePaused?.() === true) return {
|
|
36151
|
+
success: true,
|
|
36152
|
+
kind: "pending"
|
|
36153
|
+
};
|
|
35723
36154
|
const currentDeviceKey = this.deps.ledger.getConfig(runnerConfig.deviceId)?.deviceKey ?? null;
|
|
35724
36155
|
this.deps.ledger.setConfig(runnerConfig.deviceId, runnerConfig);
|
|
35725
36156
|
if (this.deps.loadShed.dispatchDeferralCheck(runnerConfig.deviceId)) return {
|
|
@@ -35810,7 +36241,10 @@ var PlacementService = class {
|
|
|
35810
36241
|
try {
|
|
35811
36242
|
await this.attachOn(targetNodeId, attachConfig);
|
|
35812
36243
|
this.deps.ledger.setConfig(runnerConfig.deviceId, attachConfig);
|
|
35813
|
-
if (targetNodeId === this.deps.localNodeId())
|
|
36244
|
+
if (targetNodeId === this.deps.localNodeId()) {
|
|
36245
|
+
const hasAudioSource = await this.resolveHasAudioSource(runnerConfig);
|
|
36246
|
+
this.deps.watchdog()?.register(this.deps.buildWatchdogCamera(runnerConfig, String(runnerConfig.deviceId), hasAudioSource));
|
|
36247
|
+
}
|
|
35814
36248
|
this.deps.ledger.recordAssignment(runnerConfig.deviceId, targetNodeId, reason, reason === "manual");
|
|
35815
36249
|
} finally {
|
|
35816
36250
|
dispatchGuard.dispose();
|
|
@@ -36110,6 +36544,7 @@ var PlacementService = class {
|
|
|
36110
36544
|
* an absent field would produce.
|
|
36111
36545
|
*/
|
|
36112
36546
|
async attachOn(nodeId, config) {
|
|
36547
|
+
if (this.deps.maintenancePaused?.() === true) throw new Error("pipeline dispatch is paused for storage migration");
|
|
36113
36548
|
const ctx = this.deps.ctx();
|
|
36114
36549
|
const api = ctx.api;
|
|
36115
36550
|
if (!api) throw new Error(`attachOn(${nodeId}, ${config.deviceId}): this.ctx.api not available`);
|
|
@@ -36144,6 +36579,7 @@ var PlacementService = class {
|
|
|
36144
36579
|
meta: logMeta
|
|
36145
36580
|
});
|
|
36146
36581
|
}).catch(() => {});
|
|
36582
|
+
this.inFlightAttaches++;
|
|
36147
36583
|
try {
|
|
36148
36584
|
await api.pipelineRunner.attachCamera.mutate({
|
|
36149
36585
|
...config,
|
|
@@ -36173,8 +36609,17 @@ var PlacementService = class {
|
|
|
36173
36609
|
});
|
|
36174
36610
|
}
|
|
36175
36611
|
throw err;
|
|
36612
|
+
} finally {
|
|
36613
|
+
this.inFlightAttaches--;
|
|
36614
|
+
if (this.inFlightAttaches === 0) for (const resolve of this.idleWaiters.splice(0)) resolve();
|
|
36176
36615
|
}
|
|
36177
36616
|
}
|
|
36617
|
+
/** Wait until every attach that began before the maintenance gate closed has
|
|
36618
|
+
* settled. New attaches are rejected at the top of `attachOn`. */
|
|
36619
|
+
waitForMigrationIdle() {
|
|
36620
|
+
if (this.inFlightAttaches === 0) return Promise.resolve();
|
|
36621
|
+
return new Promise((resolve) => this.idleWaiters.push(resolve));
|
|
36622
|
+
}
|
|
36178
36623
|
async detachOn(nodeId, deviceId) {
|
|
36179
36624
|
const api = this.deps.ctx().api;
|
|
36180
36625
|
if (!api) return;
|
|
@@ -36225,7 +36670,20 @@ var PlacementService = class {
|
|
|
36225
36670
|
return;
|
|
36226
36671
|
}
|
|
36227
36672
|
await this.attachOn(nodeId, cached);
|
|
36228
|
-
if (nodeId === this.deps.localNodeId())
|
|
36673
|
+
if (nodeId === this.deps.localNodeId()) {
|
|
36674
|
+
const hasAudioSource = await this.resolveHasAudioSource(cached);
|
|
36675
|
+
this.deps.watchdog()?.register(this.deps.buildWatchdogCamera(cached, String(cached.deviceId), hasAudioSource));
|
|
36676
|
+
}
|
|
36677
|
+
}
|
|
36678
|
+
/**
|
|
36679
|
+
* Resolve the watchdog's audio-track evidence for ONE dispatch. Skipped
|
|
36680
|
+
* entirely (and answered `false`) when `audioMode` could not arm the stage
|
|
36681
|
+
* anyway — `buildWatchdogCamera` needs both — so a camera with on-motion
|
|
36682
|
+
* audio costs no broker round-trip.
|
|
36683
|
+
*/
|
|
36684
|
+
async resolveHasAudioSource(config) {
|
|
36685
|
+
if (config.audioMode !== "always-on") return false;
|
|
36686
|
+
return this.deps.hasAudioSource(config.deviceId, config.motionStreamId);
|
|
36229
36687
|
}
|
|
36230
36688
|
/**
|
|
36231
36689
|
* Normalizes the `decision.kind === 'pending'` → `setPendingReason` +
|
|
@@ -37597,7 +38055,9 @@ var PipelineWatchdog = class {
|
|
|
37597
38055
|
for (const [stage] of cam.continuousStages) stages.set(stage, {
|
|
37598
38056
|
lastSeenMs: now,
|
|
37599
38057
|
attempts: 0,
|
|
37600
|
-
lastAttemptMs: 0
|
|
38058
|
+
lastAttemptMs: 0,
|
|
38059
|
+
everSeen: false,
|
|
38060
|
+
registeredAtMs: now
|
|
37601
38061
|
});
|
|
37602
38062
|
this.state.set(cam.deviceId, stages);
|
|
37603
38063
|
}
|
|
@@ -37611,20 +38071,34 @@ var PipelineWatchdog = class {
|
|
|
37611
38071
|
if (!rt) return;
|
|
37612
38072
|
rt.lastSeenMs = this.deps.now();
|
|
37613
38073
|
rt.attempts = 0;
|
|
38074
|
+
rt.everSeen = true;
|
|
37614
38075
|
}
|
|
37615
38076
|
tick() {
|
|
37616
38077
|
const now = this.deps.now();
|
|
37617
38078
|
for (const cam of this.cameras.values()) {
|
|
37618
|
-
const { line, stalled, recoveries } = this.evaluate(cam, now);
|
|
38079
|
+
const { line, stalled, recoveries, disarms } = this.evaluate(cam, now);
|
|
37619
38080
|
if (stalled) this.deps.logger.warn(line, { tags: { deviceId: cam.deviceId } });
|
|
37620
38081
|
else this.deps.logger.info(line, { tags: { deviceId: cam.deviceId } });
|
|
38082
|
+
for (const d of disarms) {
|
|
38083
|
+
this.deps.logger.warn("watchdog stage never produced a signal — disarming", {
|
|
38084
|
+
tags: { deviceId: cam.deviceId },
|
|
38085
|
+
meta: {
|
|
38086
|
+
stage: d.stage,
|
|
38087
|
+
streamId: d.streamId,
|
|
38088
|
+
sinceRegisteredMs: d.sinceRegisteredMs
|
|
38089
|
+
}
|
|
38090
|
+
});
|
|
38091
|
+
this.state.get(cam.deviceId)?.delete(d.stage);
|
|
38092
|
+
}
|
|
37621
38093
|
for (const r of recoveries) {
|
|
37622
38094
|
const rt = this.state.get(cam.deviceId)?.get(r.stage);
|
|
38095
|
+
let attempt = this.deps.thresholds.maxRecoveryAttempts;
|
|
37623
38096
|
if (rt) {
|
|
37624
38097
|
if (rt.attempts < this.deps.thresholds.maxRecoveryAttempts) rt.attempts += 1;
|
|
37625
38098
|
rt.lastAttemptMs = now;
|
|
38099
|
+
attempt = rt.attempts;
|
|
37626
38100
|
}
|
|
37627
|
-
this.deps.recover(cam.deviceId, r.stage, r.streamId);
|
|
38101
|
+
this.deps.recover(cam.deviceId, r.stage, r.streamId, attempt);
|
|
37628
38102
|
}
|
|
37629
38103
|
}
|
|
37630
38104
|
}
|
|
@@ -37639,6 +38113,7 @@ var PipelineWatchdog = class {
|
|
|
37639
38113
|
evaluate(cam, now) {
|
|
37640
38114
|
const parts = [];
|
|
37641
38115
|
const recoveries = [];
|
|
38116
|
+
const disarms = [];
|
|
37642
38117
|
let stalled = false;
|
|
37643
38118
|
const stageOrder = [
|
|
37644
38119
|
"audio",
|
|
@@ -37665,7 +38140,14 @@ var PipelineWatchdog = class {
|
|
|
37665
38140
|
const thresholdMs = this.deps.thresholds[STAGE_THRESHOLD_KEY[stage]];
|
|
37666
38141
|
const sSec = Math.round(staleness / 1e3);
|
|
37667
38142
|
if (staleness <= thresholdMs) parts.push(`${stage}=${stageMode[stage]}@${streamId}(OK ${sSec}s)`);
|
|
37668
|
-
else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts) {
|
|
38143
|
+
else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts && !rt.everSeen) {
|
|
38144
|
+
disarms.push({
|
|
38145
|
+
stage,
|
|
38146
|
+
streamId,
|
|
38147
|
+
sinceRegisteredMs: now - rt.registeredAtMs
|
|
38148
|
+
});
|
|
38149
|
+
parts.push(`${stage}=${stageMode[stage]}@${streamId}(NEVER SIGNALLED ${sSec}s · disarmed)`);
|
|
38150
|
+
} else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts) {
|
|
37669
38151
|
stalled = true;
|
|
37670
38152
|
if (now - rt.lastAttemptMs >= this.deps.thresholds.exhaustedRetryMs) {
|
|
37671
38153
|
recoveries.push({
|
|
@@ -37686,7 +38168,8 @@ var PipelineWatchdog = class {
|
|
|
37686
38168
|
return {
|
|
37687
38169
|
line: `[watchdog] cam${cam.deviceId} ${cam.deviceName} · ${parts.join(" · ")} → ${stalled ? "STALLED" : "OK"}`,
|
|
37688
38170
|
stalled,
|
|
37689
|
-
recoveries
|
|
38171
|
+
recoveries,
|
|
38172
|
+
disarms
|
|
37690
38173
|
};
|
|
37691
38174
|
}
|
|
37692
38175
|
};
|
|
@@ -38583,6 +39066,14 @@ var ReconcileController = class ReconcileController {
|
|
|
38583
39066
|
/** Shared in-flight pass so a session dispatch waits for runner truth instead of racing it. */
|
|
38584
39067
|
attachmentReconcilePromise = null;
|
|
38585
39068
|
/**
|
|
39069
|
+
* Desired-but-nonexistent devices and how far their vanish streak has got
|
|
39070
|
+
* (`dispatch-reconcile.ts`). PURELY in-memory and seeded EMPTY on boot: it is
|
|
39071
|
+
* a confirmation counter, not a record — after a restart the first pass only
|
|
39072
|
+
* ever re-observes, never destroys. Cleared whenever the registry read fails,
|
|
39073
|
+
* so a failed read can never advance a device toward teardown.
|
|
39074
|
+
*/
|
|
39075
|
+
vanishedDevices = /* @__PURE__ */ new Map();
|
|
39076
|
+
/**
|
|
38586
39077
|
* Per-device backoff for retrying `handleDeviceRegistered` after a
|
|
38587
39078
|
* TRANSIENT profile-slot read failure. Timers are `unref`'d and cleared on
|
|
38588
39079
|
* shutdown / device removal.
|
|
@@ -38651,13 +39142,24 @@ var ReconcileController = class ReconcileController {
|
|
|
38651
39142
|
return;
|
|
38652
39143
|
}
|
|
38653
39144
|
const desired = desiredDeviceIdsFromSlots(slots);
|
|
39145
|
+
const liveDeviceIds = await this.readLiveDeviceIds();
|
|
39146
|
+
const { existing, vanished } = liveDeviceIds === null ? {
|
|
39147
|
+
existing: desired,
|
|
39148
|
+
vanished: /* @__PURE__ */ new Set()
|
|
39149
|
+
} : splitDesiredByExistence(desired, liveDeviceIds);
|
|
39150
|
+
if (liveDeviceIds === null) {
|
|
39151
|
+
this.deps.logger.warn("dispatch reconcile: device existence filter SKIPPED — registry read failed", { meta: { desired: desired.size } });
|
|
39152
|
+
this.vanishedDevices = /* @__PURE__ */ new Map();
|
|
39153
|
+
}
|
|
38654
39154
|
const known = new Set(this.deps.ledger.listConfigIds());
|
|
38655
|
-
const gap = computeDispatchGap(
|
|
39155
|
+
const gap = computeDispatchGap(existing, known);
|
|
38656
39156
|
this.deps.logger.info("dispatch reconcile", { meta: {
|
|
38657
39157
|
desired: desired.size,
|
|
38658
39158
|
known: known.size,
|
|
38659
|
-
gap
|
|
39159
|
+
gap,
|
|
39160
|
+
vanished: vanished.size
|
|
38660
39161
|
} });
|
|
39162
|
+
await this.tearDownVanishedDevices(vanished);
|
|
38661
39163
|
for (const deviceId of gap) try {
|
|
38662
39164
|
await this.deps.handleDeviceRegistered(deviceId);
|
|
38663
39165
|
} catch (err) {
|
|
@@ -38676,6 +39178,53 @@ var ReconcileController = class ReconcileController {
|
|
|
38676
39178
|
}
|
|
38677
39179
|
}
|
|
38678
39180
|
/**
|
|
39181
|
+
* The live device registry as an id set, or `null` when the read FAILED.
|
|
39182
|
+
* `null` is the caller's signal to skip the existence filter entirely — it
|
|
39183
|
+
* must never be confused with "the registry is empty", which would prune the
|
|
39184
|
+
* whole fleet.
|
|
39185
|
+
*
|
|
39186
|
+
* Asks for the slim projection: this needs identity only, never
|
|
39187
|
+
* `config`/`sourceInfo` (a full listing of 293 devices measured 271 KB).
|
|
39188
|
+
*/
|
|
39189
|
+
async readLiveDeviceIds() {
|
|
39190
|
+
const api = this.deps.api();
|
|
39191
|
+
if (!api) return null;
|
|
39192
|
+
try {
|
|
39193
|
+
const devices = await api.deviceManager.listAll.query({ projection: "slim" });
|
|
39194
|
+
return new Set(devices.map((device) => device.id));
|
|
39195
|
+
} catch (err) {
|
|
39196
|
+
this.deps.logger.debug("dispatch reconcile: deviceManager.listAll failed", { meta: { error: errMsg(err) } });
|
|
39197
|
+
return null;
|
|
39198
|
+
}
|
|
39199
|
+
}
|
|
39200
|
+
/**
|
|
39201
|
+
* Tear down the desired devices the registry no longer has — but only once
|
|
39202
|
+
* TWO consecutive passes agree they are gone (`reconcileVanishedDevices`,
|
|
39203
|
+
* D49). Teardown destroys work, so a single listing that omits a device
|
|
39204
|
+
* (which a partial or racy read looks exactly like) is not enough.
|
|
39205
|
+
*
|
|
39206
|
+
* Runs the SAME `handleDeviceUnregistered` the `DeviceUnregistered` event
|
|
39207
|
+
* runs, so there is exactly one teardown path.
|
|
39208
|
+
*/
|
|
39209
|
+
async tearDownVanishedDevices(vanished) {
|
|
39210
|
+
const { tearDown, nextState } = reconcileVanishedDevices(vanished, this.vanishedDevices);
|
|
39211
|
+
this.vanishedDevices = nextState;
|
|
39212
|
+
for (const deviceId of tearDown) {
|
|
39213
|
+
this.deps.logger.warn("dispatch reconcile: device no longer exists — tearing down instead of dispatching", {
|
|
39214
|
+
tags: { deviceId },
|
|
39215
|
+
meta: { confirmedPasses: 2 }
|
|
39216
|
+
});
|
|
39217
|
+
try {
|
|
39218
|
+
await this.deps.handleDeviceUnregistered(deviceId);
|
|
39219
|
+
} catch (err) {
|
|
39220
|
+
this.deps.logger.warn("dispatch reconcile: teardown of vanished device failed", {
|
|
39221
|
+
tags: { deviceId },
|
|
39222
|
+
meta: { error: errMsg(err) }
|
|
39223
|
+
});
|
|
39224
|
+
}
|
|
39225
|
+
}
|
|
39226
|
+
}
|
|
39227
|
+
/**
|
|
38679
39228
|
* Converge the in-memory `assignments` map to runner truth (Phase 1 of the
|
|
38680
39229
|
* placement-derived-state design). Runner attachments (`getLocalCameras`) are
|
|
38681
39230
|
* authoritative: detach duplicates/orphans, and drop stale assignments
|
|
@@ -38986,6 +39535,7 @@ var ReconcileController = class ReconcileController {
|
|
|
38986
39535
|
this.slotReadRetryTimers.clear();
|
|
38987
39536
|
this.slotReadRetryAttempts.clear();
|
|
38988
39537
|
this.remoteHealthAttempts.clear();
|
|
39538
|
+
this.vanishedDevices = /* @__PURE__ */ new Map();
|
|
38989
39539
|
}
|
|
38990
39540
|
};
|
|
38991
39541
|
//#endregion
|
|
@@ -40051,6 +40601,31 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40051
40601
|
getInitTimestamp: () => deps.initTimestamp
|
|
40052
40602
|
});
|
|
40053
40603
|
const inferenceRotation = new RoundRobinInferenceDeviceRotation();
|
|
40604
|
+
/**
|
|
40605
|
+
* Does this camera's source carry an audio track? Read from the broker that
|
|
40606
|
+
* owns the stream (`BrokerStats.audio === null` is the documented no-audio
|
|
40607
|
+
* signal), pinned to the cluster's source owner exactly like the
|
|
40608
|
+
* camera-status broker stage. FAILS OPEN: no api, an RPC error, or a broker
|
|
40609
|
+
* that has not detected the track yet (`undefined`, cold start) all answer
|
|
40610
|
+
* `true`, because the only consumer is the watchdog's diagnostic audio stage
|
|
40611
|
+
* and a false negative silently stops watching a working camera.
|
|
40612
|
+
*/
|
|
40613
|
+
const resolveHasAudioSource = async (deviceId, camStreamId) => {
|
|
40614
|
+
const api = deps.ctx().api;
|
|
40615
|
+
if (!api) return true;
|
|
40616
|
+
try {
|
|
40617
|
+
return (await api.streamBroker.getBrokerStats.query({ brokerId: makeSourceBrokerId(deviceId, camStreamId) }, nodePin(topology.assignSource(deviceId)))).audio !== null;
|
|
40618
|
+
} catch (err) {
|
|
40619
|
+
deps.ctx().logger.debug("audio-source probe failed — assuming the source has audio", {
|
|
40620
|
+
tags: { deviceId },
|
|
40621
|
+
meta: {
|
|
40622
|
+
camStreamId,
|
|
40623
|
+
error: errMsg(err)
|
|
40624
|
+
}
|
|
40625
|
+
});
|
|
40626
|
+
return true;
|
|
40627
|
+
}
|
|
40628
|
+
};
|
|
40054
40629
|
const placement = new PlacementService({
|
|
40055
40630
|
ctx: () => deps.ctx(),
|
|
40056
40631
|
ctxIfReady: () => deps.ctxIfReady(),
|
|
@@ -40066,12 +40641,14 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40066
40641
|
getInferenceDeviceCaps: (nodeId) => deps.getInferenceDeviceCaps(nodeId),
|
|
40067
40642
|
readPipelineDevicePin: (deviceId) => deps.readPipelineDevicePin(deviceId),
|
|
40068
40643
|
inferenceRotation,
|
|
40069
|
-
buildWatchdogCamera: (config, deviceName) => detectionWiring.buildWatchdogCamera(config, deviceName),
|
|
40644
|
+
buildWatchdogCamera: (config, deviceName, hasAudioSource) => detectionWiring.buildWatchdogCamera(config, deviceName, hasAudioSource),
|
|
40645
|
+
hasAudioSource: (deviceId, camStreamId) => resolveHasAudioSource(deviceId, camStreamId),
|
|
40070
40646
|
isIdleSessionCamera: (deviceId) => deps.isIdleSessionCamera(deviceId),
|
|
40071
40647
|
activeSessionNode: (deviceId) => session.activeSessionNode(deviceId),
|
|
40072
40648
|
closeActiveSession: (deviceId) => session.closeSessionForReassign(deviceId),
|
|
40073
40649
|
reconcilePlacementFromRunners: () => reconcile.reconcilePlacementFromRunners(),
|
|
40074
|
-
emitResolvedCameraUpdated: (deviceId) => deps.emitResolvedCameraUpdated(deviceId)
|
|
40650
|
+
emitResolvedCameraUpdated: (deviceId) => deps.emitResolvedCameraUpdated(deviceId),
|
|
40651
|
+
maintenancePaused: () => deps.maintenancePaused()
|
|
40075
40652
|
});
|
|
40076
40653
|
const session = new SessionDispatchController({
|
|
40077
40654
|
ledger,
|
|
@@ -40140,6 +40717,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40140
40717
|
loadShed,
|
|
40141
40718
|
logger: deps.ctx().logger,
|
|
40142
40719
|
handleDeviceRegistered: (deviceId) => deps.handleDeviceRegistered(deviceId),
|
|
40720
|
+
handleDeviceUnregistered: (deviceId) => deps.handleDeviceUnregistered(deviceId),
|
|
40143
40721
|
dispatchCamera: (config) => deps.dispatchCamera(config),
|
|
40144
40722
|
detachOn: (nodeId, deviceId) => deps.detachOn(nodeId, deviceId),
|
|
40145
40723
|
localNodeId: () => localNodeId,
|
|
@@ -40240,7 +40818,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40240
40818
|
const pipelineWatchdog = new PipelineWatchdog({
|
|
40241
40819
|
now: () => Date.now(),
|
|
40242
40820
|
logger: deps.ctx().logger,
|
|
40243
|
-
recover: (deviceId, stage, streamId) => detectionWiring.recoverPipelineStage(deviceId, stage, streamId),
|
|
40821
|
+
recover: (deviceId, stage, streamId, attempt) => detectionWiring.recoverPipelineStage(deviceId, stage, streamId, attempt),
|
|
40244
40822
|
thresholds: DEFAULT_WATCHDOG_THRESHOLDS
|
|
40245
40823
|
});
|
|
40246
40824
|
pipelineWatchdog.start(deps.watchdogIntervalMs);
|
|
@@ -41096,6 +41674,9 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
41096
41674
|
/** Periodic auto-rebalance sweep timer (drift correction under hysteresis). */
|
|
41097
41675
|
autoRebalanceTimer = null;
|
|
41098
41676
|
initTimestamp = 0;
|
|
41677
|
+
/** Storage migration maintenance lease. It only gates dispatch; it does not
|
|
41678
|
+
* change any camera wrapper or persistent pipeline configuration. */
|
|
41679
|
+
storageMigrationLeaseId = null;
|
|
41099
41680
|
constructor() {
|
|
41100
41681
|
super({});
|
|
41101
41682
|
}
|
|
@@ -41143,6 +41724,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
41143
41724
|
rebalance: (opts) => this.rebalance(opts),
|
|
41144
41725
|
setCameraPipelineForAgent: (input) => this.setCameraPipelineForAgent(input),
|
|
41145
41726
|
dispatchCamera: (config) => this.dispatchCamera(config),
|
|
41727
|
+
maintenancePaused: () => this.storageMigrationLeaseId !== null,
|
|
41146
41728
|
setTopology: (topology) => {
|
|
41147
41729
|
this.topology = topology;
|
|
41148
41730
|
},
|
|
@@ -41289,8 +41871,29 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
41289
41871
|
this.settingsStore = null;
|
|
41290
41872
|
}
|
|
41291
41873
|
async dispatchCamera(runnerConfig) {
|
|
41874
|
+
if (this.storageMigrationLeaseId !== null) return {
|
|
41875
|
+
success: true,
|
|
41876
|
+
kind: "pending"
|
|
41877
|
+
};
|
|
41292
41878
|
return this.placement.dispatchCamera(runnerConfig);
|
|
41293
41879
|
}
|
|
41880
|
+
async pauseForStorageMigration(input) {
|
|
41881
|
+
if (this.storageMigrationLeaseId !== null && this.storageMigrationLeaseId !== input.leaseId) throw new Error("pipeline dispatch is already paused by another storage migration");
|
|
41882
|
+
this.storageMigrationLeaseId = input.leaseId;
|
|
41883
|
+
try {
|
|
41884
|
+
await this.placement.waitForMigrationIdle();
|
|
41885
|
+
return { paused: true };
|
|
41886
|
+
} catch (err) {
|
|
41887
|
+
this.storageMigrationLeaseId = null;
|
|
41888
|
+
throw err;
|
|
41889
|
+
}
|
|
41890
|
+
}
|
|
41891
|
+
async resumeForStorageMigration(input) {
|
|
41892
|
+
if (this.storageMigrationLeaseId === null) return { resumed: true };
|
|
41893
|
+
if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("pipeline storage migration lease does not match");
|
|
41894
|
+
this.storageMigrationLeaseId = null;
|
|
41895
|
+
return { resumed: true };
|
|
41896
|
+
}
|
|
41294
41897
|
async releaseCamera(input) {
|
|
41295
41898
|
return this.placement.releaseCamera(input);
|
|
41296
41899
|
}
|