@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.js
CHANGED
|
@@ -8393,12 +8393,11 @@ var RecordingConfigSchema = object({
|
|
|
8393
8393
|
/**
|
|
8394
8394
|
* Entity-relocation job state (storage entity-routing spec, Phase 4).
|
|
8395
8395
|
*
|
|
8396
|
-
* One shape shared by the recorder
|
|
8397
|
-
*
|
|
8398
|
-
*
|
|
8399
|
-
*
|
|
8400
|
-
*
|
|
8401
|
-
* row on the owning addon's surface.
|
|
8396
|
+
* One shape shared by the recorder and pipeline-analytics internal movers.
|
|
8397
|
+
* The public admin surface is `storage-migration`; child jobs remain in RAM
|
|
8398
|
+
* because copy-if-absent, verify, delete and index/row repoint are resumable.
|
|
8399
|
+
* Each completed/failed run also lands one durable ops-log row on its owning
|
|
8400
|
+
* addon surface.
|
|
8402
8401
|
*/
|
|
8403
8402
|
var RelocateJobStateSchema = _enum([
|
|
8404
8403
|
"running",
|
|
@@ -8425,19 +8424,100 @@ var RelocateJobSchema = object({
|
|
|
8425
8424
|
finishedAt: number().nullable(),
|
|
8426
8425
|
error: string().nullable()
|
|
8427
8426
|
});
|
|
8427
|
+
/** Profile-derived footage selection used only by the migration coordinator:
|
|
8428
|
+
* `recordings` owns high+mid; `recordingsLow` owns low. */
|
|
8429
|
+
var RelocateFootageClassSchema = _enum(["recordings", "recordingsLow"]);
|
|
8428
8430
|
var RelocateFootageInputSchema = object({
|
|
8429
|
-
deviceId: number().optional(),
|
|
8430
8431
|
fromLocationId: string(),
|
|
8431
8432
|
toLocationId: string(),
|
|
8432
8433
|
entities: array(_enum(["segments"])).optional(),
|
|
8434
|
+
/** Limits relocation to the logical profile class. Omit only for the
|
|
8435
|
+
* pre-orchestration compatibility path. */
|
|
8436
|
+
footageClass: RelocateFootageClassSchema.optional(),
|
|
8433
8437
|
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
8434
8438
|
* never allowed to starve live writers. */
|
|
8435
8439
|
throttleMbps: number().min(1).max(1e3).optional()
|
|
8436
8440
|
});
|
|
8437
|
-
|
|
8438
|
-
|
|
8441
|
+
/** Internal, lease-scoped participant operation. It is intentionally separate
|
|
8442
|
+
* from persistent recording settings: a migration never changes
|
|
8443
|
+
* `RecordingConfig.enabled` or camera wrapper bindings. */
|
|
8444
|
+
var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
|
|
8445
|
+
var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
|
|
8446
|
+
var StorageMigrationMediaMoveInputSchema = object({
|
|
8439
8447
|
toLocationId: string(),
|
|
8440
8448
|
throttleMbps: number().min(1).max(1e3).optional()
|
|
8449
|
+
}).extend({ leaseId: string().min(1) });
|
|
8450
|
+
/** The independently selectable logical storage classes. `recordings`
|
|
8451
|
+
* encompasses the high and mid segment profiles; `recordingsLow` is low
|
|
8452
|
+
* segments; `eventMedia` is post-analysis blobs. */
|
|
8453
|
+
var StorageMigrationClassSchema = _enum([
|
|
8454
|
+
"recordings",
|
|
8455
|
+
"recordingsLow",
|
|
8456
|
+
"eventMedia"
|
|
8457
|
+
]);
|
|
8458
|
+
/** A destination is always an existing, fully-qualified location id. The
|
|
8459
|
+
* migration API intentionally never changes a source location's `basePath`:
|
|
8460
|
+
* callers create a new `<type>:<slug>` location, then select it here. */
|
|
8461
|
+
var StorageMigrationDestinationsSchema = object({
|
|
8462
|
+
recordings: string().min(1).optional(),
|
|
8463
|
+
recordingsLow: string().min(1).optional(),
|
|
8464
|
+
eventMedia: string().min(1).optional()
|
|
8465
|
+
}).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
|
|
8466
|
+
/** Shared input for planning and starting an orchestrated storage migration. */
|
|
8467
|
+
var StorageMigrationInputSchema = object({
|
|
8468
|
+
destinations: StorageMigrationDestinationsSchema,
|
|
8469
|
+
throttleMbps: number().min(1).max(1e3).optional()
|
|
8470
|
+
});
|
|
8471
|
+
/** The durable coordinator state machine. The only phase that changes default
|
|
8472
|
+
* locations is `repointing`, after every selected mover has completed and been
|
|
8473
|
+
* verified. */
|
|
8474
|
+
var StorageMigrationPhaseSchema = _enum([
|
|
8475
|
+
"planning",
|
|
8476
|
+
"pausing",
|
|
8477
|
+
"moving",
|
|
8478
|
+
"verifying",
|
|
8479
|
+
"repointing",
|
|
8480
|
+
"refreshing",
|
|
8481
|
+
"resuming",
|
|
8482
|
+
"done",
|
|
8483
|
+
"failed",
|
|
8484
|
+
"cancelled"
|
|
8485
|
+
]);
|
|
8486
|
+
var StorageMigrationParticipantSchema = _enum([
|
|
8487
|
+
"pipeline",
|
|
8488
|
+
"recorder",
|
|
8489
|
+
"analytics"
|
|
8490
|
+
]);
|
|
8491
|
+
var StorageMigrationMoveSchema = object({
|
|
8492
|
+
storageClass: StorageMigrationClassSchema,
|
|
8493
|
+
fromLocationId: string(),
|
|
8494
|
+
toLocationId: string(),
|
|
8495
|
+
moverJobId: string().nullable(),
|
|
8496
|
+
state: RelocateJobStateSchema.nullable(),
|
|
8497
|
+
error: string().nullable()
|
|
8498
|
+
});
|
|
8499
|
+
var StorageMigrationJobSchema = object({
|
|
8500
|
+
jobId: string(),
|
|
8501
|
+
phase: StorageMigrationPhaseSchema,
|
|
8502
|
+
destinations: StorageMigrationDestinationsSchema,
|
|
8503
|
+
throttleMbps: number(),
|
|
8504
|
+
moves: array(StorageMigrationMoveSchema),
|
|
8505
|
+
pauseLeaseId: string().nullable(),
|
|
8506
|
+
pausedParticipants: array(StorageMigrationParticipantSchema),
|
|
8507
|
+
repointed: boolean(),
|
|
8508
|
+
cancelRequested: boolean(),
|
|
8509
|
+
startedAt: number(),
|
|
8510
|
+
updatedAt: number(),
|
|
8511
|
+
finishedAt: number().nullable(),
|
|
8512
|
+
error: string().nullable()
|
|
8513
|
+
});
|
|
8514
|
+
var StorageMigrationPlanSchema = object({
|
|
8515
|
+
destinations: StorageMigrationDestinationsSchema,
|
|
8516
|
+
moves: array(object({
|
|
8517
|
+
storageClass: StorageMigrationClassSchema,
|
|
8518
|
+
fromLocationId: string(),
|
|
8519
|
+
toLocationId: string()
|
|
8520
|
+
}))
|
|
8441
8521
|
});
|
|
8442
8522
|
/**
|
|
8443
8523
|
* `StorageLocationType` — an addon-declared id that identifies the *kind* of
|
|
@@ -16879,13 +16959,19 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
16879
16959
|
}), method(object({ deviceId: number() }), EventPruneCountsSchema, {
|
|
16880
16960
|
kind: "mutation",
|
|
16881
16961
|
auth: "admin"
|
|
16882
|
-
}), method(
|
|
16962
|
+
}), method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
16883
16963
|
kind: "mutation",
|
|
16884
16964
|
auth: "admin"
|
|
16885
|
-
}), method(object({
|
|
16886
|
-
kind: "
|
|
16965
|
+
}), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
|
|
16966
|
+
kind: "mutation",
|
|
16967
|
+
auth: "admin"
|
|
16968
|
+
}), method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
|
|
16969
|
+
kind: "mutation",
|
|
16970
|
+
auth: "admin"
|
|
16971
|
+
}), method(StorageMigrationMediaMoveInputSchema, object({ jobId: string() }), {
|
|
16972
|
+
kind: "mutation",
|
|
16887
16973
|
auth: "admin"
|
|
16888
|
-
}), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
16974
|
+
}), method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
16889
16975
|
kind: "mutation",
|
|
16890
16976
|
auth: "admin"
|
|
16891
16977
|
}), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
|
|
@@ -18725,6 +18811,16 @@ var pipelineOrchestratorCapability = {
|
|
|
18725
18811
|
nodeIdMode: "data",
|
|
18726
18812
|
exposesDeviceSettings: true,
|
|
18727
18813
|
methods: {
|
|
18814
|
+
/** Internal storage-migration participant lease. While held, dispatch
|
|
18815
|
+
* requests are accepted as pending but no new runner attachment starts. */
|
|
18816
|
+
pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
18817
|
+
kind: "mutation",
|
|
18818
|
+
auth: "admin"
|
|
18819
|
+
}),
|
|
18820
|
+
resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
|
|
18821
|
+
kind: "mutation",
|
|
18822
|
+
auth: "admin"
|
|
18823
|
+
}),
|
|
18728
18824
|
/**
|
|
18729
18825
|
* Pin a camera's pipeline to a specific agent (L1 affinity).
|
|
18730
18826
|
* The orchestrator re-evaluates the assignment immediately and persists
|
|
@@ -19672,6 +19768,13 @@ method(object({ locationId: string() }), EvictableUsageSchema), method(object({
|
|
|
19672
19768
|
locationId: string(),
|
|
19673
19769
|
targetBytes: number().int().positive()
|
|
19674
19770
|
}), EvictResultSchema, { kind: "mutation" });
|
|
19771
|
+
method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin" }), method(StorageMigrationInputSchema, object({ jobId: string() }), {
|
|
19772
|
+
kind: "mutation",
|
|
19773
|
+
auth: "admin"
|
|
19774
|
+
}), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
19775
|
+
kind: "mutation",
|
|
19776
|
+
auth: "admin"
|
|
19777
|
+
});
|
|
19675
19778
|
var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
19676
19779
|
providerId: string().min(1),
|
|
19677
19780
|
displayName: string().min(1),
|
|
@@ -19775,6 +19878,28 @@ var TerminalProfileInfoSchema = object({
|
|
|
19775
19878
|
label: string(),
|
|
19776
19879
|
description: string().optional()
|
|
19777
19880
|
});
|
|
19881
|
+
/**
|
|
19882
|
+
* A durable operator-created Terminal instance. Profiles are templates; only
|
|
19883
|
+
* an instance declares a camera.
|
|
19884
|
+
*/
|
|
19885
|
+
var TerminalInstanceInfoSchema = object({
|
|
19886
|
+
instanceId: string(),
|
|
19887
|
+
cameraStableId: string(),
|
|
19888
|
+
nodeId: string(),
|
|
19889
|
+
profileId: string(),
|
|
19890
|
+
profileLabel: string(),
|
|
19891
|
+
name: string(),
|
|
19892
|
+
enabled: boolean()
|
|
19893
|
+
});
|
|
19894
|
+
var TerminalLegacyCameraSchema = object({
|
|
19895
|
+
stableId: string(),
|
|
19896
|
+
nodeId: string(),
|
|
19897
|
+
profileId: string(),
|
|
19898
|
+
profileLabel: string(),
|
|
19899
|
+
name: string(),
|
|
19900
|
+
/** Only legacy monitor cameras can retain their historic stable identity. */
|
|
19901
|
+
adoptable: boolean()
|
|
19902
|
+
});
|
|
19778
19903
|
var TerminalOutputEventSchema = discriminatedUnion("kind", [object({
|
|
19779
19904
|
seq: number().int().positive(),
|
|
19780
19905
|
kind: literal("data"),
|
|
@@ -19791,7 +19916,29 @@ var TerminalOutputBatchSchema = object({
|
|
|
19791
19916
|
snapshot: string().optional(),
|
|
19792
19917
|
events: array(TerminalOutputEventSchema).readonly()
|
|
19793
19918
|
});
|
|
19794
|
-
method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(
|
|
19919
|
+
method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
|
|
19920
|
+
targetNodeId: string().min(1),
|
|
19921
|
+
profileId: string().min(1),
|
|
19922
|
+
name: string().trim().min(1).max(160).optional()
|
|
19923
|
+
}), TerminalInstanceInfoSchema, {
|
|
19924
|
+
kind: "mutation",
|
|
19925
|
+
auth: "admin"
|
|
19926
|
+
}), method(object({ instanceId: string().min(1) }), _void(), {
|
|
19927
|
+
kind: "mutation",
|
|
19928
|
+
auth: "admin"
|
|
19929
|
+
}), method(object({
|
|
19930
|
+
instanceId: string().min(1),
|
|
19931
|
+
enabled: boolean()
|
|
19932
|
+
}), TerminalInstanceInfoSchema, {
|
|
19933
|
+
kind: "mutation",
|
|
19934
|
+
auth: "admin"
|
|
19935
|
+
}), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
|
|
19936
|
+
stableId: string().min(1),
|
|
19937
|
+
name: string().trim().min(1).max(160).optional()
|
|
19938
|
+
}), TerminalInstanceInfoSchema, {
|
|
19939
|
+
kind: "mutation",
|
|
19940
|
+
auth: "admin"
|
|
19941
|
+
}), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
|
|
19795
19942
|
profileId: string(),
|
|
19796
19943
|
cols: number().int().positive(),
|
|
19797
19944
|
rows: number().int().positive()
|
|
@@ -19808,7 +19955,13 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
|
19808
19955
|
}), method(object({
|
|
19809
19956
|
sessionId: string(),
|
|
19810
19957
|
afterSeq: number().int().nonnegative(),
|
|
19811
|
-
waitMs: number().int().min(0).max(2e3).default(0)
|
|
19958
|
+
waitMs: number().int().min(0).max(2e3).default(0),
|
|
19959
|
+
/**
|
|
19960
|
+
* Wait when a just-opened session has no output yet. Kept opt-in so a
|
|
19961
|
+
* browser's initial repaint remains immediate; the camera snapshot
|
|
19962
|
+
* relay uses it to avoid encoding a blank startup frame.
|
|
19963
|
+
*/
|
|
19964
|
+
waitForOutput: boolean().optional()
|
|
19812
19965
|
}), TerminalOutputBatchSchema, {
|
|
19813
19966
|
kind: "mutation",
|
|
19814
19967
|
auth: "admin",
|
|
@@ -21749,6 +21902,7 @@ var FaceInfoSchema = object({
|
|
|
21749
21902
|
var FaceFilterEnum = _enum([
|
|
21750
21903
|
"unassigned",
|
|
21751
21904
|
"recognized",
|
|
21905
|
+
"identified",
|
|
21752
21906
|
"all"
|
|
21753
21907
|
]);
|
|
21754
21908
|
var MediaFileLiteSchema$1 = object({
|
|
@@ -21777,6 +21931,8 @@ method(_void(), array(IdentitySchema).readonly()), method(object({ name: string(
|
|
|
21777
21931
|
kind: "mutation",
|
|
21778
21932
|
auth: "admin"
|
|
21779
21933
|
}), method(object({
|
|
21934
|
+
/** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
|
|
21935
|
+
deviceId: number().int().optional(),
|
|
21780
21936
|
limit: number().int().positive().optional(),
|
|
21781
21937
|
filter: FaceFilterEnum.optional(),
|
|
21782
21938
|
/**
|
|
@@ -23542,6 +23698,10 @@ var OsdSourceSchema = discriminatedUnion("kind", [
|
|
|
23542
23698
|
capName: string().min(1).max(64),
|
|
23543
23699
|
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
23544
23700
|
valuePath: string().min(1).max(64)
|
|
23701
|
+
}),
|
|
23702
|
+
object({
|
|
23703
|
+
kind: literal("latest-recognition"),
|
|
23704
|
+
recognition: _enum(["person", "plate"])
|
|
23545
23705
|
})
|
|
23546
23706
|
]);
|
|
23547
23707
|
var OsdSlotBindingSchema = object({
|
|
@@ -23647,6 +23807,15 @@ method(object({ deviceId: number().int() }), object({
|
|
|
23647
23807
|
}), object({ success: literal(true) }), {
|
|
23648
23808
|
kind: "mutation",
|
|
23649
23809
|
auth: "admin"
|
|
23810
|
+
}), method(object({
|
|
23811
|
+
sourceDeviceId: number().int(),
|
|
23812
|
+
targetDeviceId: number().int()
|
|
23813
|
+
}), object({
|
|
23814
|
+
copied: number().int().nonnegative(),
|
|
23815
|
+
skipped: number().int().nonnegative()
|
|
23816
|
+
}), {
|
|
23817
|
+
kind: "mutation",
|
|
23818
|
+
auth: "admin"
|
|
23650
23819
|
}), method(object({
|
|
23651
23820
|
deviceId: number().int(),
|
|
23652
23821
|
slotId: string().min(1),
|
|
@@ -24532,13 +24701,19 @@ method(object({
|
|
|
24532
24701
|
}), {
|
|
24533
24702
|
kind: "mutation",
|
|
24534
24703
|
auth: "admin"
|
|
24535
|
-
}), method(
|
|
24704
|
+
}), method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
24536
24705
|
kind: "mutation",
|
|
24537
24706
|
auth: "admin"
|
|
24538
|
-
}), method(object({
|
|
24539
|
-
kind: "
|
|
24707
|
+
}), method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
|
|
24708
|
+
kind: "mutation",
|
|
24709
|
+
auth: "admin"
|
|
24710
|
+
}), method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
|
|
24711
|
+
kind: "mutation",
|
|
24712
|
+
auth: "admin"
|
|
24713
|
+
}), method(StorageMigrationFootageMoveInputSchema, object({ jobId: string() }), {
|
|
24714
|
+
kind: "mutation",
|
|
24540
24715
|
auth: "admin"
|
|
24541
|
-
}), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
24716
|
+
}), method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
24542
24717
|
kind: "mutation",
|
|
24543
24718
|
auth: "admin"
|
|
24544
24719
|
});
|
|
@@ -28737,6 +28912,12 @@ Object.freeze({
|
|
|
28737
28912
|
addonId: null,
|
|
28738
28913
|
access: "delete"
|
|
28739
28914
|
},
|
|
28915
|
+
"osdManager.copyDeviceConfiguration": {
|
|
28916
|
+
capName: "osd-manager",
|
|
28917
|
+
capScope: "system",
|
|
28918
|
+
addonId: null,
|
|
28919
|
+
access: "create"
|
|
28920
|
+
},
|
|
28740
28921
|
"osdManager.getConditionSupport": {
|
|
28741
28922
|
capName: "osd-manager",
|
|
28742
28923
|
capScope: "system",
|
|
@@ -28833,7 +29014,7 @@ Object.freeze({
|
|
|
28833
29014
|
addonId: null,
|
|
28834
29015
|
access: "create"
|
|
28835
29016
|
},
|
|
28836
|
-
"pipelineAnalytics.
|
|
29017
|
+
"pipelineAnalytics.cancelStorageMigrationMove": {
|
|
28837
29018
|
capName: "pipeline-analytics",
|
|
28838
29019
|
capScope: "device",
|
|
28839
29020
|
addonId: null,
|
|
@@ -28905,12 +29086,6 @@ Object.freeze({
|
|
|
28905
29086
|
addonId: null,
|
|
28906
29087
|
access: "view"
|
|
28907
29088
|
},
|
|
28908
|
-
"pipelineAnalytics.getMediaRelocateStatus": {
|
|
28909
|
-
capName: "pipeline-analytics",
|
|
28910
|
-
capScope: "device",
|
|
28911
|
-
addonId: null,
|
|
28912
|
-
access: "view"
|
|
28913
|
-
},
|
|
28914
29089
|
"pipelineAnalytics.getMotionEvents": {
|
|
28915
29090
|
capName: "pipeline-analytics",
|
|
28916
29091
|
capScope: "device",
|
|
@@ -28947,6 +29122,12 @@ Object.freeze({
|
|
|
28947
29122
|
addonId: null,
|
|
28948
29123
|
access: "view"
|
|
28949
29124
|
},
|
|
29125
|
+
"pipelineAnalytics.getStorageMigrationMoveStatus": {
|
|
29126
|
+
capName: "pipeline-analytics",
|
|
29127
|
+
capScope: "device",
|
|
29128
|
+
addonId: null,
|
|
29129
|
+
access: "view"
|
|
29130
|
+
},
|
|
28950
29131
|
"pipelineAnalytics.getTrack": {
|
|
28951
29132
|
capName: "pipeline-analytics",
|
|
28952
29133
|
capScope: "device",
|
|
@@ -29025,6 +29206,12 @@ Object.freeze({
|
|
|
29025
29206
|
addonId: null,
|
|
29026
29207
|
access: "view"
|
|
29027
29208
|
},
|
|
29209
|
+
"pipelineAnalytics.pauseForStorageMigration": {
|
|
29210
|
+
capName: "pipeline-analytics",
|
|
29211
|
+
capScope: "device",
|
|
29212
|
+
addonId: null,
|
|
29213
|
+
access: "create"
|
|
29214
|
+
},
|
|
29028
29215
|
"pipelineAnalytics.proposeRetrainAnnotations": {
|
|
29029
29216
|
capName: "pipeline-analytics",
|
|
29030
29217
|
capScope: "device",
|
|
@@ -29055,7 +29242,7 @@ Object.freeze({
|
|
|
29055
29242
|
addonId: null,
|
|
29056
29243
|
access: "create"
|
|
29057
29244
|
},
|
|
29058
|
-
"pipelineAnalytics.
|
|
29245
|
+
"pipelineAnalytics.refreshStorageLocationsForMigration": {
|
|
29059
29246
|
capName: "pipeline-analytics",
|
|
29060
29247
|
capScope: "device",
|
|
29061
29248
|
addonId: null,
|
|
@@ -29067,6 +29254,12 @@ Object.freeze({
|
|
|
29067
29254
|
addonId: null,
|
|
29068
29255
|
access: "create"
|
|
29069
29256
|
},
|
|
29257
|
+
"pipelineAnalytics.resumeForStorageMigration": {
|
|
29258
|
+
capName: "pipeline-analytics",
|
|
29259
|
+
capScope: "device",
|
|
29260
|
+
addonId: null,
|
|
29261
|
+
access: "create"
|
|
29262
|
+
},
|
|
29070
29263
|
"pipelineAnalytics.saveRetrainAnnotations": {
|
|
29071
29264
|
capName: "pipeline-analytics",
|
|
29072
29265
|
capScope: "device",
|
|
@@ -29091,6 +29284,12 @@ Object.freeze({
|
|
|
29091
29284
|
addonId: null,
|
|
29092
29285
|
access: "create"
|
|
29093
29286
|
},
|
|
29287
|
+
"pipelineAnalytics.startStorageMigrationMove": {
|
|
29288
|
+
capName: "pipeline-analytics",
|
|
29289
|
+
capScope: "device",
|
|
29290
|
+
addonId: null,
|
|
29291
|
+
access: "create"
|
|
29292
|
+
},
|
|
29094
29293
|
"pipelineAnalytics.wipeAllAnalytics": {
|
|
29095
29294
|
capName: "pipeline-analytics",
|
|
29096
29295
|
capScope: "device",
|
|
@@ -29457,6 +29656,12 @@ Object.freeze({
|
|
|
29457
29656
|
addonId: null,
|
|
29458
29657
|
access: "view"
|
|
29459
29658
|
},
|
|
29659
|
+
"pipelineOrchestrator.pauseForStorageMigration": {
|
|
29660
|
+
capName: "pipeline-orchestrator",
|
|
29661
|
+
capScope: "system",
|
|
29662
|
+
addonId: null,
|
|
29663
|
+
access: "create"
|
|
29664
|
+
},
|
|
29460
29665
|
"pipelineOrchestrator.rebalance": {
|
|
29461
29666
|
capName: "pipeline-orchestrator",
|
|
29462
29667
|
capScope: "system",
|
|
@@ -29481,6 +29686,12 @@ Object.freeze({
|
|
|
29481
29686
|
addonId: null,
|
|
29482
29687
|
access: "view"
|
|
29483
29688
|
},
|
|
29689
|
+
"pipelineOrchestrator.resumeForStorageMigration": {
|
|
29690
|
+
capName: "pipeline-orchestrator",
|
|
29691
|
+
capScope: "system",
|
|
29692
|
+
addonId: null,
|
|
29693
|
+
access: "create"
|
|
29694
|
+
},
|
|
29484
29695
|
"pipelineOrchestrator.saveTemplate": {
|
|
29485
29696
|
capName: "pipeline-orchestrator",
|
|
29486
29697
|
capScope: "system",
|
|
@@ -29877,7 +30088,7 @@ Object.freeze({
|
|
|
29877
30088
|
addonId: null,
|
|
29878
30089
|
access: "create"
|
|
29879
30090
|
},
|
|
29880
|
-
"recording.
|
|
30091
|
+
"recording.cancelStorageMigrationMove": {
|
|
29881
30092
|
capName: "recording",
|
|
29882
30093
|
capScope: "system",
|
|
29883
30094
|
addonId: null,
|
|
@@ -29913,7 +30124,7 @@ Object.freeze({
|
|
|
29913
30124
|
addonId: null,
|
|
29914
30125
|
access: "view"
|
|
29915
30126
|
},
|
|
29916
|
-
"recording.
|
|
30127
|
+
"recording.getStorageMigrationMoveStatus": {
|
|
29917
30128
|
capName: "recording",
|
|
29918
30129
|
capScope: "system",
|
|
29919
30130
|
addonId: null,
|
|
@@ -29937,6 +30148,12 @@ Object.freeze({
|
|
|
29937
30148
|
addonId: null,
|
|
29938
30149
|
access: "view"
|
|
29939
30150
|
},
|
|
30151
|
+
"recording.pauseForStorageMigration": {
|
|
30152
|
+
capName: "recording",
|
|
30153
|
+
capScope: "system",
|
|
30154
|
+
addonId: null,
|
|
30155
|
+
access: "create"
|
|
30156
|
+
},
|
|
29940
30157
|
"recording.pruneFootage": {
|
|
29941
30158
|
capName: "recording",
|
|
29942
30159
|
capScope: "system",
|
|
@@ -29955,7 +30172,7 @@ Object.freeze({
|
|
|
29955
30172
|
addonId: null,
|
|
29956
30173
|
access: "view"
|
|
29957
30174
|
},
|
|
29958
|
-
"recording.
|
|
30175
|
+
"recording.refreshStorageLocationsForMigration": {
|
|
29959
30176
|
capName: "recording",
|
|
29960
30177
|
capScope: "system",
|
|
29961
30178
|
addonId: null,
|
|
@@ -29979,12 +30196,24 @@ Object.freeze({
|
|
|
29979
30196
|
addonId: null,
|
|
29980
30197
|
access: "create"
|
|
29981
30198
|
},
|
|
30199
|
+
"recording.resumeForStorageMigration": {
|
|
30200
|
+
capName: "recording",
|
|
30201
|
+
capScope: "system",
|
|
30202
|
+
addonId: null,
|
|
30203
|
+
access: "create"
|
|
30204
|
+
},
|
|
29982
30205
|
"recording.setDeviceConfig": {
|
|
29983
30206
|
capName: "recording",
|
|
29984
30207
|
capScope: "system",
|
|
29985
30208
|
addonId: null,
|
|
29986
30209
|
access: "create"
|
|
29987
30210
|
},
|
|
30211
|
+
"recording.startStorageMigrationMove": {
|
|
30212
|
+
capName: "recording",
|
|
30213
|
+
capScope: "system",
|
|
30214
|
+
addonId: null,
|
|
30215
|
+
access: "create"
|
|
30216
|
+
},
|
|
29988
30217
|
"recordingExport.cancelExport": {
|
|
29989
30218
|
capName: "recordingExport",
|
|
29990
30219
|
capScope: "system",
|
|
@@ -30375,6 +30604,30 @@ Object.freeze({
|
|
|
30375
30604
|
addonId: null,
|
|
30376
30605
|
access: "view"
|
|
30377
30606
|
},
|
|
30607
|
+
"storageMigration.cancel": {
|
|
30608
|
+
capName: "storage-migration",
|
|
30609
|
+
capScope: "system",
|
|
30610
|
+
addonId: null,
|
|
30611
|
+
access: "create"
|
|
30612
|
+
},
|
|
30613
|
+
"storageMigration.plan": {
|
|
30614
|
+
capName: "storage-migration",
|
|
30615
|
+
capScope: "system",
|
|
30616
|
+
addonId: null,
|
|
30617
|
+
access: "view"
|
|
30618
|
+
},
|
|
30619
|
+
"storageMigration.start": {
|
|
30620
|
+
capName: "storage-migration",
|
|
30621
|
+
capScope: "system",
|
|
30622
|
+
addonId: null,
|
|
30623
|
+
access: "create"
|
|
30624
|
+
},
|
|
30625
|
+
"storageMigration.status": {
|
|
30626
|
+
capName: "storage-migration",
|
|
30627
|
+
capScope: "system",
|
|
30628
|
+
addonId: null,
|
|
30629
|
+
access: "view"
|
|
30630
|
+
},
|
|
30378
30631
|
"storageProvider.abortUpload": {
|
|
30379
30632
|
capName: "storage-provider",
|
|
30380
30633
|
capScope: "system",
|
|
@@ -30753,12 +31006,42 @@ Object.freeze({
|
|
|
30753
31006
|
addonId: null,
|
|
30754
31007
|
access: "create"
|
|
30755
31008
|
},
|
|
31009
|
+
"terminalSession.adoptLegacyMonitor": {
|
|
31010
|
+
capName: "terminal-session",
|
|
31011
|
+
capScope: "system",
|
|
31012
|
+
addonId: null,
|
|
31013
|
+
access: "create"
|
|
31014
|
+
},
|
|
30756
31015
|
"terminalSession.close": {
|
|
30757
31016
|
capName: "terminal-session",
|
|
30758
31017
|
capScope: "system",
|
|
30759
31018
|
addonId: null,
|
|
30760
31019
|
access: "create"
|
|
30761
31020
|
},
|
|
31021
|
+
"terminalSession.createInstance": {
|
|
31022
|
+
capName: "terminal-session",
|
|
31023
|
+
capScope: "system",
|
|
31024
|
+
addonId: null,
|
|
31025
|
+
access: "create"
|
|
31026
|
+
},
|
|
31027
|
+
"terminalSession.deleteInstance": {
|
|
31028
|
+
capName: "terminal-session",
|
|
31029
|
+
capScope: "system",
|
|
31030
|
+
addonId: null,
|
|
31031
|
+
access: "delete"
|
|
31032
|
+
},
|
|
31033
|
+
"terminalSession.listInstances": {
|
|
31034
|
+
capName: "terminal-session",
|
|
31035
|
+
capScope: "system",
|
|
31036
|
+
addonId: null,
|
|
31037
|
+
access: "view"
|
|
31038
|
+
},
|
|
31039
|
+
"terminalSession.listLegacyCameras": {
|
|
31040
|
+
capName: "terminal-session",
|
|
31041
|
+
capScope: "system",
|
|
31042
|
+
addonId: null,
|
|
31043
|
+
access: "view"
|
|
31044
|
+
},
|
|
30762
31045
|
"terminalSession.listProfiles": {
|
|
30763
31046
|
capName: "terminal-session",
|
|
30764
31047
|
capScope: "system",
|
|
@@ -30789,6 +31072,12 @@ Object.freeze({
|
|
|
30789
31072
|
addonId: null,
|
|
30790
31073
|
access: "create"
|
|
30791
31074
|
},
|
|
31075
|
+
"terminalSession.setInstanceEnabled": {
|
|
31076
|
+
capName: "terminal-session",
|
|
31077
|
+
capScope: "system",
|
|
31078
|
+
addonId: null,
|
|
31079
|
+
access: "create"
|
|
31080
|
+
},
|
|
30792
31081
|
"terminalSession.writeInput": {
|
|
30793
31082
|
capName: "terminal-session",
|
|
30794
31083
|
capScope: "system",
|
|
@@ -34699,6 +34988,48 @@ var DeviceFeaturesMirror = class {
|
|
|
34699
34988
|
}
|
|
34700
34989
|
};
|
|
34701
34990
|
//#endregion
|
|
34991
|
+
//#region src/watchdog-camera.ts
|
|
34992
|
+
/**
|
|
34993
|
+
* Derive the watchdog camera descriptor from a runner config. Captures which
|
|
34994
|
+
* stages are expected to flow CONTINUOUSLY (always-on audio, analyzer motion,
|
|
34995
|
+
* always-on detection) → the camStreamId feeding each. Audio has no dedicated
|
|
34996
|
+
* stream field on the runner config, so it shares the motion stream id.
|
|
34997
|
+
*
|
|
34998
|
+
* `pipelineEnabled === false` (the `object-detection` switch, D62/ADR-0109)
|
|
34999
|
+
* arms NOTHING: `startDetection` deliberately performs no attach and opens no
|
|
35000
|
+
* decode session for such a camera, so no continuous stage can ever produce a
|
|
35001
|
+
* signal. The camera is still returned (and still registered by the caller) so
|
|
35002
|
+
* its watchdog row stays visible — a switched-off camera must read as off, not
|
|
35003
|
+
* as broken.
|
|
35004
|
+
*
|
|
35005
|
+
* `hasAudioSource` is the caller's evidence that the camera's source actually
|
|
35006
|
+
* carries an audio track (`BrokerStats.audio !== null`). It fails OPEN: an
|
|
35007
|
+
* unknown or failed read passes `true`, because the audio stage is diagnostic
|
|
35008
|
+
* and a false negative silently stops watching a working camera.
|
|
35009
|
+
*/
|
|
35010
|
+
function buildWatchdogCamera(config, deviceName, hasAudioSource) {
|
|
35011
|
+
const continuousStages = /* @__PURE__ */ new Map();
|
|
35012
|
+
if (config.pipelineEnabled === false) return {
|
|
35013
|
+
deviceId: config.deviceId,
|
|
35014
|
+
deviceName,
|
|
35015
|
+
audioMode: config.audioMode,
|
|
35016
|
+
detectionMode: config.detectionMode,
|
|
35017
|
+
motionSources: config.motionSources,
|
|
35018
|
+
continuousStages
|
|
35019
|
+
};
|
|
35020
|
+
if (config.audioMode === "always-on" && hasAudioSource) continuousStages.set("audio", config.motionStreamId);
|
|
35021
|
+
if (config.motionSources.includes("analyzer")) continuousStages.set("motion", config.motionStreamId);
|
|
35022
|
+
if (config.detectionMode === "always-on") continuousStages.set("detection", config.detectionStreamId);
|
|
35023
|
+
return {
|
|
35024
|
+
deviceId: config.deviceId,
|
|
35025
|
+
deviceName,
|
|
35026
|
+
audioMode: config.audioMode,
|
|
35027
|
+
detectionMode: config.detectionMode,
|
|
35028
|
+
motionSources: config.motionSources,
|
|
35029
|
+
continuousStages
|
|
35030
|
+
};
|
|
35031
|
+
}
|
|
35032
|
+
//#endregion
|
|
34702
35033
|
//#region src/detection-wiring-controller.ts
|
|
34703
35034
|
var DetectionWiringController = class {
|
|
34704
35035
|
deps;
|
|
@@ -34903,7 +35234,8 @@ var DetectionWiringController = class {
|
|
|
34903
35234
|
await this.stopDetection(deviceId);
|
|
34904
35235
|
}
|
|
34905
35236
|
this.activeDetections.set(deviceId, config);
|
|
34906
|
-
if (config.
|
|
35237
|
+
if (!config.pipelineEnabled) log.info("object detection switched off — no attach, no decode session", { meta: { detectionMode: config.detectionMode } });
|
|
35238
|
+
else if (config.detectionMode === "on-motion") {
|
|
34907
35239
|
if (config.motionSources.includes("analyzer")) try {
|
|
34908
35240
|
await this.deps.session.ensureMotionWatch(deviceId, config);
|
|
34909
35241
|
} catch (err) {
|
|
@@ -34994,11 +35326,44 @@ var DetectionWiringController = class {
|
|
|
34994
35326
|
this.deps.forgetDeviceZones(deviceId);
|
|
34995
35327
|
this.deps.forgetDeviceZoneRules(deviceId);
|
|
34996
35328
|
this.deps.loadShed.forgetDevice(deviceId);
|
|
34997
|
-
|
|
35329
|
+
const settings = this.deps.ctx().settings;
|
|
35330
|
+
if (!settings) return;
|
|
34998
35331
|
await this.deps.settingsStore.deleteCameraSettings(deviceId);
|
|
35332
|
+
try {
|
|
35333
|
+
await settings.clearDeviceStore(deviceId);
|
|
35334
|
+
} catch (err) {
|
|
35335
|
+
this.deps.logger.warn("failed to clear per-device orchestrator settings on removal", {
|
|
35336
|
+
tags: { deviceId },
|
|
35337
|
+
meta: { error: errMsg(err) }
|
|
35338
|
+
});
|
|
35339
|
+
}
|
|
34999
35340
|
this.deps.logger.info("Purged pipeline config for removed device", { tags: { deviceId } });
|
|
35000
35341
|
}
|
|
35001
35342
|
/**
|
|
35343
|
+
* True ONLY when the device registry positively answered "this device does
|
|
35344
|
+
* not exist" (`getDevice` → `null`, per the cap contract).
|
|
35345
|
+
*
|
|
35346
|
+
* A FAILED read answers `false` — D49: a read that fails changes nothing, and
|
|
35347
|
+
* the destructive direction here is refusing to run a live camera.
|
|
35348
|
+
* Deliberately NOT routed through {@link lookupDeviceFeatures}'s mirror: that
|
|
35349
|
+
* one treats missing-as-unknown on purpose (a provider restart passes through
|
|
35350
|
+
* exactly that state and must not re-derive a battery camera's profile), and
|
|
35351
|
+
* conflating the two would make a provider restart look like a deletion.
|
|
35352
|
+
*/
|
|
35353
|
+
async isDeviceDeleted(deviceId) {
|
|
35354
|
+
const api = this.deps.api();
|
|
35355
|
+
if (!api) return false;
|
|
35356
|
+
try {
|
|
35357
|
+
return await api.deviceManager.getDevice.query({ deviceId }) === null;
|
|
35358
|
+
} catch (err) {
|
|
35359
|
+
this.deps.logger.debug("device existence check failed — proceeding as before", {
|
|
35360
|
+
tags: { deviceId },
|
|
35361
|
+
meta: { error: errMsg(err) }
|
|
35362
|
+
});
|
|
35363
|
+
return false;
|
|
35364
|
+
}
|
|
35365
|
+
}
|
|
35366
|
+
/**
|
|
35002
35367
|
* Handle a profile-slot change: build a fresh detection config from
|
|
35003
35368
|
* per-device settings and start detection if motion is enabled.
|
|
35004
35369
|
* Fires on `camera-streams.onProfileSlotsChanged` — any publish,
|
|
@@ -35008,6 +35373,11 @@ var DetectionWiringController = class {
|
|
|
35008
35373
|
if (!this.deps.ctx()) return;
|
|
35009
35374
|
const log = this.deps.logger.withTags({ deviceId });
|
|
35010
35375
|
log.info("handleDeviceRegistered", { meta: { phase: "handleDeviceRegistered" } });
|
|
35376
|
+
if (await this.isDeviceDeleted(deviceId)) {
|
|
35377
|
+
log.warn("device no longer exists — refusing to start detection");
|
|
35378
|
+
await this.handleDeviceUnregistered(deviceId);
|
|
35379
|
+
return;
|
|
35380
|
+
}
|
|
35011
35381
|
const config = await this.buildDetectionConfig(deviceId);
|
|
35012
35382
|
if (config === TRANSIENT_SLOT_READ) {
|
|
35013
35383
|
log.warn("Profile-slot read failed transiently — keeping detection, scheduling retry");
|
|
@@ -35147,28 +35517,17 @@ var DetectionWiringController = class {
|
|
|
35147
35517
|
});
|
|
35148
35518
|
}
|
|
35149
35519
|
/**
|
|
35150
|
-
* Derive the watchdog camera descriptor from a runner config
|
|
35151
|
-
*
|
|
35152
|
-
*
|
|
35153
|
-
*
|
|
35154
|
-
* the motion stream id.
|
|
35520
|
+
* Derive the watchdog camera descriptor from a runner config — thin delegate
|
|
35521
|
+
* onto the pure `buildWatchdogCamera` (`watchdog-camera.ts`), where the
|
|
35522
|
+
* arming rules live (a switched-off camera arms nothing; the audio stage
|
|
35523
|
+
* needs evidence of an audio track).
|
|
35155
35524
|
*
|
|
35156
35525
|
* Public — `PlacementService`'s `buildWatchdogCamera` closure (wired in
|
|
35157
|
-
* `orchestrator-bootstrap.ts`) calls straight through to this method
|
|
35526
|
+
* `orchestrator-bootstrap.ts`) calls straight through to this method; the
|
|
35527
|
+
* caller resolves `hasAudioSource` once per dispatch.
|
|
35158
35528
|
*/
|
|
35159
|
-
buildWatchdogCamera(config, deviceName) {
|
|
35160
|
-
|
|
35161
|
-
if (config.audioMode === "always-on") continuousStages.set("audio", config.motionStreamId);
|
|
35162
|
-
if (config.motionSources.includes("analyzer")) continuousStages.set("motion", config.motionStreamId);
|
|
35163
|
-
if (config.detectionMode === "always-on") continuousStages.set("detection", config.detectionStreamId);
|
|
35164
|
-
return {
|
|
35165
|
-
deviceId: config.deviceId,
|
|
35166
|
-
deviceName,
|
|
35167
|
-
audioMode: config.audioMode,
|
|
35168
|
-
detectionMode: config.detectionMode,
|
|
35169
|
-
motionSources: config.motionSources,
|
|
35170
|
-
continuousStages
|
|
35171
|
-
};
|
|
35529
|
+
buildWatchdogCamera(config, deviceName, hasAudioSource) {
|
|
35530
|
+
return buildWatchdogCamera(config, deviceName, hasAudioSource);
|
|
35172
35531
|
}
|
|
35173
35532
|
/**
|
|
35174
35533
|
* Watchdog recovery callback. Invoked when a camera's continuous stage
|
|
@@ -35181,7 +35540,7 @@ var DetectionWiringController = class {
|
|
|
35181
35540
|
* Public — `PipelineWatchdog`'s `recover` closure (wired in
|
|
35182
35541
|
* `orchestrator-bootstrap.ts`) calls straight through to this method.
|
|
35183
35542
|
*/
|
|
35184
|
-
recoverPipelineStage(deviceId, stage, streamId) {
|
|
35543
|
+
recoverPipelineStage(deviceId, stage, streamId, attempt) {
|
|
35185
35544
|
if (stage === "audio") {
|
|
35186
35545
|
const cfg = this.deps.ledger.getConfig(deviceId);
|
|
35187
35546
|
const audioCfg = cfg ? {
|
|
@@ -35195,6 +35554,14 @@ var DetectionWiringController = class {
|
|
|
35195
35554
|
});
|
|
35196
35555
|
});
|
|
35197
35556
|
}
|
|
35557
|
+
this.deps.logger.debug("watchdog recovery: requesting source refresh", {
|
|
35558
|
+
tags: { deviceId },
|
|
35559
|
+
meta: {
|
|
35560
|
+
stage,
|
|
35561
|
+
camStreamId: streamId,
|
|
35562
|
+
attempt
|
|
35563
|
+
}
|
|
35564
|
+
});
|
|
35198
35565
|
this.deps.eventBus.emit({
|
|
35199
35566
|
id: `watchdog-refresh-${deviceId}-${Date.now()}`,
|
|
35200
35567
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -35597,6 +35964,64 @@ function computeDispatchGap(desired, known) {
|
|
|
35597
35964
|
return [...desired].filter((id) => !known.has(id));
|
|
35598
35965
|
}
|
|
35599
35966
|
/**
|
|
35967
|
+
* Split the desired fleet (derived from broker profile slots) against the LIVE
|
|
35968
|
+
* device registry.
|
|
35969
|
+
*
|
|
35970
|
+
* The broker keeps a deleted device's profile-slot assignments forever, so
|
|
35971
|
+
* `desiredDeviceIdsFromSlots` alone re-proposes a camera the operator already
|
|
35972
|
+
* deleted — and the reconcile's gap-fill then resurrects it. Callers dispatch
|
|
35973
|
+
* `existing` only.
|
|
35974
|
+
*
|
|
35975
|
+
* The caller MUST NOT call this with a failed/partial registry read: a read
|
|
35976
|
+
* that fails changes nothing (D49), it never prunes the fleet.
|
|
35977
|
+
*/
|
|
35978
|
+
function splitDesiredByExistence(desired, liveDeviceIds) {
|
|
35979
|
+
const existing = /* @__PURE__ */ new Set();
|
|
35980
|
+
const vanished = /* @__PURE__ */ new Set();
|
|
35981
|
+
for (const deviceId of desired) if (liveDeviceIds.has(deviceId)) existing.add(deviceId);
|
|
35982
|
+
else vanished.add(deviceId);
|
|
35983
|
+
return {
|
|
35984
|
+
existing,
|
|
35985
|
+
vanished
|
|
35986
|
+
};
|
|
35987
|
+
}
|
|
35988
|
+
/**
|
|
35989
|
+
* Decide which vanished devices are CONFIRMED gone (D49's two-disagreements
|
|
35990
|
+
* rule). Teardown destroys work — stopping detection, releasing the camera and
|
|
35991
|
+
* purging its persisted config — so a single registry read is never enough: a
|
|
35992
|
+
* device must be missing in TWO consecutive reconcile passes before it is torn
|
|
35993
|
+
* down. Anything that puts the device back (a successful listing that contains
|
|
35994
|
+
* it, or a failed read the caller declines to act on) resets the streak, so the
|
|
35995
|
+
* count only ever advances on agreeing evidence.
|
|
35996
|
+
*
|
|
35997
|
+
* Teardown fires ONCE per vanish streak (`torn-down`) — the broker's slots
|
|
35998
|
+
* outlive the device, so it stays vanished on every later pass and would
|
|
35999
|
+
* otherwise be re-purged forever.
|
|
36000
|
+
*
|
|
36001
|
+
* Pure — the caller owns the (in-memory, boot-empty) state map.
|
|
36002
|
+
*/
|
|
36003
|
+
function reconcileVanishedDevices(vanished, prior) {
|
|
36004
|
+
const tearDown = [];
|
|
36005
|
+
const nextState = /* @__PURE__ */ new Map();
|
|
36006
|
+
for (const deviceId of vanished) {
|
|
36007
|
+
const state = prior.get(deviceId);
|
|
36008
|
+
if (state === void 0) {
|
|
36009
|
+
nextState.set(deviceId, "first-pass");
|
|
36010
|
+
continue;
|
|
36011
|
+
}
|
|
36012
|
+
if (state === "first-pass") {
|
|
36013
|
+
tearDown.push(deviceId);
|
|
36014
|
+
nextState.set(deviceId, "torn-down");
|
|
36015
|
+
continue;
|
|
36016
|
+
}
|
|
36017
|
+
nextState.set(deviceId, "torn-down");
|
|
36018
|
+
}
|
|
36019
|
+
return {
|
|
36020
|
+
tearDown,
|
|
36021
|
+
nextState
|
|
36022
|
+
};
|
|
36023
|
+
}
|
|
36024
|
+
/**
|
|
35600
36025
|
* Converge the orchestrator's `assignments` map to runner truth. Runner
|
|
35601
36026
|
* attachments (`getLocalCameras`) are AUTHORITATIVE; this returns the actions
|
|
35602
36027
|
* that make the map agree with them:
|
|
@@ -35743,11 +36168,17 @@ var PlacementService = class {
|
|
|
35743
36168
|
deps;
|
|
35744
36169
|
/** True while an auto-rebalance pass is running (skip overlap). */
|
|
35745
36170
|
autoRebalanceInFlight = false;
|
|
36171
|
+
inFlightAttaches = 0;
|
|
36172
|
+
idleWaiters = [];
|
|
35746
36173
|
constructor(deps) {
|
|
35747
36174
|
this.deps = deps;
|
|
35748
36175
|
}
|
|
35749
36176
|
async dispatchCamera(runnerConfig) {
|
|
35750
36177
|
if (!this.deps.ctx()) throw new Error("PipelineOrchestrator: dispatchCamera called before initialize");
|
|
36178
|
+
if (this.deps.maintenancePaused?.() === true) return {
|
|
36179
|
+
success: true,
|
|
36180
|
+
kind: "pending"
|
|
36181
|
+
};
|
|
35751
36182
|
const currentDeviceKey = this.deps.ledger.getConfig(runnerConfig.deviceId)?.deviceKey ?? null;
|
|
35752
36183
|
this.deps.ledger.setConfig(runnerConfig.deviceId, runnerConfig);
|
|
35753
36184
|
if (this.deps.loadShed.dispatchDeferralCheck(runnerConfig.deviceId)) return {
|
|
@@ -35838,7 +36269,10 @@ var PlacementService = class {
|
|
|
35838
36269
|
try {
|
|
35839
36270
|
await this.attachOn(targetNodeId, attachConfig);
|
|
35840
36271
|
this.deps.ledger.setConfig(runnerConfig.deviceId, attachConfig);
|
|
35841
|
-
if (targetNodeId === this.deps.localNodeId())
|
|
36272
|
+
if (targetNodeId === this.deps.localNodeId()) {
|
|
36273
|
+
const hasAudioSource = await this.resolveHasAudioSource(runnerConfig);
|
|
36274
|
+
this.deps.watchdog()?.register(this.deps.buildWatchdogCamera(runnerConfig, String(runnerConfig.deviceId), hasAudioSource));
|
|
36275
|
+
}
|
|
35842
36276
|
this.deps.ledger.recordAssignment(runnerConfig.deviceId, targetNodeId, reason, reason === "manual");
|
|
35843
36277
|
} finally {
|
|
35844
36278
|
dispatchGuard.dispose();
|
|
@@ -36138,6 +36572,7 @@ var PlacementService = class {
|
|
|
36138
36572
|
* an absent field would produce.
|
|
36139
36573
|
*/
|
|
36140
36574
|
async attachOn(nodeId, config) {
|
|
36575
|
+
if (this.deps.maintenancePaused?.() === true) throw new Error("pipeline dispatch is paused for storage migration");
|
|
36141
36576
|
const ctx = this.deps.ctx();
|
|
36142
36577
|
const api = ctx.api;
|
|
36143
36578
|
if (!api) throw new Error(`attachOn(${nodeId}, ${config.deviceId}): this.ctx.api not available`);
|
|
@@ -36172,6 +36607,7 @@ var PlacementService = class {
|
|
|
36172
36607
|
meta: logMeta
|
|
36173
36608
|
});
|
|
36174
36609
|
}).catch(() => {});
|
|
36610
|
+
this.inFlightAttaches++;
|
|
36175
36611
|
try {
|
|
36176
36612
|
await api.pipelineRunner.attachCamera.mutate({
|
|
36177
36613
|
...config,
|
|
@@ -36201,8 +36637,17 @@ var PlacementService = class {
|
|
|
36201
36637
|
});
|
|
36202
36638
|
}
|
|
36203
36639
|
throw err;
|
|
36640
|
+
} finally {
|
|
36641
|
+
this.inFlightAttaches--;
|
|
36642
|
+
if (this.inFlightAttaches === 0) for (const resolve of this.idleWaiters.splice(0)) resolve();
|
|
36204
36643
|
}
|
|
36205
36644
|
}
|
|
36645
|
+
/** Wait until every attach that began before the maintenance gate closed has
|
|
36646
|
+
* settled. New attaches are rejected at the top of `attachOn`. */
|
|
36647
|
+
waitForMigrationIdle() {
|
|
36648
|
+
if (this.inFlightAttaches === 0) return Promise.resolve();
|
|
36649
|
+
return new Promise((resolve) => this.idleWaiters.push(resolve));
|
|
36650
|
+
}
|
|
36206
36651
|
async detachOn(nodeId, deviceId) {
|
|
36207
36652
|
const api = this.deps.ctx().api;
|
|
36208
36653
|
if (!api) return;
|
|
@@ -36253,7 +36698,20 @@ var PlacementService = class {
|
|
|
36253
36698
|
return;
|
|
36254
36699
|
}
|
|
36255
36700
|
await this.attachOn(nodeId, cached);
|
|
36256
|
-
if (nodeId === this.deps.localNodeId())
|
|
36701
|
+
if (nodeId === this.deps.localNodeId()) {
|
|
36702
|
+
const hasAudioSource = await this.resolveHasAudioSource(cached);
|
|
36703
|
+
this.deps.watchdog()?.register(this.deps.buildWatchdogCamera(cached, String(cached.deviceId), hasAudioSource));
|
|
36704
|
+
}
|
|
36705
|
+
}
|
|
36706
|
+
/**
|
|
36707
|
+
* Resolve the watchdog's audio-track evidence for ONE dispatch. Skipped
|
|
36708
|
+
* entirely (and answered `false`) when `audioMode` could not arm the stage
|
|
36709
|
+
* anyway — `buildWatchdogCamera` needs both — so a camera with on-motion
|
|
36710
|
+
* audio costs no broker round-trip.
|
|
36711
|
+
*/
|
|
36712
|
+
async resolveHasAudioSource(config) {
|
|
36713
|
+
if (config.audioMode !== "always-on") return false;
|
|
36714
|
+
return this.deps.hasAudioSource(config.deviceId, config.motionStreamId);
|
|
36257
36715
|
}
|
|
36258
36716
|
/**
|
|
36259
36717
|
* Normalizes the `decision.kind === 'pending'` → `setPendingReason` +
|
|
@@ -37625,7 +38083,9 @@ var PipelineWatchdog = class {
|
|
|
37625
38083
|
for (const [stage] of cam.continuousStages) stages.set(stage, {
|
|
37626
38084
|
lastSeenMs: now,
|
|
37627
38085
|
attempts: 0,
|
|
37628
|
-
lastAttemptMs: 0
|
|
38086
|
+
lastAttemptMs: 0,
|
|
38087
|
+
everSeen: false,
|
|
38088
|
+
registeredAtMs: now
|
|
37629
38089
|
});
|
|
37630
38090
|
this.state.set(cam.deviceId, stages);
|
|
37631
38091
|
}
|
|
@@ -37639,20 +38099,34 @@ var PipelineWatchdog = class {
|
|
|
37639
38099
|
if (!rt) return;
|
|
37640
38100
|
rt.lastSeenMs = this.deps.now();
|
|
37641
38101
|
rt.attempts = 0;
|
|
38102
|
+
rt.everSeen = true;
|
|
37642
38103
|
}
|
|
37643
38104
|
tick() {
|
|
37644
38105
|
const now = this.deps.now();
|
|
37645
38106
|
for (const cam of this.cameras.values()) {
|
|
37646
|
-
const { line, stalled, recoveries } = this.evaluate(cam, now);
|
|
38107
|
+
const { line, stalled, recoveries, disarms } = this.evaluate(cam, now);
|
|
37647
38108
|
if (stalled) this.deps.logger.warn(line, { tags: { deviceId: cam.deviceId } });
|
|
37648
38109
|
else this.deps.logger.info(line, { tags: { deviceId: cam.deviceId } });
|
|
38110
|
+
for (const d of disarms) {
|
|
38111
|
+
this.deps.logger.warn("watchdog stage never produced a signal — disarming", {
|
|
38112
|
+
tags: { deviceId: cam.deviceId },
|
|
38113
|
+
meta: {
|
|
38114
|
+
stage: d.stage,
|
|
38115
|
+
streamId: d.streamId,
|
|
38116
|
+
sinceRegisteredMs: d.sinceRegisteredMs
|
|
38117
|
+
}
|
|
38118
|
+
});
|
|
38119
|
+
this.state.get(cam.deviceId)?.delete(d.stage);
|
|
38120
|
+
}
|
|
37649
38121
|
for (const r of recoveries) {
|
|
37650
38122
|
const rt = this.state.get(cam.deviceId)?.get(r.stage);
|
|
38123
|
+
let attempt = this.deps.thresholds.maxRecoveryAttempts;
|
|
37651
38124
|
if (rt) {
|
|
37652
38125
|
if (rt.attempts < this.deps.thresholds.maxRecoveryAttempts) rt.attempts += 1;
|
|
37653
38126
|
rt.lastAttemptMs = now;
|
|
38127
|
+
attempt = rt.attempts;
|
|
37654
38128
|
}
|
|
37655
|
-
this.deps.recover(cam.deviceId, r.stage, r.streamId);
|
|
38129
|
+
this.deps.recover(cam.deviceId, r.stage, r.streamId, attempt);
|
|
37656
38130
|
}
|
|
37657
38131
|
}
|
|
37658
38132
|
}
|
|
@@ -37667,6 +38141,7 @@ var PipelineWatchdog = class {
|
|
|
37667
38141
|
evaluate(cam, now) {
|
|
37668
38142
|
const parts = [];
|
|
37669
38143
|
const recoveries = [];
|
|
38144
|
+
const disarms = [];
|
|
37670
38145
|
let stalled = false;
|
|
37671
38146
|
const stageOrder = [
|
|
37672
38147
|
"audio",
|
|
@@ -37693,7 +38168,14 @@ var PipelineWatchdog = class {
|
|
|
37693
38168
|
const thresholdMs = this.deps.thresholds[STAGE_THRESHOLD_KEY[stage]];
|
|
37694
38169
|
const sSec = Math.round(staleness / 1e3);
|
|
37695
38170
|
if (staleness <= thresholdMs) parts.push(`${stage}=${stageMode[stage]}@${streamId}(OK ${sSec}s)`);
|
|
37696
|
-
else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts) {
|
|
38171
|
+
else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts && !rt.everSeen) {
|
|
38172
|
+
disarms.push({
|
|
38173
|
+
stage,
|
|
38174
|
+
streamId,
|
|
38175
|
+
sinceRegisteredMs: now - rt.registeredAtMs
|
|
38176
|
+
});
|
|
38177
|
+
parts.push(`${stage}=${stageMode[stage]}@${streamId}(NEVER SIGNALLED ${sSec}s · disarmed)`);
|
|
38178
|
+
} else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts) {
|
|
37697
38179
|
stalled = true;
|
|
37698
38180
|
if (now - rt.lastAttemptMs >= this.deps.thresholds.exhaustedRetryMs) {
|
|
37699
38181
|
recoveries.push({
|
|
@@ -37714,7 +38196,8 @@ var PipelineWatchdog = class {
|
|
|
37714
38196
|
return {
|
|
37715
38197
|
line: `[watchdog] cam${cam.deviceId} ${cam.deviceName} · ${parts.join(" · ")} → ${stalled ? "STALLED" : "OK"}`,
|
|
37716
38198
|
stalled,
|
|
37717
|
-
recoveries
|
|
38199
|
+
recoveries,
|
|
38200
|
+
disarms
|
|
37718
38201
|
};
|
|
37719
38202
|
}
|
|
37720
38203
|
};
|
|
@@ -38611,6 +39094,14 @@ var ReconcileController = class ReconcileController {
|
|
|
38611
39094
|
/** Shared in-flight pass so a session dispatch waits for runner truth instead of racing it. */
|
|
38612
39095
|
attachmentReconcilePromise = null;
|
|
38613
39096
|
/**
|
|
39097
|
+
* Desired-but-nonexistent devices and how far their vanish streak has got
|
|
39098
|
+
* (`dispatch-reconcile.ts`). PURELY in-memory and seeded EMPTY on boot: it is
|
|
39099
|
+
* a confirmation counter, not a record — after a restart the first pass only
|
|
39100
|
+
* ever re-observes, never destroys. Cleared whenever the registry read fails,
|
|
39101
|
+
* so a failed read can never advance a device toward teardown.
|
|
39102
|
+
*/
|
|
39103
|
+
vanishedDevices = /* @__PURE__ */ new Map();
|
|
39104
|
+
/**
|
|
38614
39105
|
* Per-device backoff for retrying `handleDeviceRegistered` after a
|
|
38615
39106
|
* TRANSIENT profile-slot read failure. Timers are `unref`'d and cleared on
|
|
38616
39107
|
* shutdown / device removal.
|
|
@@ -38679,13 +39170,24 @@ var ReconcileController = class ReconcileController {
|
|
|
38679
39170
|
return;
|
|
38680
39171
|
}
|
|
38681
39172
|
const desired = desiredDeviceIdsFromSlots(slots);
|
|
39173
|
+
const liveDeviceIds = await this.readLiveDeviceIds();
|
|
39174
|
+
const { existing, vanished } = liveDeviceIds === null ? {
|
|
39175
|
+
existing: desired,
|
|
39176
|
+
vanished: /* @__PURE__ */ new Set()
|
|
39177
|
+
} : splitDesiredByExistence(desired, liveDeviceIds);
|
|
39178
|
+
if (liveDeviceIds === null) {
|
|
39179
|
+
this.deps.logger.warn("dispatch reconcile: device existence filter SKIPPED — registry read failed", { meta: { desired: desired.size } });
|
|
39180
|
+
this.vanishedDevices = /* @__PURE__ */ new Map();
|
|
39181
|
+
}
|
|
38682
39182
|
const known = new Set(this.deps.ledger.listConfigIds());
|
|
38683
|
-
const gap = computeDispatchGap(
|
|
39183
|
+
const gap = computeDispatchGap(existing, known);
|
|
38684
39184
|
this.deps.logger.info("dispatch reconcile", { meta: {
|
|
38685
39185
|
desired: desired.size,
|
|
38686
39186
|
known: known.size,
|
|
38687
|
-
gap
|
|
39187
|
+
gap,
|
|
39188
|
+
vanished: vanished.size
|
|
38688
39189
|
} });
|
|
39190
|
+
await this.tearDownVanishedDevices(vanished);
|
|
38689
39191
|
for (const deviceId of gap) try {
|
|
38690
39192
|
await this.deps.handleDeviceRegistered(deviceId);
|
|
38691
39193
|
} catch (err) {
|
|
@@ -38704,6 +39206,53 @@ var ReconcileController = class ReconcileController {
|
|
|
38704
39206
|
}
|
|
38705
39207
|
}
|
|
38706
39208
|
/**
|
|
39209
|
+
* The live device registry as an id set, or `null` when the read FAILED.
|
|
39210
|
+
* `null` is the caller's signal to skip the existence filter entirely — it
|
|
39211
|
+
* must never be confused with "the registry is empty", which would prune the
|
|
39212
|
+
* whole fleet.
|
|
39213
|
+
*
|
|
39214
|
+
* Asks for the slim projection: this needs identity only, never
|
|
39215
|
+
* `config`/`sourceInfo` (a full listing of 293 devices measured 271 KB).
|
|
39216
|
+
*/
|
|
39217
|
+
async readLiveDeviceIds() {
|
|
39218
|
+
const api = this.deps.api();
|
|
39219
|
+
if (!api) return null;
|
|
39220
|
+
try {
|
|
39221
|
+
const devices = await api.deviceManager.listAll.query({ projection: "slim" });
|
|
39222
|
+
return new Set(devices.map((device) => device.id));
|
|
39223
|
+
} catch (err) {
|
|
39224
|
+
this.deps.logger.debug("dispatch reconcile: deviceManager.listAll failed", { meta: { error: errMsg(err) } });
|
|
39225
|
+
return null;
|
|
39226
|
+
}
|
|
39227
|
+
}
|
|
39228
|
+
/**
|
|
39229
|
+
* Tear down the desired devices the registry no longer has — but only once
|
|
39230
|
+
* TWO consecutive passes agree they are gone (`reconcileVanishedDevices`,
|
|
39231
|
+
* D49). Teardown destroys work, so a single listing that omits a device
|
|
39232
|
+
* (which a partial or racy read looks exactly like) is not enough.
|
|
39233
|
+
*
|
|
39234
|
+
* Runs the SAME `handleDeviceUnregistered` the `DeviceUnregistered` event
|
|
39235
|
+
* runs, so there is exactly one teardown path.
|
|
39236
|
+
*/
|
|
39237
|
+
async tearDownVanishedDevices(vanished) {
|
|
39238
|
+
const { tearDown, nextState } = reconcileVanishedDevices(vanished, this.vanishedDevices);
|
|
39239
|
+
this.vanishedDevices = nextState;
|
|
39240
|
+
for (const deviceId of tearDown) {
|
|
39241
|
+
this.deps.logger.warn("dispatch reconcile: device no longer exists — tearing down instead of dispatching", {
|
|
39242
|
+
tags: { deviceId },
|
|
39243
|
+
meta: { confirmedPasses: 2 }
|
|
39244
|
+
});
|
|
39245
|
+
try {
|
|
39246
|
+
await this.deps.handleDeviceUnregistered(deviceId);
|
|
39247
|
+
} catch (err) {
|
|
39248
|
+
this.deps.logger.warn("dispatch reconcile: teardown of vanished device failed", {
|
|
39249
|
+
tags: { deviceId },
|
|
39250
|
+
meta: { error: errMsg(err) }
|
|
39251
|
+
});
|
|
39252
|
+
}
|
|
39253
|
+
}
|
|
39254
|
+
}
|
|
39255
|
+
/**
|
|
38707
39256
|
* Converge the in-memory `assignments` map to runner truth (Phase 1 of the
|
|
38708
39257
|
* placement-derived-state design). Runner attachments (`getLocalCameras`) are
|
|
38709
39258
|
* authoritative: detach duplicates/orphans, and drop stale assignments
|
|
@@ -39014,6 +39563,7 @@ var ReconcileController = class ReconcileController {
|
|
|
39014
39563
|
this.slotReadRetryTimers.clear();
|
|
39015
39564
|
this.slotReadRetryAttempts.clear();
|
|
39016
39565
|
this.remoteHealthAttempts.clear();
|
|
39566
|
+
this.vanishedDevices = /* @__PURE__ */ new Map();
|
|
39017
39567
|
}
|
|
39018
39568
|
};
|
|
39019
39569
|
//#endregion
|
|
@@ -40079,6 +40629,31 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40079
40629
|
getInitTimestamp: () => deps.initTimestamp
|
|
40080
40630
|
});
|
|
40081
40631
|
const inferenceRotation = new RoundRobinInferenceDeviceRotation();
|
|
40632
|
+
/**
|
|
40633
|
+
* Does this camera's source carry an audio track? Read from the broker that
|
|
40634
|
+
* owns the stream (`BrokerStats.audio === null` is the documented no-audio
|
|
40635
|
+
* signal), pinned to the cluster's source owner exactly like the
|
|
40636
|
+
* camera-status broker stage. FAILS OPEN: no api, an RPC error, or a broker
|
|
40637
|
+
* that has not detected the track yet (`undefined`, cold start) all answer
|
|
40638
|
+
* `true`, because the only consumer is the watchdog's diagnostic audio stage
|
|
40639
|
+
* and a false negative silently stops watching a working camera.
|
|
40640
|
+
*/
|
|
40641
|
+
const resolveHasAudioSource = async (deviceId, camStreamId) => {
|
|
40642
|
+
const api = deps.ctx().api;
|
|
40643
|
+
if (!api) return true;
|
|
40644
|
+
try {
|
|
40645
|
+
return (await api.streamBroker.getBrokerStats.query({ brokerId: makeSourceBrokerId(deviceId, camStreamId) }, nodePin(topology.assignSource(deviceId)))).audio !== null;
|
|
40646
|
+
} catch (err) {
|
|
40647
|
+
deps.ctx().logger.debug("audio-source probe failed — assuming the source has audio", {
|
|
40648
|
+
tags: { deviceId },
|
|
40649
|
+
meta: {
|
|
40650
|
+
camStreamId,
|
|
40651
|
+
error: errMsg(err)
|
|
40652
|
+
}
|
|
40653
|
+
});
|
|
40654
|
+
return true;
|
|
40655
|
+
}
|
|
40656
|
+
};
|
|
40082
40657
|
const placement = new PlacementService({
|
|
40083
40658
|
ctx: () => deps.ctx(),
|
|
40084
40659
|
ctxIfReady: () => deps.ctxIfReady(),
|
|
@@ -40094,12 +40669,14 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40094
40669
|
getInferenceDeviceCaps: (nodeId) => deps.getInferenceDeviceCaps(nodeId),
|
|
40095
40670
|
readPipelineDevicePin: (deviceId) => deps.readPipelineDevicePin(deviceId),
|
|
40096
40671
|
inferenceRotation,
|
|
40097
|
-
buildWatchdogCamera: (config, deviceName) => detectionWiring.buildWatchdogCamera(config, deviceName),
|
|
40672
|
+
buildWatchdogCamera: (config, deviceName, hasAudioSource) => detectionWiring.buildWatchdogCamera(config, deviceName, hasAudioSource),
|
|
40673
|
+
hasAudioSource: (deviceId, camStreamId) => resolveHasAudioSource(deviceId, camStreamId),
|
|
40098
40674
|
isIdleSessionCamera: (deviceId) => deps.isIdleSessionCamera(deviceId),
|
|
40099
40675
|
activeSessionNode: (deviceId) => session.activeSessionNode(deviceId),
|
|
40100
40676
|
closeActiveSession: (deviceId) => session.closeSessionForReassign(deviceId),
|
|
40101
40677
|
reconcilePlacementFromRunners: () => reconcile.reconcilePlacementFromRunners(),
|
|
40102
|
-
emitResolvedCameraUpdated: (deviceId) => deps.emitResolvedCameraUpdated(deviceId)
|
|
40678
|
+
emitResolvedCameraUpdated: (deviceId) => deps.emitResolvedCameraUpdated(deviceId),
|
|
40679
|
+
maintenancePaused: () => deps.maintenancePaused()
|
|
40103
40680
|
});
|
|
40104
40681
|
const session = new SessionDispatchController({
|
|
40105
40682
|
ledger,
|
|
@@ -40168,6 +40745,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40168
40745
|
loadShed,
|
|
40169
40746
|
logger: deps.ctx().logger,
|
|
40170
40747
|
handleDeviceRegistered: (deviceId) => deps.handleDeviceRegistered(deviceId),
|
|
40748
|
+
handleDeviceUnregistered: (deviceId) => deps.handleDeviceUnregistered(deviceId),
|
|
40171
40749
|
dispatchCamera: (config) => deps.dispatchCamera(config),
|
|
40172
40750
|
detachOn: (nodeId, deviceId) => deps.detachOn(nodeId, deviceId),
|
|
40173
40751
|
localNodeId: () => localNodeId,
|
|
@@ -40268,7 +40846,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40268
40846
|
const pipelineWatchdog = new PipelineWatchdog({
|
|
40269
40847
|
now: () => Date.now(),
|
|
40270
40848
|
logger: deps.ctx().logger,
|
|
40271
|
-
recover: (deviceId, stage, streamId) => detectionWiring.recoverPipelineStage(deviceId, stage, streamId),
|
|
40849
|
+
recover: (deviceId, stage, streamId, attempt) => detectionWiring.recoverPipelineStage(deviceId, stage, streamId, attempt),
|
|
40272
40850
|
thresholds: DEFAULT_WATCHDOG_THRESHOLDS
|
|
40273
40851
|
});
|
|
40274
40852
|
pipelineWatchdog.start(deps.watchdogIntervalMs);
|
|
@@ -41124,6 +41702,9 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
41124
41702
|
/** Periodic auto-rebalance sweep timer (drift correction under hysteresis). */
|
|
41125
41703
|
autoRebalanceTimer = null;
|
|
41126
41704
|
initTimestamp = 0;
|
|
41705
|
+
/** Storage migration maintenance lease. It only gates dispatch; it does not
|
|
41706
|
+
* change any camera wrapper or persistent pipeline configuration. */
|
|
41707
|
+
storageMigrationLeaseId = null;
|
|
41127
41708
|
constructor() {
|
|
41128
41709
|
super({});
|
|
41129
41710
|
}
|
|
@@ -41171,6 +41752,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
41171
41752
|
rebalance: (opts) => this.rebalance(opts),
|
|
41172
41753
|
setCameraPipelineForAgent: (input) => this.setCameraPipelineForAgent(input),
|
|
41173
41754
|
dispatchCamera: (config) => this.dispatchCamera(config),
|
|
41755
|
+
maintenancePaused: () => this.storageMigrationLeaseId !== null,
|
|
41174
41756
|
setTopology: (topology) => {
|
|
41175
41757
|
this.topology = topology;
|
|
41176
41758
|
},
|
|
@@ -41317,8 +41899,29 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
41317
41899
|
this.settingsStore = null;
|
|
41318
41900
|
}
|
|
41319
41901
|
async dispatchCamera(runnerConfig) {
|
|
41902
|
+
if (this.storageMigrationLeaseId !== null) return {
|
|
41903
|
+
success: true,
|
|
41904
|
+
kind: "pending"
|
|
41905
|
+
};
|
|
41320
41906
|
return this.placement.dispatchCamera(runnerConfig);
|
|
41321
41907
|
}
|
|
41908
|
+
async pauseForStorageMigration(input) {
|
|
41909
|
+
if (this.storageMigrationLeaseId !== null && this.storageMigrationLeaseId !== input.leaseId) throw new Error("pipeline dispatch is already paused by another storage migration");
|
|
41910
|
+
this.storageMigrationLeaseId = input.leaseId;
|
|
41911
|
+
try {
|
|
41912
|
+
await this.placement.waitForMigrationIdle();
|
|
41913
|
+
return { paused: true };
|
|
41914
|
+
} catch (err) {
|
|
41915
|
+
this.storageMigrationLeaseId = null;
|
|
41916
|
+
throw err;
|
|
41917
|
+
}
|
|
41918
|
+
}
|
|
41919
|
+
async resumeForStorageMigration(input) {
|
|
41920
|
+
if (this.storageMigrationLeaseId === null) return { resumed: true };
|
|
41921
|
+
if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("pipeline storage migration lease does not match");
|
|
41922
|
+
this.storageMigrationLeaseId = null;
|
|
41923
|
+
return { resumed: true };
|
|
41924
|
+
}
|
|
41322
41925
|
async releaseCamera(input) {
|
|
41323
41926
|
return this.placement.releaseCamera(input);
|
|
41324
41927
|
}
|