@camstack/addon-post-analysis 1.2.95 → 1.2.97
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-BdojoP7i.js → dist-BQsMSX2A.js} +29 -3
- package/dist/{dist-C-_TL-I1.mjs → dist-ecY_-nDx.mjs} +29 -3
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CeFPqm3h.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DyXoKV7Z.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-BExsGl4h.mjs → hostInit-CkEakaQn.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +425 -165
- package/dist/pipeline-analytics/index.mjs +425 -165
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as encodeVectorBase64, A as RetrainStatusSchema, At as literal, B as TrackSourceSchema, C as NcSnoozeSchema, Ct as isDeviceScopedCap, D as OpsLogEntrySchema, Dt as array, E as NcTaxonomySchema, Et as _enum, F as SceneMonitorSchema, Ft as string, G as audioMetricsCapability, H as addonWidgetsSourceCapability, I as TIMELAPSE_DENSE_FLOOR_SEC, It as unknown, J as cosineSimilarity$1, K as audioModeOf, L as TimelapseRuleInputSchema, Lt as EventCategory, M as SCENE_DEFAULT_ANCHOR_THRESHOLD, Mt as object, N as SCENE_DEFAULT_UNCOVERED_POLICY, Nt as partialRecord, Ot as boolean, P as SCENE_DIVERGED, Pt as record, R as TimelapseRulePatchSchema, S as NcSnoozeInputSchema, St as hydrateSchema, T as NcSystemEventKindSchema, Tt as sleep, U as alarmPanelCapability, V as VISIT_MERGE_GAP_MS, W as assertTimelapseCadences, X as defineCustomActions, Y as customAction, Z as deriveRecordingMode, _ as NcRuleInputSchema, _t as zoneAnalyticsCapability, at as notificationRulesCapability, b as NcRuleTargetSchema, bt as DeviceType, c as EVENT_PAD_MS, ct as plateGalleryCapability, d as NC_ALARM_SYSTEM_EVENT_KINDS, et as faceGalleryCapability, f as NC_CONDITION_CATALOG, ft as sceneMonitorCapability, g as NcConditionDescriptorSchema, gt as videoclipsCapability, h as NC_TAXONOMY, ht as vectorDimFromBase64, i as DETECTION_MACRO_CLASSES, it as kebabToCamel, jt as number, k as RECORDING_EXPORT_MAX_READ_BYTES, kt as discriminatedUnion, l as LabelAttributionSchema, lt as readDeviceStateFrom, mt as systemEventFilterApplies, n as DEFAULT_EVENT_COLOR, nt as isDetectionMacroClass, o as DeclaredDevices, p as NC_DEFAULT_SNOOZE_MINUTES, pt as subKindsOf, q as buildEventKindDescriptor, rt as isScheduleActive, s as EVENT_KIND_BY_CAP, st as pipelineAnalyticsCapability, t as BaseDevice, u as MACRO_LABELS, ut as readTimelapseGeneratedAt, v as NcRulePatchSchema, vt as errMsg, w as NcSnoozeSuppressedSchema, wt as nodePin, x as NcScheduleSchema, xt as createEvent, y as NcRuleSchema, yt as BaseAddon, z as TimelapseRuleSchema } from "../dist-
|
|
1
|
+
import { $ as encodeVectorBase64, A as RetrainStatusSchema, At as literal, B as TrackSourceSchema, C as NcSnoozeSchema, Ct as isDeviceScopedCap, D as OpsLogEntrySchema, Dt as array, E as NcTaxonomySchema, Et as _enum, F as SceneMonitorSchema, Ft as string, G as audioMetricsCapability, H as addonWidgetsSourceCapability, I as TIMELAPSE_DENSE_FLOOR_SEC, It as unknown, J as cosineSimilarity$1, K as audioModeOf, L as TimelapseRuleInputSchema, Lt as EventCategory, M as SCENE_DEFAULT_ANCHOR_THRESHOLD, Mt as object, N as SCENE_DEFAULT_UNCOVERED_POLICY, Nt as partialRecord, Ot as boolean, P as SCENE_DIVERGED, Pt as record, R as TimelapseRulePatchSchema, S as NcSnoozeInputSchema, St as hydrateSchema, T as NcSystemEventKindSchema, Tt as sleep, U as alarmPanelCapability, V as VISIT_MERGE_GAP_MS, W as assertTimelapseCadences, X as defineCustomActions, Y as customAction, Z as deriveRecordingMode, _ as NcRuleInputSchema, _t as zoneAnalyticsCapability, at as notificationRulesCapability, b as NcRuleTargetSchema, bt as DeviceType, c as EVENT_PAD_MS, ct as plateGalleryCapability, d as NC_ALARM_SYSTEM_EVENT_KINDS, et as faceGalleryCapability, f as NC_CONDITION_CATALOG, ft as sceneMonitorCapability, g as NcConditionDescriptorSchema, gt as videoclipsCapability, h as NC_TAXONOMY, ht as vectorDimFromBase64, i as DETECTION_MACRO_CLASSES, it as kebabToCamel, jt as number, k as RECORDING_EXPORT_MAX_READ_BYTES, kt as discriminatedUnion, l as LabelAttributionSchema, lt as readDeviceStateFrom, mt as systemEventFilterApplies, n as DEFAULT_EVENT_COLOR, nt as isDetectionMacroClass, o as DeclaredDevices, p as NC_DEFAULT_SNOOZE_MINUTES, pt as subKindsOf, q as buildEventKindDescriptor, rt as isScheduleActive, s as EVENT_KIND_BY_CAP, st as pipelineAnalyticsCapability, t as BaseDevice, u as MACRO_LABELS, ut as readTimelapseGeneratedAt, v as NcRulePatchSchema, vt as errMsg, w as NcSnoozeSuppressedSchema, wt as nodePin, x as NcScheduleSchema, xt as createEvent, y as NcRuleSchema, yt as BaseAddon, z as TimelapseRuleSchema } from "../dist-ecY_-nDx.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -5555,24 +5555,39 @@ async function downscaleFullFrameJpeg(jpeg, maxWidth = 640, maxHeight = 360) {
|
|
|
5555
5555
|
}).jpeg({ quality: FULL_FRAME_QUALITY }).toBuffer();
|
|
5556
5556
|
}
|
|
5557
5557
|
//#endregion
|
|
5558
|
-
//#region src/notification-center/
|
|
5558
|
+
//#region src/notification-center/still-shelf.ts
|
|
5559
5559
|
/**
|
|
5560
|
-
* The
|
|
5560
|
+
* The still shelf — the PHOTOGRAPH a notification carries when nothing it can
|
|
5561
|
+
* name owns a frame.
|
|
5561
5562
|
*
|
|
5562
|
-
* ## Why a
|
|
5563
|
+
* ## Why a trigger needs one at all
|
|
5563
5564
|
*
|
|
5564
|
-
* The attachment ladder resolves media by OWNER, and
|
|
5565
|
-
*
|
|
5566
|
-
*
|
|
5565
|
+
* The attachment ladder resolves media by OWNER, and most triggers have one: an
|
|
5566
|
+
* object or package event owns its crops, a closed track owns its best shot, a
|
|
5567
|
+
* doorbell press owns the marker track the same press projected
|
|
5567
5568
|
* (`sensor-marker-projector.ts`), an occupancy edge names one of the objects it
|
|
5568
|
-
* counted (`chooseOccupancyMediaOwner`).
|
|
5569
|
-
*
|
|
5570
|
-
*
|
|
5571
|
-
*
|
|
5569
|
+
* counted (`chooseOccupancyMediaOwner`). Two triggers own nothing, and for the
|
|
5570
|
+
* same reason in both cases — the subject is an ABSENCE:
|
|
5571
|
+
*
|
|
5572
|
+
* - an **audio** match: nothing was boxed, nothing was tracked, and —
|
|
5573
|
+
* deliberately — nothing is persisted at all. A confirmed window is a claim
|
|
5574
|
+
* about sound that has already stopped, and `event-intake.ts` states why
|
|
5575
|
+
* replaying it later would be wrong.
|
|
5576
|
+
* - an **occupancy** edge whose scope is EMPTY — "posto libero". The vehicle
|
|
5577
|
+
* whose departure IS the news has left, so `chooseOccupancyMediaOwner` names
|
|
5578
|
+
* nobody and the ladder logs `no still could be resolved … owners=[]`.
|
|
5572
5579
|
*
|
|
5573
5580
|
* So the only honest picture is a PHOTOGRAPH of the camera taken at the moment
|
|
5574
|
-
* of the
|
|
5575
|
-
*
|
|
5581
|
+
* of the trigger. Not of the sound, not of the object that left — of what the
|
|
5582
|
+
* camera can see now that it happened. For a freed parking space that is
|
|
5583
|
+
* exactly the answer the operator wants: the space, empty.
|
|
5584
|
+
*
|
|
5585
|
+
* **One shelf, not two.** The mechanism is identical down to the reuse window,
|
|
5586
|
+
* and the only thing that differs between the two triggers is the GATE deciding
|
|
5587
|
+
* that a photograph is owed at all — which belongs at the trigger site, where
|
|
5588
|
+
* the rules are, and not here. A twin module would be a second derivation of
|
|
5589
|
+
* one thing. The trigger rides along as {@link NcStillTrigger} only so the logs
|
|
5590
|
+
* can say which absence they are about.
|
|
5576
5591
|
*
|
|
5577
5592
|
* ## Three properties, and each one is a decision
|
|
5578
5593
|
*
|
|
@@ -5580,27 +5595,30 @@ async function downscaleFullFrameJpeg(jpeg, maxWidth = 640, maxHeight = 360) {
|
|
|
5580
5595
|
* TTL that covers the outbox's whole retry horizon — and nowhere else. The
|
|
5581
5596
|
* alternative was the doorbell's: materialise a synthetic marker track through
|
|
5582
5597
|
* `SyntheticTrackMaterializer` and let the notification name it. That would put
|
|
5583
|
-
* a durable Track on the camera's timeline for every confirmed window
|
|
5584
|
-
* the digest's `listTracks`, retention, and the audio-
|
|
5585
|
-
* operator ceilings (`audio-marker-projector.ts` exists
|
|
5586
|
-
* many audio markers a camera may emit). A notification
|
|
5587
|
-
* timeline history as a side effect of wanting a picture.
|
|
5588
|
-
*
|
|
5589
|
-
* **The capture STARTS immediately and is never awaited.**
|
|
5590
|
-
* — a scream is over before a snapshot round-trip completes
|
|
5591
|
-
*
|
|
5592
|
-
*
|
|
5593
|
-
*
|
|
5594
|
-
*
|
|
5595
|
-
*
|
|
5596
|
-
*
|
|
5597
|
-
*
|
|
5598
|
+
* a durable Track on the camera's timeline for every confirmed window and every
|
|
5599
|
+
* emptied zone, feeding the digest's `listTracks`, retention, and the audio-
|
|
5600
|
+
* marker feature's own operator ceilings (`audio-marker-projector.ts` exists
|
|
5601
|
+
* precisely to bound how many audio markers a camera may emit). A notification
|
|
5602
|
+
* must not manufacture timeline history as a side effect of wanting a picture.
|
|
5603
|
+
*
|
|
5604
|
+
* **The capture STARTS immediately and is never awaited.** The subject is
|
|
5605
|
+
* transient — a scream is over before a snapshot round-trip completes, and a
|
|
5606
|
+
* freed space is about to be taken by the next car — so the fetch is kicked off
|
|
5607
|
+
* at the trigger, before the rule evaluation runs, and the owner id is minted
|
|
5608
|
+
* synchronously so the outbox row can name it. The bytes land while the row
|
|
5609
|
+
* waits in the queue, and the dispatcher's existing bounded still-wait (or the
|
|
5610
|
+
* pause its own footage render already costs) picks them up. A camera that
|
|
5611
|
+
* never answers costs the picture and never the notification.
|
|
5612
|
+
*
|
|
5613
|
+
* **Two triggers seconds apart share ONE capture.** A barking dog confirms
|
|
5598
5614
|
* repeatedly and a label-mode rule has no re-arm timer at all (D157) — the
|
|
5599
5615
|
* rule's cooldown is its only brake, and the cooldown is applied AFTER this.
|
|
5600
5616
|
* Without a reuse window this would photograph a camera at whatever rate the
|
|
5601
|
-
* sound happens to occur. Each
|
|
5617
|
+
* sound happens to occur. Each trigger still gets its OWN owner id, so two
|
|
5602
5618
|
* outbox rows are never mistaken for one subject; they merely point at the same
|
|
5603
|
-
* frame, which is the truth — the scene did not change in ten seconds.
|
|
5619
|
+
* frame, which is the truth — the scene did not change in ten seconds. The
|
|
5620
|
+
* window is per CAMERA and trigger-agnostic for the same reason: a sound and an
|
|
5621
|
+
* emptied zone ten seconds apart are two claims about one scene.
|
|
5604
5622
|
*
|
|
5605
5623
|
* Nothing here is silent: a capture that lands and a camera that refuses each
|
|
5606
5624
|
* emit one line carrying `tags: { deviceId }`, because "why did 617 get a photo
|
|
@@ -5608,13 +5626,13 @@ async function downscaleFullFrameJpeg(jpeg, maxWidth = 640, maxHeight = 360) {
|
|
|
5608
5626
|
*/
|
|
5609
5627
|
/**
|
|
5610
5628
|
* The owner-id namespace. It is what routes a lookup here instead of to the
|
|
5611
|
-
* media store, and it is the reason the dispatcher needs no audio
|
|
5612
|
-
* `getMediaForOwner` answers for
|
|
5629
|
+
* media store, and it is the reason the dispatcher needs no audio or occupancy
|
|
5630
|
+
* branch — `getMediaForOwner` answers for all of them under one signature.
|
|
5613
5631
|
*/
|
|
5614
|
-
var
|
|
5632
|
+
var NC_STILL_SHELF_PREFIX = "nc-still:";
|
|
5615
5633
|
/** True for an owner id this shelf minted. */
|
|
5616
|
-
function
|
|
5617
|
-
return id.startsWith(
|
|
5634
|
+
function isStillShelfId(id) {
|
|
5635
|
+
return id.startsWith(NC_STILL_SHELF_PREFIX);
|
|
5618
5636
|
}
|
|
5619
5637
|
/**
|
|
5620
5638
|
* How long the bytes are held.
|
|
@@ -5624,16 +5642,16 @@ function isAudioStillId(id) {
|
|
|
5624
5642
|
* ships text-only, which is the correct degradation for a photograph of a scene
|
|
5625
5643
|
* that is a quarter of an hour stale anyway.
|
|
5626
5644
|
*/
|
|
5627
|
-
var
|
|
5645
|
+
var NC_STILL_SHELF_TTL_MS = 15 * 6e4;
|
|
5628
5646
|
/** A hanging snapshot cap must not pin a capture slot forever. */
|
|
5629
5647
|
var SNAPSHOT_TIMEOUT_MS = 8e3;
|
|
5630
5648
|
/**
|
|
5631
|
-
* Hard bound on held captures. Reached only if every camera on the hub
|
|
5632
|
-
*
|
|
5649
|
+
* Hard bound on held captures. Reached only if every camera on the hub triggers
|
|
5650
|
+
* one inside a single TTL; the oldest is dropped first, which costs a
|
|
5633
5651
|
* fifteen-minute-old picture nobody is waiting for.
|
|
5634
5652
|
*/
|
|
5635
5653
|
var MAX_CAPTURES = 64;
|
|
5636
|
-
var
|
|
5654
|
+
var NcStillShelf = class {
|
|
5637
5655
|
deps;
|
|
5638
5656
|
/** captureId → the photograph. */
|
|
5639
5657
|
captures = /* @__PURE__ */ new Map();
|
|
@@ -5645,14 +5663,14 @@ var NcAudioStillShelf = class {
|
|
|
5645
5663
|
this.deps = deps;
|
|
5646
5664
|
}
|
|
5647
5665
|
/**
|
|
5648
|
-
* Photograph `deviceId` for a
|
|
5649
|
-
* subject should name. SYNCHRONOUS by contract: the caller is on the
|
|
5650
|
-
*
|
|
5666
|
+
* Photograph `deviceId` for a trigger at `atMs`, and return the owner id the
|
|
5667
|
+
* subject should name. SYNCHRONOUS by contract: the caller is on the trigger
|
|
5668
|
+
* path and the outbox row is built from what this returns.
|
|
5651
5669
|
*/
|
|
5652
|
-
capture(deviceId, atMs) {
|
|
5670
|
+
capture(deviceId, atMs, trigger) {
|
|
5653
5671
|
const now = this.deps.now();
|
|
5654
5672
|
this.prune(now);
|
|
5655
|
-
const ownerId = `${
|
|
5673
|
+
const ownerId = `${NC_STILL_SHELF_PREFIX}${randomUUID()}`;
|
|
5656
5674
|
const recent = this.newest.get(deviceId);
|
|
5657
5675
|
if (recent !== void 0 && now - recent.at < 1e4 && this.captures.has(recent.captureId)) {
|
|
5658
5676
|
this.owners.set(ownerId, recent.captureId);
|
|
@@ -5661,8 +5679,9 @@ var NcAudioStillShelf = class {
|
|
|
5661
5679
|
const captureId = randomUUID();
|
|
5662
5680
|
this.captures.set(captureId, {
|
|
5663
5681
|
deviceId,
|
|
5682
|
+
trigger,
|
|
5664
5683
|
startedAt: now,
|
|
5665
|
-
expiresAt: now +
|
|
5684
|
+
expiresAt: now + NC_STILL_SHELF_TTL_MS,
|
|
5666
5685
|
files: []
|
|
5667
5686
|
});
|
|
5668
5687
|
this.newest.set(deviceId, {
|
|
@@ -5670,7 +5689,7 @@ var NcAudioStillShelf = class {
|
|
|
5670
5689
|
at: now
|
|
5671
5690
|
});
|
|
5672
5691
|
this.owners.set(ownerId, captureId);
|
|
5673
|
-
this.fetch(captureId, deviceId, atMs);
|
|
5692
|
+
this.fetch(captureId, deviceId, atMs, trigger);
|
|
5674
5693
|
return ownerId;
|
|
5675
5694
|
}
|
|
5676
5695
|
/**
|
|
@@ -5706,11 +5725,11 @@ var NcAudioStillShelf = class {
|
|
|
5706
5725
|
this.owners.clear();
|
|
5707
5726
|
this.newest.clear();
|
|
5708
5727
|
}
|
|
5709
|
-
async fetch(captureId, deviceId, atMs) {
|
|
5728
|
+
async fetch(captureId, deviceId, atMs, trigger) {
|
|
5710
5729
|
try {
|
|
5711
5730
|
const shot = await withTimeout$3(this.deps.getSnapshot(deviceId), SNAPSHOT_TIMEOUT_MS);
|
|
5712
5731
|
if (shot === null || shot.base64.length === 0) {
|
|
5713
|
-
this.reportMiss(deviceId, "the camera returned no snapshot");
|
|
5732
|
+
this.reportMiss(deviceId, trigger, "the camera returned no snapshot");
|
|
5714
5733
|
return;
|
|
5715
5734
|
}
|
|
5716
5735
|
const raw = Buffer.from(shot.base64, "base64");
|
|
@@ -5722,17 +5741,18 @@ var NcAudioStillShelf = class {
|
|
|
5722
5741
|
if (held === void 0) return;
|
|
5723
5742
|
this.captures.set(captureId, {
|
|
5724
5743
|
...held,
|
|
5725
|
-
files:
|
|
5744
|
+
files: stillShelfMedia(data, atMs)
|
|
5726
5745
|
});
|
|
5727
|
-
this.deps.logger.info(
|
|
5746
|
+
this.deps.logger.info(`${trigger} still captured`, {
|
|
5728
5747
|
tags: { deviceId },
|
|
5729
5748
|
meta: {
|
|
5749
|
+
trigger,
|
|
5730
5750
|
bytes: data.byteLength,
|
|
5731
5751
|
tookMs: this.deps.now() - held.startedAt
|
|
5732
5752
|
}
|
|
5733
5753
|
});
|
|
5734
5754
|
} catch (err) {
|
|
5735
|
-
this.reportMiss(deviceId, err instanceof Error ? err.message : String(err));
|
|
5755
|
+
this.reportMiss(deviceId, trigger, err instanceof Error ? err.message : String(err));
|
|
5736
5756
|
}
|
|
5737
5757
|
}
|
|
5738
5758
|
/**
|
|
@@ -5740,10 +5760,13 @@ var NcAudioStillShelf = class {
|
|
|
5740
5760
|
* on a notification they DID receive, so it is a warn and it carries the
|
|
5741
5761
|
* camera — the only key the question is ever asked with.
|
|
5742
5762
|
*/
|
|
5743
|
-
reportMiss(deviceId, reason) {
|
|
5744
|
-
this.deps.logger.warn(
|
|
5763
|
+
reportMiss(deviceId, trigger, reason) {
|
|
5764
|
+
this.deps.logger.warn(`the camera did not answer the ${trigger} still — this notification ships text-only`, {
|
|
5745
5765
|
tags: { deviceId },
|
|
5746
|
-
meta: {
|
|
5766
|
+
meta: {
|
|
5767
|
+
trigger,
|
|
5768
|
+
reason
|
|
5769
|
+
}
|
|
5747
5770
|
});
|
|
5748
5771
|
}
|
|
5749
5772
|
};
|
|
@@ -5758,10 +5781,10 @@ var NcAudioStillShelf = class {
|
|
|
5758
5781
|
* so. Inventing a centre crop would answer a different question from the one
|
|
5759
5782
|
* the operator asked.
|
|
5760
5783
|
*/
|
|
5761
|
-
function
|
|
5784
|
+
function stillShelfMedia(data, timestamp) {
|
|
5762
5785
|
const base64 = data.toString("base64");
|
|
5763
5786
|
const file = (kind) => ({
|
|
5764
|
-
key: `${
|
|
5787
|
+
key: `${NC_STILL_SHELF_PREFIX}${kind}`,
|
|
5765
5788
|
kind,
|
|
5766
5789
|
base64,
|
|
5767
5790
|
sizeBytes: data.byteLength,
|
|
@@ -7427,7 +7450,7 @@ function subjectFromSensorEvent(ev, markerTrackId) {
|
|
|
7427
7450
|
* (see the {@link evaluateRule} audio gate + the matchers below).
|
|
7428
7451
|
*
|
|
7429
7452
|
* `stillOwnerId` is the audio still shelf's owner id — the photograph of the
|
|
7430
|
-
* camera taken at the match (`
|
|
7453
|
+
* camera taken at the match (`still-shelf.ts`). It rides `trackId` because that
|
|
7431
7454
|
* is the field the attachment ladder reads, exactly as a doorbell press rides
|
|
7432
7455
|
* its marker track's id; nothing else about the subject is track-scoped, and
|
|
7433
7456
|
* `buildEntries` keys an audio row on its record id, never on this. Absent when
|
|
@@ -7519,10 +7542,18 @@ function audioSubjectFor(hit, spec) {
|
|
|
7519
7542
|
* chosen at commit (`chooseOccupancyMediaOwner`) — rides on `trackId`, and that
|
|
7520
7543
|
* is the whole of what gives an occupancy notification a picture: the ordinary
|
|
7521
7544
|
* media-owner ladder resolves it, exactly as it resolves a doorbell's marker
|
|
7522
|
-
* track.
|
|
7523
|
-
*
|
|
7524
|
-
|
|
7525
|
-
|
|
7545
|
+
* track.
|
|
7546
|
+
*
|
|
7547
|
+
* `stillOwnerId` is the SECOND source of that picture and the only one an
|
|
7548
|
+
* emptied scope can have: with no member left to name — "posto libero", where
|
|
7549
|
+
* the object whose departure is the news has gone — the trigger site photographs
|
|
7550
|
+
* the camera and holds the bytes on the still shelf (`still-shelf.ts`), exactly
|
|
7551
|
+
* as an audio match does. The member track always WINS: it is a picture of the
|
|
7552
|
+
* counted object, and a photograph is only ever the stand-in for not having one.
|
|
7553
|
+
* Neither present (no snapshot cap on this node) ⇒ text-only, as before.
|
|
7554
|
+
*/
|
|
7555
|
+
function subjectFromOccupancyEvent(edge, stillOwnerId) {
|
|
7556
|
+
const mediaOwner = edge.mediaTrackId ?? stillOwnerId;
|
|
7526
7557
|
return {
|
|
7527
7558
|
kind: "occupancy-event",
|
|
7528
7559
|
recordId: `occ:${edge.deviceId}:${edge.zoneId ?? "@frame"}:${edge.className ?? "@all"}:t${edge.threshold}:${edge.occupied ? "occ" : "free"}:${edge.timestamp}`,
|
|
@@ -7530,17 +7561,28 @@ function subjectFromOccupancyEvent(edge) {
|
|
|
7530
7561
|
timestamp: edge.timestamp,
|
|
7531
7562
|
classNames: [edge.className ?? "occupancy"],
|
|
7532
7563
|
zones: [],
|
|
7533
|
-
...
|
|
7564
|
+
...mediaOwner !== void 0 ? { trackId: mediaOwner } : {},
|
|
7534
7565
|
source: "pipeline",
|
|
7535
|
-
occupancy:
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7566
|
+
occupancy: occupancySubjectFromEdge(edge)
|
|
7567
|
+
};
|
|
7568
|
+
}
|
|
7569
|
+
/**
|
|
7570
|
+
* The scope + level half of an occupancy subject, ALONE.
|
|
7571
|
+
*
|
|
7572
|
+
* Split out for one reason: the still-shelf gate must ask `matchesOccupancy`
|
|
7573
|
+
* whether a rule would fire on an edge BEFORE the subject that would carry the
|
|
7574
|
+
* answer exists (the owner id it mints is an input to that subject). Sharing
|
|
7575
|
+
* this keeps the gate reading the same numbers the verdict will.
|
|
7576
|
+
*/
|
|
7577
|
+
function occupancySubjectFromEdge(edge) {
|
|
7578
|
+
return {
|
|
7579
|
+
...edge.zoneId !== void 0 ? { zoneId: edge.zoneId } : {},
|
|
7580
|
+
...edge.zoneName !== void 0 ? { zoneName: edge.zoneName } : {},
|
|
7581
|
+
...edge.className !== void 0 ? { className: edge.className } : {},
|
|
7582
|
+
count: edge.count,
|
|
7583
|
+
previousCount: edge.previousCount,
|
|
7584
|
+
occupied: edge.occupied,
|
|
7585
|
+
threshold: edge.threshold
|
|
7544
7586
|
};
|
|
7545
7587
|
}
|
|
7546
7588
|
/** Build the subject for a `package-event` evaluation (a persisted `package`
|
|
@@ -7667,6 +7709,13 @@ function matchesAudio(cond, s) {
|
|
|
7667
7709
|
}
|
|
7668
7710
|
return false;
|
|
7669
7711
|
}
|
|
7712
|
+
/**
|
|
7713
|
+
* Exported for ONE caller: the still-shelf gate in the centre, which must know
|
|
7714
|
+
* — before the outbox row exists — whether any enabled rule would fire on THIS
|
|
7715
|
+
* edge and wants a picture for it. Sharing the predicate is what keeps that gate
|
|
7716
|
+
* from becoming a second, drifting opinion about which edge belongs to which
|
|
7717
|
+
* rule. It is pure and reads nothing.
|
|
7718
|
+
*/
|
|
7670
7719
|
function matchesOccupancy(occ, s) {
|
|
7671
7720
|
if ((occ.zoneId ?? void 0) !== (s.zoneId ?? void 0)) return false;
|
|
7672
7721
|
if ((occ.className ?? void 0) !== (s.className ?? void 0)) return false;
|
|
@@ -9874,7 +9923,7 @@ function incomingFromSensorEvent(event, markerTrackId) {
|
|
|
9874
9923
|
* Audio-event persist (classified episode).
|
|
9875
9924
|
*
|
|
9876
9925
|
* `stillOwnerId` is the audio still shelf's owner id for the photograph taken
|
|
9877
|
-
* at this episode (`
|
|
9926
|
+
* at this episode (`still-shelf.ts`). Absent when no rule that could fire asked
|
|
9878
9927
|
* for a picture — the same shape `incomingFromSensorEvent` carries its marker.
|
|
9879
9928
|
*/
|
|
9880
9929
|
function incomingFromAudioEvent(event, stillOwnerId) {
|
|
@@ -9955,10 +10004,16 @@ function audioHitMeta(hit) {
|
|
|
9955
10004
|
samples: window.samples
|
|
9956
10005
|
};
|
|
9957
10006
|
}
|
|
9958
|
-
/**
|
|
9959
|
-
|
|
10007
|
+
/**
|
|
10008
|
+
* A committed ZoneAnalytics occupancy edge.
|
|
10009
|
+
*
|
|
10010
|
+
* `stillOwnerId` — see {@link subjectFromOccupancyEvent}. An edge whose scope is
|
|
10011
|
+
* empty owns no member track, so the only picture it can carry is a photograph
|
|
10012
|
+
* of its camera taken at the edge, held on the still shelf under this id.
|
|
10013
|
+
*/
|
|
10014
|
+
function incomingFromOccupancyEdge(edge, stillOwnerId) {
|
|
9960
10015
|
return {
|
|
9961
|
-
subject: subjectFromOccupancyEvent(edge),
|
|
10016
|
+
subject: subjectFromOccupancyEvent(edge, stillOwnerId),
|
|
9962
10017
|
kind: "occupancy-event",
|
|
9963
10018
|
origin: "pipeline",
|
|
9964
10019
|
log: () => ({
|
|
@@ -14144,7 +14199,11 @@ function mosaicTextPlan(captions) {
|
|
|
14144
14199
|
};
|
|
14145
14200
|
}
|
|
14146
14201
|
/** Grid shape for `n` tiles: as square as possible, rows never exceeding cols
|
|
14147
|
-
* by more than one — a 3×3 for nine, a 3×2 for five, never a 1×9 strip.
|
|
14202
|
+
* by more than one — a 3×3 for nine, a 3×2 for five, never a 1×9 strip.
|
|
14203
|
+
*
|
|
14204
|
+
* `rows` is the load-bearing half — it is how many rows {@link mosaicRowCounts}
|
|
14205
|
+
* distributes into. `cols` describes the WIDEST row, and is no longer a cell
|
|
14206
|
+
* count: see {@link mosaicRowCounts} for why a `cols × rows` grid was the bug. */
|
|
14148
14207
|
function mosaicGrid(n) {
|
|
14149
14208
|
const count = Math.max(1, Math.floor(n));
|
|
14150
14209
|
const cols = Math.ceil(Math.sqrt(count));
|
|
@@ -14154,31 +14213,84 @@ function mosaicGrid(n) {
|
|
|
14154
14213
|
};
|
|
14155
14214
|
}
|
|
14156
14215
|
/**
|
|
14216
|
+
* How many tiles each ROW carries, top to bottom — and the reason the mosaic
|
|
14217
|
+
* can no longer have a hole in it.
|
|
14218
|
+
*
|
|
14219
|
+
* The layout used to be a `cols × rows` GRID, and a grid holds more cells than
|
|
14220
|
+
* tiles for every count that is not a perfect fit: 3 into a 2×2, 5 into a 3×2,
|
|
14221
|
+
* 7 and 8 into a 3×3. The composite only ever wrote the tiles, so the spare
|
|
14222
|
+
* cells stayed the canvas — `MOSAIC_BACKGROUND`, rgb 17 — and the operator's
|
|
14223
|
+
* three-tile digest was a quarter black. Measured on the shipped renderer:
|
|
14224
|
+
* 24.8% of a three-tile mosaic was background. His words: *"se ci sono 3
|
|
14225
|
+
* elementi non dovrebbe renderizzare buchi neri"*.
|
|
14226
|
+
*
|
|
14227
|
+
* Counting ROWS instead removes the failure by construction rather than by
|
|
14228
|
+
* patching it. Every row spans the full width and divides it among the tiles it
|
|
14229
|
+
* actually has, so a row is full whatever the remainder and there is no such
|
|
14230
|
+
* thing as a spare cell. The shapes fall out of it, and they are the ones asked
|
|
14231
|
+
* for: 1 ⇒ full frame, 2 ⇒ side by side, 3 ⇒ one big over two small, 4 ⇒ 2×2.
|
|
14232
|
+
*
|
|
14233
|
+
* **The fewer-tiled row comes FIRST.** Tiles arrive in the selection's order,
|
|
14234
|
+
* so row 0 is the head of the ranking; putting the remainder at the top would
|
|
14235
|
+
* make the WORST tile the big one.
|
|
14236
|
+
*
|
|
14237
|
+
* Every cell keeps {@link TILE_ASPECT}, so rows differ in height — which is
|
|
14238
|
+
* also what stops the letterbox: a full-width hero tile is 16:9 exactly, not a
|
|
14239
|
+
* 16:9 picture centred in a double-height cell with bars over and under it.
|
|
14240
|
+
*/
|
|
14241
|
+
function mosaicRowCounts(n) {
|
|
14242
|
+
const count = Math.max(1, Math.floor(n));
|
|
14243
|
+
const { rows } = mosaicGrid(count);
|
|
14244
|
+
const base = Math.floor(count / rows);
|
|
14245
|
+
const remainder = count % rows;
|
|
14246
|
+
const counts = [];
|
|
14247
|
+
for (let row = 0; row < rows; row += 1) counts.push(row < rows - remainder ? base : base + 1);
|
|
14248
|
+
return counts;
|
|
14249
|
+
}
|
|
14250
|
+
/**
|
|
14157
14251
|
* Where every tile lands. Pure — no bytes, no sharp, no clock.
|
|
14158
14252
|
*
|
|
14159
14253
|
* Exported because the rectangles are DATA the artefact should carry: a viewer
|
|
14160
14254
|
* that wants a tappable mosaic reads these rather than re-deriving a layout
|
|
14161
14255
|
* that could drift from the one actually rendered.
|
|
14256
|
+
*
|
|
14257
|
+
* The rectangles COVER the picture area exactly — that is the property the
|
|
14258
|
+
* spec asserts, and it is the one the old grid broke.
|
|
14162
14259
|
*/
|
|
14163
14260
|
function mosaicLayout(input) {
|
|
14164
|
-
const
|
|
14165
|
-
const
|
|
14166
|
-
const
|
|
14261
|
+
const rowCounts = mosaicRowCounts(input.tileCount);
|
|
14262
|
+
const width = Math.max(1, Math.floor(input.maxWidth ?? 1920));
|
|
14263
|
+
const firstRowCount = rowCounts[0] ?? 1;
|
|
14264
|
+
const tileWidth = Math.floor(width / firstRowCount);
|
|
14167
14265
|
const tileHeight = Math.round(tileWidth * TILE_ASPECT);
|
|
14168
14266
|
const titleHeight = input.withTitle === true ? Math.round(tileHeight * TITLE_BAND_FRACTION) : 0;
|
|
14169
14267
|
const tiles = [];
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
|
|
14175
|
-
|
|
14176
|
-
|
|
14268
|
+
let y = titleHeight;
|
|
14269
|
+
let index = 0;
|
|
14270
|
+
for (const count of rowCounts) {
|
|
14271
|
+
const nominalWidth = Math.floor(width / count);
|
|
14272
|
+
const rowHeight = Math.round(nominalWidth * TILE_ASPECT);
|
|
14273
|
+
let x = 0;
|
|
14274
|
+
for (let inRow = 0; inRow < count; inRow += 1) {
|
|
14275
|
+
const w = inRow === count - 1 ? width - x : nominalWidth;
|
|
14276
|
+
tiles.push({
|
|
14277
|
+
index,
|
|
14278
|
+
x,
|
|
14279
|
+
y,
|
|
14280
|
+
w,
|
|
14281
|
+
h: rowHeight
|
|
14282
|
+
});
|
|
14283
|
+
x += w;
|
|
14284
|
+
index += 1;
|
|
14285
|
+
}
|
|
14286
|
+
y += rowHeight;
|
|
14287
|
+
}
|
|
14177
14288
|
return {
|
|
14178
|
-
width
|
|
14179
|
-
height:
|
|
14180
|
-
cols,
|
|
14181
|
-
rows,
|
|
14289
|
+
width,
|
|
14290
|
+
height: y,
|
|
14291
|
+
cols: Math.max(...rowCounts),
|
|
14292
|
+
rows: rowCounts.length,
|
|
14293
|
+
rowCounts,
|
|
14182
14294
|
tileWidth,
|
|
14183
14295
|
tileHeight,
|
|
14184
14296
|
titleHeight,
|
|
@@ -14242,36 +14354,20 @@ function titleSvg(width, height, caption) {
|
|
|
14242
14354
|
*
|
|
14243
14355
|
* A tile sharp cannot read is DROPPED and counted, never composited as a blank
|
|
14244
14356
|
* cell — a black square in a digest reads as "the camera saw nothing", which is
|
|
14245
|
-
* the opposite of what happened. The
|
|
14246
|
-
*
|
|
14357
|
+
* the opposite of what happened. The layout is computed from the SURVIVORS, and
|
|
14358
|
+
* because {@link mosaicRowCounts} counts rows rather than grid cells there is no
|
|
14359
|
+
* remainder to leave empty either.
|
|
14247
14360
|
*
|
|
14248
|
-
* @throws
|
|
14249
|
-
* treats
|
|
14361
|
+
* @throws when every tile was dropped, and when sharp cannot produce the final
|
|
14362
|
+
* canvas at all; the caller treats both as "no mosaic" and still
|
|
14363
|
+
* delivers the counts.
|
|
14250
14364
|
*/
|
|
14251
14365
|
async function renderMosaic(request, logger) {
|
|
14252
14366
|
const plan = mosaicTextPlan(request.captions);
|
|
14253
|
-
const
|
|
14254
|
-
const probeLayout = mosaicLayout({
|
|
14255
|
-
tileCount: Math.max(1, request.tiles.length),
|
|
14256
|
-
...request.maxWidth !== void 0 ? { maxWidth: request.maxWidth } : {}
|
|
14257
|
-
});
|
|
14367
|
+
const usable = [];
|
|
14258
14368
|
for (const tile of request.tiles) try {
|
|
14259
|
-
|
|
14260
|
-
|
|
14261
|
-
width: probeLayout.tileWidth,
|
|
14262
|
-
height: probeLayout.tileHeight,
|
|
14263
|
-
fit: "contain",
|
|
14264
|
-
background: MOSAIC_BACKGROUND
|
|
14265
|
-
});
|
|
14266
|
-
const resized = await (label.length === 0 ? fitted : fitted.composite([{
|
|
14267
|
-
input: tileLabelSvg(probeLayout.tileWidth, probeLayout.tileHeight, label),
|
|
14268
|
-
top: 0,
|
|
14269
|
-
left: 0
|
|
14270
|
-
}])).jpeg({ quality: 82 }).toBuffer();
|
|
14271
|
-
decoded.push({
|
|
14272
|
-
tile,
|
|
14273
|
-
resized
|
|
14274
|
-
});
|
|
14369
|
+
await sharp(tile.jpeg).metadata();
|
|
14370
|
+
usable.push(tile);
|
|
14275
14371
|
} catch (err) {
|
|
14276
14372
|
logger.warn("summary mosaic: a tile could not be decoded and was dropped", {
|
|
14277
14373
|
tags: { deviceId: tile.deviceId },
|
|
@@ -14281,23 +14377,34 @@ async function renderMosaic(request, logger) {
|
|
|
14281
14377
|
}
|
|
14282
14378
|
});
|
|
14283
14379
|
}
|
|
14284
|
-
const droppedTiles = request.tiles.length -
|
|
14380
|
+
const droppedTiles = request.tiles.length - usable.length;
|
|
14381
|
+
if (usable.length === 0) throw new Error(`summary mosaic: no tile could be decoded (${request.tiles.length} offered)`);
|
|
14285
14382
|
const caption = plan.titleBand ? request.caption?.trim() ?? "" : "";
|
|
14286
14383
|
const layout = mosaicLayout({
|
|
14287
|
-
tileCount:
|
|
14384
|
+
tileCount: usable.length,
|
|
14288
14385
|
...request.maxWidth !== void 0 ? { maxWidth: request.maxWidth } : {},
|
|
14289
14386
|
withTitle: caption.length > 0
|
|
14290
14387
|
});
|
|
14291
|
-
const composites = await Promise.all(
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14388
|
+
const composites = await Promise.all(usable.map(async (tile, index) => {
|
|
14389
|
+
const rect = layout.tiles[index];
|
|
14390
|
+
if (rect === void 0) throw new Error("summary mosaic: layout lost a tile");
|
|
14391
|
+
const label = plan.tileLabels ? tile.label?.trim() ?? "" : "";
|
|
14392
|
+
const fitted = sharp(tile.jpeg).resize({
|
|
14393
|
+
width: rect.w,
|
|
14394
|
+
height: rect.h,
|
|
14295
14395
|
fit: "contain",
|
|
14296
14396
|
background: MOSAIC_BACKGROUND
|
|
14297
|
-
})
|
|
14298
|
-
|
|
14299
|
-
|
|
14300
|
-
|
|
14397
|
+
});
|
|
14398
|
+
return {
|
|
14399
|
+
input: await (label.length === 0 ? fitted : fitted.composite([{
|
|
14400
|
+
input: tileLabelSvg(rect.w, rect.h, label),
|
|
14401
|
+
top: 0,
|
|
14402
|
+
left: 0
|
|
14403
|
+
}])).jpeg({ quality: 82 }).toBuffer(),
|
|
14404
|
+
top: rect.y,
|
|
14405
|
+
left: rect.x
|
|
14406
|
+
};
|
|
14407
|
+
}));
|
|
14301
14408
|
if (caption.length > 0 && layout.titleHeight > 0) composites.unshift({
|
|
14302
14409
|
input: titleSvg(layout.width, layout.titleHeight, caption),
|
|
14303
14410
|
top: 0,
|
|
@@ -14513,6 +14620,36 @@ function buildSummaryOutboxInputs(input) {
|
|
|
14513
14620
|
* the quota degrades honestly: the best cameras get their one tile, the rest
|
|
14514
14621
|
* are counted as dropped and the count rides the log line — never silently.
|
|
14515
14622
|
*
|
|
14623
|
+
* ## Only tracks the pipeline PROMOTED
|
|
14624
|
+
*
|
|
14625
|
+
* A digest is retrospective. The operator's own framing, verbatim: *"ideale
|
|
14626
|
+
* sarebbe considerare solo tracce che vengono alla fine promosse a tracce, dato
|
|
14627
|
+
* che il summary non è atteso di essere realtime"* — the window is closed
|
|
14628
|
+
* before this function is called, so the selection may wait for the pipeline's
|
|
14629
|
+
* verdict instead of guessing at it.
|
|
14630
|
+
*
|
|
14631
|
+
* The verdict is `SortTracker`'s own promotion threshold: a candidate is not
|
|
14632
|
+
* emitted as a track until it has been matched on `minHits` frames
|
|
14633
|
+
* (`isConfirmedForEmit`, per-device setting, *"minimum consecutive detection
|
|
14634
|
+
* hits before a track is promoted to output"*). The gate here re-applies that
|
|
14635
|
+
* same threshold to what the CLOSED row actually left behind — the count of
|
|
14636
|
+
* recorded sightings on its trajectory — and the port that builds a candidate
|
|
14637
|
+
* is the only thing that knows the device's `minHits`.
|
|
14638
|
+
*
|
|
14639
|
+
* This is not a movement heuristic and deliberately so: `totalDistance` and
|
|
14640
|
+
* `state` were both measured against live traffic and both hide real passages.
|
|
14641
|
+
* What they cannot see is the shape this gate is for — a row whose whole life
|
|
14642
|
+
* is one or two frames. Measured on device 617 (2026-08-19 04:12): two rows,
|
|
14643
|
+
* `closedReason` NULL so the parked pass is blind to them, dwell 53 ms,
|
|
14644
|
+
* `totalDistance` 0, two recorded sightings each. And `minImportance` cannot
|
|
14645
|
+
* cover it either, structurally: the static-track gate that demotes a frozen
|
|
14646
|
+
* subject needs two centroids to measure anything
|
|
14647
|
+
* (`computeStaticTrackMetrics`), so a single-sighting row is exactly the shape
|
|
14648
|
+
* it never scores down — 33 of the live hub's 34 such rows sit at or above 0.2.
|
|
14649
|
+
*
|
|
14650
|
+
* There is no rule field to turn it off. A digest of things that were never
|
|
14651
|
+
* tracks has no audience, and this repo has shipped knobs nobody read.
|
|
14652
|
+
*
|
|
14516
14653
|
* ## Zones are scoped PER CAMERA
|
|
14517
14654
|
*
|
|
14518
14655
|
* `filters.zones` is a flat id list on a rule that spans cameras, and zone ids
|
|
@@ -14536,15 +14673,20 @@ function dwellSeconds(candidate) {
|
|
|
14536
14673
|
* branch that discards work silently reads as "it never happened".
|
|
14537
14674
|
*/
|
|
14538
14675
|
function filterSummaryCandidates(candidates, filters, zoneOwner) {
|
|
14539
|
-
const
|
|
14540
|
-
const
|
|
14676
|
+
const promoted = candidates.filter((candidate) => candidate.confirmed !== false);
|
|
14677
|
+
const droppedUnconfirmed = candidates.length - promoted.length;
|
|
14678
|
+
const unknownConfirmation = promoted.filter((candidate) => candidate.confirmed === void 0).length;
|
|
14679
|
+
const moving = filters?.includeStationary === true ? promoted : promoted.filter((candidate) => candidate.stationary !== true);
|
|
14680
|
+
const droppedStationary = promoted.length - moving.length;
|
|
14541
14681
|
if (filters === void 0) return {
|
|
14542
14682
|
kept: [...moving],
|
|
14543
14683
|
droppedByClass: 0,
|
|
14544
14684
|
droppedByZone: 0,
|
|
14545
14685
|
droppedByImportance: 0,
|
|
14546
14686
|
droppedByDwell: 0,
|
|
14547
|
-
droppedStationary
|
|
14687
|
+
droppedStationary,
|
|
14688
|
+
droppedUnconfirmed,
|
|
14689
|
+
unknownConfirmation
|
|
14548
14690
|
};
|
|
14549
14691
|
const include = filters.classes;
|
|
14550
14692
|
const exclude = new Set(filters.classesExclude ?? []);
|
|
@@ -14590,7 +14732,9 @@ function filterSummaryCandidates(candidates, filters, zoneOwner) {
|
|
|
14590
14732
|
droppedByZone,
|
|
14591
14733
|
droppedByImportance,
|
|
14592
14734
|
droppedByDwell,
|
|
14593
|
-
droppedStationary
|
|
14735
|
+
droppedStationary,
|
|
14736
|
+
droppedUnconfirmed,
|
|
14737
|
+
unknownConfirmation
|
|
14594
14738
|
};
|
|
14595
14739
|
}
|
|
14596
14740
|
/**
|
|
@@ -14966,7 +15110,9 @@ var NcSummaryProducer = class {
|
|
|
14966
15110
|
droppedByZone: filtered.droppedByZone,
|
|
14967
15111
|
droppedByImportance: filtered.droppedByImportance,
|
|
14968
15112
|
droppedByDwell: filtered.droppedByDwell,
|
|
14969
|
-
droppedStationary: filtered.droppedStationary
|
|
15113
|
+
droppedStationary: filtered.droppedStationary,
|
|
15114
|
+
droppedUnconfirmed: filtered.droppedUnconfirmed,
|
|
15115
|
+
unknownConfirmation: filtered.unknownConfirmation
|
|
14970
15116
|
} });
|
|
14971
15117
|
if (opts.stamp) await this.deps.store.markGenerated(rule.id, window.endMs);
|
|
14972
15118
|
return {
|
|
@@ -15030,6 +15176,8 @@ var NcSummaryProducer = class {
|
|
|
15030
15176
|
scanned: candidates.length,
|
|
15031
15177
|
matched: filtered.kept.length,
|
|
15032
15178
|
droppedStationary: filtered.droppedStationary,
|
|
15179
|
+
droppedUnconfirmed: filtered.droppedUnconfirmed,
|
|
15180
|
+
unknownConfirmation: filtered.unknownConfirmation,
|
|
15033
15181
|
droppedByClass: filtered.droppedByClass,
|
|
15034
15182
|
tiles: loaded.length,
|
|
15035
15183
|
cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
|
|
@@ -16072,7 +16220,7 @@ function isSyntheticId(id) {
|
|
|
16072
16220
|
* - and, since 2026-08-15, a `trackId` for both AUDIO kinds: a sound owns no
|
|
16073
16221
|
* frame, so production photographs the camera at the match and holds the
|
|
16074
16222
|
* bytes on the still shelf under an owner id the subject names
|
|
16075
|
-
* (`
|
|
16223
|
+
* (`still-shelf.ts`). The tester's RAM-held frame stands in for exactly that
|
|
16076
16224
|
* photograph, and — unlike every other kind here — it is taken the same way
|
|
16077
16225
|
* production takes it, at the same moment, from the same cap.
|
|
16078
16226
|
*
|
|
@@ -18739,11 +18887,12 @@ var NotificationCenter = class NotificationCenter {
|
|
|
18739
18887
|
*/
|
|
18740
18888
|
syntheticMedia = /* @__PURE__ */ new Map();
|
|
18741
18889
|
/**
|
|
18742
|
-
* The photograph a
|
|
18743
|
-
*
|
|
18744
|
-
*
|
|
18890
|
+
* The photograph a notification carries when the subject it is about is an
|
|
18891
|
+
* ABSENCE — a sound that owns no frame, an emptied occupancy scope that owns
|
|
18892
|
+
* no member. See `still-shelf.ts`. Null when this wiring has no snapshot cap,
|
|
18893
|
+
* which leaves those notifications text-only rather than broken.
|
|
18745
18894
|
*/
|
|
18746
|
-
|
|
18895
|
+
stills;
|
|
18747
18896
|
/** Per-device rate limit for the "matched NO rule" report — see `reportNoMatch`. */
|
|
18748
18897
|
lastNoMatchReportAt = /* @__PURE__ */ new Map();
|
|
18749
18898
|
noMatchSuppressed = /* @__PURE__ */ new Map();
|
|
@@ -19040,9 +19189,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19040
19189
|
this.logger = deps.logger;
|
|
19041
19190
|
this.now = deps.now ?? (() => Date.now());
|
|
19042
19191
|
const getSnapshot = deps.getSnapshot;
|
|
19043
|
-
this.
|
|
19192
|
+
this.stills = getSnapshot !== void 0 ? new NcStillShelf({
|
|
19044
19193
|
getSnapshot,
|
|
19045
|
-
logger: this.logger.child("
|
|
19194
|
+
logger: this.logger.child("still-shelf"),
|
|
19046
19195
|
now: this.now
|
|
19047
19196
|
}) : null;
|
|
19048
19197
|
this.rules = new NcRuleStore({
|
|
@@ -19132,8 +19281,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19132
19281
|
getMediaForOwner: async (ownerKind, ownerId) => {
|
|
19133
19282
|
const held = isSyntheticId(ownerId) ? this.syntheticMedia.get(ownerId) : void 0;
|
|
19134
19283
|
if (held !== void 0) return held.files;
|
|
19135
|
-
const
|
|
19136
|
-
if (
|
|
19284
|
+
const shelved = isStillShelfId(ownerId) ? this.stills?.get(ownerId) : void 0;
|
|
19285
|
+
if (shelved !== void 0) return shelved;
|
|
19137
19286
|
return deps.dispatcher.getMediaForOwner(ownerKind, ownerId);
|
|
19138
19287
|
},
|
|
19139
19288
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
@@ -19339,7 +19488,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19339
19488
|
}
|
|
19340
19489
|
this.timelapseScheduler?.stop();
|
|
19341
19490
|
this.summaryProducer?.stop();
|
|
19342
|
-
this.
|
|
19491
|
+
this.stills?.clear();
|
|
19343
19492
|
this.addonUpdates.dispose();
|
|
19344
19493
|
this.livenessHoldoff.dispose();
|
|
19345
19494
|
this.evaluationActive = false;
|
|
@@ -19691,14 +19840,14 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19691
19840
|
* direction that DESTROYS the media.
|
|
19692
19841
|
*/
|
|
19693
19842
|
audioStillOwner(deviceId, atMs, mode) {
|
|
19694
|
-
const shelf = this.
|
|
19843
|
+
const shelf = this.stills;
|
|
19695
19844
|
if (shelf === null || !this.evaluationActive) return void 0;
|
|
19696
19845
|
for (const rule of this.rules.listEnabled("immediate")) {
|
|
19697
19846
|
if (rule.media.attach === "none") continue;
|
|
19698
19847
|
if (!(mode === "condition" ? rule.conditions.audio !== void 0 : rule.conditions.audio === void 0 && referencesAudioClass(rule.conditions.classes))) continue;
|
|
19699
19848
|
const devices = rule.conditions.devices;
|
|
19700
19849
|
if (devices !== void 0 && devices.length > 0 && !devices.includes(deviceId)) continue;
|
|
19701
|
-
return shelf.capture(deviceId, atMs);
|
|
19850
|
+
return shelf.capture(deviceId, atMs, "audio");
|
|
19702
19851
|
}
|
|
19703
19852
|
}
|
|
19704
19853
|
/**
|
|
@@ -19757,7 +19906,48 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19757
19906
|
* Public so a test / a future out-of-band edge source can drive it directly.
|
|
19758
19907
|
*/
|
|
19759
19908
|
onOccupancyEdge(edge) {
|
|
19760
|
-
this.consumeEvent(incomingFromOccupancyEdge(edge));
|
|
19909
|
+
this.consumeEvent(incomingFromOccupancyEdge(edge, this.occupancyStillOwner(edge)));
|
|
19910
|
+
}
|
|
19911
|
+
/**
|
|
19912
|
+
* Start photographing this edge's camera NOW and return the owner id its
|
|
19913
|
+
* subject should name — or `undefined` when nothing would use it.
|
|
19914
|
+
*
|
|
19915
|
+
* **Only an edge that owns NOBODY.** `chooseOccupancyMediaOwner` names a
|
|
19916
|
+
* member track whenever the scope holds one, and that picture is of the
|
|
19917
|
+
* counted object itself; a photograph is strictly the stand-in for not having
|
|
19918
|
+
* one. The case this exists for is "posto libero" (camera 617): the vehicle
|
|
19919
|
+
* whose departure IS the notification has left, so the scope is empty, the
|
|
19920
|
+
* ladder resolves nothing and the delivery logged `no still could be resolved
|
|
19921
|
+
* … owners=[]`. A became-free edge has no owner BY CONSTRUCTION, and a
|
|
19922
|
+
* became-occupied edge can lack one too (every member parked, i.e.
|
|
19923
|
+
* `stationary:`) — so the gate is the ABSENCE itself, not the polarity. That
|
|
19924
|
+
* is the honest predicate: photograph exactly the edges whose media cannot be
|
|
19925
|
+
* resolved, and no others.
|
|
19926
|
+
*
|
|
19927
|
+
* **The capture is not awaited**, for the audio shelf's reason and one of its
|
|
19928
|
+
* own: a freed space is about to be taken by the next car, and a still fetched
|
|
19929
|
+
* after the outbox drained would show it occupied again.
|
|
19930
|
+
*
|
|
19931
|
+
* **The gate is the RULE's media policy AND the engine's own verdict.** The
|
|
19932
|
+
* watcher commits both polarities of every watched key, so a hub with only a
|
|
19933
|
+
* became-occupied rule still produces free edges — photographing those would
|
|
19934
|
+
* be a snapshot no notification can ever carry. `matchesOccupancy` is the same
|
|
19935
|
+
* predicate `evaluateRule` will apply, shared rather than restated. The rule
|
|
19936
|
+
* cache is an in-memory mirror, so this is not a fallible read (D49).
|
|
19937
|
+
*/
|
|
19938
|
+
occupancyStillOwner(edge) {
|
|
19939
|
+
const shelf = this.stills;
|
|
19940
|
+
if (shelf === null || !this.evaluationActive) return void 0;
|
|
19941
|
+
if (edge.mediaTrackId !== void 0) return void 0;
|
|
19942
|
+
const subject = occupancySubjectFromEdge(edge);
|
|
19943
|
+
for (const rule of this.rules.listEnabled("device-event")) {
|
|
19944
|
+
if (rule.media.attach === "none") continue;
|
|
19945
|
+
const occ = rule.conditions.occupancy;
|
|
19946
|
+
if (occ === void 0 || !matchesOccupancy(occ, subject)) continue;
|
|
19947
|
+
const devices = rule.conditions.devices;
|
|
19948
|
+
if (devices !== void 0 && devices.length > 0 && !devices.includes(edge.deviceId)) continue;
|
|
19949
|
+
return shelf.capture(edge.deviceId, edge.timestamp, "occupancy");
|
|
19950
|
+
}
|
|
19761
19951
|
}
|
|
19762
19952
|
/**
|
|
19763
19953
|
* Feed ONE audio inference frame into the sampling-window matcher.
|
|
@@ -20724,7 +20914,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20724
20914
|
kind,
|
|
20725
20915
|
ruleId: rule.id,
|
|
20726
20916
|
rule: rule.name,
|
|
20727
|
-
snapshotWired: this.
|
|
20917
|
+
snapshotWired: this.stills !== null
|
|
20728
20918
|
}
|
|
20729
20919
|
});
|
|
20730
20920
|
}
|
|
@@ -20889,6 +21079,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20889
21079
|
if (subject.kind === "audio-window" || subject.kind === "audio-event") return { outcome: "disabled" };
|
|
20890
21080
|
const trackId = subject.trackId;
|
|
20891
21081
|
if (trackId === void 0) return { outcome: "disabled" };
|
|
21082
|
+
if (isStillShelfId(trackId)) return { outcome: "disabled" };
|
|
20892
21083
|
const member = {
|
|
20893
21084
|
trackId,
|
|
20894
21085
|
className: subject.classNames[0] ?? "event",
|
|
@@ -22131,6 +22322,16 @@ function planRuleTest(input) {
|
|
|
22131
22322
|
* never be re-owned through a patch.
|
|
22132
22323
|
* - `nc.setRuleTargetEnabled` — a user opting HIS OWN target out of a rule
|
|
22133
22324
|
* VISIBLE to him (his own personal rule OR a global/admin rule).
|
|
22325
|
+
* - the TIMELAPSE and DIGEST families (`nc.*TimelapseRule*`,
|
|
22326
|
+
* `nc.*SummaryRule*`) repeat that model exactly over their own stores. The
|
|
22327
|
+
* digest half was `auth: 'admin'` in v1 and was relaxed once the operator
|
|
22328
|
+
* closed the cost question; `nc.listSummaryRules` reports the current answer
|
|
22329
|
+
* as `canAuthor` so a client talking to an older hub degrades to a read-only
|
|
22330
|
+
* list instead of discovering the refusal as a 403 on a filled-in form.
|
|
22331
|
+
* - the three TEST actions (`nc.injectTestEvent`, `nc.testRule`,
|
|
22332
|
+
* `nc.testTimelapseRule`, `nc.testSummaryRule`) stay `auth: 'admin'`
|
|
22333
|
+
* regardless of who owns the rule: they DELIVER, to every target the rule
|
|
22334
|
+
* addresses, on other people's phones.
|
|
22134
22335
|
*
|
|
22135
22336
|
* Target-ownership limits (personal-rule delivery target list + the opt-out
|
|
22136
22337
|
* target) are enforced against the notifiers target catalog; ADMINS bypass the
|
|
@@ -22349,7 +22550,25 @@ var ncActions = defineCustomActions({
|
|
|
22349
22550
|
auth: "admin",
|
|
22350
22551
|
caller: "required"
|
|
22351
22552
|
}),
|
|
22352
|
-
"nc.listSummaryRules": customAction(object({}), object({
|
|
22553
|
+
"nc.listSummaryRules": customAction(object({}), object({
|
|
22554
|
+
rules: array(NcViewerSummaryRuleSchema),
|
|
22555
|
+
/**
|
|
22556
|
+
* DOES THIS HUB LET THE CALLER AUTHOR ONE? The capability probe, and the
|
|
22557
|
+
* only honest way a client can know.
|
|
22558
|
+
*
|
|
22559
|
+
* `nc.upsertSummaryRule` was `auth: 'admin'` until the operator closed
|
|
22560
|
+
* that decision, and a viewer talking to a hub that still refuses has to
|
|
22561
|
+
* find out BEFORE it offers a "New summary rule" button — otherwise the
|
|
22562
|
+
* only feedback is a 403 taken in front of the operator on a form they
|
|
22563
|
+
* had already filled in. ADDITIVE and therefore `.optional()` on every
|
|
22564
|
+
* client mirror: absent means an older hub, which the viewer reads as "no
|
|
22565
|
+
* authoring" and degrades to a read-only list with a note.
|
|
22566
|
+
*
|
|
22567
|
+
* Derived from the same predicate the handlers enforce — never a constant
|
|
22568
|
+
* `true` — so a hub that re-tightened the auth could not say otherwise.
|
|
22569
|
+
*/
|
|
22570
|
+
canAuthor: boolean()
|
|
22571
|
+
}), { caller: "required" }),
|
|
22353
22572
|
/**
|
|
22354
22573
|
* ONE action for create AND update, like `nc.upsertTimelapseRule`: a digest
|
|
22355
22574
|
* rule is authored as a whole (cameras + window + filters + targets) and the
|
|
@@ -22358,10 +22577,18 @@ var ncActions = defineCustomActions({
|
|
|
22358
22577
|
*
|
|
22359
22578
|
* `ruleId` absent ⇒ create from `rule`; present ⇒ patch.
|
|
22360
22579
|
*
|
|
22361
|
-
* `
|
|
22362
|
-
*
|
|
22363
|
-
*
|
|
22364
|
-
*
|
|
22580
|
+
* `caller: 'required'`, NOT `auth: 'admin'`. It was admin-only in v1 because
|
|
22581
|
+
* a digest spans cameras and consumes the local vision model, and that cost
|
|
22582
|
+
* had not been watched yet. The operator closed the decision, so this now
|
|
22583
|
+
* carries exactly the auth the two rule families beside it carry: ownership
|
|
22584
|
+
* is enforced in the handler, `ownerUserId` is STAMPED from the resolved
|
|
22585
|
+
* caller, a personal rule may deliver only to targets that caller OWNS, and an
|
|
22586
|
+
* admin still authors GLOBAL rules. No new ownership model was invented for
|
|
22587
|
+
* it — `NcSummaryStore` has had `ownerUserId` / `listForOwner` since it was
|
|
22588
|
+
* written and they were simply not reachable.
|
|
22589
|
+
*
|
|
22590
|
+
* `nc.testSummaryRule` stays admin: that one DELIVERS, on other people's
|
|
22591
|
+
* phones.
|
|
22365
22592
|
*/
|
|
22366
22593
|
"nc.upsertSummaryRule": customAction(object({
|
|
22367
22594
|
ruleId: string().optional(),
|
|
@@ -22369,12 +22596,10 @@ var ncActions = defineCustomActions({
|
|
|
22369
22596
|
patch: NcSummaryRulePatchSchema.optional()
|
|
22370
22597
|
}), object({ rule: NcSummaryRuleSchema }), {
|
|
22371
22598
|
kind: "mutation",
|
|
22372
|
-
auth: "admin",
|
|
22373
22599
|
caller: "required"
|
|
22374
22600
|
}),
|
|
22375
22601
|
"nc.deleteSummaryRule": customAction(object({ ruleId: string() }), object({ success: literal(true) }), {
|
|
22376
22602
|
kind: "mutation",
|
|
22377
|
-
auth: "admin",
|
|
22378
22603
|
caller: "required"
|
|
22379
22604
|
}),
|
|
22380
22605
|
/**
|
|
@@ -22468,6 +22693,17 @@ var ncActions = defineCustomActions({
|
|
|
22468
22693
|
* a surface that distinguishes them reports what other people own. */
|
|
22469
22694
|
"nc.getArtifact": customAction(object({ artifactId: string().min(1) }), object({ artifact: NcArtifactRecordSchema.nullable() }), { caller: "required" })
|
|
22470
22695
|
});
|
|
22696
|
+
/**
|
|
22697
|
+
* Does THIS build let a non-admin author a digest rule?
|
|
22698
|
+
*
|
|
22699
|
+
* A named constant rather than a literal `true` at the call site, so the one
|
|
22700
|
+
* fact `nc.listSummaryRules.canAuthor` reports and the auth the upsert actually
|
|
22701
|
+
* carries sit beside each other and are changed together. If the operator ever
|
|
22702
|
+
* re-tightens the action, this goes to `false` in the same edit and the viewer
|
|
22703
|
+
* degrades on the next list — instead of the app discovering it by taking a 403
|
|
22704
|
+
* on a form the operator had already filled in.
|
|
22705
|
+
*/
|
|
22706
|
+
var SUMMARY_AUTHORING_OPEN = true;
|
|
22471
22707
|
/** Fail-closed caller resolution — an absent forwarded caller is NEVER admin. */
|
|
22472
22708
|
function requireCaller$1(caller) {
|
|
22473
22709
|
if (!caller || typeof caller.userId !== "string" || caller.userId.length === 0) throw new Error("forbidden: authenticated caller required");
|
|
@@ -22528,6 +22764,22 @@ function makeNcActionHandlers(deps) {
|
|
|
22528
22764
|
if (editor === void 0) throw new Error("this node does not host the notification text catalog — use the hub");
|
|
22529
22765
|
return editor;
|
|
22530
22766
|
};
|
|
22767
|
+
/**
|
|
22768
|
+
* A digest rule the caller may EDIT. Same shape and same admin bypass as
|
|
22769
|
+
* {@link assertTimelapseEditable} — deliberately, because a second ownership
|
|
22770
|
+
* model for the third rule family is a second place to get it wrong.
|
|
22771
|
+
*
|
|
22772
|
+
* A GLOBAL rule (no `ownerUserId`, what an admin authors) is VISIBLE to
|
|
22773
|
+
* everyone and editable by admins only: `listForOwner` returns it, so a user
|
|
22774
|
+
* sees it in the list, and `readOnly` says so.
|
|
22775
|
+
*/
|
|
22776
|
+
const assertSummaryEditable = (ruleId, caller, sum) => {
|
|
22777
|
+
const rule = sum.store.get(ruleId);
|
|
22778
|
+
if (rule === null) throw new Error(`forbidden: summary rule not found: ${ruleId}`);
|
|
22779
|
+
if (caller.isAdmin) return rule;
|
|
22780
|
+
if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: summary rule not owned: ${ruleId}`);
|
|
22781
|
+
return rule;
|
|
22782
|
+
};
|
|
22531
22783
|
/** A timelapse rule the caller may EDIT. Same admin bypass as `NcRule`. */
|
|
22532
22784
|
const assertTimelapseEditable = (ruleId, caller, tl) => {
|
|
22533
22785
|
const rule = tl.store.get(ruleId);
|
|
@@ -22798,21 +23050,23 @@ function makeNcActionHandlers(deps) {
|
|
|
22798
23050
|
"nc.listSummaryRules": async (_input, caller) => {
|
|
22799
23051
|
const c = requireCaller$1(caller);
|
|
22800
23052
|
const sum = requireSummary();
|
|
22801
|
-
return {
|
|
22802
|
-
|
|
22803
|
-
|
|
22804
|
-
|
|
23053
|
+
return {
|
|
23054
|
+
rules: (c.isAdmin ? sum.store.list() : sum.store.listForOwner(c.userId)).map((r) => ({
|
|
23055
|
+
...r,
|
|
23056
|
+
readOnly: !c.isAdmin && r.ownerUserId !== c.userId
|
|
23057
|
+
})),
|
|
23058
|
+
canAuthor: SUMMARY_AUTHORING_OPEN
|
|
23059
|
+
};
|
|
22805
23060
|
},
|
|
22806
23061
|
"nc.upsertSummaryRule": async (input, caller) => {
|
|
22807
23062
|
const c = requireCaller$1(caller);
|
|
22808
|
-
if (!c.isAdmin) throw new Error("forbidden: admin required to author a summary rule");
|
|
22809
23063
|
const sum = requireSummary();
|
|
22810
23064
|
if (input.ruleId === void 0) {
|
|
22811
23065
|
if (input.rule === void 0) throw new Error("upsertSummaryRule: rule or ruleId required");
|
|
22812
23066
|
const parsed = NcSummaryRuleInputSchema.parse(input.rule);
|
|
22813
23067
|
assertSummaryBudget(parsed);
|
|
22814
23068
|
await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
|
|
22815
|
-
const rule = await sum.store.create(parsed, c.userId);
|
|
23069
|
+
const rule = await sum.store.create(parsed, c.userId, c.isAdmin ? void 0 : c.userId);
|
|
22816
23070
|
deps.logger.info("summary rule created", { meta: {
|
|
22817
23071
|
ruleId: rule.id,
|
|
22818
23072
|
owner: c.userId,
|
|
@@ -22820,8 +23074,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22820
23074
|
} });
|
|
22821
23075
|
return { rule };
|
|
22822
23076
|
}
|
|
22823
|
-
const existing =
|
|
22824
|
-
if (existing === null) throw new Error(`forbidden: summary rule not found: ${input.ruleId}`);
|
|
23077
|
+
const existing = assertSummaryEditable(input.ruleId, c, sum);
|
|
22825
23078
|
if (input.patch === void 0) throw new Error("upsertSummaryRule: patch required");
|
|
22826
23079
|
const patch = NcSummaryRulePatchSchema.parse(input.patch);
|
|
22827
23080
|
assertSummaryBudget({
|
|
@@ -22838,8 +23091,9 @@ function makeNcActionHandlers(deps) {
|
|
|
22838
23091
|
},
|
|
22839
23092
|
"nc.deleteSummaryRule": async (input, caller) => {
|
|
22840
23093
|
const c = requireCaller$1(caller);
|
|
22841
|
-
|
|
22842
|
-
|
|
23094
|
+
const sum = requireSummary();
|
|
23095
|
+
assertSummaryEditable(input.ruleId, c, sum);
|
|
23096
|
+
await sum.store.delete(input.ruleId);
|
|
22843
23097
|
deps.logger.info("summary rule deleted", { meta: {
|
|
22844
23098
|
ruleId: input.ruleId,
|
|
22845
23099
|
owner: c.userId
|
|
@@ -44118,7 +44372,9 @@ var TrackStore = class {
|
|
|
44118
44372
|
*/
|
|
44119
44373
|
rowToTrack(id, data, projection) {
|
|
44120
44374
|
const slim = projection === "slim";
|
|
44121
|
-
const
|
|
44375
|
+
const storedPositions = data["positions"] ?? [];
|
|
44376
|
+
const observations = storedPositions.length;
|
|
44377
|
+
const positions = slim ? [] : storedPositions;
|
|
44122
44378
|
const snapshots = slim ? [] : data["snapshots"] ?? [];
|
|
44123
44379
|
const zones = data["zonesVisited"] ?? [];
|
|
44124
44380
|
const classes = data["classes"];
|
|
@@ -44175,6 +44431,7 @@ var TrackStore = class {
|
|
|
44175
44431
|
...typeof hasFace === "boolean" ? { hasFace } : {},
|
|
44176
44432
|
...typeof hasRider === "boolean" ? { hasRider } : {},
|
|
44177
44433
|
...closedReason !== void 0 ? { closedReason } : {},
|
|
44434
|
+
observations,
|
|
44178
44435
|
...envelope !== null ? { envelope } : {}
|
|
44179
44436
|
};
|
|
44180
44437
|
}
|
|
@@ -52484,13 +52741,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
52484
52741
|
buildSummaryPorts(stores) {
|
|
52485
52742
|
return {
|
|
52486
52743
|
listTracks: async ({ deviceId, sinceMs, untilMs, limit }) => {
|
|
52487
|
-
|
|
52744
|
+
const tracks = await stores.trackStore.queryHistorical({
|
|
52488
52745
|
deviceId,
|
|
52489
52746
|
since: sinceMs,
|
|
52490
52747
|
until: untilMs,
|
|
52491
52748
|
projection: "slim",
|
|
52492
52749
|
limit
|
|
52493
|
-
})
|
|
52750
|
+
});
|
|
52751
|
+
const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
|
|
52752
|
+
return tracks.map((t) => ({
|
|
52494
52753
|
trackId: t.trackId,
|
|
52495
52754
|
deviceId: t.deviceId,
|
|
52496
52755
|
className: t.className,
|
|
@@ -52499,7 +52758,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
52499
52758
|
...t.importance !== void 0 ? { importance: t.importance } : {},
|
|
52500
52759
|
zones: [...t.zonesVisited],
|
|
52501
52760
|
...t.classes !== void 0 ? { classes: [...t.classes] } : {},
|
|
52502
|
-
...t.closedReason === "stationary-promotion" ? { stationary: true } : {}
|
|
52761
|
+
...t.closedReason === "stationary-promotion" ? { stationary: true } : {},
|
|
52762
|
+
...t.observations !== void 0 ? { confirmed: t.observations >= minHits } : {}
|
|
52503
52763
|
}));
|
|
52504
52764
|
},
|
|
52505
52765
|
listMediaInfo: async (trackId) => {
|