@camstack/addon-post-analysis 1.2.64 → 1.2.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-CSTtfEOC.mjs → dist-4dhFDogy.mjs} +208 -10
- package/dist/{dist-BQ-M7jn9.js → dist-DTClXZLm.js} +231 -9
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-lSMqbbY-.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Ck9Z2cZR.mjs} +2 -2
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-EHTrRb5u.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DO3ZGk1V.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-D5rpu5pY.mjs → hostInit-DvCGVFX8.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +1111 -12
- package/dist/pipeline-analytics/index.mjs +1111 -12
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -7390,8 +7390,31 @@ var AdoptionJobSchema = object({
|
|
|
7390
7390
|
error: string().nullable()
|
|
7391
7391
|
});
|
|
7392
7392
|
/**
|
|
7393
|
-
* Per-camera FUNCTION SWITCHES
|
|
7394
|
-
*
|
|
7393
|
+
* Per-camera FUNCTION SWITCHES.
|
|
7394
|
+
*
|
|
7395
|
+
* ## The aggregate group is being withdrawn — the BADGE is not (D113)
|
|
7396
|
+
*
|
|
7397
|
+
* This file shipped as "the one coherent on/off surface over the pipeline
|
|
7398
|
+
* functions an operator thinks in terms of". The operator's verdict on
|
|
7399
|
+
* 2026-08-12 was that the coherent surface bought complexity and no clarity:
|
|
7400
|
+
* every function already had a settings page of its own, and a second place to
|
|
7401
|
+
* turn it off is a second place to look. Each switch is going back to its own
|
|
7402
|
+
* component's original options — detection to the detection-pipeline wrapper
|
|
7403
|
+
* binding, audio analysis to its own, recording to `RecordingConfig.enabled`
|
|
7404
|
+
* (which was always first-class; the switch was a veneer over
|
|
7405
|
+
* `recording.setDeviceConfig`), notifications to a notification-center
|
|
7406
|
+
* per-device setting, the two camera planes to their own components.
|
|
7407
|
+
*
|
|
7408
|
+
* What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
|
|
7409
|
+
* thing that lets a status surface say DISABLED instead of BROKEN, recomposed
|
|
7410
|
+
* straight from the authorities with no group in the middle. That rule was
|
|
7411
|
+
* never about a control panel.
|
|
7412
|
+
*
|
|
7413
|
+
* Everything else here — {@link CAMERA_SWITCH_CATALOG}, {@link CameraSwitch},
|
|
7414
|
+
* {@link deriveCameraSwitches}, the `pipelineOrchestrator.getCameraSwitches` /
|
|
7415
|
+
* `setCameraSwitch` pair — is a COMPATIBILITY surface for as long as deployed
|
|
7416
|
+
* viewers (v1.0.305) and the admin UI still call it. It is deleted when they
|
|
7417
|
+
* stop; nothing new may be built on it.
|
|
7395
7418
|
*
|
|
7396
7419
|
* ## This file adds no state
|
|
7397
7420
|
*
|
|
@@ -7753,6 +7776,22 @@ var RecordingConfigSchema = object({
|
|
|
7753
7776
|
scrubThumbnails: ScrubThumbnailPresetSchema.optional()
|
|
7754
7777
|
}).strict();
|
|
7755
7778
|
/**
|
|
7779
|
+
* Derive the {@link RecordingStorageModeSchema} summary from the authoritative
|
|
7780
|
+
* bands: `continuous` when any band records continuously, `events` when a band
|
|
7781
|
+
* exists at all, else `off`. Pure.
|
|
7782
|
+
*
|
|
7783
|
+
* THE one definition: the recorder stamps `config.mode` with it on save and
|
|
7784
|
+
* reports it as the cap's `activeMode`, and the settings UI reads it back to
|
|
7785
|
+
* decide which authoring tab a config opens in. Two copies would drift, and the
|
|
7786
|
+
* symptom (a status dot disagreeing with what is being recorded) is exactly the
|
|
7787
|
+
* bug this field was added to fix.
|
|
7788
|
+
*/
|
|
7789
|
+
function deriveRecordingMode(config) {
|
|
7790
|
+
if (!config.enabled || config.bands.length === 0) return "off";
|
|
7791
|
+
if (config.bands.some((band) => band.mode === "continuous")) return "continuous";
|
|
7792
|
+
return "events";
|
|
7793
|
+
}
|
|
7794
|
+
/**
|
|
7756
7795
|
* Entity-relocation job state (storage entity-routing spec, Phase 4).
|
|
7757
7796
|
*
|
|
7758
7797
|
* One shape shared by the recorder and pipeline-analytics internal movers.
|
|
@@ -18882,9 +18921,16 @@ var CameraStatusSchema = object({
|
|
|
18882
18921
|
audio: CameraAudioStatusSchema.nullable(),
|
|
18883
18922
|
recording: CameraRecordingStatusSchema.nullable(),
|
|
18884
18923
|
/**
|
|
18885
|
-
* Per-camera
|
|
18924
|
+
* Per-camera functions an OPERATOR has turned off
|
|
18886
18925
|
* ([D61](../../../../docs/decisions/adr-0067.md)).
|
|
18887
18926
|
*
|
|
18927
|
+
* Composed from the AUTHORITIES themselves — the wrapper bindings,
|
|
18928
|
+
* `RecordingConfig.enabled`, the notification mute, the broker's audio
|
|
18929
|
+
* policy, the camera's own microphone — via `composeSwitchedOff`, not from
|
|
18930
|
+
* the deprecated `getCameraSwitches` group ([D113](../../../../docs/decisions/adr-0113.md)).
|
|
18931
|
+
* The badge outlives the control panel: the panel was a convenience, this is
|
|
18932
|
+
* the difference between a camera being off and a camera being dead.
|
|
18933
|
+
*
|
|
18888
18934
|
* This is the difference between DISABLED and BROKEN. A camera whose
|
|
18889
18935
|
* `detection` block reports zero fps and whose `switchedOff` contains
|
|
18890
18936
|
* `'object-detection'` was switched off by a person; the same camera with an
|
|
@@ -25937,10 +25983,42 @@ method(object({
|
|
|
25937
25983
|
*/
|
|
25938
25984
|
/** Playback-speed multiplier for the render (1 = realtime). */
|
|
25939
25985
|
var ExportSpeedSchema = number().min(.25).max(32);
|
|
25986
|
+
/**
|
|
25987
|
+
* One dense interval, in SECONDS FROM THE EXPORT'S OWN `fromMs`.
|
|
25988
|
+
*
|
|
25989
|
+
* Relative and not absolute epoch on purpose: the renderer's frame-select
|
|
25990
|
+
* expression sees ffmpeg's `t`, which starts at 0 for the export's source
|
|
25991
|
+
* playlist. Handing it absolute epochs would make every call site responsible
|
|
25992
|
+
* for the same subtraction, and the one that forgot would emit a filter that
|
|
25993
|
+
* selects nothing — silently, as a uniform timelapse.
|
|
25994
|
+
*/
|
|
25995
|
+
var ExportDenseRangeSchema = object({
|
|
25996
|
+
fromSec: number().nonnegative(),
|
|
25997
|
+
toSec: number().nonnegative()
|
|
25998
|
+
}).refine((r) => r.toSec > r.fromSec, { message: "dense range must have toSec > fromSec" });
|
|
25999
|
+
/**
|
|
26000
|
+
* Dense-interval overlay for a timelapse: sample at `dense.everyMs` INSIDE the
|
|
26001
|
+
* listed ranges and at the base `everyMs` everywhere else.
|
|
26002
|
+
*
|
|
26003
|
+
* `everyMs` must be strictly smaller than the base cadence — a dense rate that
|
|
26004
|
+
* is not denser renders a uniform timelapse the operator believes is two-rate.
|
|
26005
|
+
*/
|
|
26006
|
+
var ExportDenseSchema = object({
|
|
26007
|
+
everyMs: number().int().positive(),
|
|
26008
|
+
ranges: array(ExportDenseRangeSchema).min(1).max(200)
|
|
26009
|
+
});
|
|
25940
26010
|
/** Timelapse cadence — sample one source frame per `everyMs`, output at `outputFps`. */
|
|
25941
26011
|
var ExportTimelapseSchema = object({
|
|
25942
26012
|
everyMs: number().int().positive(),
|
|
25943
|
-
outputFps: number().int().min(1).max(60).optional()
|
|
26013
|
+
outputFps: number().int().min(1).max(60).optional(),
|
|
26014
|
+
/** Optional second, FASTER rate over the intervals that matter. */
|
|
26015
|
+
dense: ExportDenseSchema.optional()
|
|
26016
|
+
}).superRefine((v, ctx) => {
|
|
26017
|
+
if (v.dense !== void 0 && v.dense.everyMs >= v.everyMs) ctx.addIssue({
|
|
26018
|
+
code: ZodIssueCode.custom,
|
|
26019
|
+
message: "dense.everyMs must be strictly smaller than the base everyMs",
|
|
26020
|
+
path: ["dense", "everyMs"]
|
|
26021
|
+
});
|
|
25944
26022
|
});
|
|
25945
26023
|
/**
|
|
25946
26024
|
* Render options. `speed` and `timelapse` are mutually exclusive. `includeAudio`
|
|
@@ -25998,6 +26076,38 @@ var ExportDownloadSchema = object({
|
|
|
25998
26076
|
url: string(),
|
|
25999
26077
|
endpoints: array(string())
|
|
26000
26078
|
});
|
|
26079
|
+
/**
|
|
26080
|
+
* Hard ceiling on ONE {@link recordingExportCapability} byte read — 50 MiB.
|
|
26081
|
+
*
|
|
26082
|
+
* Two independent reasons land on the same number, which is why it is this one
|
|
26083
|
+
* and not a rounder guess:
|
|
26084
|
+
*
|
|
26085
|
+
* - **Nobody would accept more.** The roomiest byte cap any notifier backend
|
|
26086
|
+
* declares is telegram's 50 MiB, and the degrade engine DROPS an over-cap
|
|
26087
|
+
* attachment outright rather than degrading it to a link. Bytes above this
|
|
26088
|
+
* are read, encoded and moved to be thrown away at the last step.
|
|
26089
|
+
* - **The envelope is unary.** A base64 payload is held whole, ~1.33× its
|
|
26090
|
+
* size, in the provider AND in the caller — on a hub this repo has already
|
|
26091
|
+
* OOM'd once (D9/D18). A bounded on-demand read at human speed is the shape
|
|
26092
|
+
* those records permit; an unbounded one is the shape they forbid.
|
|
26093
|
+
*
|
|
26094
|
+
* Above it the provider REFUSES with a log line rather than truncating: half a
|
|
26095
|
+
* video is worse than a notification that says there is no attachment.
|
|
26096
|
+
*/
|
|
26097
|
+
var RECORDING_EXPORT_MAX_READ_BYTES = 50 * 1024 * 1024;
|
|
26098
|
+
/**
|
|
26099
|
+
* A finished export's bytes, inline.
|
|
26100
|
+
*
|
|
26101
|
+
* `bytes` is the DECODED length — the number the caller bounds and logs
|
|
26102
|
+
* against, so nobody has to infer it from the base64 length.
|
|
26103
|
+
*/
|
|
26104
|
+
var ExportBytesSchema = object({
|
|
26105
|
+
base64: string(),
|
|
26106
|
+
contentType: string(),
|
|
26107
|
+
/** Suggested filename, extension included. */
|
|
26108
|
+
name: string(),
|
|
26109
|
+
bytes: number().int().nonnegative()
|
|
26110
|
+
});
|
|
26001
26111
|
method(object({
|
|
26002
26112
|
deviceId: number(),
|
|
26003
26113
|
profile: string(),
|
|
@@ -26022,6 +26132,9 @@ method(object({
|
|
|
26022
26132
|
}), method(object({ exportId: string() }), ExportDownloadSchema, {
|
|
26023
26133
|
kind: "query",
|
|
26024
26134
|
auth: "protected"
|
|
26135
|
+
}), method(object({ exportId: string() }), ExportBytesSchema, {
|
|
26136
|
+
kind: "query",
|
|
26137
|
+
auth: "protected"
|
|
26025
26138
|
});
|
|
26026
26139
|
/**
|
|
26027
26140
|
* scene-monitor — device-scoped reference-region state cap. An operator marks
|
|
@@ -28488,9 +28601,10 @@ var DeclaredDevices = class {
|
|
|
28488
28601
|
}
|
|
28489
28602
|
const integrationId = spec.integrationId ?? await this.ensureIntegration(spec.integrationName);
|
|
28490
28603
|
const index = await this.readIndex();
|
|
28604
|
+
const live = await this.readLiveByStableId();
|
|
28491
28605
|
const outcomes = [];
|
|
28492
28606
|
for (const declaration of spec.devices) {
|
|
28493
|
-
const outcome = await this.applyDeclaration(declaration, integrationId, index);
|
|
28607
|
+
const outcome = await this.applyDeclaration(declaration, integrationId, index, live);
|
|
28494
28608
|
if (outcome !== null) outcomes.push(outcome);
|
|
28495
28609
|
}
|
|
28496
28610
|
return {
|
|
@@ -28536,6 +28650,26 @@ var DeclaredDevices = class {
|
|
|
28536
28650
|
return new Map(rows.map((row) => [row.stableId, row]));
|
|
28537
28651
|
}
|
|
28538
28652
|
/**
|
|
28653
|
+
* Devices this kernel already has CONSTRUCTED, by stableId.
|
|
28654
|
+
*
|
|
28655
|
+
* Distinct from {@link readIndex}, and the distinction is the bug: the index
|
|
28656
|
+
* is persisted rows, this is live objects. A row without an object must be
|
|
28657
|
+
* adopted; an object must be left exactly as it is.
|
|
28658
|
+
*
|
|
28659
|
+
* Failure is non-fatal and deliberately so — an empty map degrades to the
|
|
28660
|
+
* previous behaviour (attempt the adopt) rather than skipping a device that
|
|
28661
|
+
* genuinely needs bringing up.
|
|
28662
|
+
*/
|
|
28663
|
+
async readLiveByStableId() {
|
|
28664
|
+
try {
|
|
28665
|
+
const devices = await this.ports.devices.getAll();
|
|
28666
|
+
return new Map(devices.map((device) => [device.stableId, device]));
|
|
28667
|
+
} catch (err) {
|
|
28668
|
+
this.ports.logger.warn("could not read live devices — falling back to adopt-by-row", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
28669
|
+
return /* @__PURE__ */ new Map();
|
|
28670
|
+
}
|
|
28671
|
+
}
|
|
28672
|
+
/**
|
|
28539
28673
|
* One declaration: adopt what exists, create what does not.
|
|
28540
28674
|
*
|
|
28541
28675
|
* The create branch is the destructive one — it seeds `initialMeta`, and
|
|
@@ -28544,8 +28678,15 @@ var DeclaredDevices = class {
|
|
|
28544
28678
|
* the declared name over the operator's rename. D49: that branch needs a
|
|
28545
28679
|
* second read to agree.
|
|
28546
28680
|
*/
|
|
28547
|
-
async applyDeclaration(declaration, integrationId, index) {
|
|
28681
|
+
async applyDeclaration(declaration, integrationId, index, live) {
|
|
28548
28682
|
try {
|
|
28683
|
+
const alreadyLive = live.get(declaration.stableId);
|
|
28684
|
+
if (alreadyLive !== void 0) return {
|
|
28685
|
+
stableId: declaration.stableId,
|
|
28686
|
+
deviceId: alreadyLive.id,
|
|
28687
|
+
device: alreadyLive,
|
|
28688
|
+
created: false
|
|
28689
|
+
};
|
|
28549
28690
|
let existing = index.get(declaration.stableId);
|
|
28550
28691
|
if (existing === void 0) {
|
|
28551
28692
|
existing = (await this.readIndex()).get(declaration.stableId);
|
|
@@ -32886,6 +33027,12 @@ Object.freeze({
|
|
|
32886
33027
|
addonId: null,
|
|
32887
33028
|
access: "view"
|
|
32888
33029
|
},
|
|
33030
|
+
"recordingExport.readExportBytes": {
|
|
33031
|
+
capName: "recordingExport",
|
|
33032
|
+
capScope: "system",
|
|
33033
|
+
addonId: null,
|
|
33034
|
+
access: "view"
|
|
33035
|
+
},
|
|
32889
33036
|
"sceneMonitor.captureReference": {
|
|
32890
33037
|
capName: "scene-monitor",
|
|
32891
33038
|
capScope: "device",
|
|
@@ -34343,7 +34490,23 @@ var TimelapseRuleInputSchema = object({
|
|
|
34343
34490
|
/** Canonical notification priority ordinal (1..5); per-target overridable. */
|
|
34344
34491
|
priority: PriorityField.default(3)
|
|
34345
34492
|
});
|
|
34346
|
-
|
|
34493
|
+
/**
|
|
34494
|
+
* Partial patch for an update — any subset of the INPUT fields, with NO
|
|
34495
|
+
* defaults (an absent key means "leave unchanged", never "reset to default").
|
|
34496
|
+
* Provenance and ownership are absent by construction: a patch can rename or
|
|
34497
|
+
* retune a rule, never re-own it or forge its generation state.
|
|
34498
|
+
*
|
|
34499
|
+
* CLEAR SIGNAL: `template` is the one OPTIONAL field an editor can REMOVE, so
|
|
34500
|
+
* it is `.nullable()` here and NOWHERE else. Wire representation:
|
|
34501
|
+
* - key absent → leave the template unchanged
|
|
34502
|
+
* - `"template": null` → CLEAR it (the persisted rule loses the key)
|
|
34503
|
+
* - `"template": {...}` → replace it
|
|
34504
|
+
* `undefined` is deliberately NOT the clear signal: it does not survive
|
|
34505
|
+
* `JSON.stringify`, so a viewer editor could not express "remove". The
|
|
34506
|
+
* PERSISTED rule never carries `null` — the store drops the key (see
|
|
34507
|
+
* `TimelapseStore.update`).
|
|
34508
|
+
*/
|
|
34509
|
+
var TimelapseRulePatchSchema = object({
|
|
34347
34510
|
name: NameField.optional(),
|
|
34348
34511
|
enabled: boolean().optional(),
|
|
34349
34512
|
deviceIds: DeviceIdsField.optional(),
|
|
@@ -34364,15 +34527,50 @@ var TimelapseRuleSchema = TimelapseRuleInputSchema.extend({
|
|
|
34364
34527
|
*/
|
|
34365
34528
|
ownerUserId: string().optional(),
|
|
34366
34529
|
/**
|
|
34367
|
-
* Epoch-ms of the
|
|
34368
|
-
*
|
|
34530
|
+
* Epoch-ms of the NEWEST successful generation across every camera of this
|
|
34531
|
+
* rule. What a UI shows, and the compatibility floor for
|
|
34532
|
+
* {@link readTimelapseGeneratedAt}. Absent = never generated.
|
|
34369
34533
|
*/
|
|
34370
34534
|
lastGeneratedAt: number().optional(),
|
|
34535
|
+
/**
|
|
34536
|
+
* PER-CAMERA generation state, keyed by `String(deviceId)` — the
|
|
34537
|
+
* re-generation guard's real durable state.
|
|
34538
|
+
*
|
|
34539
|
+
* One rule covers several cameras and each renders its own video, so a rule
|
|
34540
|
+
* -wide stamp is wrong in the direction that DESTROYS work: camera A
|
|
34541
|
+
* succeeding at 06:05 tells camera B, whose render failed, that it is
|
|
34542
|
+
* already done — and B's night is gone for good, because the window will not
|
|
34543
|
+
* come back.
|
|
34544
|
+
*
|
|
34545
|
+
* ADDITIVE, so the migration is free: a row written before this field simply
|
|
34546
|
+
* has no map, and {@link readTimelapseGeneratedAt} falls back to
|
|
34547
|
+
* {@link TimelapseRuleSchema.shape.lastGeneratedAt}. Reading an old row as
|
|
34548
|
+
* "never generated" would re-render and re-notify every camera of every rule
|
|
34549
|
+
* once, on the deploy that shipped the map.
|
|
34550
|
+
*/
|
|
34551
|
+
generatedByDevice: record(string(), number()).optional(),
|
|
34371
34552
|
/** userId of the caller who created the rule (server-stamped). */
|
|
34372
34553
|
createdBy: string(),
|
|
34373
34554
|
createdAt: number(),
|
|
34374
34555
|
updatedAt: number()
|
|
34375
34556
|
});
|
|
34557
|
+
/**
|
|
34558
|
+
* The last successful generation for ONE camera of a rule, epoch-ms.
|
|
34559
|
+
*
|
|
34560
|
+
* The per-device map wins; a rule with no map falls back to the rule-wide
|
|
34561
|
+
* `lastGeneratedAt` (the compatible-migration path — see
|
|
34562
|
+
* {@link TimelapseRuleSchema}); a rule with neither returns 0, which every
|
|
34563
|
+
* guard reads as "never generated".
|
|
34564
|
+
*
|
|
34565
|
+
* Read through this helper and never off the field directly: the fallback is
|
|
34566
|
+
* the whole migration, and a call site that forgot it would re-render an
|
|
34567
|
+
* entire rule set once.
|
|
34568
|
+
*/
|
|
34569
|
+
function readTimelapseGeneratedAt(rule, deviceId) {
|
|
34570
|
+
const map = rule.generatedByDevice;
|
|
34571
|
+
if (map !== void 0) return map[String(deviceId)] ?? 0;
|
|
34572
|
+
return rule.lastGeneratedAt ?? 0;
|
|
34573
|
+
}
|
|
34376
34574
|
object({
|
|
34377
34575
|
/**
|
|
34378
34576
|
* Fraction of the box's own size added on EACH side before cutting.
|
|
@@ -34525,4 +34723,4 @@ function vectorDimFromBase64(encoded) {
|
|
|
34525
34723
|
return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
|
|
34526
34724
|
}
|
|
34527
34725
|
//#endregion
|
|
34528
|
-
export {
|
|
34726
|
+
export { isDeviceScopedCap as $, customAction as A, pipelineAnalyticsCapability as B, TimelapseRuleSchema as C, audioMetricsCapability as D, alarmPanelCapability as E, faceGalleryCapability as F, vectorDimFromBase64 as G, readDeviceStateFrom as H, hfModelUrl as I, errMsg as J, videoclipsCapability as K, isScheduleActive as L, deriveRecordingMode as M, embeddingEncoderCapability as N, buildEventKindDescriptor as O, encodeVectorBase64 as P, hydrateSchema as Q, kebabToCamel as R, TimelapseRulePatchSchema as S, addonWidgetsSourceCapability as T, readTimelapseGeneratedAt as U, plateGalleryCapability as V, subKindsOf as W, DeviceType as X, BaseAddon as Y, createEvent as Z, NcTaxonomySchema as _, EVENT_PAD_MS as a, literal as at, RetrainStatusSchema as b, NC_CONDITION_CATALOG as c, record as ct, NcRuleInputSchema as d, EventCategory as dt, nodePin as et, NcRulePatchSchema as f, NcSnoozeSuppressedSchema as g, NcSnoozeSchema as h, EVENT_KIND_BY_CAP as i, boolean as it, defineCustomActions as j, cosineSimilarity as k, NC_TAXONOMY as l, string as lt, NcSnoozeInputSchema as m, DEFAULT_EVENT_COLOR as n, _enum as nt, LabelAttributionSchema as o, number as ot, NcRuleSchema as p, zoneAnalyticsCapability as q, DeclaredDevices as r, array as rt, MACRO_LABELS as s, object as st, BaseDevice as t, sleep as tt, NcConditionDescriptorSchema as u, unknown as ut, OpsLogEntrySchema as v, TrackSourceSchema as w, TimelapseRuleInputSchema as x, RECORDING_EXPORT_MAX_READ_BYTES as y, notificationRulesCapability as z };
|
|
@@ -7421,8 +7421,31 @@ var AdoptionJobSchema = object({
|
|
|
7421
7421
|
error: string().nullable()
|
|
7422
7422
|
});
|
|
7423
7423
|
/**
|
|
7424
|
-
* Per-camera FUNCTION SWITCHES
|
|
7425
|
-
*
|
|
7424
|
+
* Per-camera FUNCTION SWITCHES.
|
|
7425
|
+
*
|
|
7426
|
+
* ## The aggregate group is being withdrawn — the BADGE is not (D113)
|
|
7427
|
+
*
|
|
7428
|
+
* This file shipped as "the one coherent on/off surface over the pipeline
|
|
7429
|
+
* functions an operator thinks in terms of". The operator's verdict on
|
|
7430
|
+
* 2026-08-12 was that the coherent surface bought complexity and no clarity:
|
|
7431
|
+
* every function already had a settings page of its own, and a second place to
|
|
7432
|
+
* turn it off is a second place to look. Each switch is going back to its own
|
|
7433
|
+
* component's original options — detection to the detection-pipeline wrapper
|
|
7434
|
+
* binding, audio analysis to its own, recording to `RecordingConfig.enabled`
|
|
7435
|
+
* (which was always first-class; the switch was a veneer over
|
|
7436
|
+
* `recording.setDeviceConfig`), notifications to a notification-center
|
|
7437
|
+
* per-device setting, the two camera planes to their own components.
|
|
7438
|
+
*
|
|
7439
|
+
* What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
|
|
7440
|
+
* thing that lets a status surface say DISABLED instead of BROKEN, recomposed
|
|
7441
|
+
* straight from the authorities with no group in the middle. That rule was
|
|
7442
|
+
* never about a control panel.
|
|
7443
|
+
*
|
|
7444
|
+
* Everything else here — {@link CAMERA_SWITCH_CATALOG}, {@link CameraSwitch},
|
|
7445
|
+
* {@link deriveCameraSwitches}, the `pipelineOrchestrator.getCameraSwitches` /
|
|
7446
|
+
* `setCameraSwitch` pair — is a COMPATIBILITY surface for as long as deployed
|
|
7447
|
+
* viewers (v1.0.305) and the admin UI still call it. It is deleted when they
|
|
7448
|
+
* stop; nothing new may be built on it.
|
|
7426
7449
|
*
|
|
7427
7450
|
* ## This file adds no state
|
|
7428
7451
|
*
|
|
@@ -7784,6 +7807,22 @@ var RecordingConfigSchema = object({
|
|
|
7784
7807
|
scrubThumbnails: ScrubThumbnailPresetSchema.optional()
|
|
7785
7808
|
}).strict();
|
|
7786
7809
|
/**
|
|
7810
|
+
* Derive the {@link RecordingStorageModeSchema} summary from the authoritative
|
|
7811
|
+
* bands: `continuous` when any band records continuously, `events` when a band
|
|
7812
|
+
* exists at all, else `off`. Pure.
|
|
7813
|
+
*
|
|
7814
|
+
* THE one definition: the recorder stamps `config.mode` with it on save and
|
|
7815
|
+
* reports it as the cap's `activeMode`, and the settings UI reads it back to
|
|
7816
|
+
* decide which authoring tab a config opens in. Two copies would drift, and the
|
|
7817
|
+
* symptom (a status dot disagreeing with what is being recorded) is exactly the
|
|
7818
|
+
* bug this field was added to fix.
|
|
7819
|
+
*/
|
|
7820
|
+
function deriveRecordingMode(config) {
|
|
7821
|
+
if (!config.enabled || config.bands.length === 0) return "off";
|
|
7822
|
+
if (config.bands.some((band) => band.mode === "continuous")) return "continuous";
|
|
7823
|
+
return "events";
|
|
7824
|
+
}
|
|
7825
|
+
/**
|
|
7787
7826
|
* Entity-relocation job state (storage entity-routing spec, Phase 4).
|
|
7788
7827
|
*
|
|
7789
7828
|
* One shape shared by the recorder and pipeline-analytics internal movers.
|
|
@@ -18913,9 +18952,16 @@ var CameraStatusSchema = object({
|
|
|
18913
18952
|
audio: CameraAudioStatusSchema.nullable(),
|
|
18914
18953
|
recording: CameraRecordingStatusSchema.nullable(),
|
|
18915
18954
|
/**
|
|
18916
|
-
* Per-camera
|
|
18955
|
+
* Per-camera functions an OPERATOR has turned off
|
|
18917
18956
|
* ([D61](../../../../docs/decisions/adr-0067.md)).
|
|
18918
18957
|
*
|
|
18958
|
+
* Composed from the AUTHORITIES themselves — the wrapper bindings,
|
|
18959
|
+
* `RecordingConfig.enabled`, the notification mute, the broker's audio
|
|
18960
|
+
* policy, the camera's own microphone — via `composeSwitchedOff`, not from
|
|
18961
|
+
* the deprecated `getCameraSwitches` group ([D113](../../../../docs/decisions/adr-0113.md)).
|
|
18962
|
+
* The badge outlives the control panel: the panel was a convenience, this is
|
|
18963
|
+
* the difference between a camera being off and a camera being dead.
|
|
18964
|
+
*
|
|
18919
18965
|
* This is the difference between DISABLED and BROKEN. A camera whose
|
|
18920
18966
|
* `detection` block reports zero fps and whose `switchedOff` contains
|
|
18921
18967
|
* `'object-detection'` was switched off by a person; the same camera with an
|
|
@@ -25968,10 +26014,42 @@ method(object({
|
|
|
25968
26014
|
*/
|
|
25969
26015
|
/** Playback-speed multiplier for the render (1 = realtime). */
|
|
25970
26016
|
var ExportSpeedSchema = number().min(.25).max(32);
|
|
26017
|
+
/**
|
|
26018
|
+
* One dense interval, in SECONDS FROM THE EXPORT'S OWN `fromMs`.
|
|
26019
|
+
*
|
|
26020
|
+
* Relative and not absolute epoch on purpose: the renderer's frame-select
|
|
26021
|
+
* expression sees ffmpeg's `t`, which starts at 0 for the export's source
|
|
26022
|
+
* playlist. Handing it absolute epochs would make every call site responsible
|
|
26023
|
+
* for the same subtraction, and the one that forgot would emit a filter that
|
|
26024
|
+
* selects nothing — silently, as a uniform timelapse.
|
|
26025
|
+
*/
|
|
26026
|
+
var ExportDenseRangeSchema = object({
|
|
26027
|
+
fromSec: number().nonnegative(),
|
|
26028
|
+
toSec: number().nonnegative()
|
|
26029
|
+
}).refine((r) => r.toSec > r.fromSec, { message: "dense range must have toSec > fromSec" });
|
|
26030
|
+
/**
|
|
26031
|
+
* Dense-interval overlay for a timelapse: sample at `dense.everyMs` INSIDE the
|
|
26032
|
+
* listed ranges and at the base `everyMs` everywhere else.
|
|
26033
|
+
*
|
|
26034
|
+
* `everyMs` must be strictly smaller than the base cadence — a dense rate that
|
|
26035
|
+
* is not denser renders a uniform timelapse the operator believes is two-rate.
|
|
26036
|
+
*/
|
|
26037
|
+
var ExportDenseSchema = object({
|
|
26038
|
+
everyMs: number().int().positive(),
|
|
26039
|
+
ranges: array(ExportDenseRangeSchema).min(1).max(200)
|
|
26040
|
+
});
|
|
25971
26041
|
/** Timelapse cadence — sample one source frame per `everyMs`, output at `outputFps`. */
|
|
25972
26042
|
var ExportTimelapseSchema = object({
|
|
25973
26043
|
everyMs: number().int().positive(),
|
|
25974
|
-
outputFps: number().int().min(1).max(60).optional()
|
|
26044
|
+
outputFps: number().int().min(1).max(60).optional(),
|
|
26045
|
+
/** Optional second, FASTER rate over the intervals that matter. */
|
|
26046
|
+
dense: ExportDenseSchema.optional()
|
|
26047
|
+
}).superRefine((v, ctx) => {
|
|
26048
|
+
if (v.dense !== void 0 && v.dense.everyMs >= v.everyMs) ctx.addIssue({
|
|
26049
|
+
code: ZodIssueCode.custom,
|
|
26050
|
+
message: "dense.everyMs must be strictly smaller than the base everyMs",
|
|
26051
|
+
path: ["dense", "everyMs"]
|
|
26052
|
+
});
|
|
25975
26053
|
});
|
|
25976
26054
|
/**
|
|
25977
26055
|
* Render options. `speed` and `timelapse` are mutually exclusive. `includeAudio`
|
|
@@ -26029,6 +26107,38 @@ var ExportDownloadSchema = object({
|
|
|
26029
26107
|
url: string(),
|
|
26030
26108
|
endpoints: array(string())
|
|
26031
26109
|
});
|
|
26110
|
+
/**
|
|
26111
|
+
* Hard ceiling on ONE {@link recordingExportCapability} byte read — 50 MiB.
|
|
26112
|
+
*
|
|
26113
|
+
* Two independent reasons land on the same number, which is why it is this one
|
|
26114
|
+
* and not a rounder guess:
|
|
26115
|
+
*
|
|
26116
|
+
* - **Nobody would accept more.** The roomiest byte cap any notifier backend
|
|
26117
|
+
* declares is telegram's 50 MiB, and the degrade engine DROPS an over-cap
|
|
26118
|
+
* attachment outright rather than degrading it to a link. Bytes above this
|
|
26119
|
+
* are read, encoded and moved to be thrown away at the last step.
|
|
26120
|
+
* - **The envelope is unary.** A base64 payload is held whole, ~1.33× its
|
|
26121
|
+
* size, in the provider AND in the caller — on a hub this repo has already
|
|
26122
|
+
* OOM'd once (D9/D18). A bounded on-demand read at human speed is the shape
|
|
26123
|
+
* those records permit; an unbounded one is the shape they forbid.
|
|
26124
|
+
*
|
|
26125
|
+
* Above it the provider REFUSES with a log line rather than truncating: half a
|
|
26126
|
+
* video is worse than a notification that says there is no attachment.
|
|
26127
|
+
*/
|
|
26128
|
+
var RECORDING_EXPORT_MAX_READ_BYTES = 50 * 1024 * 1024;
|
|
26129
|
+
/**
|
|
26130
|
+
* A finished export's bytes, inline.
|
|
26131
|
+
*
|
|
26132
|
+
* `bytes` is the DECODED length — the number the caller bounds and logs
|
|
26133
|
+
* against, so nobody has to infer it from the base64 length.
|
|
26134
|
+
*/
|
|
26135
|
+
var ExportBytesSchema = object({
|
|
26136
|
+
base64: string(),
|
|
26137
|
+
contentType: string(),
|
|
26138
|
+
/** Suggested filename, extension included. */
|
|
26139
|
+
name: string(),
|
|
26140
|
+
bytes: number().int().nonnegative()
|
|
26141
|
+
});
|
|
26032
26142
|
method(object({
|
|
26033
26143
|
deviceId: number(),
|
|
26034
26144
|
profile: string(),
|
|
@@ -26053,6 +26163,9 @@ method(object({
|
|
|
26053
26163
|
}), method(object({ exportId: string() }), ExportDownloadSchema, {
|
|
26054
26164
|
kind: "query",
|
|
26055
26165
|
auth: "protected"
|
|
26166
|
+
}), method(object({ exportId: string() }), ExportBytesSchema, {
|
|
26167
|
+
kind: "query",
|
|
26168
|
+
auth: "protected"
|
|
26056
26169
|
});
|
|
26057
26170
|
/**
|
|
26058
26171
|
* scene-monitor — device-scoped reference-region state cap. An operator marks
|
|
@@ -28519,9 +28632,10 @@ var DeclaredDevices = class {
|
|
|
28519
28632
|
}
|
|
28520
28633
|
const integrationId = spec.integrationId ?? await this.ensureIntegration(spec.integrationName);
|
|
28521
28634
|
const index = await this.readIndex();
|
|
28635
|
+
const live = await this.readLiveByStableId();
|
|
28522
28636
|
const outcomes = [];
|
|
28523
28637
|
for (const declaration of spec.devices) {
|
|
28524
|
-
const outcome = await this.applyDeclaration(declaration, integrationId, index);
|
|
28638
|
+
const outcome = await this.applyDeclaration(declaration, integrationId, index, live);
|
|
28525
28639
|
if (outcome !== null) outcomes.push(outcome);
|
|
28526
28640
|
}
|
|
28527
28641
|
return {
|
|
@@ -28567,6 +28681,26 @@ var DeclaredDevices = class {
|
|
|
28567
28681
|
return new Map(rows.map((row) => [row.stableId, row]));
|
|
28568
28682
|
}
|
|
28569
28683
|
/**
|
|
28684
|
+
* Devices this kernel already has CONSTRUCTED, by stableId.
|
|
28685
|
+
*
|
|
28686
|
+
* Distinct from {@link readIndex}, and the distinction is the bug: the index
|
|
28687
|
+
* is persisted rows, this is live objects. A row without an object must be
|
|
28688
|
+
* adopted; an object must be left exactly as it is.
|
|
28689
|
+
*
|
|
28690
|
+
* Failure is non-fatal and deliberately so — an empty map degrades to the
|
|
28691
|
+
* previous behaviour (attempt the adopt) rather than skipping a device that
|
|
28692
|
+
* genuinely needs bringing up.
|
|
28693
|
+
*/
|
|
28694
|
+
async readLiveByStableId() {
|
|
28695
|
+
try {
|
|
28696
|
+
const devices = await this.ports.devices.getAll();
|
|
28697
|
+
return new Map(devices.map((device) => [device.stableId, device]));
|
|
28698
|
+
} catch (err) {
|
|
28699
|
+
this.ports.logger.warn("could not read live devices — falling back to adopt-by-row", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
28700
|
+
return /* @__PURE__ */ new Map();
|
|
28701
|
+
}
|
|
28702
|
+
}
|
|
28703
|
+
/**
|
|
28570
28704
|
* One declaration: adopt what exists, create what does not.
|
|
28571
28705
|
*
|
|
28572
28706
|
* The create branch is the destructive one — it seeds `initialMeta`, and
|
|
@@ -28575,8 +28709,15 @@ var DeclaredDevices = class {
|
|
|
28575
28709
|
* the declared name over the operator's rename. D49: that branch needs a
|
|
28576
28710
|
* second read to agree.
|
|
28577
28711
|
*/
|
|
28578
|
-
async applyDeclaration(declaration, integrationId, index) {
|
|
28712
|
+
async applyDeclaration(declaration, integrationId, index, live) {
|
|
28579
28713
|
try {
|
|
28714
|
+
const alreadyLive = live.get(declaration.stableId);
|
|
28715
|
+
if (alreadyLive !== void 0) return {
|
|
28716
|
+
stableId: declaration.stableId,
|
|
28717
|
+
deviceId: alreadyLive.id,
|
|
28718
|
+
device: alreadyLive,
|
|
28719
|
+
created: false
|
|
28720
|
+
};
|
|
28580
28721
|
let existing = index.get(declaration.stableId);
|
|
28581
28722
|
if (existing === void 0) {
|
|
28582
28723
|
existing = (await this.readIndex()).get(declaration.stableId);
|
|
@@ -32917,6 +33058,12 @@ Object.freeze({
|
|
|
32917
33058
|
addonId: null,
|
|
32918
33059
|
access: "view"
|
|
32919
33060
|
},
|
|
33061
|
+
"recordingExport.readExportBytes": {
|
|
33062
|
+
capName: "recordingExport",
|
|
33063
|
+
capScope: "system",
|
|
33064
|
+
addonId: null,
|
|
33065
|
+
access: "view"
|
|
33066
|
+
},
|
|
32920
33067
|
"sceneMonitor.captureReference": {
|
|
32921
33068
|
capName: "scene-monitor",
|
|
32922
33069
|
capScope: "device",
|
|
@@ -34374,7 +34521,23 @@ var TimelapseRuleInputSchema = object({
|
|
|
34374
34521
|
/** Canonical notification priority ordinal (1..5); per-target overridable. */
|
|
34375
34522
|
priority: PriorityField.default(3)
|
|
34376
34523
|
});
|
|
34377
|
-
|
|
34524
|
+
/**
|
|
34525
|
+
* Partial patch for an update — any subset of the INPUT fields, with NO
|
|
34526
|
+
* defaults (an absent key means "leave unchanged", never "reset to default").
|
|
34527
|
+
* Provenance and ownership are absent by construction: a patch can rename or
|
|
34528
|
+
* retune a rule, never re-own it or forge its generation state.
|
|
34529
|
+
*
|
|
34530
|
+
* CLEAR SIGNAL: `template` is the one OPTIONAL field an editor can REMOVE, so
|
|
34531
|
+
* it is `.nullable()` here and NOWHERE else. Wire representation:
|
|
34532
|
+
* - key absent → leave the template unchanged
|
|
34533
|
+
* - `"template": null` → CLEAR it (the persisted rule loses the key)
|
|
34534
|
+
* - `"template": {...}` → replace it
|
|
34535
|
+
* `undefined` is deliberately NOT the clear signal: it does not survive
|
|
34536
|
+
* `JSON.stringify`, so a viewer editor could not express "remove". The
|
|
34537
|
+
* PERSISTED rule never carries `null` — the store drops the key (see
|
|
34538
|
+
* `TimelapseStore.update`).
|
|
34539
|
+
*/
|
|
34540
|
+
var TimelapseRulePatchSchema = object({
|
|
34378
34541
|
name: NameField.optional(),
|
|
34379
34542
|
enabled: boolean().optional(),
|
|
34380
34543
|
deviceIds: DeviceIdsField.optional(),
|
|
@@ -34395,15 +34558,50 @@ var TimelapseRuleSchema = TimelapseRuleInputSchema.extend({
|
|
|
34395
34558
|
*/
|
|
34396
34559
|
ownerUserId: string().optional(),
|
|
34397
34560
|
/**
|
|
34398
|
-
* Epoch-ms of the
|
|
34399
|
-
*
|
|
34561
|
+
* Epoch-ms of the NEWEST successful generation across every camera of this
|
|
34562
|
+
* rule. What a UI shows, and the compatibility floor for
|
|
34563
|
+
* {@link readTimelapseGeneratedAt}. Absent = never generated.
|
|
34400
34564
|
*/
|
|
34401
34565
|
lastGeneratedAt: number().optional(),
|
|
34566
|
+
/**
|
|
34567
|
+
* PER-CAMERA generation state, keyed by `String(deviceId)` — the
|
|
34568
|
+
* re-generation guard's real durable state.
|
|
34569
|
+
*
|
|
34570
|
+
* One rule covers several cameras and each renders its own video, so a rule
|
|
34571
|
+
* -wide stamp is wrong in the direction that DESTROYS work: camera A
|
|
34572
|
+
* succeeding at 06:05 tells camera B, whose render failed, that it is
|
|
34573
|
+
* already done — and B's night is gone for good, because the window will not
|
|
34574
|
+
* come back.
|
|
34575
|
+
*
|
|
34576
|
+
* ADDITIVE, so the migration is free: a row written before this field simply
|
|
34577
|
+
* has no map, and {@link readTimelapseGeneratedAt} falls back to
|
|
34578
|
+
* {@link TimelapseRuleSchema.shape.lastGeneratedAt}. Reading an old row as
|
|
34579
|
+
* "never generated" would re-render and re-notify every camera of every rule
|
|
34580
|
+
* once, on the deploy that shipped the map.
|
|
34581
|
+
*/
|
|
34582
|
+
generatedByDevice: record(string(), number()).optional(),
|
|
34402
34583
|
/** userId of the caller who created the rule (server-stamped). */
|
|
34403
34584
|
createdBy: string(),
|
|
34404
34585
|
createdAt: number(),
|
|
34405
34586
|
updatedAt: number()
|
|
34406
34587
|
});
|
|
34588
|
+
/**
|
|
34589
|
+
* The last successful generation for ONE camera of a rule, epoch-ms.
|
|
34590
|
+
*
|
|
34591
|
+
* The per-device map wins; a rule with no map falls back to the rule-wide
|
|
34592
|
+
* `lastGeneratedAt` (the compatible-migration path — see
|
|
34593
|
+
* {@link TimelapseRuleSchema}); a rule with neither returns 0, which every
|
|
34594
|
+
* guard reads as "never generated".
|
|
34595
|
+
*
|
|
34596
|
+
* Read through this helper and never off the field directly: the fallback is
|
|
34597
|
+
* the whole migration, and a call site that forgot it would re-render an
|
|
34598
|
+
* entire rule set once.
|
|
34599
|
+
*/
|
|
34600
|
+
function readTimelapseGeneratedAt(rule, deviceId) {
|
|
34601
|
+
const map = rule.generatedByDevice;
|
|
34602
|
+
if (map !== void 0) return map[String(deviceId)] ?? 0;
|
|
34603
|
+
return rule.lastGeneratedAt ?? 0;
|
|
34604
|
+
}
|
|
34407
34605
|
object({
|
|
34408
34606
|
/**
|
|
34409
34607
|
* Fraction of the box's own size added on EACH side before cutting.
|
|
@@ -34682,6 +34880,12 @@ Object.defineProperty(exports, "OpsLogEntrySchema", {
|
|
|
34682
34880
|
return OpsLogEntrySchema;
|
|
34683
34881
|
}
|
|
34684
34882
|
});
|
|
34883
|
+
Object.defineProperty(exports, "RECORDING_EXPORT_MAX_READ_BYTES", {
|
|
34884
|
+
enumerable: true,
|
|
34885
|
+
get: function() {
|
|
34886
|
+
return RECORDING_EXPORT_MAX_READ_BYTES;
|
|
34887
|
+
}
|
|
34888
|
+
});
|
|
34685
34889
|
Object.defineProperty(exports, "RetrainStatusSchema", {
|
|
34686
34890
|
enumerable: true,
|
|
34687
34891
|
get: function() {
|
|
@@ -34694,6 +34898,12 @@ Object.defineProperty(exports, "TimelapseRuleInputSchema", {
|
|
|
34694
34898
|
return TimelapseRuleInputSchema;
|
|
34695
34899
|
}
|
|
34696
34900
|
});
|
|
34901
|
+
Object.defineProperty(exports, "TimelapseRulePatchSchema", {
|
|
34902
|
+
enumerable: true,
|
|
34903
|
+
get: function() {
|
|
34904
|
+
return TimelapseRulePatchSchema;
|
|
34905
|
+
}
|
|
34906
|
+
});
|
|
34697
34907
|
Object.defineProperty(exports, "TimelapseRuleSchema", {
|
|
34698
34908
|
enumerable: true,
|
|
34699
34909
|
get: function() {
|
|
@@ -34784,6 +34994,12 @@ Object.defineProperty(exports, "defineCustomActions", {
|
|
|
34784
34994
|
return defineCustomActions;
|
|
34785
34995
|
}
|
|
34786
34996
|
});
|
|
34997
|
+
Object.defineProperty(exports, "deriveRecordingMode", {
|
|
34998
|
+
enumerable: true,
|
|
34999
|
+
get: function() {
|
|
35000
|
+
return deriveRecordingMode;
|
|
35001
|
+
}
|
|
35002
|
+
});
|
|
34787
35003
|
Object.defineProperty(exports, "embeddingEncoderCapability", {
|
|
34788
35004
|
enumerable: true,
|
|
34789
35005
|
get: function() {
|
|
@@ -34886,6 +35102,12 @@ Object.defineProperty(exports, "readDeviceStateFrom", {
|
|
|
34886
35102
|
return readDeviceStateFrom;
|
|
34887
35103
|
}
|
|
34888
35104
|
});
|
|
35105
|
+
Object.defineProperty(exports, "readTimelapseGeneratedAt", {
|
|
35106
|
+
enumerable: true,
|
|
35107
|
+
get: function() {
|
|
35108
|
+
return readTimelapseGeneratedAt;
|
|
35109
|
+
}
|
|
35110
|
+
});
|
|
34889
35111
|
Object.defineProperty(exports, "record", {
|
|
34890
35112
|
enumerable: true,
|
|
34891
35113
|
get: function() {
|