@camstack/addon-post-analysis 1.2.611 → 1.2.613
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/{clip-model-registry-D1PS6-db.js → clip-model-registry-CM-m6e1l.js} +575 -41
- package/dist/{clip-model-registry-3TQUwS_R.mjs → clip-model-registry-D6tdLEFB.mjs} +558 -42
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +559 -1180
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D_6mbip_.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-PyXxg2Wg.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-mydSlNUz.mjs +26 -0
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BB2kgLY1.mjs +26 -0
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.js-C0AuF9av.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.js-Cx9AsA5q.mjs} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-Bm-iyjmq.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-CkwmHEi0.mjs} +8 -8
- package/dist/pipeline-analytics/{hostInit-Tp2cEslm.mjs → hostInit-RCMuxWxC.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +1298 -359
- package/dist/pipeline-analytics/index.mjs +1298 -359
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BX7AfZj6.mjs +0 -26
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DZcBAOjR.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as MediaFileKindEnum, $i as
|
|
1
|
+
import { $ as MediaFileKindEnum, $i as sleep$1, $n as coreBlockIdFromAddonId, $r as readDeviceStateFrom, $t as NcTemplateVarDescriptorSchema, A as DeclaredDevices, Ai as templateCarriesSummary, An as TrackSideSchema, Ar as isEventOwnerType, At as NcAlarmSensorsInputSchema, B as FULL_IMAGE_BBOX, Bi as errMsg, Bn as applyModeDelaysEdits, Br as kebabToCamel, Bt as NcRuleSchema, C as ClearTrackRelationResultSchema, Ci as summarizeReview, Cn as TRACK_RELATIONS_DEFAULT_LIMIT, Cr as isAlertEpisodeOpen, Ct as NC_SNOOZE_MAX_MINUTES, D as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Di as systemEventFilterApplies, Dn as TrackPuritySchema, Dr as isDefaultAlarmProfileId, Dt as NC_TEMPLATE_VARS, E as DEFAULT_EVENT_COLOR, Ei as systemEventFamilyProblems, En as TimelapseRuleSchema, Er as isAudioClassName, Et as NC_TAXONOMY, F as EVENT_DENSITY_BATCH_MAX, Fi as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Fn as alarmPanelCapability, Fr as isRootSuppliableStepId, Ft as NcLegacyRuleActionSequenceSchema, G as ListTrackRelationsResultSchema, Gi as createEvent, Gn as audioMetricsCapability, Gr as normalizeNcMediaPolicy, Gt as NcSnoozeInputSchema, H as LABEL_SUPPLY_MACRO, Hi as CamProfileSchema, Hn as applySecureOnArmEdit, Hr as mayWriteToLocation, Ht as NcRuleUpdateInputSchema, I as EVENT_KIND_BY_CAP, Ii as canonicalHash, In as alertEpisodeId, Ir as isScheduleActive, It as NcRuleActionSchema, J as MAX_BIRTH_DECISION_RECORDS, Ji as defineRecordKinds, Jn as buildViewerLink, Jr as parseAlertEpisodePayload, Jt as NcStateSummarySectionIdSchema, K as LockStateSchema, Ki as customAction, Kn as audioModeOf, Kr as notificationRulesCapability, Kt as NcSnoozeSchema, L as EVENT_OWNER_TYPES, Li as evaluateSensorEdge, Ln as alertEpisodeKind, Lr as isSystemDelivery, Lt as NcRuleActionsSchema, M as EMBEDDING_MODEL_FAILED, Mi as vectorDimFromBase64, Mn as activateAlarmProfile, Mr as isLabelSupplyStepId, Mt as NcAlarmSettingsPatchSchema, N as EMBEDDING_REQUIREMENTS_FAILED, Ni as viewerLinkBaseOf, Nn as addonWidgetsSourceCapability, Nr as isMediaOwnerType, Nt as NcConditionDescriptorSchema, O as DETECTION_MACRO_CLASSES, Oi as systemEventSummaryScheduleOf, On as TrackRelationKindSchema, Or as isDetectionMacroClass, Ot as NcAlarmConfigSchema, P as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Pi as zoneAnalyticsCapability, Pn as alarmDeviceSecureOnArm, Pr as isRetiredClearKey, Pt as NcConditionsSchema, Q as MOTION_CLOSE_AFTER_MS, Qn as consumableLowPercentOf, Qr as plateGalleryCapability, Qt as NcTemplatePreviewSchema, R as EXPORT_DENSE_MAX_RANGES, Ri as isSourceCap, Rn as alertEpisodePayload, Rr as isSystemEventSummaryRule, Rt as NcRuleInputSchema, S as ClearTrackRelationInputSchema, Si as subKindsOf, Sn as TIMELAPSE_DENSE_FLOOR_SEC, Sr as isAlarmControllableCap, St as NC_SENSOR_TRANSITION_KINDS, T as DEFAULT_DETAIL_CROP_CONVENTION, Ti as systemEventDeliveryProblems, Tn as TimelapseRulePatchSchema, Tr as isAppLinkBase, Tt as NC_TANK_SYSTEM_EVENT_KINDS, U as LabelAttributionSchema, Ui as RECORD_MAX_DATA_BYTES, Un as armRemovalReconcile, Ur as memoizePinnedFfmpeg, Ut as NcScheduleSchema, V as FailureCounters, Vi as BaseAddon, Vn as applyNonBlockingEdit, Vr as linkedStepOf, Vt as NcRuleTargetSchema, W as ListTrackRelationsInputSchema, Wi as RECORD_QUERY_MAX, Wn as assertTimelapseCadences, Wr as ncMediaPolicyError, Wt as NcSharedSequenceSchema, X as MAX_TRACK_PURITIES, Xi as isDeviceScopedCap, Xn as classifyRetrainReview, Xr as pickFaceRescoreIntervalMinutes, Xt as NcTaxonomySchema, Y as MAX_BIRTH_LATENCY_PROXY_MS, Yi as hydrateSchema, Yn as calendarEventLocalSpan, Yt as NcSystemEventKindSchema, Z as MAX_TRACK_RELATIONS, Zi as nodePin, Zn as cloneAlarmSettings, Zr as pipelineAnalyticsCapability, Zt as NcTemplatePreviewInputSchema, _ as BIRTH_DECISION_DEFAULT_LIMIT, _a as EventCategory, _i as secureActuationFor, _n as SCENE_RESET_RECAPTURES, _r as evictionPolicyOfLocation, _t as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, aa as literal, ai as resolveModeDelays, an as RebuildObjectEmbeddingsRefusalSchema, ar as deriveRecordingMode, at as NC_ALARM_NOTIFY_FIELDS, b as BucketAccumulator, bi as startMoverUnderClearance, bn as SetTrackPurityResultSchema, br as gifDeriveSpeed, bt as NC_GROUPING_MAX_SEC, c as readClusterModelPinStrict, ca as object, cn as RetrainStatusSchema, cr as deviceRemovalHookCapability, ct as NC_AUDIO_CONFIRM_HITS_DEFAULT, d as APPEARANCE_BLOCKS, da as strictObject, di as ruleAutoTriggersAlarm, dn as SCENE_DEFAULT_ANCHOR_THRESHOLD, dr as embeddingRefusalOf, dt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, ea as ZodObject, ei as readMotionTriggeredBy, en as OPS_LOG_DEFAULT_LIMIT, er as cosineSimilarity$1, et as MotionTriggeredBySchema, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fa as string, fi as ruleBelongsToAlarm$1, fn as SCENE_DEFAULT_CHECK_INTERVAL_SEC, ft as NC_CLIP_RETENTION_MAX_SEC, g as ArchivedDebugNoteSchema, ga as unknown, gi as sceneMonitorCapability, gn as SCENE_DIVERGED, gr as eventKindLabel, gt as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, h as AlarmArmModeSchema, ha as union, hi as ruleKindOf, hn as SCENE_DEFAULT_UNCOVERED_POLICY, hr as ensureAlarmProfiles, ht as NC_CONFIRM_DEFAULT_TIMEOUT_MS, ia as discriminatedUnion, ii as resolveLocationMode, in as RECORDING_SOURCE_CAMSTACK_ADDON, ir as derivePostTriggeredAction, it as NC_ALARM_DELAY_MAX_SEC, j as DetectionSourceSchema, ji as templateVarsFor, jn as TrackSourceSchema, jr as isKnownTemplateVar, jt as NcAlarmSensorsSchema, k as DETECTION_PIPELINE_CAP_NAME, ki as systemEventSummarySplitOf, kn as TrackRelationSchema, kr as isEventKindLabelKey, kt as NcAlarmPostTriggeredActionSchema, l as resolveClusterModelPin, la as partialRecord, li as retrainRevisionConflictMessage, ln as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, lt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, m as AUDIO_MACRO_LABELS, ma as tuple, mi as ruleGroupingApplies, mn as SCENE_DEFAULT_QUIET_SECONDS, mr as encodeVectorBase64, mt as NC_CONFIRM_DEFAULT_MAX_IMAGE_PX, na as array, ni as renameAlarmProfile, nr as deleteAlarmProfile, nt as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, o as CLUSTER_MODEL_OWNER_ADDON_ID, oa as looseObject, oi as resolveModeNotifyPolicy, on as RetrainDismissedSchema, or as describeSystemEventDeliveryProblem, ot as NC_ALARM_SYSTEM_EVENT_KINDS, p as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, pa as templateLiteral, pi as ruleDeviceCompatCapQueries, pn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, pt as NC_CONDITION_CATALOG, q as MAX_ARCHIVED_DEBUG_NOTES, qi as defineCustomActions, qn as buildEventKindDescriptor, qr as occupancyScope, qt as NcSnoozeSuppressedSchema, ra as boolean, ri as resolveClipSearchMinScore, rn as RECORDING_EXPORT_MAX_READ_BYTES, rr as deriveDetailCropRect, rt as NC_ALARM_DELAY_FIELDS, s as FACE_EMBEDDING_STEP_ID, sa as number, si as resolveNcMediaImage, sn as RetrainOriginSchema, sr as describeSystemEventFamilyProblem, st as NC_ALARM_TRIGGERED_MAX_SEC, t as BUILTIN_CLIP_MODELS, ta as _enum, ti as readTimelapseGeneratedAt, tn as OpsLogEntrySchema, tr as createAlarmProfile, tt as NC_ALARM_DEFAULT_PROFILE_ID, u as APPEARANCE_BIN_COUNT, ua as record, ui as rootSupplyServes, un as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, ut as NC_CLIP_POST_ROLL_DEFAULT_SEC, v as BaseDevice, vi as sensorRaisedOf, vn as SceneMonitorSchema, vr as faceGalleryCapability, vt as NC_DEFAULT_SNOOZE_MINUTES, w as CoverStateSchema, wi as syncActiveAlarmProfileSettings, wn as TimelapseRuleInputSchema, wr as isAlertEpisodeSeverity, wt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, x as COCO_TO_MACRO, xi as storageOccupancyCapability, xn as SetTrackRelationInputSchema, xr as gifFromMp4Args, xt as NC_GROUPING_MIN_SEC, y as BirthDecisionRecordSchema, yi as servedTemplateVars, yn as SetTrackPurityInputSchema, yr as failureContributionCapability, yt as NC_GROUPING_DEFAULT_SEC, z as FIRST_LEVEL_MACRO_CLASSES, zi as DeviceType, zn as applyBlockingEdit, zr as isVectorIndexDimConflict, zt as NcRulePatchSchema } from "../clip-model-registry-D6tdLEFB.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs$1 from "node:fs";
|
|
4
4
|
import { createWriteStream, promises } from "node:fs";
|
|
@@ -928,6 +928,7 @@ function actionTtlMs(subject) {
|
|
|
928
928
|
case "snooze": return SNOOZE_ACTION_TTL_MS;
|
|
929
929
|
case "alarm":
|
|
930
930
|
if (subject.action === "arm-anyway") return ARM_ANYWAY_TTL_MS;
|
|
931
|
+
if (subject.action === "bypass-input") return DEFAULT_ACTION_TTL_MS;
|
|
931
932
|
return subject.action === "disarm" ? ALARM_DISARM_TTL_MS : DEFAULT_ACTION_TTL_MS;
|
|
932
933
|
case "sequence": return DEFAULT_ACTION_TTL_MS;
|
|
933
934
|
case "addon-update": return DEFAULT_ACTION_TTL_MS;
|
|
@@ -1012,6 +1013,7 @@ function describeGrant(grant) {
|
|
|
1012
1013
|
case "snooze": return grant.classes === void 0 || grant.classes.length === 0 ? `Silenced for ${grant.minutes} min` : `Silenced for ${grant.minutes} min — ${grant.classes.join(", ")}`;
|
|
1013
1014
|
case "alarm":
|
|
1014
1015
|
if (grant.action === "arm-anyway") return "Alarm armed, around the open devices";
|
|
1016
|
+
if (grant.action === "bypass-input") return `Input bypassed until disarm — ${grant.ruleName}`;
|
|
1015
1017
|
return grant.action === "disarm" ? "Alarm disarmed" : `Alarm armed${grant.mode !== void 0 ? ` (${grant.mode})` : ""}`;
|
|
1016
1018
|
case "addon-update": return `${grant.packages.length} addon update(s) on ${grant.nodeId}`;
|
|
1017
1019
|
case "server-update": return `Server update on ${grant.nodeId}`;
|
|
@@ -1039,17 +1041,26 @@ function grantMeta(grant) {
|
|
|
1039
1041
|
minutes: grant.minutes,
|
|
1040
1042
|
classes: grant.classes ?? null
|
|
1041
1043
|
};
|
|
1042
|
-
case "alarm":
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1044
|
+
case "alarm":
|
|
1045
|
+
if (grant.action === "bypass-input") return {
|
|
1046
|
+
...common,
|
|
1047
|
+
alarm: grant.action,
|
|
1048
|
+
ruleId: grant.ruleId,
|
|
1049
|
+
rule: grant.ruleName,
|
|
1050
|
+
inputDeviceId: grant.inputDeviceId,
|
|
1051
|
+
panelDeviceId: grant.panelDeviceId
|
|
1052
|
+
};
|
|
1053
|
+
return grant.action === "arm-anyway" ? {
|
|
1054
|
+
...common,
|
|
1055
|
+
alarm: grant.action,
|
|
1056
|
+
refusalId: grant.refusalId,
|
|
1057
|
+
panelDeviceId: grant.panelDeviceId
|
|
1058
|
+
} : {
|
|
1059
|
+
...common,
|
|
1060
|
+
alarm: grant.action,
|
|
1061
|
+
mode: grant.mode ?? null,
|
|
1062
|
+
panelDeviceId: grant.panelDeviceId
|
|
1063
|
+
};
|
|
1053
1064
|
case "addon-update": return {
|
|
1054
1065
|
...common,
|
|
1055
1066
|
nodeId: grant.nodeId,
|
|
@@ -1573,6 +1584,128 @@ var NcAlarmExclusions = class {
|
|
|
1573
1584
|
}
|
|
1574
1585
|
};
|
|
1575
1586
|
//#endregion
|
|
1587
|
+
//#region src/notification-center/alarm/alarm-input-bypasses.ts
|
|
1588
|
+
/**
|
|
1589
|
+
* The alarm INPUTS the operator silenced for the rest of this arm — "Bypass
|
|
1590
|
+
* this input until disarm".
|
|
1591
|
+
*
|
|
1592
|
+
* An input is ONE (rule, device) pair: the rule gated on `armed_*` and the
|
|
1593
|
+
* device whose match pressed the panel (the sensor when the match was about
|
|
1594
|
+
* one, else the camera — `intrusionDeviceOf`). A false positive that keeps
|
|
1595
|
+
* re-triggering the alarm is exactly one such pair; bypassing it leaves every
|
|
1596
|
+
* other device of the same rule, and the same device under every other rule,
|
|
1597
|
+
* armed.
|
|
1598
|
+
*
|
|
1599
|
+
* Not an exclusion (`alarm-exclusions.ts`): an exclusion is an opening that was
|
|
1600
|
+
* open at ARM time and lifts on its own two-phase edge. A bypass never lifts by
|
|
1601
|
+
* itself. It belongs to the arm SESSION and ends with it — on a disarm (any
|
|
1602
|
+
* path: the operator, the bridge, the cap, a `disableAfter` auto-disarm) and on
|
|
1603
|
+
* a new arm. A bounded siren that RE-ARMS itself is the same session, and the
|
|
1604
|
+
* bypass survives it — that re-trigger is the thing it exists to stop.
|
|
1605
|
+
*
|
|
1606
|
+
* Persisted with the panel's machine, in the panel's own device config, for the
|
|
1607
|
+
* exclusions' reason: a deploy in the middle of the night must not quietly hand
|
|
1608
|
+
* the false positive back its siren. No second store (D62): this is state of
|
|
1609
|
+
* the arm, and it lives where the arm lives.
|
|
1610
|
+
*/
|
|
1611
|
+
/** At most this many bypassed inputs per arm — a bound on the persisted blob, not a target. */
|
|
1612
|
+
var NC_ALARM_INPUT_BYPASS_MAX = 200;
|
|
1613
|
+
/** One bypassed input, as persisted. Read back through {@link NcAlarmInputBypasses.restore}. */
|
|
1614
|
+
var NcAlarmInputBypassRecordSchema = object({
|
|
1615
|
+
ruleId: string().min(1),
|
|
1616
|
+
/** The rule's name when it was bypassed — a fallback for the view; the mirror's name wins. */
|
|
1617
|
+
ruleName: string(),
|
|
1618
|
+
/** The INPUT: the sensor of the match, else its camera (`intrusionDeviceOf`). */
|
|
1619
|
+
deviceId: number().int(),
|
|
1620
|
+
/** The match's camera — kept so a rule edit that drops the camera can be recognised. */
|
|
1621
|
+
cameraId: number().int(),
|
|
1622
|
+
/** When the bypass was made. */
|
|
1623
|
+
at: number(),
|
|
1624
|
+
/** Presses dropped since — the count the status shows. */
|
|
1625
|
+
dropped: number().int().min(0)
|
|
1626
|
+
});
|
|
1627
|
+
var NcAlarmInputBypasses = class {
|
|
1628
|
+
entries = [];
|
|
1629
|
+
/** Bypass one input. Idempotent: the same pair twice is one bypass. */
|
|
1630
|
+
add(request, at) {
|
|
1631
|
+
if (this.find(request.ruleId, request.deviceId) !== void 0) return "already-bypassed";
|
|
1632
|
+
if (this.entries.length >= NC_ALARM_INPUT_BYPASS_MAX) return "full";
|
|
1633
|
+
this.entries = [...this.entries, {
|
|
1634
|
+
ruleId: request.ruleId,
|
|
1635
|
+
ruleName: request.ruleName,
|
|
1636
|
+
deviceId: request.deviceId,
|
|
1637
|
+
cameraId: request.cameraId,
|
|
1638
|
+
at,
|
|
1639
|
+
dropped: 0
|
|
1640
|
+
}];
|
|
1641
|
+
return "bypassed";
|
|
1642
|
+
}
|
|
1643
|
+
/** True while this (rule, input) pair must not press the panel. */
|
|
1644
|
+
blocks(ruleId, deviceId) {
|
|
1645
|
+
return this.find(ruleId, deviceId) !== void 0;
|
|
1646
|
+
}
|
|
1647
|
+
/** Count one dropped press of a bypassed input; returns the new count, or null when not bypassed. */
|
|
1648
|
+
recordDrop(ruleId, deviceId) {
|
|
1649
|
+
const current = this.find(ruleId, deviceId);
|
|
1650
|
+
if (current === void 0) return null;
|
|
1651
|
+
const dropped = current.dropped + 1;
|
|
1652
|
+
this.entries = this.entries.map((e) => e === current ? {
|
|
1653
|
+
...e,
|
|
1654
|
+
dropped
|
|
1655
|
+
} : e);
|
|
1656
|
+
return dropped;
|
|
1657
|
+
}
|
|
1658
|
+
/** Every bypassed input, in the order it was bypassed. */
|
|
1659
|
+
snapshot() {
|
|
1660
|
+
return this.entries;
|
|
1661
|
+
}
|
|
1662
|
+
get size() {
|
|
1663
|
+
return this.entries.length;
|
|
1664
|
+
}
|
|
1665
|
+
/** Drop every bypass — the arm session ended. Returns what was dropped. */
|
|
1666
|
+
clear() {
|
|
1667
|
+
const dropped = this.entries;
|
|
1668
|
+
this.entries = [];
|
|
1669
|
+
return dropped;
|
|
1670
|
+
}
|
|
1671
|
+
/** Drop the bypasses `keep` refuses (a rule deleted, disabled or no longer naming the input). */
|
|
1672
|
+
prune(keep) {
|
|
1673
|
+
const removed = this.entries.filter((e) => !keep(e));
|
|
1674
|
+
if (removed.length > 0) this.entries = this.entries.filter((e) => keep(e));
|
|
1675
|
+
return removed;
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Rebuild from the persisted blob — defensively, per entry: an entry a later
|
|
1679
|
+
* release wrote in another shape is dropped, never the whole list. Returns
|
|
1680
|
+
* how many entries were dropped so the caller can say so.
|
|
1681
|
+
*/
|
|
1682
|
+
restore(raw) {
|
|
1683
|
+
if (!Array.isArray(raw)) {
|
|
1684
|
+
this.entries = [];
|
|
1685
|
+
return 0;
|
|
1686
|
+
}
|
|
1687
|
+
const kept = [];
|
|
1688
|
+
let dropped = 0;
|
|
1689
|
+
for (const item of raw) {
|
|
1690
|
+
const parsed = NcAlarmInputBypassRecordSchema.safeParse(item);
|
|
1691
|
+
if (!parsed.success || kept.length >= NC_ALARM_INPUT_BYPASS_MAX) {
|
|
1692
|
+
dropped += 1;
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (kept.some((e) => e.ruleId === parsed.data.ruleId && e.deviceId === parsed.data.deviceId)) {
|
|
1696
|
+
dropped += 1;
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
kept.push(parsed.data);
|
|
1700
|
+
}
|
|
1701
|
+
this.entries = kept;
|
|
1702
|
+
return dropped;
|
|
1703
|
+
}
|
|
1704
|
+
find(ruleId, deviceId) {
|
|
1705
|
+
return this.entries.find((e) => e.ruleId === ruleId && e.deviceId === deviceId);
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
//#endregion
|
|
1576
1709
|
//#region src/notification-center/alarm/alarm-mode-delays.ts
|
|
1577
1710
|
/**
|
|
1578
1711
|
* The persisted per-mode delay overrides, read through a guard (D697).
|
|
@@ -1873,6 +2006,10 @@ function alarmPushDecision(phase, heard) {
|
|
|
1873
2006
|
kind: "suppress",
|
|
1874
2007
|
reason: "alarm-sensor-excluded"
|
|
1875
2008
|
} };
|
|
2009
|
+
case "bypassed": return { verdict: {
|
|
2010
|
+
kind: "suppress",
|
|
2011
|
+
reason: "alarm-input-bypassed"
|
|
2012
|
+
} };
|
|
1876
2013
|
case "triggered": return { verdict: { kind: "intrusion" } };
|
|
1877
2014
|
case "starts-entry-delay": return heard.pending ? { verdict: {
|
|
1878
2015
|
kind: "suppress",
|
|
@@ -2171,6 +2308,18 @@ var ncAlarmPanelSchema = object({
|
|
|
2171
2308
|
*/
|
|
2172
2309
|
exclusions: array(unknown()).default([]),
|
|
2173
2310
|
/**
|
|
2311
|
+
* The (rule, input) pairs the operator bypassed for the rest of THIS arm
|
|
2312
|
+
* session (`alarm-input-bypasses.ts`). Persisted for the exclusions' reason;
|
|
2313
|
+
* read back through a guard, never a cast.
|
|
2314
|
+
*/
|
|
2315
|
+
inputBypasses: array(unknown()).default([]),
|
|
2316
|
+
/**
|
|
2317
|
+
* When the current arm session began (the last arm). A bypass request about a
|
|
2318
|
+
* match older than this belongs to an arm that is over. Absent on an older
|
|
2319
|
+
* blob ⇒ no session check until the next arm.
|
|
2320
|
+
*/
|
|
2321
|
+
armSessionAt: number().optional(),
|
|
2322
|
+
/**
|
|
2174
2323
|
* Per arm mode, the covered devices whose being open does not refuse the arm
|
|
2175
2324
|
* (D630). Loose on purpose — `mode` is a string here — and read back through
|
|
2176
2325
|
* {@link nonBlockingFromPersisted}, never a cast: a blob written by a later
|
|
@@ -2429,6 +2578,8 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2429
2578
|
attribution = new NcAlarmAttribution();
|
|
2430
2579
|
/** Sensors this arm is ignoring — see {@link NcAlarmExclusions}. */
|
|
2431
2580
|
exclusions = new NcAlarmExclusions();
|
|
2581
|
+
/** The inputs this arm session ignores — see {@link NcAlarmInputBypasses}. */
|
|
2582
|
+
bypasses = new NcAlarmInputBypasses();
|
|
2432
2583
|
/** Who answers "is anything open" — null until the centre wires itself in. */
|
|
2433
2584
|
armGuard = null;
|
|
2434
2585
|
/**
|
|
@@ -2913,6 +3064,11 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2913
3064
|
kind: "excluded",
|
|
2914
3065
|
sensorDeviceId: intrusionDeviceOf(ref)
|
|
2915
3066
|
};
|
|
3067
|
+
if (this.bypasses.blocks(ref.ruleId, intrusionDeviceOf(ref))) return {
|
|
3068
|
+
kind: "bypassed",
|
|
3069
|
+
ruleId: ref.ruleId,
|
|
3070
|
+
sensorDeviceId: intrusionDeviceOf(ref)
|
|
3071
|
+
};
|
|
2916
3072
|
const mode = armModeOf(this.machine.target, NC_ALARM_MODES);
|
|
2917
3073
|
if (mode === null) return { kind: "none" };
|
|
2918
3074
|
const state = stateAt(this.machine, now);
|
|
@@ -3105,6 +3261,77 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
3105
3261
|
this.publish();
|
|
3106
3262
|
}
|
|
3107
3263
|
/**
|
|
3264
|
+
* "Bypass this input until disarm" — ONE (rule, input) pair stops pressing
|
|
3265
|
+
* the panel for the rest of this arm session.
|
|
3266
|
+
*
|
|
3267
|
+
* Refused on a disarmed panel (there is no session to bypass in) and for a
|
|
3268
|
+
* notification about an arm that is already over (`stale`). It never cancels
|
|
3269
|
+
* an entry delay or a siren already running — it stops the NEXT presses of
|
|
3270
|
+
* that input; the disarm stays the operator's own button.
|
|
3271
|
+
*/
|
|
3272
|
+
bypassInput(input, now = Date.now()) {
|
|
3273
|
+
const refuse = (reason) => {
|
|
3274
|
+
this.ctx.logger.info("alarm input bypass refused", {
|
|
3275
|
+
tags: { deviceId: input.deviceId },
|
|
3276
|
+
meta: {
|
|
3277
|
+
panelDeviceId: this.id,
|
|
3278
|
+
ruleId: input.ruleId,
|
|
3279
|
+
rule: input.ruleName,
|
|
3280
|
+
reason
|
|
3281
|
+
}
|
|
3282
|
+
});
|
|
3283
|
+
return {
|
|
3284
|
+
outcome: "refused",
|
|
3285
|
+
reason
|
|
3286
|
+
};
|
|
3287
|
+
};
|
|
3288
|
+
if (this.machine.target === "disarmed") return refuse("disarmed");
|
|
3289
|
+
const sessionAt = this.config.get("armSessionAt");
|
|
3290
|
+
if (input.notedAt !== void 0 && sessionAt !== void 0 && input.notedAt < sessionAt) return refuse("stale");
|
|
3291
|
+
const added = this.bypasses.add(input, now);
|
|
3292
|
+
if (added === "full") return refuse("full");
|
|
3293
|
+
if (added === "bypassed") {
|
|
3294
|
+
this.persist();
|
|
3295
|
+
this.ctx.logger.info("alarm input bypassed until disarm", {
|
|
3296
|
+
tags: { deviceId: input.deviceId },
|
|
3297
|
+
meta: {
|
|
3298
|
+
panelDeviceId: this.id,
|
|
3299
|
+
ruleId: input.ruleId,
|
|
3300
|
+
rule: input.ruleName,
|
|
3301
|
+
cameraId: input.cameraId,
|
|
3302
|
+
state: stateAt(this.machine, now),
|
|
3303
|
+
bypassed: this.bypasses.size
|
|
3304
|
+
}
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
return {
|
|
3308
|
+
outcome: added,
|
|
3309
|
+
bypassed: this.bypasses.snapshot()
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
/** The inputs this arm session ignores — what the live view reports. A read, never a copy. */
|
|
3313
|
+
inputBypasses() {
|
|
3314
|
+
return this.bypasses.snapshot();
|
|
3315
|
+
}
|
|
3316
|
+
/**
|
|
3317
|
+
* Drop the bypasses whose input is no longer an input — the rule was
|
|
3318
|
+
* deleted, disabled, or no longer names the device. Called from the centre's
|
|
3319
|
+
* rule-reload tick; one line per drop, tagged with the input.
|
|
3320
|
+
*/
|
|
3321
|
+
pruneInputBypasses(keep) {
|
|
3322
|
+
const removed = this.bypasses.prune(keep);
|
|
3323
|
+
if (removed.length === 0) return;
|
|
3324
|
+
this.persist();
|
|
3325
|
+
for (const entry of removed) this.ctx.logger.info("alarm input bypass dropped — the rule no longer has this input", {
|
|
3326
|
+
tags: { deviceId: entry.deviceId },
|
|
3327
|
+
meta: {
|
|
3328
|
+
panelDeviceId: this.id,
|
|
3329
|
+
ruleId: entry.ruleId,
|
|
3330
|
+
rule: entry.ruleName
|
|
3331
|
+
}
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3108
3335
|
* One opening's reading (`open` / `secure`, D630's vocabulary — a cover is
|
|
3109
3336
|
* secure only when `closed`, a lock only when `locked`), from whoever saw
|
|
3110
3337
|
* it: the sensor's own event, or the centre's reconcile tick.
|
|
@@ -3304,6 +3531,8 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
3304
3531
|
this.persist();
|
|
3305
3532
|
}
|
|
3306
3533
|
const resolved = this.delaysFor(mode);
|
|
3534
|
+
this.endInputBypasses("arm");
|
|
3535
|
+
this.config.set("armSessionAt", now);
|
|
3307
3536
|
this.machine = arm(mode, resolved, now);
|
|
3308
3537
|
this.ctx.logger.info("alarm delays applied", {
|
|
3309
3538
|
tags: { deviceId: this.id },
|
|
@@ -3429,6 +3658,21 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
3429
3658
|
this.announceIgnoredTrigger(sensor, now, moved);
|
|
3430
3659
|
return "ignored-excluded";
|
|
3431
3660
|
}
|
|
3661
|
+
if (source !== null && sensor !== void 0 && this.bypasses.blocks(source.ruleId, sensor)) {
|
|
3662
|
+
const dropped = this.bypasses.recordDrop(source.ruleId, sensor);
|
|
3663
|
+
this.persist();
|
|
3664
|
+
this.ctx.logger.info("alarm trigger dropped — this input is bypassed until disarm", {
|
|
3665
|
+
tags: { deviceId: sensor },
|
|
3666
|
+
meta: {
|
|
3667
|
+
panelDeviceId: this.id,
|
|
3668
|
+
ruleId: source.ruleId,
|
|
3669
|
+
rule: source.ruleName,
|
|
3670
|
+
state: stateAt(this.machine, now),
|
|
3671
|
+
dropped
|
|
3672
|
+
}
|
|
3673
|
+
});
|
|
3674
|
+
return "ignored-bypassed";
|
|
3675
|
+
}
|
|
3432
3676
|
const armed = armModeOf(this.machine.target, NC_ALARM_MODES);
|
|
3433
3677
|
const resolved = this.delaysFor(armed);
|
|
3434
3678
|
const next = trigger(this.machine, resolved, now);
|
|
@@ -3488,6 +3732,7 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
3488
3732
|
publish(now = Date.now()) {
|
|
3489
3733
|
const autoCleared = this.machine.clearAt !== void 0 && now >= this.machine.clearAt && this.machine.clearToDisarmed === true;
|
|
3490
3734
|
this.machine = settle(this.machine, now);
|
|
3735
|
+
if (this.machine.target === "disarmed") this.endInputBypasses("disarm");
|
|
3491
3736
|
const state = stateAt(this.machine, now);
|
|
3492
3737
|
this.runtimeState.setCapState("alarm-panel", this.status(now));
|
|
3493
3738
|
this.persist();
|
|
@@ -3736,9 +3981,31 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
3736
3981
|
persist() {
|
|
3737
3982
|
this.config.set("machine", { ...this.machine });
|
|
3738
3983
|
this.config.set("exclusions", [...this.exclusions.snapshot()]);
|
|
3984
|
+
this.config.set("inputBypasses", [...this.bypasses.snapshot()]);
|
|
3985
|
+
}
|
|
3986
|
+
/** The session is over: drop every bypass, one line per input. */
|
|
3987
|
+
endInputBypasses(why) {
|
|
3988
|
+
const ended = this.bypasses.clear();
|
|
3989
|
+
if (ended.length === 0) return;
|
|
3990
|
+
this.config.set("inputBypasses", []);
|
|
3991
|
+
for (const entry of ended) this.ctx.logger.info("alarm input bypass ended with the arm session — watched again", {
|
|
3992
|
+
tags: { deviceId: entry.deviceId },
|
|
3993
|
+
meta: {
|
|
3994
|
+
panelDeviceId: this.id,
|
|
3995
|
+
ruleId: entry.ruleId,
|
|
3996
|
+
rule: entry.ruleName,
|
|
3997
|
+
why,
|
|
3998
|
+
dropped: entry.dropped
|
|
3999
|
+
}
|
|
4000
|
+
});
|
|
3739
4001
|
}
|
|
3740
4002
|
restore() {
|
|
3741
4003
|
this.exclusions.restore(this.config.get("exclusions"));
|
|
4004
|
+
const unreadable = this.bypasses.restore(this.config.get("inputBypasses"));
|
|
4005
|
+
if (unreadable > 0) this.ctx.logger.info("alarm input bypasses: persisted entries this release cannot read were dropped", {
|
|
4006
|
+
tags: { deviceId: this.id },
|
|
4007
|
+
meta: { dropped: unreadable }
|
|
4008
|
+
});
|
|
3742
4009
|
const restored = machineFromPersisted(this.config.get("machine"));
|
|
3743
4010
|
if (restored === null) return;
|
|
3744
4011
|
this.machine = restored;
|
|
@@ -3792,6 +4059,9 @@ function ncAlarmPanelPort(panel) {
|
|
|
3792
4059
|
excludedDevices: () => panel.excludedDevices(),
|
|
3793
4060
|
exclusionPhases: () => panel.exclusionPhases(),
|
|
3794
4061
|
forgetExcluded: (deviceId) => panel.forgetExcluded(deviceId),
|
|
4062
|
+
bypassInput: (input) => panel.bypassInput(input),
|
|
4063
|
+
inputBypasses: () => panel.inputBypasses(),
|
|
4064
|
+
pruneInputBypasses: (keep) => panel.pruneInputBypasses(keep),
|
|
3795
4065
|
currentState: () => panel.currentState(),
|
|
3796
4066
|
currentTarget: () => panel.currentTarget(),
|
|
3797
4067
|
armWith: (mode) => panel.armWith(mode),
|
|
@@ -5366,6 +5636,53 @@ async function notificationLinkEndpoints(api, logger) {
|
|
|
5366
5636
|
});
|
|
5367
5637
|
}
|
|
5368
5638
|
//#endregion
|
|
5639
|
+
//#region src/notification-center/tracks-at-instant.ts
|
|
5640
|
+
/**
|
|
5641
|
+
* How many persisted rows the wiring reads per instant. A scene holds a handful
|
|
5642
|
+
* of subjects; the read is newest-first from the instant, so this bounds a
|
|
5643
|
+
* busy street without missing the tracks that were on screen.
|
|
5644
|
+
*/
|
|
5645
|
+
var NC_TRACKS_AT_INSTANT_LIMIT = 25;
|
|
5646
|
+
/**
|
|
5647
|
+
* Every source track with a position within `toleranceMs` of `timestamp`, its
|
|
5648
|
+
* nearest box normalised, deduplicated by track id (a live copy and a
|
|
5649
|
+
* persisted copy of one track are one track). Pure.
|
|
5650
|
+
*/
|
|
5651
|
+
function tracksAtInstant(sources, timestamp, toleranceMs, fallbackDims) {
|
|
5652
|
+
const out = /* @__PURE__ */ new Map();
|
|
5653
|
+
for (const track of sources) {
|
|
5654
|
+
if (out.has(track.trackId)) continue;
|
|
5655
|
+
if (track.firstSeen > timestamp + toleranceMs || track.lastSeen < timestamp - toleranceMs) continue;
|
|
5656
|
+
const w = track.frameWidth ?? fallbackDims?.w ?? 0;
|
|
5657
|
+
const h = track.frameHeight ?? fallbackDims?.h ?? 0;
|
|
5658
|
+
if (w <= 0 || h <= 0) continue;
|
|
5659
|
+
let best = null;
|
|
5660
|
+
let bestDelta = Number.POSITIVE_INFINITY;
|
|
5661
|
+
for (const p of track.positions) {
|
|
5662
|
+
const delta = Math.abs(p.timestamp - timestamp);
|
|
5663
|
+
if (delta < bestDelta) {
|
|
5664
|
+
bestDelta = delta;
|
|
5665
|
+
best = p;
|
|
5666
|
+
}
|
|
5667
|
+
}
|
|
5668
|
+
if (best === null || bestDelta > toleranceMs) continue;
|
|
5669
|
+
const label = track.subLabel ?? track.label;
|
|
5670
|
+
out.set(track.trackId, {
|
|
5671
|
+
trackId: track.trackId,
|
|
5672
|
+
className: track.className,
|
|
5673
|
+
...label !== void 0 && label !== track.className ? { label } : {},
|
|
5674
|
+
...best.score !== void 0 ? { confidence: best.score } : {},
|
|
5675
|
+
bbox: {
|
|
5676
|
+
x: best.bbox.x / w,
|
|
5677
|
+
y: best.bbox.y / h,
|
|
5678
|
+
w: best.bbox.w / w,
|
|
5679
|
+
h: best.bbox.h / h
|
|
5680
|
+
}
|
|
5681
|
+
});
|
|
5682
|
+
}
|
|
5683
|
+
return [...out.values()];
|
|
5684
|
+
}
|
|
5685
|
+
//#endregion
|
|
5369
5686
|
//#region src/notification-center/artifact-read-plane.ts
|
|
5370
5687
|
/**
|
|
5371
5688
|
* Parse a single-range `Range` header against a known length.
|
|
@@ -23049,7 +23366,7 @@ function referenceOf(boxes) {
|
|
|
23049
23366
|
function displaced(ref, b) {
|
|
23050
23367
|
return Math.hypot(b.x + b.w / 2 - ref.x, b.y + b.h / 2 - ref.y) > ref.span;
|
|
23051
23368
|
}
|
|
23052
|
-
function judge(ref, run, b) {
|
|
23369
|
+
function judge$1(ref, run, b) {
|
|
23053
23370
|
const next = displaced(ref, b) ? run + 1 : 0;
|
|
23054
23371
|
return {
|
|
23055
23372
|
early: [],
|
|
@@ -23069,7 +23386,7 @@ function judgeAll(boxes) {
|
|
|
23069
23386
|
};
|
|
23070
23387
|
for (const b of boxes) {
|
|
23071
23388
|
if (probe.moved) return probe;
|
|
23072
|
-
probe = judge(ref, probe.run, b);
|
|
23389
|
+
probe = judge$1(ref, probe.run, b);
|
|
23073
23390
|
}
|
|
23074
23391
|
return probe;
|
|
23075
23392
|
}
|
|
@@ -23084,7 +23401,7 @@ function isMeasurableBox(box) {
|
|
|
23084
23401
|
function stepMovedProbe(probe, box) {
|
|
23085
23402
|
if (probe.moved || !isMeasurableBox(box)) return probe;
|
|
23086
23403
|
if (probe.reference !== null) {
|
|
23087
|
-
const next = judge(probe.reference, probe.run, box);
|
|
23404
|
+
const next = judge$1(probe.reference, probe.run, box);
|
|
23088
23405
|
return next.run === probe.run && !next.moved ? probe : next;
|
|
23089
23406
|
}
|
|
23090
23407
|
const early = [...probe.early, box];
|
|
@@ -26195,6 +26512,11 @@ var MAX_ARM_BUTTONS = 3;
|
|
|
26195
26512
|
* | `alarm-arm-refused` | **Arma comunque**, bound to the row's refusal |
|
|
26196
26513
|
* | `alarm-sensor-bypass` | nothing — it reports, there is nothing to undo |
|
|
26197
26514
|
*
|
|
26515
|
+
* `alarm-triggered` and `alarm-pending` rows a RULE caused also carry **Bypass
|
|
26516
|
+
* this input**, bound to the row's (rule, input) pair: that one input stops
|
|
26517
|
+
* pressing the panel until the next disarm. It never cancels the countdown or
|
|
26518
|
+
* the siren already running — Disarm beside it does that.
|
|
26519
|
+
*
|
|
26198
26520
|
* The `alarm-arming` row is the one that earns its keep: that notification
|
|
26199
26521
|
* arrives at the START of the exit delay, so its Disarm is the only alarm
|
|
26200
26522
|
* button an operator can press to cancel something rather than to undo it.
|
|
@@ -26231,7 +26553,7 @@ function buildAlarmButtons(input) {
|
|
|
26231
26553
|
};
|
|
26232
26554
|
});
|
|
26233
26555
|
const actionId = "alarm-disarm";
|
|
26234
|
-
|
|
26556
|
+
const disarm = {
|
|
26235
26557
|
id: actionId,
|
|
26236
26558
|
label: input.labels.disarm,
|
|
26237
26559
|
url: input.mintUrl({
|
|
@@ -26241,6 +26563,19 @@ function buildAlarmButtons(input) {
|
|
|
26241
26563
|
icon: "disarm",
|
|
26242
26564
|
destructive: true,
|
|
26243
26565
|
mode: "background"
|
|
26566
|
+
};
|
|
26567
|
+
const bypassable = input.bypassable;
|
|
26568
|
+
if (bypassable === void 0 || input.kind !== "alarm-triggered" && input.kind !== "alarm-pending") return [disarm];
|
|
26569
|
+
const bypassId = "alarm-bypass-input";
|
|
26570
|
+
return [disarm, {
|
|
26571
|
+
id: bypassId,
|
|
26572
|
+
label: input.labels.bypassInput,
|
|
26573
|
+
url: input.mintUrl({
|
|
26574
|
+
actionId: bypassId,
|
|
26575
|
+
action: "bypass-input",
|
|
26576
|
+
...bypassable
|
|
26577
|
+
}),
|
|
26578
|
+
mode: "background"
|
|
26244
26579
|
}];
|
|
26245
26580
|
}
|
|
26246
26581
|
/**
|
|
@@ -26475,6 +26810,14 @@ function buildAlarmLive(input) {
|
|
|
26475
26810
|
deviceId: e.deviceId,
|
|
26476
26811
|
name: input.name(e.deviceId),
|
|
26477
26812
|
phase: e.phase
|
|
26813
|
+
})),
|
|
26814
|
+
bypassedInputs: input.bypasses.map((b) => ({
|
|
26815
|
+
ruleId: b.ruleId,
|
|
26816
|
+
ruleName: input.ruleName(b.ruleId) ?? b.ruleName,
|
|
26817
|
+
deviceId: b.deviceId,
|
|
26818
|
+
name: input.name(b.deviceId),
|
|
26819
|
+
since: b.at,
|
|
26820
|
+
dropped: b.dropped
|
|
26478
26821
|
}))
|
|
26479
26822
|
};
|
|
26480
26823
|
}
|
|
@@ -28700,78 +29043,31 @@ function isCoolingDown(rule, lastFiredAt, now) {
|
|
|
28700
29043
|
return now - lastFiredAt < rule.throttle.cooldownSec * 1e3;
|
|
28701
29044
|
}
|
|
28702
29045
|
/**
|
|
28703
|
-
* The media policy
|
|
28704
|
-
*
|
|
28705
|
-
*
|
|
28706
|
-
*
|
|
28707
|
-
*
|
|
28708
|
-
* none of to chase. No zone crop, no gif, no clip, no profile — a sensor
|
|
28709
|
-
* subject has no zone condition to crop to and no reason to animate.
|
|
28710
|
-
*/
|
|
28711
|
-
var NC_SENSOR_MEDIA_POLICY = { attach: "keyFrame" };
|
|
28712
|
-
/**
|
|
28713
|
-
* The media policy actually used to build a delivery, for a rule of this
|
|
28714
|
-
* KIND. A sensor rule's STORED `media` is never read at evaluation — the
|
|
28715
|
-
* editor offers no media control for one any more, and `media.attach` /
|
|
28716
|
-
* `.gif` / `.clip` / `.profile` on an existing sensor rule (authored before
|
|
28717
|
-
* this ruling, from the viewer or the API) are IGNORED here, never refused at
|
|
28718
|
-
* load. See {@link mediaPolicyMatchesSensorDefault} for the sampled-log gate.
|
|
29046
|
+
* The media policy a delivery is built from: the rule's own, with its picture
|
|
29047
|
+
* mode RESOLVED (`resolveNcMediaImage`) — a sensor rule included. Until D897 a
|
|
29048
|
+
* sensor rule's stored media was ignored for a fixed one-snapshot policy; it
|
|
29049
|
+
* now goes through the same selector and the same render stage as a detection
|
|
29050
|
+
* rule, its source frame being the linked camera's.
|
|
28719
29051
|
*/
|
|
28720
29052
|
function effectiveMediaPolicy(rule, kind) {
|
|
28721
|
-
return
|
|
28722
|
-
|
|
28723
|
-
|
|
28724
|
-
|
|
28725
|
-
* gate the boot/evaluation path uses to decide whether an existing sensor
|
|
28726
|
-
* rule's stored choice is being silently overridden (and so is worth a
|
|
28727
|
-
* sampled log line) or already matches and there is nothing to say.
|
|
28728
|
-
*/
|
|
28729
|
-
function mediaPolicyMatchesSensorDefault(media) {
|
|
28730
|
-
return media.attach === NC_SENSOR_MEDIA_POLICY.attach && media.frame === void 0 && media.zoneCrop !== true && media.gif !== true && media.clip !== true && media.profile === void 0;
|
|
29053
|
+
return {
|
|
29054
|
+
...rule.media,
|
|
29055
|
+
image: resolveNcMediaImage(rule.media, kind)
|
|
29056
|
+
};
|
|
28731
29057
|
}
|
|
28732
|
-
/** `effectiveMediaPolicy`, resolving the kind itself — the call-site shortcut
|
|
28733
|
-
* every production caller uses; `effectiveMediaPolicy` stays the unit under
|
|
28734
|
-
* test because `ruleKindOf` needs no fixture of its own there. */
|
|
29058
|
+
/** `effectiveMediaPolicy`, resolving the kind itself — the call-site shortcut. */
|
|
28735
29059
|
function effectiveMediaPolicyForRule(rule) {
|
|
28736
29060
|
return effectiveMediaPolicy(rule, ruleKindOf(rule));
|
|
28737
29061
|
}
|
|
28738
|
-
/**
|
|
28739
|
-
|
|
28740
|
-
|
|
28741
|
-
* (small variant first — the degrade engine enforces per-backend byte
|
|
28742
|
-
* caps, but there is no reason to ship a native 4K frame by default).
|
|
28743
|
-
*/
|
|
28744
|
-
function attachmentKindPreference(policy, ownerKind) {
|
|
28745
|
-
if (policy === "keyFrame") return ownerKind === "track" ? [
|
|
28746
|
-
"keyFrameSmall",
|
|
28747
|
-
"keyFrame",
|
|
28748
|
-
SCENE_TWIN_KIND,
|
|
28749
|
-
ROLLING_SCENE_TWIN_KIND,
|
|
28750
|
-
"fullFrameBoxed",
|
|
28751
|
-
"firstFrame",
|
|
28752
|
-
"lastFrame",
|
|
28753
|
-
"snapshot"
|
|
28754
|
-
] : [
|
|
28755
|
-
"fullFrameBoxed",
|
|
28756
|
-
"fullFrame",
|
|
28757
|
-
"crop"
|
|
28758
|
-
];
|
|
28759
|
-
return ownerKind === "track" ? [
|
|
28760
|
-
"thumbnail",
|
|
28761
|
-
"thumbnailSmall",
|
|
28762
|
-
"crop",
|
|
28763
|
-
"keyFrameSmall",
|
|
28764
|
-
"firstFrame"
|
|
28765
|
-
] : [
|
|
28766
|
-
"crop",
|
|
28767
|
-
"thumbnail",
|
|
28768
|
-
"fullFrameBoxed"
|
|
28769
|
-
];
|
|
29062
|
+
/** True when a rule ships no still at all. */
|
|
29063
|
+
function ruleShipsNoStill(rule) {
|
|
29064
|
+
return effectiveMediaPolicyForRule(rule).image === "none";
|
|
28770
29065
|
}
|
|
28771
29066
|
/**
|
|
28772
29067
|
* The media kinds whose stored bytes carry a BURNT-IN detection box.
|
|
28773
29068
|
*
|
|
28774
|
-
* This is the family boundary for {@link
|
|
29069
|
+
* This is the family boundary for {@link cleanSourcePreference} (D897; the retired
|
|
29070
|
+
* `framePreference` before it), and it exists as one
|
|
28775
29071
|
* exported constant because the alternative was tried and failed in production:
|
|
28776
29072
|
* the `boxed` ladder listed `snapshot` / `lastFrame` / `firstFrame` as *"the
|
|
28777
29073
|
* kinds that actually carry a burnt-in box today"* while, eleven lines below,
|
|
@@ -28787,7 +29083,7 @@ function attachmentKindPreference(policy, ownerKind) {
|
|
|
28787
29083
|
*
|
|
28788
29084
|
* ## Why `full` still ends with three of these kinds (D654)
|
|
28789
29085
|
*
|
|
28790
|
-
* `framePreference('full', 'track')` used to reach these kinds as soon as the
|
|
29086
|
+
* The retired `framePreference('full', 'track')` used to reach these kinds as soon as the
|
|
28791
29087
|
* key frame was out of band, so a rule asking for the clean scene was answered
|
|
28792
29088
|
* with an annotated one: of 38 `frame:'full'` deliveries sha256-matched back to
|
|
28793
29089
|
* stored media, **19 (50%) were `firstFrame` / `snapshot` / `lastFrame`**.
|
|
@@ -28850,90 +29146,36 @@ function isAnnotatedSceneKind(kind) {
|
|
|
28850
29146
|
return ANNOTATED_SCENE_KINDS.includes(kind);
|
|
28851
29147
|
}
|
|
28852
29148
|
/**
|
|
28853
|
-
* The
|
|
28854
|
-
*
|
|
28855
|
-
*
|
|
28856
|
-
* is
|
|
28857
|
-
*
|
|
28858
|
-
*
|
|
28859
|
-
*
|
|
28860
|
-
*
|
|
28861
|
-
*
|
|
28862
|
-
*
|
|
28863
|
-
*
|
|
28864
|
-
*
|
|
28865
|
-
*
|
|
28866
|
-
|
|
28867
|
-
|
|
28868
|
-
* that decides nothing is worse than no knob: the operator set it and believed
|
|
28869
|
-
* they had asked for something.
|
|
28870
|
-
*
|
|
28871
|
-
* The annotated frames were there the whole time under different names.
|
|
28872
|
-
* `firstFrame` / `snapshot` / `lastFrame` are written by `boxedTimelineFrame`
|
|
28873
|
-
* — the track's OWN box burned onto the native full frame — so `boxed` now asks
|
|
28874
|
-
* for them, newest-moment-first-per-family, before degrading to the clean
|
|
28875
|
-
* scene. No new capture path, no new kind, and the option answers its own
|
|
28876
|
-
* question again.
|
|
28877
|
-
*
|
|
28878
|
-
* The strictness is about FAMILY, and the family boundary is
|
|
28879
|
-
* {@link ANNOTATED_SCENE_KINDS} — ONE declaration, consumed by both ladders, so
|
|
28880
|
-
* `boxed` and `full` can never again hold different opinions about the same
|
|
28881
|
-
* kind. They did, and it shipped: see that constant.
|
|
28882
|
-
*/
|
|
28883
|
-
function framePreference(frame, ownerKind) {
|
|
28884
|
-
if (frame === "cropped") return ownerKind === "track" ? [
|
|
28885
|
-
"thumbnail",
|
|
28886
|
-
"thumbnailSmall",
|
|
28887
|
-
"crop"
|
|
28888
|
-
] : [
|
|
28889
|
-
"crop",
|
|
28890
|
-
"thumbnail",
|
|
28891
|
-
"thumbnailSmall"
|
|
28892
|
-
];
|
|
28893
|
-
if (frame === "boxed") return [
|
|
28894
|
-
...ANNOTATED_SCENE_KINDS,
|
|
28895
|
-
...CLEAN_SCENE_KINDS,
|
|
28896
|
-
SCENE_TWIN_KIND,
|
|
28897
|
-
ROLLING_SCENE_TWIN_KIND
|
|
28898
|
-
];
|
|
29149
|
+
* The SOURCE ladder of every notification picture (D897): the clean whole-scene
|
|
29150
|
+
* kinds of an owner, best first, then — LAST — the annotated tiles, for a
|
|
29151
|
+
* track that holds no clean frame at all (written before the D654 twin, or a
|
|
29152
|
+
* twin write that failed). The picture is rendered from what this picks, by
|
|
29153
|
+
* mode (`notification-image.ts`), so a rule no longer chooses a stored KIND:
|
|
29154
|
+
* `crop` cuts the subject out of the clean frame, `scene-boxes` draws on it.
|
|
29155
|
+
*
|
|
29156
|
+
* `keyFrame` leads because it is the native best-detection frame — the crop
|
|
29157
|
+
* keeps its resolution; the scene modes downscale it once at render. The twins
|
|
29158
|
+
* (`sceneFrame` at a boxed tile's instant, `lastSceneFrame` at the rolling
|
|
29159
|
+
* one) are what MOMENT finds near the trigger when the best shot is later.
|
|
29160
|
+
* A still-shelf photograph holds `keyFrameSmall` / `keyFrame` / `fullFrame`,
|
|
29161
|
+
* so a sound or a linked-camera still resolves through the same ladder.
|
|
29162
|
+
*/
|
|
29163
|
+
function cleanSourcePreference(ownerKind) {
|
|
28899
29164
|
return ownerKind === "track" ? [
|
|
28900
29165
|
"keyFrame",
|
|
28901
29166
|
"keyFrameSmall",
|
|
28902
29167
|
SCENE_TWIN_KIND,
|
|
28903
29168
|
ROLLING_SCENE_TWIN_KIND,
|
|
29169
|
+
"fullFrame",
|
|
28904
29170
|
"firstFrame",
|
|
28905
29171
|
"lastFrame",
|
|
28906
|
-
"snapshot"
|
|
29172
|
+
"snapshot",
|
|
29173
|
+
"fullFrameBoxed"
|
|
28907
29174
|
] : [
|
|
28908
29175
|
"fullFrame",
|
|
28909
29176
|
"keyFrame",
|
|
28910
|
-
"keyFrameSmall"
|
|
28911
|
-
|
|
28912
|
-
}
|
|
28913
|
-
/**
|
|
28914
|
-
* Derive the `best-matching` media signal from a matched rule's condition
|
|
28915
|
-
* summary ({@link NcEvaluation.matchedOn}). Identity takes priority over plate
|
|
28916
|
-
* (D-3 ordering: a face rule that ALSO plate-matched attaches the face crop).
|
|
28917
|
-
*/
|
|
28918
|
-
function matchSignal(matchedOn) {
|
|
28919
|
-
if (matchedOn === void 0) return null;
|
|
28920
|
-
if (matchedOn.includes("identities")) return "face";
|
|
28921
|
-
if (matchedOn.includes("plates")) return "plate";
|
|
28922
|
-
return null;
|
|
28923
|
-
}
|
|
28924
|
-
/**
|
|
28925
|
-
* Ordered media-kind preference for a `best-matching` attachment. The
|
|
28926
|
-
* signal-specific crop (`faceCrop` / `plateCrop`, both event-owned) leads;
|
|
28927
|
-
* then the plain `best` subject ladder, then the `keyFrame` clean-scene
|
|
28928
|
-
* ladder — so a missing specific crop degrades to best → keyFrame → none
|
|
28929
|
-
* (the dispatcher returns null when nothing resolves) without ever blocking
|
|
28930
|
-
* the send. A `null` signal is exactly the `best` ladder.
|
|
28931
|
-
*/
|
|
28932
|
-
function bestMatchingKindPreference(signal, ownerKind) {
|
|
28933
|
-
return [
|
|
28934
|
-
...signal === "face" ? ["faceCrop"] : signal === "plate" ? ["plateCrop"] : [],
|
|
28935
|
-
...attachmentKindPreference("best", ownerKind),
|
|
28936
|
-
...attachmentKindPreference("keyFrame", ownerKind)
|
|
29177
|
+
"keyFrameSmall",
|
|
29178
|
+
"fullFrameBoxed"
|
|
28937
29179
|
];
|
|
28938
29180
|
}
|
|
28939
29181
|
//#endregion
|
|
@@ -31328,7 +31570,8 @@ var NcGroupMemberFateSchema = union([_enum([
|
|
|
31328
31570
|
"close-failed",
|
|
31329
31571
|
"alarm-entry-delay-started",
|
|
31330
31572
|
"alarm-entry-delay",
|
|
31331
|
-
"alarm-sensor-excluded"
|
|
31573
|
+
"alarm-sensor-excluded",
|
|
31574
|
+
"alarm-input-bypassed"
|
|
31332
31575
|
]), templateLiteral(["failed-condition:", string()])]);
|
|
31333
31576
|
/** Read cap, so a client cannot ask for the whole ring in one call. */
|
|
31334
31577
|
var MAX_QUERY_LIMIT = 200;
|
|
@@ -31640,6 +31883,8 @@ var NcPruneRecordSchema = object({
|
|
|
31640
31883
|
});
|
|
31641
31884
|
/** Rules × deleted subjects — small; a generous ceiling. */
|
|
31642
31885
|
var LOAD_LIMIT$1 = 1e4;
|
|
31886
|
+
/** Page size of the load's records query. */
|
|
31887
|
+
var LOAD_PAGE = 1e3;
|
|
31643
31888
|
function keyOf$1(row) {
|
|
31644
31889
|
return `${row.kind}:${row.ruleId}:${row.field}:${row.subject}`;
|
|
31645
31890
|
}
|
|
@@ -31665,18 +31910,23 @@ NC_DEVICE_PRUNES_SPEC.fromRecord, NC_DEVICE_PRUNES_SPEC.toValue, NC_DEVICE_PRUNE
|
|
|
31665
31910
|
var KEPT_OUTCOMES = new Set(["kept-disabled", "kept"]);
|
|
31666
31911
|
var NcPruneLedger = class {
|
|
31667
31912
|
ledger;
|
|
31668
|
-
|
|
31913
|
+
/**
|
|
31914
|
+
* The SAME handle the ledger writes through — the records kind wherever
|
|
31915
|
+
* `ctx.records` exists. The load reads it, never the legacy table: no boot
|
|
31916
|
+
* declares that table any more, so a direct read of it fails forever.
|
|
31917
|
+
*/
|
|
31918
|
+
kind;
|
|
31669
31919
|
logger;
|
|
31670
31920
|
loaded = false;
|
|
31671
31921
|
/** Bumped on every {@link record} write — a load ignores keys written after it began. */
|
|
31672
31922
|
writes = 0;
|
|
31673
31923
|
writtenAt = /* @__PURE__ */ new Map();
|
|
31674
31924
|
constructor(deps) {
|
|
31675
|
-
this.store = deps.store;
|
|
31676
31925
|
this.logger = deps.logger;
|
|
31926
|
+
this.kind = resolveDurableKind(NC_DEVICE_PRUNES_SPEC, ncDurableRecordKinds["device-prunes"], deps);
|
|
31677
31927
|
this.ledger = new DurableLedger({
|
|
31678
31928
|
spec: NC_DEVICE_PRUNES_SPEC,
|
|
31679
|
-
kind:
|
|
31929
|
+
kind: this.kind,
|
|
31680
31930
|
logger: deps.logger
|
|
31681
31931
|
});
|
|
31682
31932
|
}
|
|
@@ -31692,15 +31942,7 @@ var NcPruneLedger = class {
|
|
|
31692
31942
|
const startedAt = this.writes;
|
|
31693
31943
|
const writtenSince = (key) => (this.writtenAt.get(key) ?? 0) > startedAt;
|
|
31694
31944
|
try {
|
|
31695
|
-
const
|
|
31696
|
-
collection: NC_DEVICE_PRUNES_COLLECTION,
|
|
31697
|
-
filter: { limit: LOAD_LIMIT$1 }
|
|
31698
|
-
});
|
|
31699
|
-
const next = /* @__PURE__ */ new Map();
|
|
31700
|
-
for (const record of records) {
|
|
31701
|
-
const row = NC_DEVICE_PRUNES_SPEC.fromRecord(record.id, record.data);
|
|
31702
|
-
if (row !== null) next.set(keyOf$1(row), row);
|
|
31703
|
-
}
|
|
31945
|
+
const next = await this.readAll();
|
|
31704
31946
|
for (const key of heldBefore) if (!next.has(key) && !writtenSince(key)) this.ledger.evict(key);
|
|
31705
31947
|
for (const [key, row] of next) if (!writtenSince(key)) this.ledger.stage(row);
|
|
31706
31948
|
this.loaded = true;
|
|
@@ -31712,6 +31954,26 @@ var NcPruneLedger = class {
|
|
|
31712
31954
|
} });
|
|
31713
31955
|
}
|
|
31714
31956
|
}
|
|
31957
|
+
/** Every row the kind holds, paged. THROWS on a failed page — `load` is the guard. */
|
|
31958
|
+
async readAll() {
|
|
31959
|
+
const next = /* @__PURE__ */ new Map();
|
|
31960
|
+
let seen = 0;
|
|
31961
|
+
let cursor;
|
|
31962
|
+
while (seen < LOAD_LIMIT$1) {
|
|
31963
|
+
const page = await this.kind.query({
|
|
31964
|
+
limit: Math.min(LOAD_PAGE, LOAD_LIMIT$1 - seen),
|
|
31965
|
+
...cursor === void 0 ? {} : { cursor }
|
|
31966
|
+
});
|
|
31967
|
+
seen += page.records.length;
|
|
31968
|
+
for (const record of page.records) {
|
|
31969
|
+
const row = NC_DEVICE_PRUNES_SPEC.fromRecord(record.id, record.data);
|
|
31970
|
+
if (row !== null) next.set(keyOf$1(row), row);
|
|
31971
|
+
}
|
|
31972
|
+
if (page.nextCursor === null || page.records.length === 0) break;
|
|
31973
|
+
cursor = page.nextCursor;
|
|
31974
|
+
}
|
|
31975
|
+
return next;
|
|
31976
|
+
}
|
|
31715
31977
|
/** Has a load ever SUCCEEDED? The pruner acts on nothing until it has. */
|
|
31716
31978
|
isLoaded() {
|
|
31717
31979
|
return this.loaded;
|
|
@@ -32115,6 +32377,7 @@ var en_default = {
|
|
|
32115
32377
|
},
|
|
32116
32378
|
"button.alarm.disarm": "Disarm",
|
|
32117
32379
|
"button.alarm.armAnyway": "Arm anyway",
|
|
32380
|
+
"button.alarm.bypassInput": "Bypass until disarm",
|
|
32118
32381
|
"button.alarm.arm.home": "Arm home",
|
|
32119
32382
|
"button.alarm.arm.away": "Arm away",
|
|
32120
32383
|
"button.alarm.arm.night": "Arm night",
|
|
@@ -32501,6 +32764,7 @@ var it_default = {
|
|
|
32501
32764
|
},
|
|
32502
32765
|
"button.alarm.disarm": "Disarma",
|
|
32503
32766
|
"button.alarm.armAnyway": "Arma comunque",
|
|
32767
|
+
"button.alarm.bypassInput": "Escludi fino al disinserimento",
|
|
32504
32768
|
"button.alarm.arm.home": "Arma casa",
|
|
32505
32769
|
"button.alarm.arm.away": "Arma fuori",
|
|
32506
32770
|
"button.alarm.arm.night": "Arma notte",
|
|
@@ -33969,6 +34233,65 @@ function sameConfirm(a, b) {
|
|
|
33969
34233
|
return a?.enabled === true === (b?.enabled === true) && a?.expect?.op === b?.expect?.op && a?.expect?.count === b?.expect?.count;
|
|
33970
34234
|
}
|
|
33971
34235
|
//#endregion
|
|
34236
|
+
//#region src/notification-center/media-guard.ts
|
|
34237
|
+
/**
|
|
34238
|
+
* The hub's half of D897: a rule's picture is ONE mode (`media.image`) plus
|
|
34239
|
+
* `showTriggerZone`, and both are judged against the rule they are saved on.
|
|
34240
|
+
*
|
|
34241
|
+
* - `showTriggerZone` draws the zone that FIRED the rule. A rule whose trigger
|
|
34242
|
+
* is not a zone (no `zones` condition, no occupancy zone) has nothing to
|
|
34243
|
+
* draw, so the flag is refused rather than stored as a knob that does
|
|
34244
|
+
* nothing.
|
|
34245
|
+
* - `image` must be a mode the rule's kind can honour
|
|
34246
|
+
* (`imageModesForKind`): a sound owns no region of the frame, so it is never
|
|
34247
|
+
* offered `crop`.
|
|
34248
|
+
*
|
|
34249
|
+
* The predicate is `ncMediaPolicyError` (`@camstack/types`), read by the admin
|
|
34250
|
+
* editor too. It runs on both save routes — the cap provider
|
|
34251
|
+
* (`notificationRules.createRule/updateRule`) and the rule bridge
|
|
34252
|
+
* (`nc.createRule/updateRule`) — beside the other guards.
|
|
34253
|
+
*
|
|
34254
|
+
* Only media the CALLER sent is judged: a create's `media`, an update's
|
|
34255
|
+
* `media`, against the MERGED delivery and conditions. A stored rule is never
|
|
34256
|
+
* judged on load — `normalizeNcMediaPolicy` drops a flag it cannot draw there,
|
|
34257
|
+
* because a refusal at load would cost the rule its notifications.
|
|
34258
|
+
*
|
|
34259
|
+
* Not in Zod for the reason `rule-family-guard.ts` gives: a refine would also
|
|
34260
|
+
* run when the store LOADS a rule.
|
|
34261
|
+
*/
|
|
34262
|
+
var NcMediaPolicyRefusal = class extends Error {
|
|
34263
|
+
reason;
|
|
34264
|
+
constructor(reason) {
|
|
34265
|
+
super(`rule refused: ${reason}`);
|
|
34266
|
+
this.name = "NcMediaPolicyRefusal";
|
|
34267
|
+
this.reason = reason;
|
|
34268
|
+
}
|
|
34269
|
+
};
|
|
34270
|
+
function judge(subject, ctx) {
|
|
34271
|
+
if (subject.media === void 0) return;
|
|
34272
|
+
const reason = ncMediaPolicyError(subject.media, subject);
|
|
34273
|
+
if (reason === null) return;
|
|
34274
|
+
ctx.logger.warn("nc rule refused: media policy the rule cannot honour", { meta: {
|
|
34275
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
34276
|
+
by: ctx.by,
|
|
34277
|
+
reason
|
|
34278
|
+
} });
|
|
34279
|
+
throw new NcMediaPolicyRefusal(reason);
|
|
34280
|
+
}
|
|
34281
|
+
/** The create path of both routes. */
|
|
34282
|
+
function guardRuleMedia(rule, ctx) {
|
|
34283
|
+
judge(rule, ctx);
|
|
34284
|
+
}
|
|
34285
|
+
/** The update path of both routes: the patch's media on the MERGED rule. */
|
|
34286
|
+
function guardPatchMedia(existing, patch, ctx) {
|
|
34287
|
+
if (existing === null || patch.media === void 0) return;
|
|
34288
|
+
judge({
|
|
34289
|
+
delivery: patch.delivery ?? existing.delivery,
|
|
34290
|
+
conditions: patch.conditions ?? existing.conditions,
|
|
34291
|
+
media: patch.media
|
|
34292
|
+
}, ctx);
|
|
34293
|
+
}
|
|
34294
|
+
//#endregion
|
|
33972
34295
|
//#region src/notification-center/occupancy-watcher.ts
|
|
33973
34296
|
/** Sentinel key segments for the "no zone" (whole-frame) and "no class" scopes. */
|
|
33974
34297
|
var FRAME_SCOPE = "@frame";
|
|
@@ -36238,6 +36561,29 @@ var NcAlarmStatusSchema = object({
|
|
|
36238
36561
|
deviceId: number().int().nullable(),
|
|
36239
36562
|
live: NcAlarmConfigSchema.shape.live.unwrap().nullable()
|
|
36240
36563
|
});
|
|
36564
|
+
/** `nc.bypassAlarmInput` — one (rule, input) pair, until the next disarm. */
|
|
36565
|
+
var NcAlarmBypassInputRequestSchema = object({
|
|
36566
|
+
ruleId: string().min(1),
|
|
36567
|
+
/** The input: the sensor whose match pressed the panel, else the camera. */
|
|
36568
|
+
deviceId: number().int()
|
|
36569
|
+
});
|
|
36570
|
+
var NcAlarmBypassInputResultSchema = object({
|
|
36571
|
+
outcome: _enum([
|
|
36572
|
+
"bypassed",
|
|
36573
|
+
"already-bypassed",
|
|
36574
|
+
"refused"
|
|
36575
|
+
]),
|
|
36576
|
+
/**
|
|
36577
|
+
* Why a `refused` did nothing: the panel is disarmed, the rule is not an
|
|
36578
|
+
* alarm rule with this input, or the per-arm bound is reached. Null otherwise.
|
|
36579
|
+
*/
|
|
36580
|
+
reason: _enum([
|
|
36581
|
+
"disarmed",
|
|
36582
|
+
"stale",
|
|
36583
|
+
"full",
|
|
36584
|
+
"not-an-input"
|
|
36585
|
+
]).nullable()
|
|
36586
|
+
});
|
|
36241
36587
|
/**
|
|
36242
36588
|
* The action catalog — the tRPC contract Group B (the viewer client) consumes
|
|
36243
36589
|
* against `addonId: 'pipeline-analytics'`. Every entry that depends on the
|
|
@@ -36366,6 +36712,17 @@ var ncActions = defineCustomActions({
|
|
|
36366
36712
|
caller: "required"
|
|
36367
36713
|
}),
|
|
36368
36714
|
/**
|
|
36715
|
+
* "Bypass this input until disarm" from the app — the same panel path the
|
|
36716
|
+
* trigger notification's button takes, for the same principals who may
|
|
36717
|
+
* disarm here (admin). The live view (`nc.getAlarmStatus`) then lists it
|
|
36718
|
+
* under `bypassedInputs` until the next disarm.
|
|
36719
|
+
*/
|
|
36720
|
+
"nc.bypassAlarmInput": customAction(NcAlarmBypassInputRequestSchema, NcAlarmBypassInputResultSchema, {
|
|
36721
|
+
kind: "mutation",
|
|
36722
|
+
auth: "admin",
|
|
36723
|
+
caller: "required"
|
|
36724
|
+
}),
|
|
36725
|
+
/**
|
|
36369
36726
|
* What the deleted-device / deleted-target prune did to the rules
|
|
36370
36727
|
* (`device-prune-runner.ts`) — the source of the banner both UIs show on a
|
|
36371
36728
|
* rule it switched off ("Disabled on <date>: its last device (#id) was
|
|
@@ -37046,6 +37403,10 @@ function makeNcActionHandlers(deps) {
|
|
|
37046
37403
|
logger: deps.logger,
|
|
37047
37404
|
by: c.userId
|
|
37048
37405
|
});
|
|
37406
|
+
guardRuleMedia(parsed, {
|
|
37407
|
+
logger: deps.logger,
|
|
37408
|
+
by: c.userId
|
|
37409
|
+
});
|
|
37049
37410
|
guardRuleDevicesExclude(parsed, {
|
|
37050
37411
|
logger: deps.logger,
|
|
37051
37412
|
by: c.userId
|
|
@@ -37098,6 +37459,11 @@ function makeNcActionHandlers(deps) {
|
|
|
37098
37459
|
by: c.userId,
|
|
37099
37460
|
ruleId: input.ruleId
|
|
37100
37461
|
});
|
|
37462
|
+
guardPatchMedia(existing, patch, {
|
|
37463
|
+
logger: deps.logger,
|
|
37464
|
+
by: c.userId,
|
|
37465
|
+
ruleId: input.ruleId
|
|
37466
|
+
});
|
|
37101
37467
|
guardPatchDevicesExclude(existing, patch, {
|
|
37102
37468
|
logger: deps.logger,
|
|
37103
37469
|
by: c.userId,
|
|
@@ -37203,6 +37569,10 @@ function makeNcActionHandlers(deps) {
|
|
|
37203
37569
|
await requireAlarm("nc.disarmAlarm").disarm(c.userId);
|
|
37204
37570
|
return { success: true };
|
|
37205
37571
|
},
|
|
37572
|
+
"nc.bypassAlarmInput": async (input, caller) => {
|
|
37573
|
+
const c = requireAdmin(caller);
|
|
37574
|
+
return requireAlarm("nc.bypassAlarmInput").bypassInput(input, c.userId);
|
|
37575
|
+
},
|
|
37206
37576
|
"nc.listPrunes": async (_input, caller) => {
|
|
37207
37577
|
requireAdmin(caller);
|
|
37208
37578
|
const list = deps.listPrunes;
|
|
@@ -40210,7 +40580,7 @@ var GLYPH_ADVANCE = .56;
|
|
|
40210
40580
|
var LABEL_USABLE_FRACTION = .94;
|
|
40211
40581
|
/** `<`, `>`, `&`, `'`, `"` in a camera name or an operator caption would each
|
|
40212
40582
|
* turn the overlay into a document librsvg refuses. */
|
|
40213
|
-
function escapeXml$
|
|
40583
|
+
function escapeXml$3(s) {
|
|
40214
40584
|
return s.replace(/[<>&'"]/g, (ch) => {
|
|
40215
40585
|
switch (ch) {
|
|
40216
40586
|
case "<": return "<";
|
|
@@ -40378,7 +40748,7 @@ function tileLabelSvg(width, height, label) {
|
|
|
40378
40748
|
const baseline = height - Math.round((bandHeight - fitted.fontPx) / 2) - Math.round(fitted.fontPx * .18);
|
|
40379
40749
|
return Buffer.from(`<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
40380
40750
|
<rect x="0" y="${height - bandHeight}" width="${width}" height="${bandHeight}" fill="black" fill-opacity="0.55"/>
|
|
40381
|
-
<text x="${padding}" y="${baseline}" font-size="${fitted.fontPx}" font-family="sans-serif" font-weight="bold" fill="white">${escapeXml$
|
|
40751
|
+
<text x="${padding}" y="${baseline}" font-size="${fitted.fontPx}" font-family="sans-serif" font-weight="bold" fill="white">${escapeXml$3(fitted.text)}</text>
|
|
40382
40752
|
</svg>`);
|
|
40383
40753
|
}
|
|
40384
40754
|
/** The mosaic's own title band. */
|
|
@@ -40387,7 +40757,7 @@ function titleSvg(width, height, caption) {
|
|
|
40387
40757
|
const baseline = Math.round(height / 2 + fitted.fontPx * .36);
|
|
40388
40758
|
return Buffer.from(`<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
40389
40759
|
<rect x="0" y="0" width="${width}" height="${height}" fill="#111111"/>
|
|
40390
|
-
<text x="${Math.round(width / 2)}" y="${baseline}" font-size="${fitted.fontPx}" font-family="sans-serif" font-weight="bold" text-anchor="middle" fill="white">${escapeXml$
|
|
40760
|
+
<text x="${Math.round(width / 2)}" y="${baseline}" font-size="${fitted.fontPx}" font-family="sans-serif" font-weight="bold" text-anchor="middle" fill="white">${escapeXml$3(fitted.text)}</text>
|
|
40391
40761
|
</svg>`);
|
|
40392
40762
|
}
|
|
40393
40763
|
/**
|
|
@@ -40810,11 +41180,11 @@ function withTimeout$3(promise, ms, what) {
|
|
|
40810
41180
|
* {@link NC_GROUPED_MOSAIC_MAX_TILES}; the sentence still counts every member;
|
|
40811
41181
|
* - each tile is resolved through the rule's own ladder on a DERIVED entry
|
|
40812
41182
|
* ({@link groupedTileEntry}) that carries the member's own subject, zones and
|
|
40813
|
-
* `matchedOn`,
|
|
40814
|
-
* and never zone
|
|
41183
|
+
* `matchedOn`, rendered as `crop` (N full scenes would be the same picture N times)
|
|
41184
|
+
* and never with a zone drawn.
|
|
40815
41185
|
* - a member whose owner is a PHOTOGRAPH held in RAM (a sensor's linked-camera still or a
|
|
40816
41186
|
* still-shelf capture — an alarm incident's sensor member, D705) owns no subject crop:
|
|
40817
|
-
* its tile is the whole photograph (`
|
|
41187
|
+
* its tile is the whole photograph (`clean`), or it would never land;
|
|
40818
41188
|
* - a tile is about its member ALONE: the row's system event (an alarm's `alarmSource`)
|
|
40819
41189
|
* is dropped, or the member's object read would be scoped to the source's generation.
|
|
40820
41190
|
*
|
|
@@ -40855,16 +41225,18 @@ function ownsSubjectCrop(ownerId) {
|
|
|
40855
41225
|
/**
|
|
40856
41226
|
* A NEW entry whose media read is about ONE member. Never mutates `entry`.
|
|
40857
41227
|
*
|
|
40858
|
-
* Drops the primary's
|
|
40859
|
-
*
|
|
41228
|
+
* Drops the primary's zone overlay (no zone on a tile — `mediaShowTriggerZone`, and a
|
|
41229
|
+
* legacy row's `mediaZoneIds`), its `triggerZoneIds` and `matchedOn` (the member's own
|
|
41230
|
+
* replace them, or none), and renders the tile as `crop` (`clean` for a RAM photograph —
|
|
41231
|
+
* see the header) through the same image stage as every still (D897).
|
|
40860
41232
|
*/
|
|
40861
41233
|
function groupedTileEntry(entry, m) {
|
|
40862
|
-
const { mediaZoneIds: _zoneCrop, triggerZoneIds: _primaryZones, matchedOn: _primaryMatch, ...payload } = entry.payload;
|
|
41234
|
+
const { mediaZoneIds: _zoneCrop, mediaShowTriggerZone: _zoneOverlay, mediaFrame: _legacyFrame, triggerZoneIds: _primaryZones, matchedOn: _primaryMatch, ...payload } = entry.payload;
|
|
40863
41235
|
return {
|
|
40864
41236
|
...entry,
|
|
40865
41237
|
payload: {
|
|
40866
41238
|
...payload,
|
|
40867
|
-
|
|
41239
|
+
mediaImage: ownsSubjectCrop(m.trackId) ? "crop" : "clean",
|
|
40868
41240
|
subject: memberSubject(entry.payload.subject, m, entry.deviceId),
|
|
40869
41241
|
...m.triggerZoneIds !== void 0 ? { triggerZoneIds: m.triggerZoneIds } : {},
|
|
40870
41242
|
...m.matchedOn !== void 0 ? { matchedOn: m.matchedOn } : {}
|
|
@@ -41297,8 +41669,9 @@ function zoneOverlap(point, polygons) {
|
|
|
41297
41669
|
}
|
|
41298
41670
|
return best;
|
|
41299
41671
|
}
|
|
41300
|
-
/** The trail point closest to `timestamp`, or null when none is
|
|
41301
|
-
|
|
41672
|
+
/** The trail point closest to `timestamp`, or null when none is within `maxMs`
|
|
41673
|
+
* (default {@link TRAIL_MATCH_MAX_MS}). */
|
|
41674
|
+
function trailAt(trail, timestamp, maxMs = TRAIL_MATCH_MAX_MS) {
|
|
41302
41675
|
let best = null;
|
|
41303
41676
|
let bestDelta = Number.POSITIVE_INFINITY;
|
|
41304
41677
|
for (const point of trail) {
|
|
@@ -41308,7 +41681,7 @@ function trailAt(trail, timestamp) {
|
|
|
41308
41681
|
best = point;
|
|
41309
41682
|
}
|
|
41310
41683
|
}
|
|
41311
|
-
return best !== null && bestDelta <=
|
|
41684
|
+
return best !== null && bestDelta <= maxMs ? best : null;
|
|
41312
41685
|
}
|
|
41313
41686
|
/**
|
|
41314
41687
|
* The candidates that show the subject inside the triggering zone, in the order
|
|
@@ -41326,6 +41699,407 @@ function selectTriggerZoneCandidates(input) {
|
|
|
41326
41699
|
return strict === true ? [] : candidates;
|
|
41327
41700
|
}
|
|
41328
41701
|
//#endregion
|
|
41702
|
+
//#region src/notification-center/notification-image.ts
|
|
41703
|
+
/** How long a rendered picture is reused for the same record (N targets, retries). */
|
|
41704
|
+
var NC_IMAGE_MEMO_TTL_MS = 15 * 6e4;
|
|
41705
|
+
/** At most this many rendered pictures are held. */
|
|
41706
|
+
var NC_IMAGE_MEMO_MAX = 128;
|
|
41707
|
+
/** How far a trail point may sit from the source frame's instant and still be its box. */
|
|
41708
|
+
var NC_IMAGE_BOX_MATCH_MAX_MS = 750;
|
|
41709
|
+
/** A scene picture is downscaled to at most this width (a phone shows it). */
|
|
41710
|
+
var NC_IMAGE_SCENE_MAX_WIDTH = 1920;
|
|
41711
|
+
/** A crop is downscaled to at most this long edge. */
|
|
41712
|
+
var NC_IMAGE_CROP_MAX_EDGE = 1280;
|
|
41713
|
+
/** Padding around the subject box of a crop, as a fraction of the box's side. */
|
|
41714
|
+
var NC_IMAGE_CROP_PAD = .25;
|
|
41715
|
+
/** The subject's outline. */
|
|
41716
|
+
var NC_IMAGE_SUBJECT_COLOR = "#ffb300";
|
|
41717
|
+
/** Every other track's outline. */
|
|
41718
|
+
var NC_IMAGE_OTHER_COLOR = "#f2f2f2";
|
|
41719
|
+
/** The trigger zone's outline. */
|
|
41720
|
+
var NC_IMAGE_ZONE_COLOR = "#29b6f6";
|
|
41721
|
+
/** Two tracks whose boxes overlap this much at one instant are the same subject (a fused id). */
|
|
41722
|
+
var SAME_SUBJECT_IOU = .8;
|
|
41723
|
+
var JPEG_QUALITY = 85;
|
|
41724
|
+
/** The caption of a box: class (or label) and confidence — "dog 87%". Language-free. */
|
|
41725
|
+
function imageLabelText(name, confidence) {
|
|
41726
|
+
if (confidence === void 0 || !Number.isFinite(confidence)) return name;
|
|
41727
|
+
const pct = Math.round(Math.min(1, Math.max(0, confidence)) * 100);
|
|
41728
|
+
return `${name} ${String(pct)}%`;
|
|
41729
|
+
}
|
|
41730
|
+
/** Stroke widths for an output of this size: the subject's is the thick one. */
|
|
41731
|
+
function overlayStrokes(width, height) {
|
|
41732
|
+
const subject = Math.max(4, Math.round(Math.min(width, height) / 110));
|
|
41733
|
+
return {
|
|
41734
|
+
subject,
|
|
41735
|
+
other: Math.max(2, Math.round(subject / 2.5))
|
|
41736
|
+
};
|
|
41737
|
+
}
|
|
41738
|
+
function escapeXml$2(text) {
|
|
41739
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
41740
|
+
}
|
|
41741
|
+
function clampPx(value, max) {
|
|
41742
|
+
return Math.max(0, Math.min(Math.round(value), max));
|
|
41743
|
+
}
|
|
41744
|
+
/** Scale a normalised box to an output of `width` × `height`, clamped inside it. */
|
|
41745
|
+
function toPixels(box, width, height) {
|
|
41746
|
+
const x = clampPx(box.x * width, width - 1);
|
|
41747
|
+
const y = clampPx(box.y * height, height - 1);
|
|
41748
|
+
return {
|
|
41749
|
+
x,
|
|
41750
|
+
y,
|
|
41751
|
+
w: Math.max(1, Math.min(Math.round(box.w * width), width - x)),
|
|
41752
|
+
h: Math.max(1, Math.min(Math.round(box.h * height), height - y))
|
|
41753
|
+
};
|
|
41754
|
+
}
|
|
41755
|
+
/**
|
|
41756
|
+
* The SVG drawn over a scene picture of `width` × `height`. The subject is
|
|
41757
|
+
* drawn LAST so a crowd never hides it. Captions sit on a dark plate above
|
|
41758
|
+
* the box (inside it when the box touches the top edge), white text.
|
|
41759
|
+
*/
|
|
41760
|
+
function notificationOverlaySvg(input) {
|
|
41761
|
+
const { width, height } = input;
|
|
41762
|
+
const strokes = overlayStrokes(width, height);
|
|
41763
|
+
const fontSize = Math.max(14, Math.round(height / 36));
|
|
41764
|
+
const parts = [];
|
|
41765
|
+
for (const zone of input.zones) {
|
|
41766
|
+
if (zone.length < 3) continue;
|
|
41767
|
+
const points = zone.map((p) => `${String(clampPx(p.x * width, width))},${String(clampPx(p.y * height, height))}`).join(" ");
|
|
41768
|
+
parts.push(`<polygon data-role="zone" points="${points}" fill="${NC_IMAGE_ZONE_COLOR}" fill-opacity="0.18" stroke="${NC_IMAGE_ZONE_COLOR}" stroke-width="${String(strokes.other)}" stroke-dasharray="${String(strokes.subject * 2)},${String(strokes.subject)}"/>`);
|
|
41769
|
+
}
|
|
41770
|
+
const ordered = [...input.boxes.filter((b) => b.role === "other"), ...input.boxes.filter((b) => b.role === "subject")];
|
|
41771
|
+
for (const box of ordered) {
|
|
41772
|
+
const subject = box.role === "subject";
|
|
41773
|
+
const color = subject ? NC_IMAGE_SUBJECT_COLOR : NC_IMAGE_OTHER_COLOR;
|
|
41774
|
+
const stroke = subject ? strokes.subject : strokes.other;
|
|
41775
|
+
parts.push(`<rect data-role="${box.role}" x="${String(box.x)}" y="${String(box.y)}" width="${String(box.w)}" height="${String(box.h)}" fill="none" stroke="${color}" stroke-width="${String(stroke)}"/>`);
|
|
41776
|
+
if (box.label.length === 0) continue;
|
|
41777
|
+
const plateH = Math.round(fontSize * 1.35);
|
|
41778
|
+
const plateW = Math.round(box.label.length * fontSize * .62 + fontSize * .8);
|
|
41779
|
+
const plateY = box.y - plateH >= 0 ? box.y - plateH : box.y;
|
|
41780
|
+
const plateX = Math.min(box.x, Math.max(0, width - plateW));
|
|
41781
|
+
parts.push(`<rect data-role="${box.role}-plate" x="${String(plateX)}" y="${String(plateY)}" width="${String(plateW)}" height="${String(plateH)}" fill="#000000" fill-opacity="0.72" stroke="${color}" stroke-width="${String(subject ? 2 : 1)}"/>`, `<text data-role="${box.role}-label" x="${String(plateX + Math.round(fontSize * .4))}" y="${String(plateY + Math.round(plateH * .75))}" font-family="DejaVu Sans, Helvetica, Arial, sans-serif" font-size="${String(fontSize)}" font-weight="${subject ? "700" : "400"}" fill="#ffffff">${escapeXml$2(box.label)}</text>`);
|
|
41782
|
+
}
|
|
41783
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${String(width)}" height="${String(height)}">${parts.join("")}</svg>`;
|
|
41784
|
+
}
|
|
41785
|
+
/** The pixel rectangle a `crop` cuts from a `width` × `height` frame, or null when too small. */
|
|
41786
|
+
function subjectCropRect(box, width, height) {
|
|
41787
|
+
const px = toPixels(box, width, height);
|
|
41788
|
+
const padX = Math.max(16, Math.round(px.w * NC_IMAGE_CROP_PAD));
|
|
41789
|
+
const padY = Math.max(16, Math.round(px.h * NC_IMAGE_CROP_PAD));
|
|
41790
|
+
const left = Math.max(0, px.x - padX);
|
|
41791
|
+
const top = Math.max(0, px.y - padY);
|
|
41792
|
+
const right = Math.min(width, px.x + px.w + padX);
|
|
41793
|
+
const bottom = Math.min(height, px.y + px.h + padY);
|
|
41794
|
+
if (right - left < 16 || bottom - top < 16) return null;
|
|
41795
|
+
return {
|
|
41796
|
+
left,
|
|
41797
|
+
top,
|
|
41798
|
+
width: right - left,
|
|
41799
|
+
height: bottom - top
|
|
41800
|
+
};
|
|
41801
|
+
}
|
|
41802
|
+
function iou$4(a, b) {
|
|
41803
|
+
const x1 = Math.max(a.x, b.x);
|
|
41804
|
+
const y1 = Math.max(a.y, b.y);
|
|
41805
|
+
const x2 = Math.min(a.x + a.w, b.x + b.w);
|
|
41806
|
+
const y2 = Math.min(a.y + a.h, b.y + b.h);
|
|
41807
|
+
const inter = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
|
|
41808
|
+
const union = a.w * a.h + b.w * b.h - inter;
|
|
41809
|
+
return union <= 0 ? 0 : inter / union;
|
|
41810
|
+
}
|
|
41811
|
+
/**
|
|
41812
|
+
* The tracks to draw thin: every track at the instant except the subject —
|
|
41813
|
+
* the same id, or a box that is the subject's (a fused id reads under its
|
|
41814
|
+
* survivor's name).
|
|
41815
|
+
*/
|
|
41816
|
+
function otherTracks(tracks, subjectTrackId, subjectBox) {
|
|
41817
|
+
return tracks.filter((t) => t.trackId !== subjectTrackId && (subjectBox === null || iou$4(t.bbox, subjectBox) < SAME_SUBJECT_IOU));
|
|
41818
|
+
}
|
|
41819
|
+
/**
|
|
41820
|
+
* What picture a ROW asks for. A row written since D897 carries `mediaImage`;
|
|
41821
|
+
* an older row (still in the outbox across the upgrade) is read through what
|
|
41822
|
+
* its legacy fields meant — the same mapping the rule migration applies.
|
|
41823
|
+
*/
|
|
41824
|
+
function payloadImageMode(payload) {
|
|
41825
|
+
if (payload.media === "none") return "none";
|
|
41826
|
+
if (payload.mediaImage !== void 0) return payload.mediaImage;
|
|
41827
|
+
if (payload.mediaZoneIds !== void 0 && payload.mediaZoneIds.length > 0) return "clean";
|
|
41828
|
+
if (payload.mediaFrame === "full") return "clean";
|
|
41829
|
+
if (payload.mediaFrame === "boxed") return "scene-boxes";
|
|
41830
|
+
if (payload.mediaFrame === "cropped") return "crop";
|
|
41831
|
+
if (payload.media === "keyFrame") return "clean";
|
|
41832
|
+
return "crop";
|
|
41833
|
+
}
|
|
41834
|
+
/** The zone ids a row's picture outlines: the zones that fired it, when it asked. */
|
|
41835
|
+
function payloadZoneOverlayIds(payload) {
|
|
41836
|
+
if (payload.mediaShowTriggerZone === true) return payload.triggerZoneIds ?? [];
|
|
41837
|
+
if (payload.mediaImage === void 0) return payload.mediaZoneIds ?? [];
|
|
41838
|
+
return [];
|
|
41839
|
+
}
|
|
41840
|
+
/**
|
|
41841
|
+
* Which camera the primary still SHOWS, or `composite` when it is a composed
|
|
41842
|
+
* mosaic of several. A sensor row of a linked set names its cameras primary
|
|
41843
|
+
* first; in `each` mode the primary's still is that first camera's alone.
|
|
41844
|
+
*/
|
|
41845
|
+
function primaryImageCamera(payload) {
|
|
41846
|
+
const linked = payload.subject.linkedCameraIds;
|
|
41847
|
+
if (linked === void 0 || linked.length === 0) return payload.subject.deviceId;
|
|
41848
|
+
if (linked.length > 1 && payload.subject.extraStills === void 0) return "composite";
|
|
41849
|
+
return linked[0] ?? payload.subject.deviceId;
|
|
41850
|
+
}
|
|
41851
|
+
/** True when the row's subject is a tracked subject (not a RAM photograph owner). */
|
|
41852
|
+
function hasTrackedSubject(trackId) {
|
|
41853
|
+
return trackId !== void 0 && !isLinkedStillId(trackId) && !isStillShelfId(trackId);
|
|
41854
|
+
}
|
|
41855
|
+
function toArrayBufferBytes(buf) {
|
|
41856
|
+
const out = new Uint8Array(buf.byteLength);
|
|
41857
|
+
out.set(buf);
|
|
41858
|
+
return out;
|
|
41859
|
+
}
|
|
41860
|
+
/**
|
|
41861
|
+
* Render one picture from a clean JPEG. A `crop` with no box, and every
|
|
41862
|
+
* `clean`, are the scene with nothing drawn. Throws on undecodable bytes; the
|
|
41863
|
+
* stage turns that into the source shipped as it is.
|
|
41864
|
+
*/
|
|
41865
|
+
async function renderNotificationImage(req) {
|
|
41866
|
+
const { default: sharp } = await import("sharp");
|
|
41867
|
+
const meta = await sharp(Buffer.from(req.bytes)).metadata();
|
|
41868
|
+
const srcW = meta.width ?? 0;
|
|
41869
|
+
const srcH = meta.height ?? 0;
|
|
41870
|
+
if (srcW === 0 || srcH === 0) throw new Error("source frame has no dimensions");
|
|
41871
|
+
if (req.mode === "crop" && req.subject !== null) {
|
|
41872
|
+
const rect = subjectCropRect(req.subject, srcW, srcH);
|
|
41873
|
+
if (rect !== null) return {
|
|
41874
|
+
bytes: toArrayBufferBytes(await sharp(Buffer.from(req.bytes)).extract(rect).resize({
|
|
41875
|
+
width: NC_IMAGE_CROP_MAX_EDGE,
|
|
41876
|
+
height: NC_IMAGE_CROP_MAX_EDGE,
|
|
41877
|
+
fit: "inside",
|
|
41878
|
+
withoutEnlargement: true
|
|
41879
|
+
}).jpeg({ quality: JPEG_QUALITY }).toBuffer()),
|
|
41880
|
+
name: "crop.jpg",
|
|
41881
|
+
rendered: "crop"
|
|
41882
|
+
};
|
|
41883
|
+
}
|
|
41884
|
+
const scale = Math.min(1, NC_IMAGE_SCENE_MAX_WIDTH / srcW);
|
|
41885
|
+
const outW = Math.max(1, Math.round(srcW * scale));
|
|
41886
|
+
const outH = Math.max(1, Math.round(srcH * scale));
|
|
41887
|
+
const boxes = req.mode === "scene-boxes" ? [...req.others.map((o) => ({
|
|
41888
|
+
...toPixels(o, outW, outH),
|
|
41889
|
+
role: "other",
|
|
41890
|
+
label: o.label
|
|
41891
|
+
})), ...req.subject !== null ? [{
|
|
41892
|
+
...toPixels(req.subject, outW, outH),
|
|
41893
|
+
role: "subject",
|
|
41894
|
+
label: req.subject.label
|
|
41895
|
+
}] : []] : [];
|
|
41896
|
+
const zones = req.mode === "crop" ? [] : req.zones;
|
|
41897
|
+
let pipeline = sharp(Buffer.from(req.bytes)).resize({
|
|
41898
|
+
width: outW,
|
|
41899
|
+
height: outH,
|
|
41900
|
+
fit: "fill"
|
|
41901
|
+
});
|
|
41902
|
+
if (boxes.length > 0 || zones.length > 0) {
|
|
41903
|
+
const svg = notificationOverlaySvg({
|
|
41904
|
+
width: outW,
|
|
41905
|
+
height: outH,
|
|
41906
|
+
boxes,
|
|
41907
|
+
zones
|
|
41908
|
+
});
|
|
41909
|
+
pipeline = pipeline.composite([{
|
|
41910
|
+
input: Buffer.from(svg),
|
|
41911
|
+
top: 0,
|
|
41912
|
+
left: 0
|
|
41913
|
+
}]);
|
|
41914
|
+
}
|
|
41915
|
+
const out = await pipeline.jpeg({ quality: JPEG_QUALITY }).toBuffer();
|
|
41916
|
+
const rendered = req.mode === "scene-boxes" && boxes.length > 0 ? "scene-boxes" : "clean";
|
|
41917
|
+
return {
|
|
41918
|
+
bytes: toArrayBufferBytes(out),
|
|
41919
|
+
name: `${rendered}.jpg`,
|
|
41920
|
+
rendered
|
|
41921
|
+
};
|
|
41922
|
+
}
|
|
41923
|
+
var NcNotificationImageStage = class {
|
|
41924
|
+
deps;
|
|
41925
|
+
memo = /* @__PURE__ */ new Map();
|
|
41926
|
+
render;
|
|
41927
|
+
constructor(deps) {
|
|
41928
|
+
this.deps = deps;
|
|
41929
|
+
this.render = deps.render ?? renderNotificationImage;
|
|
41930
|
+
}
|
|
41931
|
+
/** Rendered pictures currently held (spec + diagnostics). */
|
|
41932
|
+
size() {
|
|
41933
|
+
return this.memo.size;
|
|
41934
|
+
}
|
|
41935
|
+
/**
|
|
41936
|
+
* The picture for this row from this source, rendered once per record +
|
|
41937
|
+
* subject + mode + zone flag + camera and reused for every target and retry.
|
|
41938
|
+
* Null when the row ships no still.
|
|
41939
|
+
*/
|
|
41940
|
+
async notificationImage(input) {
|
|
41941
|
+
const payload = input.entry.payload;
|
|
41942
|
+
const mode = payloadImageMode(payload);
|
|
41943
|
+
if (mode === "none") return null;
|
|
41944
|
+
const zoneIds = input.withSubject ? payloadZoneOverlayIds(payload) : [];
|
|
41945
|
+
const key = [
|
|
41946
|
+
input.entry.recordId,
|
|
41947
|
+
payload.subject.trackId ?? "",
|
|
41948
|
+
mode,
|
|
41949
|
+
zoneIds.length > 0 ? "z" : "-",
|
|
41950
|
+
String(input.camera)
|
|
41951
|
+
].join("|");
|
|
41952
|
+
const now = this.deps.now();
|
|
41953
|
+
const hit = this.memo.get(key);
|
|
41954
|
+
if (hit !== void 0 && now - hit.at <= NC_IMAGE_MEMO_TTL_MS) {
|
|
41955
|
+
this.memo.delete(key);
|
|
41956
|
+
this.memo.set(key, hit);
|
|
41957
|
+
return hit.value;
|
|
41958
|
+
}
|
|
41959
|
+
const value = this.compute(input, mode, zoneIds);
|
|
41960
|
+
this.remember(key, {
|
|
41961
|
+
at: now,
|
|
41962
|
+
value
|
|
41963
|
+
});
|
|
41964
|
+
value.then((image) => {
|
|
41965
|
+
if (image.reason === "render-failed") this.memo.delete(key);
|
|
41966
|
+
}, () => this.memo.delete(key));
|
|
41967
|
+
return value;
|
|
41968
|
+
}
|
|
41969
|
+
remember(key, entry) {
|
|
41970
|
+
const now = this.deps.now();
|
|
41971
|
+
for (const [k, v] of this.memo) if (now - v.at > NC_IMAGE_MEMO_TTL_MS) this.memo.delete(k);
|
|
41972
|
+
while (this.memo.size >= NC_IMAGE_MEMO_MAX) {
|
|
41973
|
+
const oldest = this.memo.keys().next();
|
|
41974
|
+
if (oldest.done === true) break;
|
|
41975
|
+
this.memo.delete(oldest.value);
|
|
41976
|
+
}
|
|
41977
|
+
this.memo.set(key, entry);
|
|
41978
|
+
}
|
|
41979
|
+
async compute(input, mode, zoneIds) {
|
|
41980
|
+
const { entry, source, camera } = input;
|
|
41981
|
+
const payload = entry.payload;
|
|
41982
|
+
if (source.annotated) return this.asStored(input, mode, "annotated-source");
|
|
41983
|
+
if (camera === "composite") return mode === "clean" ? this.asStored(input, mode) : this.asStored(input, mode, "composite-source");
|
|
41984
|
+
const subjectTrackId = input.withSubject ? payload.subject.trackId : void 0;
|
|
41985
|
+
const subjectBox = hasTrackedSubject(subjectTrackId) ? await this.subjectBoxAt(camera, subjectTrackId, source.timestamp) : null;
|
|
41986
|
+
const subject = subjectBox !== null ? {
|
|
41987
|
+
...subjectBox,
|
|
41988
|
+
label: imageLabelText(payload.subject.label ?? payload.subject.className, payload.subject.confidence)
|
|
41989
|
+
} : null;
|
|
41990
|
+
const others = mode === "scene-boxes" ? otherTracks(await this.tracksAt(camera, source.timestamp), subjectTrackId, subjectBox).map((t) => ({
|
|
41991
|
+
...t.bbox,
|
|
41992
|
+
label: imageLabelText(t.label ?? t.className, t.confidence)
|
|
41993
|
+
})) : [];
|
|
41994
|
+
const zones = mode === "crop" || zoneIds.length === 0 ? [] : await this.zonePolygons(camera, zoneIds);
|
|
41995
|
+
try {
|
|
41996
|
+
const image = await this.render({
|
|
41997
|
+
mode,
|
|
41998
|
+
bytes: source.bytes,
|
|
41999
|
+
subject,
|
|
42000
|
+
others,
|
|
42001
|
+
zones
|
|
42002
|
+
});
|
|
42003
|
+
const reason = image.rendered === mode ? void 0 : mode === "crop" ? "no-subject-box" : "no-tracks-at-instant";
|
|
42004
|
+
if (reason !== void 0) this.reportFallback(input, mode, reason);
|
|
42005
|
+
return reason !== void 0 ? {
|
|
42006
|
+
...image,
|
|
42007
|
+
reason
|
|
42008
|
+
} : image;
|
|
42009
|
+
} catch (err) {
|
|
42010
|
+
this.deps.logger.warn("notification picture render failed — shipping the source frame", {
|
|
42011
|
+
tags: { deviceId: camera },
|
|
42012
|
+
meta: {
|
|
42013
|
+
ruleId: entry.ruleId,
|
|
42014
|
+
recordId: entry.recordId,
|
|
42015
|
+
asked: mode,
|
|
42016
|
+
kind: source.kind,
|
|
42017
|
+
error: String(err)
|
|
42018
|
+
}
|
|
42019
|
+
});
|
|
42020
|
+
return {
|
|
42021
|
+
bytes: source.bytes,
|
|
42022
|
+
name: `${source.kind}.jpg`,
|
|
42023
|
+
rendered: "clean",
|
|
42024
|
+
reason: "render-failed"
|
|
42025
|
+
};
|
|
42026
|
+
}
|
|
42027
|
+
}
|
|
42028
|
+
asStored(input, mode, reason) {
|
|
42029
|
+
if (reason !== void 0) this.reportFallback(input, mode, reason);
|
|
42030
|
+
return {
|
|
42031
|
+
bytes: input.source.bytes,
|
|
42032
|
+
name: `${input.source.kind}.jpg`,
|
|
42033
|
+
rendered: input.source.annotated ? "scene-boxes" : "clean",
|
|
42034
|
+
...reason !== void 0 ? { reason } : {}
|
|
42035
|
+
};
|
|
42036
|
+
}
|
|
42037
|
+
/** ONE line per rendered record (the memo makes it so): what was asked, what ships, why. */
|
|
42038
|
+
reportFallback(input, asked, reason) {
|
|
42039
|
+
const camera = input.camera === "composite" ? input.entry.payload.subject.deviceId : input.camera;
|
|
42040
|
+
this.deps.logger.info("notification picture shipped without the mode the rule asked for", {
|
|
42041
|
+
tags: { deviceId: camera },
|
|
42042
|
+
meta: {
|
|
42043
|
+
ruleId: input.entry.ruleId,
|
|
42044
|
+
recordId: input.entry.recordId,
|
|
42045
|
+
...input.entry.payload.subject.trackId !== void 0 ? { trackId: input.entry.payload.subject.trackId } : {},
|
|
42046
|
+
asked,
|
|
42047
|
+
reason,
|
|
42048
|
+
kind: input.source.kind,
|
|
42049
|
+
offsetMs: input.source.timestamp - input.entry.payload.subject.timestamp
|
|
42050
|
+
}
|
|
42051
|
+
});
|
|
42052
|
+
}
|
|
42053
|
+
async subjectBoxAt(camera, trackId, timestamp) {
|
|
42054
|
+
const read = this.deps.getSubjectTrail;
|
|
42055
|
+
if (read === void 0) return null;
|
|
42056
|
+
try {
|
|
42057
|
+
return trailAt(await read(camera, trackId), timestamp, NC_IMAGE_BOX_MATCH_MAX_MS)?.bbox ?? null;
|
|
42058
|
+
} catch (err) {
|
|
42059
|
+
this.deps.logger.debug("subject trail read failed — no subject box on the picture", {
|
|
42060
|
+
tags: { deviceId: camera },
|
|
42061
|
+
meta: {
|
|
42062
|
+
trackId,
|
|
42063
|
+
error: String(err)
|
|
42064
|
+
}
|
|
42065
|
+
});
|
|
42066
|
+
return null;
|
|
42067
|
+
}
|
|
42068
|
+
}
|
|
42069
|
+
async tracksAt(camera, timestamp) {
|
|
42070
|
+
const read = this.deps.getTracksAt;
|
|
42071
|
+
if (read === void 0) return [];
|
|
42072
|
+
try {
|
|
42073
|
+
return await read(camera, timestamp, NC_IMAGE_BOX_MATCH_MAX_MS);
|
|
42074
|
+
} catch (err) {
|
|
42075
|
+
this.deps.logger.debug("tracks-at-instant read failed — no other boxes on the picture", {
|
|
42076
|
+
tags: { deviceId: camera },
|
|
42077
|
+
meta: {
|
|
42078
|
+
timestamp,
|
|
42079
|
+
error: String(err)
|
|
42080
|
+
}
|
|
42081
|
+
});
|
|
42082
|
+
return [];
|
|
42083
|
+
}
|
|
42084
|
+
}
|
|
42085
|
+
async zonePolygons(camera, zoneIds) {
|
|
42086
|
+
const read = this.deps.getZonePolygons;
|
|
42087
|
+
if (read === void 0) return [];
|
|
42088
|
+
try {
|
|
42089
|
+
return await read(camera, zoneIds);
|
|
42090
|
+
} catch (err) {
|
|
42091
|
+
this.deps.logger.debug("zone polygon read failed — the trigger zone is not drawn", {
|
|
42092
|
+
tags: { deviceId: camera },
|
|
42093
|
+
meta: {
|
|
42094
|
+
zoneIds: zoneIds.join(","),
|
|
42095
|
+
error: String(err)
|
|
42096
|
+
}
|
|
42097
|
+
});
|
|
42098
|
+
return [];
|
|
42099
|
+
}
|
|
42100
|
+
}
|
|
42101
|
+
};
|
|
42102
|
+
//#endregion
|
|
41329
42103
|
//#region src/notification-center/dispatcher.ts
|
|
41330
42104
|
/**
|
|
41331
42105
|
* NcDispatcher — turns a durable outbox entry into ONE canonical
|
|
@@ -41468,12 +42242,6 @@ var NC_STILL_WAIT_INTERVAL_MS = 500;
|
|
|
41468
42242
|
* is the instant; the ladder then runs among the frames that are about it.
|
|
41469
42243
|
*/
|
|
41470
42244
|
var NC_TRIGGER_STILL_MAX_AGE_MS = TRAIL_MATCH_MAX_MS;
|
|
41471
|
-
/** Bound on the once-per-record memo of the annotated-fallback line. */
|
|
41472
|
-
var NC_ANNOTATED_FALLBACK_MEMO_MAX = 5e3;
|
|
41473
|
-
function cleanSceneAsk(policy, frame) {
|
|
41474
|
-
if (frame !== void 0) return frame === "full" ? "full" : null;
|
|
41475
|
-
return policy === "keyFrame" ? "keyFrame" : null;
|
|
41476
|
-
}
|
|
41477
42245
|
/**
|
|
41478
42246
|
* Past this age the poll is pointless and only adds latency: the seconds in
|
|
41479
42247
|
* which this subject's media was being written are long gone, so a row that has
|
|
@@ -41507,10 +42275,8 @@ var NcDispatcher = class {
|
|
|
41507
42275
|
now;
|
|
41508
42276
|
sleep;
|
|
41509
42277
|
targetCacheTtlMs;
|
|
41510
|
-
/**
|
|
41511
|
-
|
|
41512
|
-
* {@link NC_ANNOTATED_FALLBACK_MEMO_MAX}. */
|
|
41513
|
-
reportedAnnotatedFallbacks = /* @__PURE__ */ new Set();
|
|
42278
|
+
/** The ONE render stage of every notification picture (D897). */
|
|
42279
|
+
imageStage;
|
|
41514
42280
|
texts;
|
|
41515
42281
|
/** One recorded cut per moment, and the recorder-busy breaker (D686). */
|
|
41516
42282
|
recordedCuts = new NcRecordedCutMemo();
|
|
@@ -41520,6 +42286,13 @@ var NcDispatcher = class {
|
|
|
41520
42286
|
this.sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
41521
42287
|
this.targetCacheTtlMs = deps.targetCacheTtlMs ?? DEFAULT_TARGET_CACHE_TTL_MS;
|
|
41522
42288
|
this.texts = deps.texts ?? new NcTextCatalog();
|
|
42289
|
+
this.imageStage = new NcNotificationImageStage({
|
|
42290
|
+
logger: deps.logger,
|
|
42291
|
+
now: this.now,
|
|
42292
|
+
...deps.getSubjectTrail !== void 0 ? { getSubjectTrail: deps.getSubjectTrail } : {},
|
|
42293
|
+
...deps.getTracksAt !== void 0 ? { getTracksAt: deps.getTracksAt } : {},
|
|
42294
|
+
...deps.getZonePolygons !== void 0 ? { getZonePolygons: deps.getZonePolygons } : {}
|
|
42295
|
+
});
|
|
41523
42296
|
}
|
|
41524
42297
|
/**
|
|
41525
42298
|
* Drop held recorded cuts past their TTL (re-review R4). Driven by the
|
|
@@ -42593,16 +43366,14 @@ var NcDispatcher = class {
|
|
|
42593
43366
|
* analyst to judge ({@link NcBuiltNotification.grouped}). */
|
|
42594
43367
|
async resolveAttachments(entry) {
|
|
42595
43368
|
const out = [];
|
|
42596
|
-
const zoneIdsWanted = entry.payload.mediaZoneIds;
|
|
42597
|
-
const stillOverride = zoneIdsWanted !== void 0 && zoneIdsWanted.length > 0 ? "keyFrame" : void 0;
|
|
42598
43369
|
const groupView = readOutboxGroup(entry.payload.group);
|
|
42599
43370
|
const groupMembers = groupedTileMembers(groupView);
|
|
42600
43371
|
const groupTiles = entry.payload.media === "none" ? null : groupMembers;
|
|
42601
43372
|
const memberTrackIds = (groupView?.members ?? []).map((m) => m.trackId);
|
|
42602
|
-
const firstLook = groupTiles !== null ? null : await this.resolveAttachment(entry,
|
|
43373
|
+
const firstLook = groupTiles !== null ? null : await this.resolveAttachment(entry, canStillGainMedia(entry) ? "trigger-only" : "any");
|
|
42603
43374
|
const footagePending = this.renderFootage(entry);
|
|
42604
43375
|
const mosaic = groupView !== void 0 && groupTiles !== null ? await this.groupedMosaic(entry, groupView, groupTiles) : null;
|
|
42605
|
-
const still = mosaic !== null ? mosaic.attachment : firstLook !== null || entry.payload.media === "none" ? firstLook : await this.waitForStill(entry
|
|
43376
|
+
const still = mosaic !== null ? mosaic.attachment : firstLook !== null || entry.payload.media === "none" ? firstLook : await this.waitForStill(entry);
|
|
42606
43377
|
const grouped = groupView !== void 0 && groupMembers !== null ? {
|
|
42607
43378
|
windowKey: groupView.key,
|
|
42608
43379
|
members: groupView.members.length,
|
|
@@ -42610,17 +43381,8 @@ var NcDispatcher = class {
|
|
|
42610
43381
|
} : null;
|
|
42611
43382
|
const footage = await footagePending;
|
|
42612
43383
|
const extras = await this.linkedExtraStills(entry);
|
|
42613
|
-
if (still !== null)
|
|
42614
|
-
|
|
42615
|
-
if (groupTiles === null && zoneIds !== void 0 && zoneIds.length > 0) {
|
|
42616
|
-
const cropped = await this.zoneCrop(entry.payload.subject.deviceId, zoneIds, still.bytes);
|
|
42617
|
-
out.push(cropped !== null ? {
|
|
42618
|
-
...still,
|
|
42619
|
-
bytes: cropped,
|
|
42620
|
-
name: "zone.jpg"
|
|
42621
|
-
} : still);
|
|
42622
|
-
} else out.push(still);
|
|
42623
|
-
} else if (entry.payload.media !== "none" && extras.length > 0) this.deps.logger.info("no still of the primary camera could be resolved — the other linked cameras' stills ship without it", {
|
|
43384
|
+
if (still !== null) out.push(still);
|
|
43385
|
+
else if (entry.payload.media !== "none" && extras.length > 0) this.deps.logger.info("no still of the primary camera could be resolved — the other linked cameras' stills ship without it", {
|
|
42624
43386
|
tags: { deviceId: entry.payload.subject.deviceId },
|
|
42625
43387
|
meta: {
|
|
42626
43388
|
recordKind: entry.recordKind,
|
|
@@ -42636,7 +43398,7 @@ var NcDispatcher = class {
|
|
|
42636
43398
|
recordKind: entry.recordKind,
|
|
42637
43399
|
recordId: entry.recordId,
|
|
42638
43400
|
policy: entry.payload.media,
|
|
42639
|
-
...entry.payload.
|
|
43401
|
+
...entry.payload.mediaImage !== void 0 ? { image: entry.payload.mediaImage } : {},
|
|
42640
43402
|
...memberTrackIds.length > 1 ? { memberTrackIds } : {},
|
|
42641
43403
|
owners: [...entry.payload.subject.eventId !== void 0 ? [`object:${entry.payload.subject.eventId}`] : [], ...entry.payload.subject.trackId !== void 0 ? [`track:${entry.payload.subject.trackId}`] : []]
|
|
42642
43404
|
}
|
|
@@ -42664,7 +43426,7 @@ var NcDispatcher = class {
|
|
|
42664
43426
|
for (const { ownerId, cameraId } of subject.extraStills ?? []) {
|
|
42665
43427
|
const file = await this.linkedStillFile(entry, ownerId, cameraId);
|
|
42666
43428
|
const bytes = file === void 0 ? null : decodeStillBytes(file.base64);
|
|
42667
|
-
if (bytes === null) {
|
|
43429
|
+
if (file === void 0 || bytes === null) {
|
|
42668
43430
|
this.deps.logger.info("linked camera picture missing at send — skipped", {
|
|
42669
43431
|
tags: { deviceId: cameraId },
|
|
42670
43432
|
meta: {
|
|
@@ -42675,9 +43437,21 @@ var NcDispatcher = class {
|
|
|
42675
43437
|
});
|
|
42676
43438
|
continue;
|
|
42677
43439
|
}
|
|
43440
|
+
const image = await this.imageStage.notificationImage({
|
|
43441
|
+
entry,
|
|
43442
|
+
source: {
|
|
43443
|
+
bytes,
|
|
43444
|
+
kind: file.kind,
|
|
43445
|
+
timestamp: file.timestamp,
|
|
43446
|
+
annotated: isAnnotatedSceneKind(file.kind)
|
|
43447
|
+
},
|
|
43448
|
+
camera: cameraId,
|
|
43449
|
+
withSubject: false
|
|
43450
|
+
});
|
|
43451
|
+
if (image === null) continue;
|
|
42678
43452
|
out.push({
|
|
42679
43453
|
mediaType: "image",
|
|
42680
|
-
bytes,
|
|
43454
|
+
bytes: image.bytes,
|
|
42681
43455
|
mime: "image/jpeg",
|
|
42682
43456
|
name: `camera-${String(cameraId)}.jpg`
|
|
42683
43457
|
});
|
|
@@ -43134,14 +43908,14 @@ var NcDispatcher = class {
|
|
|
43134
43908
|
* Bounded and then abandoned — a text-only notification about a person at the
|
|
43135
43909
|
* door beats a punctual one nobody received.
|
|
43136
43910
|
*/
|
|
43137
|
-
async waitForStill(entry
|
|
43911
|
+
async waitForStill(entry) {
|
|
43138
43912
|
const subject = entry.payload.subject;
|
|
43139
|
-
if (this.now() - subject.timestamp > NC_STILL_WAIT_MAX_AGE_MS) return this.resolveAttachment(entry,
|
|
43913
|
+
if (this.now() - subject.timestamp > NC_STILL_WAIT_MAX_AGE_MS) return this.resolveAttachment(entry, "any");
|
|
43140
43914
|
if (subject.eventId === void 0 && subject.trackId === void 0) return null;
|
|
43141
43915
|
const attempts = Math.floor(NC_STILL_WAIT_MS / NC_STILL_WAIT_INTERVAL_MS);
|
|
43142
43916
|
for (let i = 0; i < attempts; i += 1) {
|
|
43143
43917
|
await this.sleep(NC_STILL_WAIT_INTERVAL_MS);
|
|
43144
|
-
const still = await this.resolveAttachment(entry,
|
|
43918
|
+
const still = await this.resolveAttachment(entry, "trigger-only");
|
|
43145
43919
|
if (still !== null) {
|
|
43146
43920
|
this.deps.logger.info("the still landed during the bounded wait", {
|
|
43147
43921
|
tags: { deviceId: entry.payload.subject.deviceId },
|
|
@@ -43155,55 +43929,7 @@ var NcDispatcher = class {
|
|
|
43155
43929
|
return still;
|
|
43156
43930
|
}
|
|
43157
43931
|
}
|
|
43158
|
-
return this.resolveAttachment(entry,
|
|
43159
|
-
}
|
|
43160
|
-
/** Crop a JPEG to the padded bbox of the given zones (normalized polygons →
|
|
43161
|
-
* pixel rect via sharp metadata). Null on any failure — caller falls back
|
|
43162
|
-
* to the uncropped still. */
|
|
43163
|
-
async zoneCrop(deviceId, zoneIds, jpeg) {
|
|
43164
|
-
try {
|
|
43165
|
-
const points = (await this.deps.getZonePolygons?.(deviceId, zoneIds) ?? []).flat();
|
|
43166
|
-
if (points.length === 0) return null;
|
|
43167
|
-
const { default: sharp } = await import("sharp");
|
|
43168
|
-
const img = sharp(Buffer.from(jpeg));
|
|
43169
|
-
const meta = await img.metadata();
|
|
43170
|
-
const W = meta.width ?? 0;
|
|
43171
|
-
const H = meta.height ?? 0;
|
|
43172
|
-
if (W === 0 || H === 0) return null;
|
|
43173
|
-
let minX = 1;
|
|
43174
|
-
let minY = 1;
|
|
43175
|
-
let maxX = 0;
|
|
43176
|
-
let maxY = 0;
|
|
43177
|
-
for (const p of points) {
|
|
43178
|
-
if (p.x < minX) minX = p.x;
|
|
43179
|
-
if (p.y < minY) minY = p.y;
|
|
43180
|
-
if (p.x > maxX) maxX = p.x;
|
|
43181
|
-
if (p.y > maxY) maxY = p.y;
|
|
43182
|
-
}
|
|
43183
|
-
if (maxX <= minX || maxY <= minY) return null;
|
|
43184
|
-
const padX = (maxX - minX) * .1;
|
|
43185
|
-
const padY = (maxY - minY) * .1;
|
|
43186
|
-
const left = Math.max(0, Math.floor((minX - padX) * W));
|
|
43187
|
-
const top = Math.max(0, Math.floor((minY - padY) * H));
|
|
43188
|
-
const width = Math.min(W - left, Math.ceil((maxX - minX + 2 * padX) * W));
|
|
43189
|
-
const height = Math.min(H - top, Math.ceil((maxY - minY + 2 * padY) * H));
|
|
43190
|
-
if (width < 16 || height < 16) return null;
|
|
43191
|
-
const outBuf = await img.extract({
|
|
43192
|
-
left,
|
|
43193
|
-
top,
|
|
43194
|
-
width,
|
|
43195
|
-
height
|
|
43196
|
-
}).jpeg({ quality: 82 }).toBuffer();
|
|
43197
|
-
const bytes = new Uint8Array(outBuf.byteLength);
|
|
43198
|
-
bytes.set(outBuf);
|
|
43199
|
-
return bytes;
|
|
43200
|
-
} catch (err) {
|
|
43201
|
-
this.deps.logger.debug("zone crop failed — attaching uncropped still", { meta: {
|
|
43202
|
-
deviceId,
|
|
43203
|
-
error: String(err)
|
|
43204
|
-
} });
|
|
43205
|
-
return null;
|
|
43206
|
-
}
|
|
43932
|
+
return this.resolveAttachment(entry, "any");
|
|
43207
43933
|
}
|
|
43208
43934
|
/**
|
|
43209
43935
|
* The grouped MOSAIC (D692 §6.2): one tile per member, at most
|
|
@@ -43335,7 +44061,7 @@ var NcDispatcher = class {
|
|
|
43335
44061
|
const tileEntry = entries[index];
|
|
43336
44062
|
const m = tiles[index];
|
|
43337
44063
|
if (tileEntry === void 0 || m === void 0) return;
|
|
43338
|
-
const still = await this.resolveAttachment(tileEntry,
|
|
44064
|
+
const still = await this.resolveAttachment(tileEntry, freshness);
|
|
43339
44065
|
if (still !== null) found.set(index, {
|
|
43340
44066
|
trackId: m.trackId,
|
|
43341
44067
|
className: m.className,
|
|
@@ -43467,53 +44193,32 @@ var NcDispatcher = class {
|
|
|
43467
44193
|
return ordered;
|
|
43468
44194
|
}
|
|
43469
44195
|
/**
|
|
43470
|
-
*
|
|
44196
|
+
* The row's still: the CLEAN source frame of the trigger moment, rendered by
|
|
44197
|
+
* the row's picture mode (D897, `notification-image.ts`).
|
|
43471
44198
|
*
|
|
43472
|
-
* The
|
|
43473
|
-
*
|
|
43474
|
-
*
|
|
43475
|
-
*
|
|
43476
|
-
*
|
|
43477
|
-
* answers "why does my full-frame notification have a box on it".
|
|
44199
|
+
* The source ladder is the same for every mode — `cleanSourcePreference`,
|
|
44200
|
+
* the track owner first (the scene frames live there), then the object
|
|
44201
|
+
* event's — and MOMENT then PLACE filter it exactly as before. What the
|
|
44202
|
+
* rule asked to SEE is decided once, by the render, never by which stored
|
|
44203
|
+
* kind happened to land.
|
|
43478
44204
|
*/
|
|
43479
|
-
|
|
43480
|
-
if (
|
|
43481
|
-
if (this.reportedAnnotatedFallbacks.has(entry.recordId)) return;
|
|
43482
|
-
if (this.reportedAnnotatedFallbacks.size >= NC_ANNOTATED_FALLBACK_MEMO_MAX) this.reportedAnnotatedFallbacks.clear();
|
|
43483
|
-
this.reportedAnnotatedFallbacks.add(entry.recordId);
|
|
43484
|
-
this.deps.logger.warn("clean scene asked — only an annotated frame is stored, shipping it", {
|
|
43485
|
-
tags: { deviceId: entry.payload.subject.deviceId },
|
|
43486
|
-
meta: {
|
|
43487
|
-
ruleId: entry.ruleId,
|
|
43488
|
-
recordId: entry.recordId,
|
|
43489
|
-
ownerId,
|
|
43490
|
-
asked,
|
|
43491
|
-
kind
|
|
43492
|
-
}
|
|
43493
|
-
});
|
|
43494
|
-
}
|
|
43495
|
-
async resolveAttachment(entry, policyOverride, freshness = "any") {
|
|
43496
|
-
const policy = policyOverride ?? entry.payload.media;
|
|
43497
|
-
if (policy === "none") return null;
|
|
43498
|
-
const frame = policyOverride === void 0 ? entry.payload.mediaFrame : void 0;
|
|
44205
|
+
async resolveAttachment(entry, freshness = "any") {
|
|
44206
|
+
if (entry.payload.media === "none") return null;
|
|
43499
44207
|
const subject = entry.payload.subject;
|
|
43500
|
-
const signal = policy === "best-matching" ? matchSignal(entry.payload.matchedOn) : null;
|
|
43501
44208
|
const owners = [];
|
|
43502
|
-
if (subject.eventId !== void 0) owners.push({
|
|
43503
|
-
kind: "object",
|
|
43504
|
-
id: subject.eventId
|
|
43505
|
-
});
|
|
43506
44209
|
if (subject.trackId !== void 0) owners.push({
|
|
43507
44210
|
kind: "track",
|
|
43508
44211
|
id: subject.trackId
|
|
43509
44212
|
});
|
|
43510
|
-
if (
|
|
44213
|
+
if (subject.eventId !== void 0) owners.push({
|
|
44214
|
+
kind: "object",
|
|
44215
|
+
id: subject.eventId
|
|
44216
|
+
});
|
|
43511
44217
|
for (const owner of owners) try {
|
|
43512
44218
|
const files = owner.kind === "object" ? await this.deps.getMediaForOwner(owner.kind, owner.id, eventMediaScopeOf(subject)) : await this.deps.getMediaForOwner(owner.kind, owner.id);
|
|
43513
44219
|
if (files.length === 0) continue;
|
|
43514
|
-
const preference = frame !== void 0 ? framePreference(frame, owner.kind) : policy === "best-matching" ? bestMatchingKindPreference(signal, owner.kind) : attachmentKindPreference(policy, owner.kind);
|
|
43515
44220
|
const ordered = [];
|
|
43516
|
-
for (const kind of
|
|
44221
|
+
for (const kind of cleanSourcePreference(owner.kind)) for (const file of files) if (file.kind === kind) ordered.push(file);
|
|
43517
44222
|
const inBand = this.narrowToTriggerInstant(entry, ordered, freshness);
|
|
43518
44223
|
if (inBand.length === 0) continue;
|
|
43519
44224
|
const shortlist = await this.narrowToTriggerZone(entry, inBand, freshness);
|
|
@@ -43521,12 +44226,23 @@ var NcDispatcher = class {
|
|
|
43521
44226
|
for (const file of shortlist) {
|
|
43522
44227
|
const bytes = decodeStillBytes(file.base64);
|
|
43523
44228
|
if (bytes === null) continue;
|
|
43524
|
-
|
|
44229
|
+
const image = await this.imageStage.notificationImage({
|
|
44230
|
+
entry,
|
|
44231
|
+
source: {
|
|
44232
|
+
bytes,
|
|
44233
|
+
kind: file.kind,
|
|
44234
|
+
timestamp: file.timestamp,
|
|
44235
|
+
annotated: isAnnotatedSceneKind(file.kind)
|
|
44236
|
+
},
|
|
44237
|
+
camera: primaryImageCamera(entry.payload),
|
|
44238
|
+
withSubject: true
|
|
44239
|
+
});
|
|
44240
|
+
if (image === null) return null;
|
|
43525
44241
|
return {
|
|
43526
44242
|
mediaType: "image",
|
|
43527
|
-
bytes,
|
|
44243
|
+
bytes: image.bytes,
|
|
43528
44244
|
mime: "image/jpeg",
|
|
43529
|
-
name:
|
|
44245
|
+
name: image.name
|
|
43530
44246
|
};
|
|
43531
44247
|
}
|
|
43532
44248
|
} catch (err) {
|
|
@@ -48330,7 +49046,20 @@ function bodyToRule(id, raw, onProjection) {
|
|
|
48330
49046
|
});
|
|
48331
49047
|
if (!parsed.success) return null;
|
|
48332
49048
|
if (projection.kind === "projected") onProjection?.(id, projection);
|
|
48333
|
-
return parsed.data;
|
|
49049
|
+
return withNormalizedMedia(parsed.data);
|
|
49050
|
+
}
|
|
49051
|
+
/**
|
|
49052
|
+
* D897: the rule with its media policy in the stored shape — `image` set, the
|
|
49053
|
+
* legacy `attach` / `frame` / `zoneCrop` folded into it. Applied on LOAD (a read
|
|
49054
|
+
* projection: a rule nobody saved since keeps its row as written) and on every
|
|
49055
|
+
* WRITE, so the first save of a rule converges its row and an editor that still
|
|
49056
|
+
* sends the legacy triple is folded on the way in.
|
|
49057
|
+
*/
|
|
49058
|
+
function withNormalizedMedia(rule) {
|
|
49059
|
+
return {
|
|
49060
|
+
...rule,
|
|
49061
|
+
media: normalizeNcMediaPolicy(rule.media, rule)
|
|
49062
|
+
};
|
|
48334
49063
|
}
|
|
48335
49064
|
function isLegacyDeps$2(deps) {
|
|
48336
49065
|
return "store" in deps;
|
|
@@ -48607,14 +49336,14 @@ var NcRuleStore = class NcRuleStore {
|
|
|
48607
49336
|
/** Create a new rule. `createdBy` is the SERVER-injected caller userId. */
|
|
48608
49337
|
async create(input, createdBy) {
|
|
48609
49338
|
const now = this.now();
|
|
48610
|
-
const rule = {
|
|
49339
|
+
const rule = withNormalizedMedia({
|
|
48611
49340
|
...input,
|
|
48612
49341
|
id: this.newId(),
|
|
48613
49342
|
createdBy,
|
|
48614
49343
|
createdAt: now,
|
|
48615
49344
|
updatedAt: now,
|
|
48616
49345
|
disabledTargetIds: []
|
|
48617
|
-
};
|
|
49346
|
+
});
|
|
48618
49347
|
await this.write(rule);
|
|
48619
49348
|
return rule;
|
|
48620
49349
|
}
|
|
@@ -48652,7 +49381,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
48652
49381
|
createdAt: existing.createdAt,
|
|
48653
49382
|
updatedAt: this.now()
|
|
48654
49383
|
};
|
|
48655
|
-
const updated = NcRuleSchema.parse(candidate);
|
|
49384
|
+
const updated = withNormalizedMedia(NcRuleSchema.parse(candidate));
|
|
48656
49385
|
await this.write(updated);
|
|
48657
49386
|
return updated;
|
|
48658
49387
|
}
|
|
@@ -55213,19 +55942,20 @@ var NC_ALARM_ANNOUNCE_RULE_ID = "nc-alarm-announce";
|
|
|
55213
55942
|
* every row a rule addresses AND by the alarm's own announcement row when it is
|
|
55214
55943
|
* the COMBINED siren (the triggering rule's pictures on the alarm's words).
|
|
55215
55944
|
*
|
|
55216
|
-
* `effectiveMediaPolicyForRule`
|
|
55217
|
-
*
|
|
55218
|
-
*
|
|
55219
|
-
*
|
|
55220
|
-
*
|
|
55945
|
+
* `effectiveMediaPolicyForRule` resolves the picture MODE (D897) — a sensor
|
|
55946
|
+
* rule's included: it goes through the same selector and render stage as a
|
|
55947
|
+
* detection rule. `triggerZoneIds` are the zones of the rule that owns the
|
|
55948
|
+
* media (the SOURCE rule of a combined alarm row), and a zone overlay is
|
|
55949
|
+
* frozen only when the rule asked for one AND fired on a zone.
|
|
55221
55950
|
*/
|
|
55222
55951
|
function mediaPolicyFields(mediaRule, cropZoneIds) {
|
|
55223
55952
|
const media = effectiveMediaPolicyForRule(mediaRule);
|
|
55953
|
+
const image = media.image ?? "crop";
|
|
55224
55954
|
return {
|
|
55225
|
-
media:
|
|
55226
|
-
|
|
55955
|
+
media: image === "none" ? "none" : "best",
|
|
55956
|
+
mediaImage: image,
|
|
55957
|
+
...media.showTriggerZone === true && cropZoneIds !== void 0 && cropZoneIds.length > 0 ? { mediaShowTriggerZone: true } : {},
|
|
55227
55958
|
...cropZoneIds !== void 0 ? { triggerZoneIds: [...cropZoneIds] } : {},
|
|
55228
|
-
...media.frame !== void 0 ? { mediaFrame: media.frame } : {},
|
|
55229
55959
|
...media.profile !== void 0 ? { mediaProfile: media.profile } : {},
|
|
55230
55960
|
...media.gif === true ? { mediaGif: true } : {},
|
|
55231
55961
|
...media.clip === true ? { mediaClip: true } : {},
|
|
@@ -55265,6 +55995,36 @@ function alarmButtonKind(kind) {
|
|
|
55265
55995
|
default: return null;
|
|
55266
55996
|
}
|
|
55267
55997
|
}
|
|
55998
|
+
/**
|
|
55999
|
+
* Is a bypassed (rule, input) pair still an input of that rule? The rule must
|
|
56000
|
+
* exist, be enabled, still press the panel, and — when it names devices —
|
|
56001
|
+
* still name the input or its camera, and not exclude the input. A false
|
|
56002
|
+
* "no" re-arms the input (protection restored), which is the safe direction.
|
|
56003
|
+
*/
|
|
56004
|
+
function alarmRuleStillHasInput(rule, entry) {
|
|
56005
|
+
if (rule === null || !rule.enabled || !ruleAutoTriggersAlarm(rule)) return false;
|
|
56006
|
+
if ((rule.conditions.devicesExclude ?? []).includes(entry.deviceId)) return false;
|
|
56007
|
+
const scope = rule.conditions.devices;
|
|
56008
|
+
if (scope === void 0 || scope.length === 0) return true;
|
|
56009
|
+
return scope.includes(entry.deviceId) || scope.includes(entry.cameraId);
|
|
56010
|
+
}
|
|
56011
|
+
/**
|
|
56012
|
+
* The (rule, input) pair an `alarm-triggered` / `alarm-pending` row is about,
|
|
56013
|
+
* from the source it FROZE (`alarmSource`). Undefined on a manual trigger.
|
|
56014
|
+
*/
|
|
56015
|
+
function rowBypassableInput(systemEvent) {
|
|
56016
|
+
const kind = systemEvent?.kind;
|
|
56017
|
+
if (kind !== "alarm-triggered" && kind !== "alarm-pending") return void 0;
|
|
56018
|
+
const source = systemEvent?.alarmSource;
|
|
56019
|
+
if (source === void 0) return void 0;
|
|
56020
|
+
return {
|
|
56021
|
+
ruleId: source.ruleId,
|
|
56022
|
+
ruleName: source.ruleName,
|
|
56023
|
+
inputDeviceId: intrusionDeviceOf(source),
|
|
56024
|
+
cameraId: source.deviceId,
|
|
56025
|
+
notedAt: source.timestamp
|
|
56026
|
+
};
|
|
56027
|
+
}
|
|
55268
56028
|
/** The refusal id an `alarm-arm-refused` row froze (`incomingFromAlarmTransition`). */
|
|
55269
56029
|
function rowRefusalId(systemEvent) {
|
|
55270
56030
|
const value = systemEvent?.data["refusalId"];
|
|
@@ -55280,6 +56040,12 @@ var ARM_ANYWAY_REJECTION_COPY = {
|
|
|
55280
56040
|
used: "Already used",
|
|
55281
56041
|
"unknown-refusal": "This refusal is no longer current — arm again"
|
|
55282
56042
|
};
|
|
56043
|
+
/** What a pressed Bypass that did nothing tells the person holding the phone. */
|
|
56044
|
+
var BYPASS_INPUT_REFUSAL_COPY = {
|
|
56045
|
+
disarmed: "The alarm is disarmed — nothing to bypass",
|
|
56046
|
+
stale: "The alarm was re-armed since this notification — nothing was bypassed",
|
|
56047
|
+
full: "Too many inputs are bypassed already — disarm to reset them"
|
|
56048
|
+
};
|
|
55283
56049
|
/**
|
|
55284
56050
|
* What the sustained-state alerts run on when nothing wired the settings in.
|
|
55285
56051
|
*
|
|
@@ -55665,9 +56431,6 @@ function renderAudioScope(devices, excluded) {
|
|
|
55665
56431
|
if (devices.length === 0) return excluded.length === 0 ? "@all" : `@all-except:${excluded.join("+")}`;
|
|
55666
56432
|
return devices.filter((id) => !excluded.includes(id)).join("+");
|
|
55667
56433
|
}
|
|
55668
|
-
/** {@link NotificationCenter.reportedSensorMediaIgnored} cap, same shape as
|
|
55669
|
-
* the dispatcher's own annotated-fallback memo. */
|
|
55670
|
-
var NC_SENSOR_MEDIA_IGNORED_MEMO_MAX = 5e3;
|
|
55671
56434
|
/**
|
|
55672
56435
|
* One KEPT member as the payload freezes it (§6.1): its latest name, its own first-matched
|
|
55673
56436
|
* event identity (D694 — the event's STORED timestamp, never a clock), its qualifying
|
|
@@ -55728,9 +56491,6 @@ var NotificationCenter = class {
|
|
|
55728
56491
|
dispatcher;
|
|
55729
56492
|
deps;
|
|
55730
56493
|
now;
|
|
55731
|
-
/** Rule ids whose stored (non-default) media policy has already been
|
|
55732
|
-
* reported as ignored at evaluation — ONE line per rule, not per fire. */
|
|
55733
|
-
reportedSensorMediaIgnored = /* @__PURE__ */ new Set();
|
|
55734
56494
|
/** Samples the device-compat guard's "accepted unchecked" WARN per rule. */
|
|
55735
56495
|
deviceCompatWarnSampler = new NcDeviceCompatWarnSampler();
|
|
55736
56496
|
/**
|
|
@@ -56100,6 +56860,7 @@ var NotificationCenter = class {
|
|
|
56100
56860
|
targetId: input.targetId
|
|
56101
56861
|
}
|
|
56102
56862
|
});
|
|
56863
|
+
const bypassable = rowBypassableInput(input.systemEvent);
|
|
56103
56864
|
const alarmButtons = alarmKind === null ? [] : buildAlarmButtons({
|
|
56104
56865
|
kind: alarmKind,
|
|
56105
56866
|
panel: panel === null ? null : {
|
|
@@ -56107,10 +56868,12 @@ var NotificationCenter = class {
|
|
|
56107
56868
|
availableModes: panel.availableModes()
|
|
56108
56869
|
},
|
|
56109
56870
|
...refusalId !== void 0 ? { refusalId } : {},
|
|
56871
|
+
...bypassable !== void 0 ? { bypassable } : {},
|
|
56110
56872
|
labels: {
|
|
56111
56873
|
disarm: this.texts.text({ key: "button.alarm.disarm" }),
|
|
56112
56874
|
arm: (mode) => this.texts.text({ key: `button.alarm.arm.${mode}` }),
|
|
56113
|
-
armAnyway: this.texts.text({ key: "button.alarm.armAnyway" })
|
|
56875
|
+
armAnyway: this.texts.text({ key: "button.alarm.armAnyway" }),
|
|
56876
|
+
bypassInput: this.texts.text({ key: "button.alarm.bypassInput" })
|
|
56114
56877
|
},
|
|
56115
56878
|
mintUrl: (button) => {
|
|
56116
56879
|
const id = randomUUID();
|
|
@@ -56147,6 +56910,16 @@ var NotificationCenter = class {
|
|
|
56147
56910
|
action: "disarm",
|
|
56148
56911
|
expiresAt
|
|
56149
56912
|
};
|
|
56913
|
+
case "bypass-input": return {
|
|
56914
|
+
...common,
|
|
56915
|
+
action: "bypass-input",
|
|
56916
|
+
ruleId: button.ruleId,
|
|
56917
|
+
ruleName: button.ruleName,
|
|
56918
|
+
inputDeviceId: button.inputDeviceId,
|
|
56919
|
+
cameraId: button.cameraId,
|
|
56920
|
+
notedAt: button.notedAt,
|
|
56921
|
+
expiresAt
|
|
56922
|
+
};
|
|
56150
56923
|
}
|
|
56151
56924
|
}
|
|
56152
56925
|
});
|
|
@@ -56334,6 +57107,7 @@ var NotificationCenter = class {
|
|
|
56334
57107
|
if (panel === null) throw new Error("this node has no alarm panel");
|
|
56335
57108
|
if (panel.deviceId !== grant.panelDeviceId) throw new Error("this button was minted for a different alarm panel");
|
|
56336
57109
|
if (grant.action === "arm-anyway") return this.applyGrantedArmAnyway(panel, grant);
|
|
57110
|
+
if (grant.action === "bypass-input") return this.applyGrantedBypassInput(panel, grant);
|
|
56337
57111
|
if (this.deps.actuate === void 0) throw new Error("no actuator is wired on this node");
|
|
56338
57112
|
await this.actuateWithAttribution({
|
|
56339
57113
|
deviceId: panel.deviceId,
|
|
@@ -56370,6 +57144,32 @@ var NotificationCenter = class {
|
|
|
56370
57144
|
return;
|
|
56371
57145
|
}
|
|
56372
57146
|
}
|
|
57147
|
+
/**
|
|
57148
|
+
* "Bypass this input until disarm", from a trigger notification — the (rule,
|
|
57149
|
+
* input) pair the grant froze, on the panel, in-process. The token is the
|
|
57150
|
+
* whole of the authority (D47), exactly as for the Disarm beside it; the
|
|
57151
|
+
* panel refuses a disarmed alarm and an arm session that is already over.
|
|
57152
|
+
*/
|
|
57153
|
+
async applyGrantedBypassInput(panel, grant) {
|
|
57154
|
+
const outcome = panel.bypassInput({
|
|
57155
|
+
ruleId: grant.ruleId,
|
|
57156
|
+
ruleName: grant.ruleName,
|
|
57157
|
+
deviceId: grant.inputDeviceId,
|
|
57158
|
+
cameraId: grant.cameraId,
|
|
57159
|
+
notedAt: grant.notedAt
|
|
57160
|
+
});
|
|
57161
|
+
if (outcome.outcome === "refused") throw new NcActionRefusedError(BYPASS_INPUT_REFUSAL_COPY[outcome.reason]);
|
|
57162
|
+
this.logger.info("alarm input bypassed from a notification button", {
|
|
57163
|
+
tags: { deviceId: grant.inputDeviceId },
|
|
57164
|
+
meta: {
|
|
57165
|
+
panelDeviceId: panel.deviceId,
|
|
57166
|
+
ruleId: grant.ruleId,
|
|
57167
|
+
rule: grant.ruleName,
|
|
57168
|
+
outcome: outcome.outcome,
|
|
57169
|
+
targetId: grant.targetId ?? null
|
|
57170
|
+
}
|
|
57171
|
+
});
|
|
57172
|
+
}
|
|
56373
57173
|
/** The registry the action plane claims against. */
|
|
56374
57174
|
get actionTokenRegistry() {
|
|
56375
57175
|
return this.actionTokens;
|
|
@@ -57777,7 +58577,7 @@ var NotificationCenter = class {
|
|
|
57777
58577
|
const shown = this.unmutedCameras(cameraIds);
|
|
57778
58578
|
const involved = [sensorId, ...rowShown];
|
|
57779
58579
|
if (!this.rules.listEnabled("device-event").some((rule) => {
|
|
57780
|
-
if (
|
|
58580
|
+
if (ruleShipsNoStill(rule)) return false;
|
|
57781
58581
|
if (!deviceEventRuleMayFireOnKind(rule, sensorKind, sensorRaised)) return false;
|
|
57782
58582
|
const excluded = rule.conditions.devicesExclude;
|
|
57783
58583
|
if (excluded !== void 0 && deviceScopeExcludes(excluded, {
|
|
@@ -57921,7 +58721,7 @@ var NotificationCenter = class {
|
|
|
57921
58721
|
const shelf = this.stills;
|
|
57922
58722
|
if (shelf === null || !this.evaluationActive) return void 0;
|
|
57923
58723
|
for (const rule of this.rules.listEnabled("immediate")) {
|
|
57924
|
-
if (rule
|
|
58724
|
+
if (ruleShipsNoStill(rule)) continue;
|
|
57925
58725
|
if (!(mode === "condition" ? rule.conditions.audio !== void 0 : rule.conditions.audio === void 0 && referencesAudioClass(rule.conditions.classes))) continue;
|
|
57926
58726
|
const devices = rule.conditions.devices;
|
|
57927
58727
|
if (devices !== void 0 && devices.length > 0 && !devices.includes(deviceId)) continue;
|
|
@@ -58093,7 +58893,7 @@ var NotificationCenter = class {
|
|
|
58093
58893
|
ruleWantsOccupancyStill(edge) {
|
|
58094
58894
|
const subject = occupancySubjectFromEdge(edge);
|
|
58095
58895
|
for (const rule of this.rules.listEnabled("device-event")) {
|
|
58096
|
-
if (
|
|
58896
|
+
if (ruleShipsNoStill(rule)) continue;
|
|
58097
58897
|
const occ = rule.conditions.occupancy;
|
|
58098
58898
|
if (occ === void 0 || !matchesOccupancy(occ, subject)) continue;
|
|
58099
58899
|
const devices = rule.conditions.devices;
|
|
@@ -58389,6 +59189,10 @@ var NotificationCenter = class {
|
|
|
58389
59189
|
logger: this.logger,
|
|
58390
59190
|
by: caller.userId
|
|
58391
59191
|
});
|
|
59192
|
+
guardRuleMedia(rule, {
|
|
59193
|
+
logger: this.logger,
|
|
59194
|
+
by: caller.userId
|
|
59195
|
+
});
|
|
58392
59196
|
guardRuleDevicesExclude(rule, {
|
|
58393
59197
|
logger: this.logger,
|
|
58394
59198
|
by: caller.userId
|
|
@@ -58436,6 +59240,11 @@ var NotificationCenter = class {
|
|
|
58436
59240
|
by: caller.userId,
|
|
58437
59241
|
ruleId
|
|
58438
59242
|
});
|
|
59243
|
+
guardPatchMedia(existing, patch, {
|
|
59244
|
+
logger: this.logger,
|
|
59245
|
+
by: caller.userId,
|
|
59246
|
+
ruleId
|
|
59247
|
+
});
|
|
58439
59248
|
guardPatchDevicesExclude(existing, patch, {
|
|
58440
59249
|
logger: this.logger,
|
|
58441
59250
|
by: caller.userId,
|
|
@@ -58832,6 +59641,49 @@ var NotificationCenter = class {
|
|
|
58832
59641
|
meta: { userId }
|
|
58833
59642
|
});
|
|
58834
59643
|
},
|
|
59644
|
+
bypassInput: async ({ ruleId, deviceId }, userId) => {
|
|
59645
|
+
const panel = requirePanel();
|
|
59646
|
+
const rule = this.rules.get(ruleId) ?? null;
|
|
59647
|
+
if (rule === null || !alarmRuleStillHasInput(rule, {
|
|
59648
|
+
deviceId,
|
|
59649
|
+
cameraId: deviceId
|
|
59650
|
+
})) {
|
|
59651
|
+
this.logger.info("alarm input bypass refused — not an input of an alarm rule", {
|
|
59652
|
+
tags: { deviceId },
|
|
59653
|
+
meta: {
|
|
59654
|
+
userId,
|
|
59655
|
+
ruleId,
|
|
59656
|
+
panelDeviceId: panel.deviceId
|
|
59657
|
+
}
|
|
59658
|
+
});
|
|
59659
|
+
return {
|
|
59660
|
+
outcome: "refused",
|
|
59661
|
+
reason: "not-an-input"
|
|
59662
|
+
};
|
|
59663
|
+
}
|
|
59664
|
+
const outcome = panel.bypassInput({
|
|
59665
|
+
ruleId,
|
|
59666
|
+
ruleName: rule.name,
|
|
59667
|
+
deviceId,
|
|
59668
|
+
cameraId: deviceId
|
|
59669
|
+
});
|
|
59670
|
+
this.logger.info("alarm input bypass requested through the bridge", {
|
|
59671
|
+
tags: { deviceId },
|
|
59672
|
+
meta: {
|
|
59673
|
+
userId,
|
|
59674
|
+
ruleId,
|
|
59675
|
+
rule: rule.name,
|
|
59676
|
+
outcome: outcome.outcome
|
|
59677
|
+
}
|
|
59678
|
+
});
|
|
59679
|
+
return outcome.outcome === "refused" ? {
|
|
59680
|
+
outcome: "refused",
|
|
59681
|
+
reason: outcome.reason
|
|
59682
|
+
} : {
|
|
59683
|
+
outcome: outcome.outcome,
|
|
59684
|
+
reason: null
|
|
59685
|
+
};
|
|
59686
|
+
},
|
|
58835
59687
|
getConfig: () => this.alarmConfig(),
|
|
58836
59688
|
getSensors: (input) => this.alarmSensors(input),
|
|
58837
59689
|
setConfig: (patch) => this.setAlarmConfig(patch)
|
|
@@ -58908,6 +59760,8 @@ var NotificationCenter = class {
|
|
|
58908
59760
|
openings,
|
|
58909
59761
|
nonBlocking: panel.settings().nonBlocking,
|
|
58910
59762
|
exclusions: panel.exclusionPhases(),
|
|
59763
|
+
bypasses: panel.inputBypasses(),
|
|
59764
|
+
ruleName: (ruleId) => this.rules.get(ruleId)?.name,
|
|
58911
59765
|
name: this.deviceNameFromDirectory()
|
|
58912
59766
|
});
|
|
58913
59767
|
}
|
|
@@ -59224,7 +60078,7 @@ var NotificationCenter = class {
|
|
|
59224
60078
|
const subject = incoming.subject;
|
|
59225
60079
|
const alarmSource = subject.systemEvent?.alarmSource;
|
|
59226
60080
|
const sourceRule = alarmSource !== void 0 ? this.rules.get(alarmSource.ruleId) : null;
|
|
59227
|
-
const media = sourceRule !== null ? mediaPolicyFields(sourceRule,
|
|
60081
|
+
const media = sourceRule !== null ? mediaPolicyFields(sourceRule, ruleCropZoneIds(sourceRule)) : { media: "none" };
|
|
59228
60082
|
const mediaDeviceId = alarmSource?.deviceId ?? panel.deviceId;
|
|
59229
60083
|
const entries = targets.map((targetId) => ({
|
|
59230
60084
|
ruleId: NC_ALARM_ANNOUNCE_RULE_ID,
|
|
@@ -60064,7 +60918,7 @@ var NotificationCenter = class {
|
|
|
60064
60918
|
buttons: unresolvable.join(", ")
|
|
60065
60919
|
}
|
|
60066
60920
|
});
|
|
60067
|
-
if (!noOwnerReported && kind === "device-event" && subject.trackId === void 0 && subject.linkedStills === void 0 &&
|
|
60921
|
+
if (!noOwnerReported && kind === "device-event" && subject.trackId === void 0 && subject.linkedStills === void 0 && !ruleShipsNoStill(rule)) {
|
|
60068
60922
|
noOwnerReported = true;
|
|
60069
60923
|
const linked = subject.linkedCameraIds;
|
|
60070
60924
|
const meta = {
|
|
@@ -60097,7 +60951,7 @@ var NotificationCenter = class {
|
|
|
60097
60951
|
meta
|
|
60098
60952
|
});
|
|
60099
60953
|
}
|
|
60100
|
-
if (!noOwnerReported && (kind === "audio-window" || kind === "audio-event") && subject.trackId === void 0 && rule
|
|
60954
|
+
if (!noOwnerReported && (kind === "audio-window" || kind === "audio-event") && subject.trackId === void 0 && !ruleShipsNoStill(rule)) {
|
|
60101
60955
|
noOwnerReported = true;
|
|
60102
60956
|
this.logger.warn("audio match has no still owner — this notification ships text-only", {
|
|
60103
60957
|
tags: { deviceId: subject.deviceId },
|
|
@@ -60185,8 +61039,17 @@ var NotificationCenter = class {
|
|
|
60185
61039
|
* Materialise legacy inline `onTrigger` sequences into the shared store and
|
|
60186
61040
|
* rewrite each rule's actions to bindings + `buttons.sequenceId`. Runs on RAW
|
|
60187
61041
|
* rows before {@link NcRuleStore.load} so Zod never skips them.
|
|
61042
|
+
*
|
|
61043
|
+
* A rewrite of the LEGACY rules table only. A records-backed centre has no
|
|
61044
|
+
* such table to read (no boot declares it; the adoption mapper read it once,
|
|
61045
|
+
* after every earlier boot had already run this rewrite on it), so it skips —
|
|
61046
|
+
* reading it anyway failed on every boot ("is not declared").
|
|
60188
61047
|
*/
|
|
60189
61048
|
async migrateLegacyRuleActions() {
|
|
61049
|
+
if (this.deps.records !== void 0) {
|
|
61050
|
+
this.logger.debug("legacy actions migration not run — rules live on records", { meta: { collection: NC_RULES_COLLECTION } });
|
|
61051
|
+
return;
|
|
61052
|
+
}
|
|
60190
61053
|
let rows;
|
|
60191
61054
|
try {
|
|
60192
61055
|
rows = await this.deps.store.query.query({
|
|
@@ -60464,24 +61327,6 @@ var NotificationCenter = class {
|
|
|
60464
61327
|
}
|
|
60465
61328
|
});
|
|
60466
61329
|
}
|
|
60467
|
-
/**
|
|
60468
|
-
* ONE line per rule id, the first time a SENSOR rule's stored media policy
|
|
60469
|
-
* is about to be overridden by {@link effectiveMediaPolicyForRule}'s
|
|
60470
|
-
* one-snapshot default (operator ruling, 2026-10-01). Never refuses the
|
|
60471
|
-
* rule — it still loads and still fires — this only says the stored choice
|
|
60472
|
-
* is not the one being used.
|
|
60473
|
-
*/
|
|
60474
|
-
warnSensorMediaIgnoredOnce(rule) {
|
|
60475
|
-
if (mediaPolicyMatchesSensorDefault(rule.media)) return;
|
|
60476
|
-
if (this.reportedSensorMediaIgnored.has(rule.id)) return;
|
|
60477
|
-
if (this.reportedSensorMediaIgnored.size >= NC_SENSOR_MEDIA_IGNORED_MEMO_MAX) this.reportedSensorMediaIgnored.clear();
|
|
60478
|
-
this.reportedSensorMediaIgnored.add(rule.id);
|
|
60479
|
-
this.logger.info("sensor rule carries a media choice — ignored at evaluation, one snapshot is always attached", { meta: {
|
|
60480
|
-
ruleId: rule.id,
|
|
60481
|
-
rule: rule.name,
|
|
60482
|
-
storedMedia: rule.media
|
|
60483
|
-
} });
|
|
60484
|
-
}
|
|
60485
61330
|
buildEntries(rule, subject, kind, matchedOn, userTargets = [], origin = "pipeline", cooldown, extras) {
|
|
60486
61331
|
const hasEventMedia = kind === "object-event" || kind === "package-event";
|
|
60487
61332
|
const isTrackScoped = kind === "object-event" || kind === "track-end";
|
|
@@ -60489,11 +61334,10 @@ var NotificationCenter = class {
|
|
|
60489
61334
|
const alarmSource = subject.systemEvent?.alarmSource;
|
|
60490
61335
|
const mediaRule = alarmSource !== void 0 ? this.rules.get(alarmSource.ruleId) ?? rule : rule;
|
|
60491
61336
|
const noMedia = isSystemEvent && alarmSource === void 0;
|
|
60492
|
-
if (ruleKindOf(mediaRule) === "sensor") this.warnSensorMediaIgnoredOnce(mediaRule);
|
|
60493
61337
|
const direct = new Set(rule.targets.map((t) => t.targetId));
|
|
60494
61338
|
const renderedZones = notificationZones(rule, subject);
|
|
60495
61339
|
const confirm = frozenConfirmPolicy(rule, isSystemEvent);
|
|
60496
|
-
const cropZoneIds = ruleCropZoneIds(
|
|
61340
|
+
const cropZoneIds = ruleCropZoneIds(mediaRule);
|
|
60497
61341
|
const allTargets = [...rule.targets, ...userTargets.filter((t) => !direct.has(t.targetId))];
|
|
60498
61342
|
const linkedOwners = !noMedia && subject.linkedStills !== void 0 ? linkedMediaOwners(subject.linkedStills, linkedCameraMode(mediaRule.media)) : void 0;
|
|
60499
61343
|
const groupV2 = extras?.groupV2 ?? (subject.systemEvent !== void 0 && !noMedia ? alarmIncidentGroup(subject.systemEvent, subject.timestamp) : void 0);
|
|
@@ -60855,6 +61699,11 @@ var NotificationCenter = class {
|
|
|
60855
61699
|
* read cannot answer for, or that reads unknown, changes nothing (D49).
|
|
60856
61700
|
* Never throws into the tick.
|
|
60857
61701
|
*/
|
|
61702
|
+
pruneAlarmInputBypasses() {
|
|
61703
|
+
const panel = this.alarmPanel;
|
|
61704
|
+
if (panel === null || panel.inputBypasses().length === 0) return;
|
|
61705
|
+
panel.pruneInputBypasses((entry) => alarmRuleStillHasInput(this.rules.get(entry.ruleId) ?? null, entry));
|
|
61706
|
+
}
|
|
60858
61707
|
async observeAlarmOpenings() {
|
|
60859
61708
|
const panel = this.alarmPanel;
|
|
60860
61709
|
const read = this.deps.readDeviceOpenings;
|
|
@@ -61139,6 +61988,7 @@ var NotificationCenter = class {
|
|
|
61139
61988
|
await this.zoneOwners.refresh();
|
|
61140
61989
|
this.sceneStates.refresh();
|
|
61141
61990
|
await this.observeAlarmOpenings();
|
|
61991
|
+
this.pruneAlarmInputBypasses();
|
|
61142
61992
|
await this.occupancyStore.pruneExcept(this.activeOccupancyKeys());
|
|
61143
61993
|
if (!this.pruneLedger.isLoaded()) await this.pruneLedger.load();
|
|
61144
61994
|
await this.pruner.reconcileDevices();
|
|
@@ -98695,6 +99545,14 @@ function planeRunsStationary(source) {
|
|
|
98695
99545
|
//#endregion
|
|
98696
99546
|
//#region src/pipeline-analytics/pipeline/suppressed-births.ts
|
|
98697
99547
|
/**
|
|
99548
|
+
* How many rejected ids a device remembers AFTER the tracker stopped carrying
|
|
99549
|
+
* them. An id's `leaving` / zone-exit events fire for a track that is absent
|
|
99550
|
+
* from the frame by definition — the frame it is lost or a few frames later —
|
|
99551
|
+
* so the event path must still recognise it once `retain` dropped it. Track ids
|
|
99552
|
+
* are never reused; the bound only caps memory on a phantom-heavy camera.
|
|
99553
|
+
*/
|
|
99554
|
+
var RETIRED_REJECTIONS_PER_DEVICE = 256;
|
|
99555
|
+
/**
|
|
98698
99556
|
* Memory of births the confirmation gate rejected, per device.
|
|
98699
99557
|
*
|
|
98700
99558
|
* The gate runs AFTER the TrackStore upsert, and it has to: the upsert's
|
|
@@ -98716,6 +99574,8 @@ function planeRunsStationary(source) {
|
|
|
98716
99574
|
*/
|
|
98717
99575
|
var SuppressedBirthRegistry = class {
|
|
98718
99576
|
byDevice = /* @__PURE__ */ new Map();
|
|
99577
|
+
/** Rejected ids the tracker no longer carries, oldest first (bounded). */
|
|
99578
|
+
retired = /* @__PURE__ */ new Map();
|
|
98719
99579
|
/** Rejected ids whose per-frame skip has already been reported once. */
|
|
98720
99580
|
skipReported = /* @__PURE__ */ new Map();
|
|
98721
99581
|
/** Record that this birth was rejected; later frames must not re-upsert it. */
|
|
@@ -98731,6 +99591,14 @@ var SuppressedBirthRegistry = class {
|
|
|
98731
99591
|
return this.byDevice.get(deviceKey)?.has(trackId) === true;
|
|
98732
99592
|
}
|
|
98733
99593
|
/**
|
|
99594
|
+
* True when an event naming this id must be dropped: the birth was rejected,
|
|
99595
|
+
* whether the tracker still carries the id or already let it go. A rejected
|
|
99596
|
+
* birth is a track that never existed — it has no `left`, no zone exit.
|
|
99597
|
+
*/
|
|
99598
|
+
suppressesEvents(deviceKey, trackId) {
|
|
99599
|
+
return this.isRejected(deviceKey, trackId) || this.retired.get(deviceKey)?.has(trackId) === true;
|
|
99600
|
+
}
|
|
99601
|
+
/**
|
|
98734
99602
|
* True the FIRST time a rejected id's frame work is skipped (so the host logs
|
|
98735
99603
|
* one line per id, not one per frame); false afterwards.
|
|
98736
99604
|
*/
|
|
@@ -98748,7 +99616,9 @@ var SuppressedBirthRegistry = class {
|
|
|
98748
99616
|
* Forget every rejected id the tracker is no longer carrying.
|
|
98749
99617
|
*
|
|
98750
99618
|
* Called once per frame with the ids present THIS frame. An id absent from
|
|
98751
|
-
* that set can never be upserted again, so
|
|
99619
|
+
* that set can never be upserted again, so it leaves the live set — but it
|
|
99620
|
+
* moves to the bounded retired memory, because its `leaving` / zone-exit
|
|
99621
|
+
* events fire exactly when it is absent (`suppressesEvents`).
|
|
98752
99622
|
*/
|
|
98753
99623
|
retain(deviceKey, currentTrackIds) {
|
|
98754
99624
|
const reported = this.skipReported.get(deviceKey);
|
|
@@ -98758,9 +99628,26 @@ var SuppressedBirthRegistry = class {
|
|
|
98758
99628
|
}
|
|
98759
99629
|
const ids = this.byDevice.get(deviceKey);
|
|
98760
99630
|
if (!ids) return;
|
|
98761
|
-
for (const id of ids)
|
|
99631
|
+
for (const id of ids) {
|
|
99632
|
+
if (currentTrackIds.has(id)) continue;
|
|
99633
|
+
ids.delete(id);
|
|
99634
|
+
this.retire(deviceKey, id);
|
|
99635
|
+
}
|
|
98762
99636
|
if (ids.size === 0) this.byDevice.delete(deviceKey);
|
|
98763
99637
|
}
|
|
99638
|
+
retire(deviceKey, trackId) {
|
|
99639
|
+
let ids = this.retired.get(deviceKey);
|
|
99640
|
+
if (!ids) {
|
|
99641
|
+
ids = /* @__PURE__ */ new Set();
|
|
99642
|
+
this.retired.set(deviceKey, ids);
|
|
99643
|
+
}
|
|
99644
|
+
ids.add(trackId);
|
|
99645
|
+
while (ids.size > RETIRED_REJECTIONS_PER_DEVICE) {
|
|
99646
|
+
const oldest = ids.values().next();
|
|
99647
|
+
if (oldest.done === true) break;
|
|
99648
|
+
ids.delete(oldest.value);
|
|
99649
|
+
}
|
|
99650
|
+
}
|
|
98764
99651
|
/** Rejected ids currently held for a device — diagnostics and tests. */
|
|
98765
99652
|
size(deviceKey) {
|
|
98766
99653
|
return this.byDevice.get(deviceKey)?.size ?? 0;
|
|
@@ -98769,6 +99656,7 @@ var SuppressedBirthRegistry = class {
|
|
|
98769
99656
|
clearDevice(deviceKey) {
|
|
98770
99657
|
this.byDevice.delete(deviceKey);
|
|
98771
99658
|
this.skipReported.delete(deviceKey);
|
|
99659
|
+
this.retired.delete(deviceKey);
|
|
98772
99660
|
}
|
|
98773
99661
|
};
|
|
98774
99662
|
//#endregion
|
|
@@ -107039,6 +107927,10 @@ Object.freeze({
|
|
|
107039
107927
|
"resolution": "auto",
|
|
107040
107928
|
"maxPoints": 1e3
|
|
107041
107929
|
},
|
|
107930
|
+
"queryMany": {
|
|
107931
|
+
"resolution": "auto",
|
|
107932
|
+
"maxPoints": 1e3
|
|
107933
|
+
},
|
|
107042
107934
|
"queryNode": {
|
|
107043
107935
|
"resolution": "auto",
|
|
107044
107936
|
"maxPoints": 1e3
|
|
@@ -107097,7 +107989,7 @@ Object.freeze({
|
|
|
107097
107989
|
"createRule": { "rule": { "__nested__": {
|
|
107098
107990
|
"enabled": true,
|
|
107099
107991
|
"conditions": {},
|
|
107100
|
-
"media": {
|
|
107992
|
+
"media": {},
|
|
107101
107993
|
"throttle": {
|
|
107102
107994
|
"cooldownSec": 60,
|
|
107103
107995
|
"scope": "rule-device"
|
|
@@ -107108,7 +108000,7 @@ Object.freeze({
|
|
|
107108
108000
|
"previewStateSummary": { "rule": { "__nested__": {
|
|
107109
108001
|
"enabled": true,
|
|
107110
108002
|
"conditions": {},
|
|
107111
|
-
"media": {
|
|
108003
|
+
"media": {},
|
|
107112
108004
|
"throttle": {
|
|
107113
108005
|
"cooldownSec": 60,
|
|
107114
108006
|
"scope": "rule-device"
|
|
@@ -107119,7 +108011,7 @@ Object.freeze({
|
|
|
107119
108011
|
"rule": { "__nested__": {
|
|
107120
108012
|
"enabled": true,
|
|
107121
108013
|
"conditions": {},
|
|
107122
|
-
"media": {
|
|
108014
|
+
"media": {},
|
|
107123
108015
|
"throttle": {
|
|
107124
108016
|
"cooldownSec": 60,
|
|
107125
108017
|
"scope": "rule-device"
|
|
@@ -107312,7 +108204,6 @@ Object.freeze({
|
|
|
107312
108204
|
],
|
|
107313
108205
|
"load-contribution": ["list"],
|
|
107314
108206
|
"log-channels": ["list"],
|
|
107315
|
-
"log-destination": ["query"],
|
|
107316
108207
|
"login-method": ["getLoginMethods"],
|
|
107317
108208
|
"mqtt-broker": ["listBrokers"],
|
|
107318
108209
|
"network-access": ["listEndpoints"],
|
|
@@ -107334,12 +108225,19 @@ Object.freeze({
|
|
|
107334
108225
|
"user-passkeys": ["listPasskeys"],
|
|
107335
108226
|
"videoclips": ["listClips", "listSources"]
|
|
107336
108227
|
});
|
|
107337
|
-
Object.freeze({
|
|
107338
|
-
"
|
|
107339
|
-
|
|
107340
|
-
|
|
107341
|
-
|
|
107342
|
-
]
|
|
108228
|
+
Object.freeze({
|
|
108229
|
+
"log-destination": [
|
|
108230
|
+
"cancelRead",
|
|
108231
|
+
"describeRead",
|
|
108232
|
+
"read"
|
|
108233
|
+
],
|
|
108234
|
+
"videoclips": [
|
|
108235
|
+
"dialClipStream",
|
|
108236
|
+
"getPlaybackOptions",
|
|
108237
|
+
"offerClipBytes",
|
|
108238
|
+
"readClipBytes"
|
|
108239
|
+
]
|
|
108240
|
+
});
|
|
107343
108241
|
/** cap name -> the numeric state field whose strict advance IS the cap's one-shot event. */
|
|
107344
108242
|
var CAP_ADVANCE_FIELDS = Object.freeze({ "doorbell": "lastPressedAt" });
|
|
107345
108243
|
Object.freeze({
|
|
@@ -117293,6 +118191,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
117293
118191
|
}
|
|
117294
118192
|
}));
|
|
117295
118193
|
},
|
|
118194
|
+
getTracksAt: async (deviceId, timestamp, toleranceMs) => {
|
|
118195
|
+
const live = stores.trackStore.getActive(deviceId);
|
|
118196
|
+
const persisted = await stores.trackStore.queryRecent({
|
|
118197
|
+
deviceIds: [deviceId],
|
|
118198
|
+
since: timestamp - toleranceMs,
|
|
118199
|
+
limit: NC_TRACKS_AT_INSTANT_LIMIT
|
|
118200
|
+
});
|
|
118201
|
+
return tracksAtInstant([...live, ...persisted.tracks], timestamp, toleranceMs, this.lastFrameDimsByDevice.get(deviceId));
|
|
118202
|
+
},
|
|
117296
118203
|
getDeviceName,
|
|
117297
118204
|
getDeviceLocation,
|
|
117298
118205
|
buildActions: (input) => this.notificationCenter?.mintButtons(input) ?? Promise.resolve([])
|
|
@@ -119658,12 +120565,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
119658
120565
|
zones: computeStationaryEntryZones(v, liveZones, trk.zoneMembershipMinOverlap),
|
|
119659
120566
|
standing: true
|
|
119660
120567
|
}));
|
|
120568
|
+
const occupancyTracked = result.tracked.filter((t) => !this.suppressedBirths.isRejected(key, t.trackId));
|
|
119661
120569
|
this.zoneAnalytics?.recordFrame({
|
|
119662
120570
|
deviceId,
|
|
119663
120571
|
timestamp: result.timestamp,
|
|
119664
120572
|
frameWidth: result.frameWidth,
|
|
119665
120573
|
frameHeight: result.frameHeight,
|
|
119666
|
-
tracked: stationaryAsTracked.length > 0 ? [...
|
|
120574
|
+
tracked: stationaryAsTracked.length > 0 ? [...occupancyTracked, ...stationaryAsTracked] : occupancyTracked,
|
|
119667
120575
|
zones: liveZones,
|
|
119668
120576
|
...stationaryViews.length > 0 ? { stationaryObjects: stationaryViews } : {}
|
|
119669
120577
|
});
|
|
@@ -120020,12 +120928,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
120020
120928
|
}
|
|
120021
120929
|
});
|
|
120022
120930
|
}
|
|
120023
|
-
|
|
120931
|
+
const keptObjectEvents = this.dropSuppressedBirthEvents(deviceId, key, result.objectEvents);
|
|
120932
|
+
const keptAppearanceEvents = this.dropSuppressedBirthEvents(deviceId, key, result.appearanceEvents);
|
|
120933
|
+
if (rejectedThisFrame.size > 0 || keptObjectEvents !== result.objectEvents || keptAppearanceEvents !== result.appearanceEvents) result = {
|
|
120024
120934
|
...result,
|
|
120025
120935
|
tracked: result.tracked.filter((t) => !rejectedThisFrame.has(t.trackId)),
|
|
120026
120936
|
rawTrackedDetections: result.rawTrackedDetections.filter((rt) => !rejectedThisFrame.has(rt.trackId)),
|
|
120027
|
-
objectEvents:
|
|
120028
|
-
appearanceEvents:
|
|
120937
|
+
objectEvents: keptObjectEvents,
|
|
120938
|
+
appearanceEvents: keptAppearanceEvents
|
|
120029
120939
|
};
|
|
120030
120940
|
if (currentTrackIds.size > 0) this.lastTrackActivityMs.set(deviceId, Date.now());
|
|
120031
120941
|
const stationarySettings = this.stationarySettingsFromCache(deviceId);
|
|
@@ -120485,6 +121395,35 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
120485
121395
|
* invented id would put `event:NaN` in a media key. A branch that accepts
|
|
120486
121396
|
* work and produces nothing must log it.
|
|
120487
121397
|
*/
|
|
121398
|
+
/**
|
|
121399
|
+
* The events of `drafts` whose track's birth the confirmation gate rejected,
|
|
121400
|
+
* removed — the SAME array when none is, so a caller can tell. A rejected
|
|
121401
|
+
* birth is a track that never existed: it has no event row, no media and no
|
|
121402
|
+
* notification, on any frame. Each drop is logged (bounded by event rate):
|
|
121403
|
+
* an accepted event that silently vanishes reads as "never happened" (D391).
|
|
121404
|
+
*/
|
|
121405
|
+
dropSuppressedBirthEvents(deviceId, key, drafts) {
|
|
121406
|
+
const isSuppressed = (e) => e.trackId !== void 0 && this.suppressedBirths.suppressesEvents(key, e.trackId);
|
|
121407
|
+
if (!drafts.some(isSuppressed)) return drafts;
|
|
121408
|
+
const kept = [];
|
|
121409
|
+
for (const e of drafts) {
|
|
121410
|
+
if (!isSuppressed(e)) {
|
|
121411
|
+
kept.push(e);
|
|
121412
|
+
continue;
|
|
121413
|
+
}
|
|
121414
|
+
this.ctx.logger.info("object event dropped — the track birth was suppressed", {
|
|
121415
|
+
tags: { deviceId },
|
|
121416
|
+
meta: {
|
|
121417
|
+
trackId: e.trackId,
|
|
121418
|
+
className: e.className,
|
|
121419
|
+
state: e.state,
|
|
121420
|
+
zones: e.zones,
|
|
121421
|
+
reason: "birth-suppressed"
|
|
121422
|
+
}
|
|
121423
|
+
});
|
|
121424
|
+
}
|
|
121425
|
+
return kept;
|
|
121426
|
+
}
|
|
120488
121427
|
async persistObjectEvents(deviceId, drafts) {
|
|
120489
121428
|
if (drafts.length === 0) return [];
|
|
120490
121429
|
const store = this.eventStore;
|