@camstack/addon-post-analysis 1.2.620 → 1.2.622
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-BBzoIoKO.mjs → clip-model-registry-EDTOCKN9.mjs} +234 -4
- package/dist/{clip-model-registry-CcgzpELQ.js → clip-model-registry-MHJ9BSUV.js} +239 -3
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BWQTWUYq.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DAyVukQp.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CTvx3PDS.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-WiCYRDId.mjs → hostInit-C4-dCKYR.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +559 -75
- package/dist/pipeline-analytics/index.mjs +559 -75
- 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-BSMSev-b.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as MediaFileKindEnum, $i as
|
|
1
|
+
import { $ as MediaFileKindEnum, $i as hydrateSchema, $n as cosineSimilarity$1, $r as projectSubjectPath, $t as OpsLogEntrySchema, A as DeclaredDevices, Ai as systemEventFilterApplies, An as activateAlarmProfile, Ar as isEventOwnerType, At as NcAlarmSensorsInputSchema, B as FULL_IMAGE_BBOX, Bi as canonicalHash, Bn as applySecureOnArmEdit, Br as kebabToCamel, Bt as NcRuleUpdateInputSchema, C as ClearTrackRelationResultSchema, Ci as subKindsOf, Cn as TimelapseRulePatchSchema, Cr as isAlertEpisodeSeverity, Ct as NC_SNOOZE_MAX_MINUTES, D as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Di as syncActiveAlarmProfileSettings, Dn as TrackRelationSchema, Dr as isDetectionMacroClass, Dt as NC_TEMPLATE_VARS, E as DEFAULT_EVENT_COLOR, Ei as summarizeReview, En as TrackRelationKindSchema, Er as isDefaultAlarmProfileId, Et as NC_TAXONOMY, F as EVENT_DENSITY_BATCH_MAX, Fi as vectorDimFromBase64, Fn as alertEpisodeKind, Fr as isRootSuppliableStepId, Ft as NcRuleActionSchema, G as ListTrackRelationsResultSchema, Gi as BaseAddon, Gn as buildEventKindDescriptor, Gr as normalizeNcMediaPolicy, Gt as NcSnoozeSuppressedSchema, H as LABEL_SUPPLY_MACRO, Hi as isSourceCap, Hn as assertTimelapseCadences, Hr as mayWriteToLocation, Ht as NcSharedSequenceSchema, I as EVENT_KIND_BY_CAP, Ii as viewerLinkBaseOf, In as alertEpisodePayload, Ir as isScheduleActive, It as NcRuleInputSchema, J as MAX_BIRTH_DECISION_RECORDS, Ji as RECORD_QUERY_MAX, Jn as calendarEventLocalSpan, Jr as parseAlertEpisodePayload, Jt as NcTaxonomySchema, K as LockStateSchema, Ki as CamProfileSchema, Kn as buildSubjectPath, Kr as notificationRulesCapability, Kt as NcStateSummarySectionIdSchema, L as EVENT_OWNER_TYPES, Li as withSystemEventClasses, Ln as applyBlockingEdit, Lr as isSystemDelivery, Lt as NcRulePatchSchema, M as EMBEDDING_MODEL_FAILED, Mi as systemEventSummarySplitOf, Mn as alarmDeviceSecureOnArm, Mr as isLabelSupplyStepId, Mt as NcAlarmSettingsPatchSchema, N as EMBEDDING_REQUIREMENTS_FAILED, Ni as templateCarriesSummary, Nn as alarmPanelCapability, Nr as isMediaOwnerType, Nt as NcConditionDescriptorSchema, O as DETECTION_MACRO_CLASSES, Oi as systemEventDeliveryProblems, On as TrackSideSchema, Or as isDrawableSubjectPath, Ot as NcAlarmConfigSchema, P as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Pi as templateVarsFor, Pn as alertEpisodeId, Pr as isRetiredClearKey, Pt as NcConditionsSchema, Q as MOTION_CLOSE_AFTER_MS, Qi as defineRecordKinds, Qn as coreBlockIdFromAddonId, Qr as plateGalleryCapability, Qt as OPS_LOG_DEFAULT_LIMIT, R as EXPORT_DENSE_MAX_RANGES, Ri as zoneAnalyticsCapability, Rn as applyModeDelaysEdits, Rr as isSystemEventSummaryRule, Rt as NcRuleSchema, S as ClearTrackRelationInputSchema, Si as storageOccupancyCapability, Sn as TimelapseRuleInputSchema, Sr as isAlertEpisodeOpen, St as NC_SENSOR_TRANSITION_KINDS, T as DEFAULT_DETAIL_CROP_CONVENTION, Ti as subjectPathSvgPoints, Tn as TrackPuritySchema, Tr as isAudioClassName, Tt as NC_TANK_SYSTEM_EVENT_KINDS, U as LabelAttributionSchema, Ui as DeviceType, Un as audioMetricsCapability, Ur as memoizePinnedFfmpeg, Ut as NcSnoozeInputSchema, V as FailureCounters, Vi as evaluateSensorEdge, Vn as armRemovalReconcile, Vr as linkedStepOf, Vt as NcScheduleSchema, W as ListTrackRelationsInputSchema, Wi as errMsg, Wn as audioModeOf, Wr as ncMediaPolicyError, Wt as NcSnoozeSchema, X as MAX_TRACK_PURITIES, Xi as customAction, Xn as cloneAlarmSettings, Xr as pickFaceRescoreIntervalMinutes, Xt as NcTemplatePreviewSchema, Y as MAX_BIRTH_LATENCY_PROXY_MS, Yi as createEvent, Yn as classifyRetrainReview, Yt as NcTemplatePreviewInputSchema, Z as MAX_TRACK_RELATIONS, Zi as defineCustomActions, Zn as consumableLowPercentOf, Zr as pipelineAnalyticsCapability, Zt as NcTemplateVarDescriptorSchema, _ as BIRTH_DECISION_DEFAULT_LIMIT, _a as templateLiteral, _i as sceneMonitorCapability, _n as SetTrackPurityInputSchema, _r as faceGalleryCapability, _t as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, aa as _enum, ai as resolveLocationMode, an as RetrainOriginSchema, ar as describeSystemEventDeliveryProblem, at as NC_ALARM_NOTIFY_FIELDS, b as BucketAccumulator, ba as unknown, bi as servedTemplateVars, bn as TIMELAPSE_DENSE_FLOOR_SEC, br as gifFromMp4Args, bt as NC_GROUPING_MAX_SEC, c as readClusterModelPinStrict, ca as discriminatedUnion, ci as resolveNcMediaImage, cn as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, ct as NC_AUDIO_CONFIRM_HITS_DEFAULT, d as APPEARANCE_BLOCKS, da as number, di as rootSupplyServes, dn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, dt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, ea as isDeviceScopedCap, ei as readDeviceStateFrom, er as createAlarmProfile, et as MotionTriggeredBySchema, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fa as object, fi as ruleAutoTriggersAlarm, fn as SCENE_DEFAULT_QUIET_SECONDS, ft as NC_CLIP_RETENTION_MAX_SEC, g as ArchivedDebugNoteSchema, ga as string, gi as ruleKindOf, gn as SceneMonitorSchema, gr as evictionPolicyOfLocation, gt as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, h as AlarmArmModeSchema, ha as strictObject, hi as ruleGroupingApplies, hn as SCENE_RESET_RECAPTURES, hr as eventKindLabel, ht as NC_CONFIRM_DEFAULT_TIMEOUT_MS, ia as ZodObject, ii as resolveClipSearchMinScore, in as RetrainDismissedSchema, ir as deriveRecordingMode, it as NC_ALARM_DELAY_MAX_SEC, j as DetectionSourceSchema, ji as systemEventSummaryScheduleOf, jn as addonWidgetsSourceCapability, jr as isKnownTemplateVar, jt as NcAlarmSensorsSchema, k as DETECTION_PIPELINE_CAP_NAME, ki as systemEventFamilyProblems, kn as TrackSourceSchema, kr as isEventKindLabelKey, kt as NcAlarmPostTriggeredActionSchema, l as resolveClusterModelPin, la as literal, ln as SCENE_DEFAULT_ANCHOR_THRESHOLD, lt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, m as AUDIO_MACRO_LABELS, ma as record, mi as ruleDeviceCompatCapQueries, mn as SCENE_DIVERGED, mr as ensureAlarmProfiles, mt as NC_CONFIRM_DEFAULT_MAX_IMAGE_PX, ni as readTimelapseGeneratedAt, nn as RECORDING_SOURCE_CAMSTACK_ADDON, nr as deriveDetailCropRect, nt as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, o as CLUSTER_MODEL_OWNER_ADDON_ID, oa as array, oi as resolveModeDelays, on as RetrainStatusSchema, or as describeSystemEventFamilyProblem, ot as NC_ALARM_SYSTEM_EVENT_KINDS, p as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, pa as partialRecord, pi as ruleBelongsToAlarm$1, pn as SCENE_DEFAULT_UNCOVERED_POLICY, pr as encodeVectorBase64, pt as NC_CONDITION_CATALOG, q as MAX_ARCHIVED_DEBUG_NOTES, qi as RECORD_MAX_DATA_BYTES, qn as buildViewerLink, qr as occupancyScope, qt as NcSystemEventKindSchema, ra as sleep$1, ri as renameAlarmProfile, rn as RebuildObjectEmbeddingsRefusalSchema, rr as derivePostTriggeredAction, rt as NC_ALARM_DELAY_FIELDS, s as FACE_EMBEDDING_STEP_ID, sa as boolean, si as resolveModeNotifyPolicy, sn as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, sr as deviceRemovalHookCapability, st as NC_ALARM_TRIGGERED_MAX_SEC, t as BUILTIN_CLIP_MODELS, ta as nodePin, ti as readMotionTriggeredBy, tn as RECORDING_EXPORT_MAX_READ_BYTES, tr as deleteAlarmProfile, tt as NC_ALARM_DEFAULT_PROFILE_ID, u as APPEARANCE_BIN_COUNT, ua as looseObject, ui as retrainRevisionConflictMessage, un as SCENE_DEFAULT_CHECK_INTERVAL_SEC, ur as embeddingRefusalOf, ut as NC_CLIP_POST_ROLL_DEFAULT_SEC, v as BaseDevice, va as tuple, vi as secureActuationFor, vn as SetTrackPurityResultSchema, vr as failureContributionCapability, vt as NC_DEFAULT_SNOOZE_MINUTES, w as CoverStateSchema, wi as subjectPathSampleFromBox, wn as TimelapseRuleSchema, wr as isAppLinkBase, wt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, x as COCO_TO_MACRO, xa as EventCategory, xi as startMoverUnderClearance, xn as TRACK_RELATIONS_DEFAULT_LIMIT, xr as isAlarmControllableCap, xt as NC_GROUPING_MIN_SEC, y as BirthDecisionRecordSchema, ya as union, yi as sensorRaisedOf, yn as SetTrackRelationInputSchema, yr as gifDeriveSpeed, yt as NC_GROUPING_DEFAULT_SEC, z as FIRST_LEVEL_MACRO_CLASSES, zi as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, zn as applyNonBlockingEdit, zr as isVectorIndexDimConflict, zt as NcRuleTargetSchema } from "../clip-model-registry-EDTOCKN9.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";
|
|
@@ -5394,15 +5394,28 @@ async function notificationLinkEndpoints(api, logger) {
|
|
|
5394
5394
|
*/
|
|
5395
5395
|
var NC_TRACKS_AT_INSTANT_LIMIT = 25;
|
|
5396
5396
|
/**
|
|
5397
|
-
*
|
|
5398
|
-
*
|
|
5399
|
-
*
|
|
5397
|
+
* How far outside its own life a track may still be drawn: one detection tick.
|
|
5398
|
+
*
|
|
5399
|
+
* The box TOLERANCE (`toleranceMs`, 750 ms) is about how sparse a trail is —
|
|
5400
|
+
* a live track keeps a position every few frames — and it must never stretch
|
|
5401
|
+
* a LIFE. A track born after the frame was not on it: on 2026-10-22 the
|
|
5402
|
+
* tracker split a dog on camera 615 and the second piece, born 676 ms after
|
|
5403
|
+
* the key frame, was drawn at its first box as a second box on the dog. One
|
|
5404
|
+
* tick (~110–150 ms at that camera's detection rate, rounded up) absorbs the
|
|
5405
|
+
* gap between a frame and the detection that confirmed it, and nothing more.
|
|
5406
|
+
*/
|
|
5407
|
+
var NC_TRACK_LIFE_SLACK_MS = 200;
|
|
5408
|
+
/**
|
|
5409
|
+
* Every source track ALIVE at `timestamp` (± {@link NC_TRACK_LIFE_SLACK_MS})
|
|
5410
|
+
* with a position within `toleranceMs` of it, its nearest box normalised,
|
|
5411
|
+
* deduplicated by track id (a live copy and a persisted copy of one track are
|
|
5412
|
+
* one track). Pure.
|
|
5400
5413
|
*/
|
|
5401
5414
|
function tracksAtInstant(sources, timestamp, toleranceMs, fallbackDims) {
|
|
5402
5415
|
const out = /* @__PURE__ */ new Map();
|
|
5403
5416
|
for (const track of sources) {
|
|
5404
5417
|
if (out.has(track.trackId)) continue;
|
|
5405
|
-
if (track.firstSeen > timestamp +
|
|
5418
|
+
if (track.firstSeen > timestamp + NC_TRACK_LIFE_SLACK_MS || track.lastSeen < timestamp - NC_TRACK_LIFE_SLACK_MS) continue;
|
|
5406
5419
|
const w = track.frameWidth ?? fallbackDims?.w ?? 0;
|
|
5407
5420
|
const h = track.frameHeight ?? fallbackDims?.h ?? 0;
|
|
5408
5421
|
if (w <= 0 || h <= 0) continue;
|
|
@@ -20443,8 +20456,8 @@ var FACE_REF_COLUMNS = ["deviceId"];
|
|
|
20443
20456
|
* D312) a predicate that silently matched nothing. The faces ledger therefore derives a
|
|
20444
20457
|
* boolean `recognized` from every write that touches `recognizedIdentityId`
|
|
20445
20458
|
* (`face-recognized.ts`), and the views are plain ranges over the `recognized_ts` /
|
|
20446
|
-
* `assigned_recognized_ts` indexes.
|
|
20447
|
-
*
|
|
20459
|
+
* `assigned_recognized_ts` indexes. Rows written before the field existed were
|
|
20460
|
+
* backfilled offline on the operator hubs on 2026-10-22 (the one-shot script is deleted).
|
|
20448
20461
|
*
|
|
20449
20462
|
* `assigned` is `BOOLEAN NOT NULL`; the backend serialises `false` to `0` at
|
|
20450
20463
|
* the bind (`serializeColumnValue`) and decodes `0` back to `false` on read, so
|
|
@@ -28489,6 +28502,33 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
28489
28502
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
28490
28503
|
});
|
|
28491
28504
|
}
|
|
28505
|
+
case "notification.rule-broken":
|
|
28506
|
+
case "notification.rule-repaired": {
|
|
28507
|
+
const ruleId = stringField(data, "ruleId");
|
|
28508
|
+
if (ruleId === void 0) return null;
|
|
28509
|
+
const broken = event.category === "notification.rule-broken";
|
|
28510
|
+
const kind = broken ? "rule-broken" : "rule-repaired";
|
|
28511
|
+
const ruleName = stringField(data, "ruleName") ?? ruleId;
|
|
28512
|
+
const reason = stringField(data, "reason") ?? "unknown";
|
|
28513
|
+
const ids = data["offendingIds"];
|
|
28514
|
+
const offendingIds = Array.isArray(ids) ? ids.filter((id) => typeof id === "string").join(", ") : "";
|
|
28515
|
+
return systemSubject({
|
|
28516
|
+
facts,
|
|
28517
|
+
kind,
|
|
28518
|
+
subject: `rule-health:${ruleId}`,
|
|
28519
|
+
titleKey: `system.${kind}.title`,
|
|
28520
|
+
bodyKey: broken ? `system.rule-broken.body.${reason}` : "system.rule-repaired.body",
|
|
28521
|
+
textParams: broken ? {
|
|
28522
|
+
ruleId,
|
|
28523
|
+
ruleName,
|
|
28524
|
+
brokenReason: reason,
|
|
28525
|
+
offendingIds
|
|
28526
|
+
} : {
|
|
28527
|
+
ruleId,
|
|
28528
|
+
ruleName
|
|
28529
|
+
}
|
|
28530
|
+
});
|
|
28531
|
+
}
|
|
28492
28532
|
case "update.available": return incomingFromUpdateAvailable(facts, data);
|
|
28493
28533
|
case "addon.updated": {
|
|
28494
28534
|
const update = parseAddonUpdateComplete(data);
|
|
@@ -30494,8 +30534,8 @@ var en_default = {
|
|
|
30494
30534
|
"occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
|
|
30495
30535
|
"occupancy.op.occupied": "occupied",
|
|
30496
30536
|
"occupancy.op.free": "free",
|
|
30497
|
-
"system.device-online.title": "
|
|
30498
|
-
"system.device-online.body": "Device {{deviceName}} is online",
|
|
30537
|
+
"system.device-online.title": "Back online — {{deviceName}}",
|
|
30538
|
+
"system.device-online.body": "Device {{deviceName}} is back online",
|
|
30499
30539
|
"system.device-offline.title": "Offline — {{deviceName}}",
|
|
30500
30540
|
"system.device-offline.body": "Device {{deviceName}} is offline",
|
|
30501
30541
|
"system.device-disabled.title": "Switched off — {{deviceName}}",
|
|
@@ -30508,14 +30548,14 @@ var en_default = {
|
|
|
30508
30548
|
"system.device-battery-normal.title": "Battery back to normal — {{deviceName}}",
|
|
30509
30549
|
"system.device-battery-normal.body": "{{deviceName}} is back to {{percentage}}% battery",
|
|
30510
30550
|
"system.device-battery-normal.body.binary": "{{deviceName}} no longer reports a low battery",
|
|
30511
|
-
"system.stream-online.title": "Stream online — {{deviceName}}",
|
|
30512
|
-
"system.stream-online.body": "Stream for camera {{deviceName}} is online",
|
|
30513
|
-
"system.stream-online.body.profiled": "{{profile}} stream for camera {{deviceName}} is online",
|
|
30551
|
+
"system.stream-online.title": "Stream back online — {{deviceName}}",
|
|
30552
|
+
"system.stream-online.body": "Stream for camera {{deviceName}} is back online",
|
|
30553
|
+
"system.stream-online.body.profiled": "{{profile}} stream for camera {{deviceName}} is back online",
|
|
30514
30554
|
"system.stream-offline.title": "Stream offline — {{deviceName}}",
|
|
30515
30555
|
"system.stream-offline.body": "Stream for camera {{deviceName}} is offline",
|
|
30516
30556
|
"system.stream-offline.body.profiled": "{{profile}} stream for camera {{deviceName}} is offline",
|
|
30517
|
-
"system.node-online.title": "Node online — {{nodeName}}",
|
|
30518
|
-
"system.node-online.body.agent": "Agent {{nodeName}} is online",
|
|
30557
|
+
"system.node-online.title": "Node back online — {{nodeName}}",
|
|
30558
|
+
"system.node-online.body.agent": "Agent {{nodeName}} is back online",
|
|
30519
30559
|
"system.node-online.body.agent.after": "Agent {{nodeName}} is back online after {{offlineFor}} offline",
|
|
30520
30560
|
"system.node-offline.title": "Node offline — {{nodeName}}",
|
|
30521
30561
|
"system.node-offline.body.agent": "Agent {{nodeName}} is offline",
|
|
@@ -30535,7 +30575,7 @@ var en_default = {
|
|
|
30535
30575
|
"one": "Camera {{deviceName}} has not written a recording segment for {{minutes}} minute",
|
|
30536
30576
|
"other": "Camera {{deviceName}} has not written a recording segment for {{minutes}} minutes"
|
|
30537
30577
|
},
|
|
30538
|
-
"system.recording-healthy.title": "Recording
|
|
30578
|
+
"system.recording-healthy.title": "Recording again — {{deviceName}}",
|
|
30539
30579
|
"system.recording-healthy.body": "Camera {{deviceName}} is writing recording segments again",
|
|
30540
30580
|
"system.recording-degraded.title": "Recording degraded — {{deviceName}}",
|
|
30541
30581
|
"system.recording-degraded.body": "Camera {{deviceName}} exhausted its writer restart budget ({{message}})",
|
|
@@ -30796,7 +30836,13 @@ var en_default = {
|
|
|
30796
30836
|
"calendar-events.line.timed": "• {{when}} {{title}}{{where}}",
|
|
30797
30837
|
"calendar-events.line.allDay": "• All day {{title}}{{where}}",
|
|
30798
30838
|
"calendar-events.where": " @ {{location}}",
|
|
30799
|
-
"calendar-events.more": "…and {{count}} more"
|
|
30839
|
+
"calendar-events.more": "…and {{count}} more",
|
|
30840
|
+
"system.rule-broken.title": "Notification rule broken — {{ruleName}}",
|
|
30841
|
+
"system.rule-broken.body.unparseable": "The rule \"{{ruleName}}\" cannot be loaded and sends nothing. Open it and save it again.",
|
|
30842
|
+
"system.rule-broken.body.device-deleted": "The rule \"{{ruleName}}\" still names a deleted device ({{offendingIds}}) and was switched off. Pick the device again.",
|
|
30843
|
+
"system.rule-broken.body.target-deleted": "The rule \"{{ruleName}}\" still sends to a deleted destination ({{offendingIds}}) and was switched off. Pick a destination again.",
|
|
30844
|
+
"system.rule-repaired.title": "Notification rule works again — {{ruleName}}",
|
|
30845
|
+
"system.rule-repaired.body": "The rule \"{{ruleName}}\" loads and names nothing deleted any more."
|
|
30800
30846
|
};
|
|
30801
30847
|
//#endregion
|
|
30802
30848
|
//#region src/notification-center/texts/it.json
|
|
@@ -30881,8 +30927,8 @@ var it_default = {
|
|
|
30881
30927
|
"occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
|
|
30882
30928
|
"occupancy.op.occupied": "occupata",
|
|
30883
30929
|
"occupancy.op.free": "libera",
|
|
30884
|
-
"system.device-online.title": "
|
|
30885
|
-
"system.device-online.body": "Il dispositivo {{deviceName}} è online",
|
|
30930
|
+
"system.device-online.title": "Di nuovo online — {{deviceName}}",
|
|
30931
|
+
"system.device-online.body": "Il dispositivo {{deviceName}} è di nuovo online",
|
|
30886
30932
|
"system.device-offline.title": "Offline — {{deviceName}}",
|
|
30887
30933
|
"system.device-offline.body": "Il dispositivo {{deviceName}} è offline",
|
|
30888
30934
|
"system.device-disabled.title": "Spento — {{deviceName}}",
|
|
@@ -30895,14 +30941,14 @@ var it_default = {
|
|
|
30895
30941
|
"system.device-battery-normal.title": "Batteria rientrata — {{deviceName}}",
|
|
30896
30942
|
"system.device-battery-normal.body": "{{deviceName}} è risalito al {{percentage}}% di batteria",
|
|
30897
30943
|
"system.device-battery-normal.body.binary": "{{deviceName}} non segnala più la batteria scarica",
|
|
30898
|
-
"system.stream-online.title": "Stream online — {{deviceName}}",
|
|
30899
|
-
"system.stream-online.body": "Lo stream della camera {{deviceName}} è online",
|
|
30900
|
-
"system.stream-online.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è online",
|
|
30944
|
+
"system.stream-online.title": "Stream di nuovo online — {{deviceName}}",
|
|
30945
|
+
"system.stream-online.body": "Lo stream della camera {{deviceName}} è di nuovo online",
|
|
30946
|
+
"system.stream-online.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è di nuovo online",
|
|
30901
30947
|
"system.stream-offline.title": "Stream offline — {{deviceName}}",
|
|
30902
30948
|
"system.stream-offline.body": "Lo stream della camera {{deviceName}} è offline",
|
|
30903
30949
|
"system.stream-offline.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è offline",
|
|
30904
|
-
"system.node-online.title": "Nodo online — {{nodeName}}",
|
|
30905
|
-
"system.node-online.body.agent": "L'agent {{nodeName}} è online",
|
|
30950
|
+
"system.node-online.title": "Nodo di nuovo online — {{nodeName}}",
|
|
30951
|
+
"system.node-online.body.agent": "L'agent {{nodeName}} è di nuovo online",
|
|
30906
30952
|
"system.node-online.body.agent.after": "L'agent {{nodeName}} è di nuovo online dopo {{offlineFor}} offline",
|
|
30907
30953
|
"system.node-offline.title": "Nodo offline — {{nodeName}}",
|
|
30908
30954
|
"system.node-offline.body.agent": "L'agent {{nodeName}} è offline",
|
|
@@ -30917,7 +30963,7 @@ var it_default = {
|
|
|
30917
30963
|
"one": "La camera {{deviceName}} non scrive un segmento da {{minutes}} minuto",
|
|
30918
30964
|
"other": "La camera {{deviceName}} non scrive un segmento da {{minutes}} minuti"
|
|
30919
30965
|
},
|
|
30920
|
-
"system.recording-healthy.title": "
|
|
30966
|
+
"system.recording-healthy.title": "Registra di nuovo — {{deviceName}}",
|
|
30921
30967
|
"system.recording-healthy.body": "La camera {{deviceName}} scrive di nuovo i segmenti",
|
|
30922
30968
|
"system.recording-degraded.title": "Registrazione degradata — {{deviceName}}",
|
|
30923
30969
|
"system.recording-degraded.body": "La camera {{deviceName}} ha esaurito i riavvii del writer ({{message}})",
|
|
@@ -31183,7 +31229,13 @@ var it_default = {
|
|
|
31183
31229
|
"calendar-events.line.timed": "• {{when}} {{title}}{{where}}",
|
|
31184
31230
|
"calendar-events.line.allDay": "• Tutto il giorno {{title}}{{where}}",
|
|
31185
31231
|
"calendar-events.where": " @ {{location}}",
|
|
31186
|
-
"calendar-events.more": "…e altri {{count}}"
|
|
31232
|
+
"calendar-events.more": "…e altri {{count}}",
|
|
31233
|
+
"system.rule-broken.title": "Regola di notifica rotta — {{ruleName}}",
|
|
31234
|
+
"system.rule-broken.body.unparseable": "La regola \"{{ruleName}}\" non si carica e non invia nulla. Aprila e salvala di nuovo.",
|
|
31235
|
+
"system.rule-broken.body.device-deleted": "La regola \"{{ruleName}}\" usa ancora un dispositivo eliminato ({{offendingIds}}) ed è stata disattivata. Scegli di nuovo il dispositivo.",
|
|
31236
|
+
"system.rule-broken.body.target-deleted": "La regola \"{{ruleName}}\" invia ancora a una destinazione eliminata ({{offendingIds}}) ed è stata disattivata. Scegli di nuovo una destinazione.",
|
|
31237
|
+
"system.rule-repaired.title": "Regola di notifica di nuovo funzionante — {{ruleName}}",
|
|
31238
|
+
"system.rule-repaired.body": "La regola \"{{ruleName}}\" si carica e non usa più nulla di eliminato."
|
|
31187
31239
|
};
|
|
31188
31240
|
//#endregion
|
|
31189
31241
|
//#region src/notification-center/text-catalog.ts
|
|
@@ -33454,6 +33506,10 @@ function systemEventKeys(kind) {
|
|
|
33454
33506
|
titleKey: `system.${kind}.title`,
|
|
33455
33507
|
bodyKey: `system.${kind}.body.cleanWater`
|
|
33456
33508
|
};
|
|
33509
|
+
if (kind === "rule-broken") return {
|
|
33510
|
+
titleKey: `system.${kind}.title`,
|
|
33511
|
+
bodyKey: `system.${kind}.body.device-deleted`
|
|
33512
|
+
};
|
|
33457
33513
|
return {
|
|
33458
33514
|
titleKey: `system.${kind}.title`,
|
|
33459
33515
|
bodyKey: `system.${kind}.body`
|
|
@@ -39694,6 +39750,99 @@ var NcRecordedCutMemo = class {
|
|
|
39694
39750
|
}
|
|
39695
39751
|
};
|
|
39696
39752
|
//#endregion
|
|
39753
|
+
//#region src/notification-center/notification-label-layout.ts
|
|
39754
|
+
/**
|
|
39755
|
+
* Where each box's caption goes on a notification picture — pure geometry,
|
|
39756
|
+
* rects in, placements out (D897 follow-up, 2026-10-22).
|
|
39757
|
+
*
|
|
39758
|
+
* A caption drawn INSIDE its box used to cover the subject whole on a small
|
|
39759
|
+
* box, and a caption next to a crowd landed on a neighbour's box. The rule:
|
|
39760
|
+
*
|
|
39761
|
+
* 1. Try, in order: OUTSIDE above the box (inside the picture), OUTSIDE below
|
|
39762
|
+
* it, then INSIDE its top edge — the last only when the box is big enough
|
|
39763
|
+
* that the plate is a thin band of it: plate height ≤
|
|
39764
|
+
* {@link NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION} of the box height and plate
|
|
39765
|
+
* width ≤ the box width. A fifth of the height leaves the head and torso of
|
|
39766
|
+
* an upright person, or most of a vehicle, visible; anything taller is the
|
|
39767
|
+
* caption hiding what it names.
|
|
39768
|
+
* 2. A candidate is refused when its plate intersects ANY other box drawn on
|
|
39769
|
+
* the picture, or a plate already placed.
|
|
39770
|
+
* 3. The subject is placed first; the other tracks follow by score, highest
|
|
39771
|
+
* first, so a crowd never costs the subject its caption.
|
|
39772
|
+
* 4. The font never shrinks to make a plate fit: no fitting candidate means
|
|
39773
|
+
* no caption for that box. A box without its caption still carries its
|
|
39774
|
+
* outline and colour.
|
|
39775
|
+
*
|
|
39776
|
+
* Coordinates are the picture's own pixels, whatever it is (a scene or a crop).
|
|
39777
|
+
*/
|
|
39778
|
+
/** The inside placement is allowed only when the plate is at most this fraction of the box height. */
|
|
39779
|
+
var NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION = .2;
|
|
39780
|
+
/** A caption is never drawn smaller than this, in output pixels. */
|
|
39781
|
+
var NC_LABEL_MIN_FONT_PX = 14;
|
|
39782
|
+
/** The dark plate a caption sits on, for this text at this font size. */
|
|
39783
|
+
function labelPlateSize(label, fontSize) {
|
|
39784
|
+
return {
|
|
39785
|
+
w: Math.round(label.length * fontSize * .62 + fontSize * .8),
|
|
39786
|
+
h: Math.round(fontSize * 1.35)
|
|
39787
|
+
};
|
|
39788
|
+
}
|
|
39789
|
+
/** Positive-area overlap; rectangles that only touch do not intersect. */
|
|
39790
|
+
function intersects(a, b) {
|
|
39791
|
+
return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
39792
|
+
}
|
|
39793
|
+
function captionOrder(boxes) {
|
|
39794
|
+
const rank = (b) => b.role === "subject" ? 0 : 1;
|
|
39795
|
+
return boxes.map((_, i) => i).sort((i, j) => {
|
|
39796
|
+
const a = boxes[i];
|
|
39797
|
+
const b = boxes[j];
|
|
39798
|
+
if (a === void 0 || b === void 0) return 0;
|
|
39799
|
+
return rank(a) - rank(b) || (b.score ?? 0) - (a.score ?? 0) || i - j;
|
|
39800
|
+
});
|
|
39801
|
+
}
|
|
39802
|
+
function candidates(box, plate, width, height) {
|
|
39803
|
+
if (plate.w > width) return [];
|
|
39804
|
+
const x = Math.min(box.x, Math.max(0, width - plate.w));
|
|
39805
|
+
const out = [];
|
|
39806
|
+
if (box.y - plate.h >= 0) out.push({
|
|
39807
|
+
x,
|
|
39808
|
+
y: box.y - plate.h,
|
|
39809
|
+
...plate,
|
|
39810
|
+
where: "above"
|
|
39811
|
+
});
|
|
39812
|
+
if (box.y + box.h + plate.h <= height) out.push({
|
|
39813
|
+
x,
|
|
39814
|
+
y: box.y + box.h,
|
|
39815
|
+
...plate,
|
|
39816
|
+
where: "below"
|
|
39817
|
+
});
|
|
39818
|
+
if (plate.h <= box.h * NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION && plate.w <= box.w) out.push({
|
|
39819
|
+
x,
|
|
39820
|
+
y: box.y,
|
|
39821
|
+
...plate,
|
|
39822
|
+
where: "inside"
|
|
39823
|
+
});
|
|
39824
|
+
return out;
|
|
39825
|
+
}
|
|
39826
|
+
/** Place every caption by the rule above. */
|
|
39827
|
+
function layoutBoxLabels(input) {
|
|
39828
|
+
const { width, height, boxes } = input;
|
|
39829
|
+
const fontSize = Math.max(NC_LABEL_MIN_FONT_PX, input.fontSize);
|
|
39830
|
+
const placements = /* @__PURE__ */ new Map();
|
|
39831
|
+
let dropped = 0;
|
|
39832
|
+
for (const i of captionOrder(boxes)) {
|
|
39833
|
+
const box = boxes[i];
|
|
39834
|
+
if (box === void 0 || box.label.length === 0) continue;
|
|
39835
|
+
const chosen = candidates(box, labelPlateSize(box.label, fontSize), width, height).find((c) => !boxes.some((other, j) => j !== i && intersects(c, other)) && ![...placements.values()].some((p) => intersects(c, p)));
|
|
39836
|
+
if (chosen === void 0) dropped += 1;
|
|
39837
|
+
else placements.set(i, chosen);
|
|
39838
|
+
}
|
|
39839
|
+
return {
|
|
39840
|
+
placements: boxes.map((_, i) => placements.get(i) ?? null),
|
|
39841
|
+
placed: placements.size,
|
|
39842
|
+
dropped
|
|
39843
|
+
};
|
|
39844
|
+
}
|
|
39845
|
+
//#endregion
|
|
39697
39846
|
//#region src/notification-center/trigger-zone-still.ts
|
|
39698
39847
|
/**
|
|
39699
39848
|
* How far a candidate's instant may sit from the nearest trail point before the
|
|
@@ -39816,6 +39965,15 @@ var NC_IMAGE_OTHER_COLOR = "#f2f2f2";
|
|
|
39816
39965
|
var NC_IMAGE_ZONE_COLOR = "#29b6f6";
|
|
39817
39966
|
/** Two tracks whose boxes overlap this much at one instant are the same subject (a fused id). */
|
|
39818
39967
|
var SAME_SUBJECT_IOU = .8;
|
|
39968
|
+
/**
|
|
39969
|
+
* A box of the SUBJECT'S CLASS overlapping the subject this much at one
|
|
39970
|
+
* instant is the subject itself — an unfused piece L2 has not joined yet
|
|
39971
|
+
* (D683 fuses 10–20 s later; a push leaves ~6 s after the trigger). The
|
|
39972
|
+
* detector's per-class NMS (`NMS_IOU_DEFAULT` 0.45) keeps one of two
|
|
39973
|
+
* same-class boxes above 0.45 in a frame, so two REAL subjects of one class
|
|
39974
|
+
* cannot overlap past it; 0.5 leaves a margin over that.
|
|
39975
|
+
*/
|
|
39976
|
+
var NC_IMAGE_SAME_CLASS_SUBJECT_IOU = .5;
|
|
39819
39977
|
var JPEG_QUALITY = 85;
|
|
39820
39978
|
/** The caption of a box: class (or label) and confidence — "dog 87%". Language-free. */
|
|
39821
39979
|
function imageLabelText(name, confidence) {
|
|
@@ -39848,15 +40006,21 @@ function toPixels(box, width, height) {
|
|
|
39848
40006
|
h: Math.max(1, Math.min(Math.round(box.h * height), height - y))
|
|
39849
40007
|
};
|
|
39850
40008
|
}
|
|
40009
|
+
/** {@link notificationOverlay}'s SVG alone. */
|
|
40010
|
+
function notificationOverlaySvg(input) {
|
|
40011
|
+
return notificationOverlay(input).svg;
|
|
40012
|
+
}
|
|
39851
40013
|
/**
|
|
39852
|
-
* The SVG drawn over a
|
|
39853
|
-
*
|
|
39854
|
-
*
|
|
40014
|
+
* The SVG drawn over a picture of `width` × `height`. The subject is drawn
|
|
40015
|
+
* LAST so a crowd never hides it. Captions sit on a dark plate, white text,
|
|
40016
|
+
* where `layoutBoxLabels` puts them — outside above, outside below, inside a
|
|
40017
|
+
* box only when it covers a thin band of it, never on another box or caption,
|
|
40018
|
+
* and nowhere at all rather than over the subject.
|
|
39855
40019
|
*/
|
|
39856
|
-
function
|
|
40020
|
+
function notificationOverlay(input) {
|
|
39857
40021
|
const { width, height } = input;
|
|
39858
40022
|
const strokes = overlayStrokes(width, height);
|
|
39859
|
-
const fontSize = Math.max(
|
|
40023
|
+
const fontSize = Math.max(NC_LABEL_MIN_FONT_PX, Math.round(height / 36));
|
|
39860
40024
|
const parts = [];
|
|
39861
40025
|
for (const zone of input.zones) {
|
|
39862
40026
|
if (zone.length < 3) continue;
|
|
@@ -39868,19 +40032,29 @@ function notificationOverlaySvg(input) {
|
|
|
39868
40032
|
parts.push(`<polyline data-role="subject-path" points="${subjectPathSvgPoints(run, width, height)}" fill="none" stroke="${NC_IMAGE_SUBJECT_COLOR}" stroke-width="${String(strokes.subject)}" stroke-linejoin="round" stroke-linecap="round" stroke-opacity="0.9"/>`);
|
|
39869
40033
|
}
|
|
39870
40034
|
const ordered = [...input.boxes.filter((b) => b.role === "other"), ...input.boxes.filter((b) => b.role === "subject")];
|
|
39871
|
-
|
|
40035
|
+
const layout = layoutBoxLabels({
|
|
40036
|
+
width,
|
|
40037
|
+
height,
|
|
40038
|
+
fontSize,
|
|
40039
|
+
boxes: ordered
|
|
40040
|
+
});
|
|
40041
|
+
const plates = [];
|
|
40042
|
+
ordered.forEach((box, i) => {
|
|
39872
40043
|
const subject = box.role === "subject";
|
|
39873
40044
|
const color = subject ? NC_IMAGE_SUBJECT_COLOR : NC_IMAGE_OTHER_COLOR;
|
|
39874
40045
|
const stroke = subject ? strokes.subject : strokes.other;
|
|
39875
40046
|
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)}"/>`);
|
|
39876
|
-
|
|
39877
|
-
|
|
39878
|
-
|
|
39879
|
-
|
|
39880
|
-
|
|
39881
|
-
|
|
39882
|
-
|
|
39883
|
-
|
|
40047
|
+
const plate = layout.placements[i];
|
|
40048
|
+
if (plate === null || plate === void 0) return;
|
|
40049
|
+
plates.push(`<rect data-role="${box.role}-plate" x="${String(plate.x)}" y="${String(plate.y)}" width="${String(plate.w)}" height="${String(plate.h)}" fill="#000000" fill-opacity="0.72" stroke="${color}" stroke-width="${String(subject ? 2 : 1)}"/>`, `<text data-role="${box.role}-label" x="${String(plate.x + Math.round(fontSize * .4))}" y="${String(plate.y + Math.round(plate.h * .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>`);
|
|
40050
|
+
});
|
|
40051
|
+
return {
|
|
40052
|
+
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${String(width)}" height="${String(height)}">${[...parts, ...plates].join("")}</svg>`,
|
|
40053
|
+
labels: {
|
|
40054
|
+
placed: layout.placed,
|
|
40055
|
+
dropped: layout.dropped
|
|
40056
|
+
}
|
|
40057
|
+
};
|
|
39884
40058
|
}
|
|
39885
40059
|
/** The pixel rectangle a `crop` cuts from a `width` × `height` frame, or null when too small. */
|
|
39886
40060
|
function subjectCropRect(box, width, height) {
|
|
@@ -39910,11 +40084,18 @@ function iou$4(a, b) {
|
|
|
39910
40084
|
}
|
|
39911
40085
|
/**
|
|
39912
40086
|
* The tracks to draw thin: every track at the instant except the subject —
|
|
39913
|
-
* the same id,
|
|
39914
|
-
* survivor's name)
|
|
39915
|
-
|
|
39916
|
-
|
|
39917
|
-
|
|
40087
|
+
* the same id, a box that is the subject's (a fused id reads under its
|
|
40088
|
+
* survivor's name), or a box of the subject's class over it past what NMS
|
|
40089
|
+
* lets two real subjects share (an unfused piece of the same subject).
|
|
40090
|
+
*/
|
|
40091
|
+
function otherTracks(tracks, subjectTrackId, subjectBox, subjectClassName) {
|
|
40092
|
+
return tracks.filter((t) => {
|
|
40093
|
+
if (t.trackId === subjectTrackId) return false;
|
|
40094
|
+
if (subjectBox === null) return true;
|
|
40095
|
+
const overlap = iou$4(t.bbox, subjectBox);
|
|
40096
|
+
if (overlap >= SAME_SUBJECT_IOU) return false;
|
|
40097
|
+
return !(t.className === subjectClassName && overlap >= NC_IMAGE_SAME_CLASS_SUBJECT_IOU);
|
|
40098
|
+
});
|
|
39918
40099
|
}
|
|
39919
40100
|
/**
|
|
39920
40101
|
* What picture a ROW asks for. A row written since D897 carries `mediaImage`;
|
|
@@ -39931,9 +40112,15 @@ function payloadImageMode(payload) {
|
|
|
39931
40112
|
if (payload.media === "keyFrame") return "clean";
|
|
39932
40113
|
return "crop";
|
|
39933
40114
|
}
|
|
39934
|
-
/**
|
|
40115
|
+
/**
|
|
40116
|
+
* The zone ids a row's picture outlines: the zone(s) THIS event triggered,
|
|
40117
|
+
* when it asked — never every zone the rule lists (a dog in Uscio drew Aiuola
|
|
40118
|
+
* too). A row that asked but froze no `matchedZoneIds` (written before the
|
|
40119
|
+
* field) draws none: an outline of a zone the subject never entered is a
|
|
40120
|
+
* picture that lies about its own reason.
|
|
40121
|
+
*/
|
|
39935
40122
|
function payloadZoneOverlayIds(payload) {
|
|
39936
|
-
if (payload.mediaShowTriggerZone === true) return payload.
|
|
40123
|
+
if (payload.mediaShowTriggerZone === true) return payload.matchedZoneIds ?? [];
|
|
39937
40124
|
if (payload.mediaImage === void 0) return payload.mediaZoneIds ?? [];
|
|
39938
40125
|
return [];
|
|
39939
40126
|
}
|
|
@@ -40022,7 +40209,8 @@ async function renderNotificationImage(req) {
|
|
|
40022
40209
|
const boxes = req.mode === "scene-boxes" ? [...req.others.map((o) => ({
|
|
40023
40210
|
...toPixels(o, outW, outH),
|
|
40024
40211
|
role: "other",
|
|
40025
|
-
label: o.label
|
|
40212
|
+
label: o.label,
|
|
40213
|
+
...o.confidence !== void 0 ? { score: o.confidence } : {}
|
|
40026
40214
|
})), ...req.subject !== null ? [{
|
|
40027
40215
|
...toPixels(req.subject, outW, outH),
|
|
40028
40216
|
role: "subject",
|
|
@@ -40040,16 +40228,18 @@ async function renderNotificationImage(req) {
|
|
|
40040
40228
|
height: outH,
|
|
40041
40229
|
fit: "fill"
|
|
40042
40230
|
});
|
|
40231
|
+
let labels;
|
|
40043
40232
|
if (boxes.length > 0 || zones.length > 0 || paths.length > 0) {
|
|
40044
|
-
const
|
|
40233
|
+
const overlay = notificationOverlay({
|
|
40045
40234
|
width: outW,
|
|
40046
40235
|
height: outH,
|
|
40047
40236
|
boxes,
|
|
40048
40237
|
zones,
|
|
40049
40238
|
paths
|
|
40050
40239
|
});
|
|
40240
|
+
if (boxes.length > 0) labels = overlay.labels;
|
|
40051
40241
|
pipeline = pipeline.composite([{
|
|
40052
|
-
input: Buffer.from(svg),
|
|
40242
|
+
input: Buffer.from(overlay.svg),
|
|
40053
40243
|
top: 0,
|
|
40054
40244
|
left: 0
|
|
40055
40245
|
}]);
|
|
@@ -40059,7 +40249,8 @@ async function renderNotificationImage(req) {
|
|
|
40059
40249
|
return {
|
|
40060
40250
|
bytes: toArrayBufferBytes(out),
|
|
40061
40251
|
name: `${rendered}.jpg`,
|
|
40062
|
-
rendered
|
|
40252
|
+
rendered,
|
|
40253
|
+
...labels !== void 0 ? { labels } : {}
|
|
40063
40254
|
};
|
|
40064
40255
|
}
|
|
40065
40256
|
var NcNotificationImageStage = class {
|
|
@@ -40133,10 +40324,19 @@ var NcNotificationImageStage = class {
|
|
|
40133
40324
|
...subjectBox,
|
|
40134
40325
|
label: imageLabelText(payload.subject.label ?? payload.subject.className, payload.subject.confidence)
|
|
40135
40326
|
} : null;
|
|
40136
|
-
const others = mode === "scene-boxes" ? otherTracks(await this.tracksAt(camera, source.timestamp), subjectTrackId, subjectBox).map((t) => ({
|
|
40327
|
+
const others = mode === "scene-boxes" ? otherTracks(await this.tracksAt(camera, source.timestamp), subjectTrackId, subjectBox, payload.subject.className).map((t) => ({
|
|
40137
40328
|
...t.bbox,
|
|
40138
|
-
label: imageLabelText(t.label ?? t.className, t.confidence)
|
|
40329
|
+
label: imageLabelText(t.label ?? t.className, t.confidence),
|
|
40330
|
+
...t.confidence !== void 0 ? { confidence: t.confidence } : {}
|
|
40139
40331
|
})) : [];
|
|
40332
|
+
if (mode !== "crop" && input.withSubject && payload.mediaShowTriggerZone === true && payload.matchedZoneIds === void 0) this.deps.logger.info("trigger zone not drawn — the row never froze which zone fired", {
|
|
40333
|
+
tags: { deviceId: camera },
|
|
40334
|
+
meta: {
|
|
40335
|
+
ruleId: entry.ruleId,
|
|
40336
|
+
recordId: entry.recordId,
|
|
40337
|
+
ruleZoneIds: (payload.triggerZoneIds ?? []).join(",")
|
|
40338
|
+
}
|
|
40339
|
+
});
|
|
40140
40340
|
const zones = mode === "crop" || zoneIds.length === 0 ? [] : await this.zonePolygons(camera, zoneIds);
|
|
40141
40341
|
try {
|
|
40142
40342
|
const image = await this.render({
|
|
@@ -40149,6 +40349,7 @@ var NcNotificationImageStage = class {
|
|
|
40149
40349
|
});
|
|
40150
40350
|
const reason = image.rendered === mode ? void 0 : mode === "crop" ? "no-subject-box" : "no-tracks-at-instant";
|
|
40151
40351
|
if (reason !== void 0) this.reportFallback(input, mode, reason);
|
|
40352
|
+
if (image.labels !== void 0) this.reportLabels(input, image.labels);
|
|
40152
40353
|
return reason !== void 0 ? {
|
|
40153
40354
|
...image,
|
|
40154
40355
|
reason
|
|
@@ -40197,6 +40398,19 @@ var NcNotificationImageStage = class {
|
|
|
40197
40398
|
}
|
|
40198
40399
|
});
|
|
40199
40400
|
}
|
|
40401
|
+
/** ONE line per rendered picture: how many captions it carries and how many had no clear spot. */
|
|
40402
|
+
reportLabels(input, labels) {
|
|
40403
|
+
const camera = input.camera === "composite" ? input.entry.payload.subject.deviceId : input.camera;
|
|
40404
|
+
this.deps.logger.debug("notification picture captions laid out", {
|
|
40405
|
+
tags: { deviceId: camera },
|
|
40406
|
+
meta: {
|
|
40407
|
+
ruleId: input.entry.ruleId,
|
|
40408
|
+
recordId: input.entry.recordId,
|
|
40409
|
+
placed: labels.placed,
|
|
40410
|
+
dropped: labels.dropped
|
|
40411
|
+
}
|
|
40412
|
+
});
|
|
40413
|
+
}
|
|
40200
40414
|
/** The subject's trail, read once per render for its box AND its path. */
|
|
40201
40415
|
async subjectTrail(camera, trackId) {
|
|
40202
40416
|
const read = this.deps.getSubjectTrail;
|
|
@@ -46890,6 +47104,27 @@ function isRecord(value) {
|
|
|
46890
47104
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46891
47105
|
}
|
|
46892
47106
|
/**
|
|
47107
|
+
* Incident 2026-10-22: a row stored with `groupingSec: 0` failed `NcRuleSchema` (min
|
|
47108
|
+
* {@link NC_GROUPING_MIN_SEC}) and the WHOLE rule was skipped as malformed on every load — a
|
|
47109
|
+
* system-event rule on which a window cannot even apply vanished from the engine and from
|
|
47110
|
+
* `listRules`. A non-positive (or non-numeric) window only ever meant "no window": dropped. A
|
|
47111
|
+
* positive one outside the range is clamped. One bad optional field never costs the rule.
|
|
47112
|
+
*/
|
|
47113
|
+
function repairStoredGroupingSec(value) {
|
|
47114
|
+
if (value === void 0) return { kind: "unchanged" };
|
|
47115
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return {
|
|
47116
|
+
kind: "repaired",
|
|
47117
|
+
from: value,
|
|
47118
|
+
to: null
|
|
47119
|
+
};
|
|
47120
|
+
const clamped = clampGroupingSec(value);
|
|
47121
|
+
return clamped === value ? { kind: "unchanged" } : {
|
|
47122
|
+
kind: "repaired",
|
|
47123
|
+
from: value,
|
|
47124
|
+
to: clamped
|
|
47125
|
+
};
|
|
47126
|
+
}
|
|
47127
|
+
/**
|
|
46893
47128
|
* Project a RAW stored rule value (`data['rule']`, before any Zod parse). A value that is not
|
|
46894
47129
|
* an object, or has no string `delivery`, is returned untouched: the schema parse that follows
|
|
46895
47130
|
* rejects it exactly as before, and a projection must never be what decides a row is valid.
|
|
@@ -46897,9 +47132,12 @@ function isRecord(value) {
|
|
|
46897
47132
|
function projectRawStoredRule(raw) {
|
|
46898
47133
|
if (!isRecord(raw) || typeof raw["delivery"] !== "string") return {
|
|
46899
47134
|
candidate: raw,
|
|
46900
|
-
projection: { kind: "unchanged" }
|
|
47135
|
+
projection: { kind: "unchanged" },
|
|
47136
|
+
groupingRepair: { kind: "unchanged" }
|
|
46901
47137
|
};
|
|
46902
|
-
const
|
|
47138
|
+
const groupingRepair = repairStoredGroupingSec(raw["groupingSec"]);
|
|
47139
|
+
const repaired = withRepairedGrouping(raw, groupingRepair);
|
|
47140
|
+
const groupingSec = repaired["groupingSec"];
|
|
46903
47141
|
const projection = projectRetiredRuleFields({
|
|
46904
47142
|
delivery: raw["delivery"],
|
|
46905
47143
|
conditions: isRecord(raw["conditions"]) ? raw["conditions"] : {},
|
|
@@ -46908,15 +47146,25 @@ function projectRawStoredRule(raw) {
|
|
|
46908
47146
|
...typeof groupingSec === "number" ? { groupingSec } : {}
|
|
46909
47147
|
});
|
|
46910
47148
|
if (projection.kind === "unchanged" || projection.groupingSec === void 0) return {
|
|
46911
|
-
candidate:
|
|
46912
|
-
projection
|
|
47149
|
+
candidate: repaired,
|
|
47150
|
+
projection,
|
|
47151
|
+
groupingRepair
|
|
46913
47152
|
};
|
|
46914
47153
|
return {
|
|
46915
47154
|
candidate: {
|
|
46916
|
-
...
|
|
47155
|
+
...repaired,
|
|
46917
47156
|
groupingSec: projection.groupingSec
|
|
46918
47157
|
},
|
|
46919
|
-
projection
|
|
47158
|
+
projection,
|
|
47159
|
+
groupingRepair
|
|
47160
|
+
};
|
|
47161
|
+
}
|
|
47162
|
+
function withRepairedGrouping(raw, repair) {
|
|
47163
|
+
if (repair.kind === "unchanged") return raw;
|
|
47164
|
+
const { groupingSec: _dropped, ...rest } = raw;
|
|
47165
|
+
return repair.to === null ? rest : {
|
|
47166
|
+
...rest,
|
|
47167
|
+
groupingSec: repair.to
|
|
46920
47168
|
};
|
|
46921
47169
|
}
|
|
46922
47170
|
/**
|
|
@@ -46969,17 +47217,45 @@ var LOAD_LIMIT = 1e4;
|
|
|
46969
47217
|
* Parse a raw body object (as returned from storage — may contain retired keys)
|
|
46970
47218
|
* into an `NcRule` applying the legacy field projection.
|
|
46971
47219
|
*/
|
|
46972
|
-
function bodyToRule(id, raw, onProjection) {
|
|
46973
|
-
const
|
|
47220
|
+
function bodyToRule(id, raw, onProjection, onGroupingRepair) {
|
|
47221
|
+
const parsed = parseStoredRule(id, raw, onProjection, onGroupingRepair);
|
|
47222
|
+
return parsed.ok ? parsed.rule : null;
|
|
47223
|
+
}
|
|
47224
|
+
function parseStoredRule(id, raw, onProjection, onGroupingRepair, onClassWidening) {
|
|
47225
|
+
const { candidate, projection, groupingRepair } = projectRawStoredRule(raw);
|
|
46974
47226
|
const parsed = NcRuleSchema.safeParse({
|
|
46975
47227
|
...candidate,
|
|
46976
47228
|
id
|
|
46977
47229
|
});
|
|
46978
|
-
if (!parsed.success)
|
|
47230
|
+
if (!parsed.success) {
|
|
47231
|
+
const issue = parsed.error.issues[0];
|
|
47232
|
+
return {
|
|
47233
|
+
ok: false,
|
|
47234
|
+
reason: issue === void 0 ? "invalid" : `${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
47235
|
+
};
|
|
47236
|
+
}
|
|
46979
47237
|
if (projection.kind === "projected") onProjection?.(id, projection);
|
|
46980
|
-
|
|
47238
|
+
if (groupingRepair.kind === "repaired") onGroupingRepair?.(id, groupingRepair);
|
|
47239
|
+
const media = withNormalizedMedia(parsed.data);
|
|
47240
|
+
const rule = withSystemEventClasses(media);
|
|
47241
|
+
if (rule !== media) onClassWidening?.(id, media, rule);
|
|
47242
|
+
return {
|
|
47243
|
+
ok: true,
|
|
47244
|
+
rule
|
|
47245
|
+
};
|
|
46981
47246
|
}
|
|
46982
47247
|
/**
|
|
47248
|
+
* D906: what a rule is stored as — its media policy folded (D897) and, on an
|
|
47249
|
+
* INSTANT system rule, every paired class it touches completed with both
|
|
47250
|
+
* edges. Applied on every WRITE; the load applies the same through
|
|
47251
|
+
* {@link parseStoredRule}, which also says once that it widened a rule.
|
|
47252
|
+
*/
|
|
47253
|
+
function normalizeStoredRule(rule) {
|
|
47254
|
+
return withSystemEventClasses(withNormalizedMedia(rule));
|
|
47255
|
+
}
|
|
47256
|
+
/** At most this many skipped rows are NAMED in the WARN line; the count is always whole. */
|
|
47257
|
+
var SKIPPED_ROWS_NAMED_MAX = 10;
|
|
47258
|
+
/**
|
|
46983
47259
|
* D897: the rule with its media policy in the stored shape — `image` set, the
|
|
46984
47260
|
* legacy `attach` / `frame` / `zoneCrop` folded into it. Applied on LOAD (a read
|
|
46985
47261
|
* projection: a rule nobody saved since keeps its row as written) and on every
|
|
@@ -47055,6 +47331,8 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47055
47331
|
now;
|
|
47056
47332
|
newId;
|
|
47057
47333
|
identityIdsByName;
|
|
47334
|
+
/** Rows the last successful {@link load} skipped; `null` before one succeeded. */
|
|
47335
|
+
skipped = null;
|
|
47058
47336
|
/** Has a {@link load} ever completed? See the note there. */
|
|
47059
47337
|
loaded = false;
|
|
47060
47338
|
/** Legacy detection rules naming sounds already reported (D690) — once each. */
|
|
@@ -47066,6 +47344,10 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47066
47344
|
pendingProjections = /* @__PURE__ */ new Map();
|
|
47067
47345
|
/** Rules whose projection was already logged in this process (D692 §9) — once each. */
|
|
47068
47346
|
projectionLogged = /* @__PURE__ */ new Set();
|
|
47347
|
+
/** Rules whose out-of-range `groupingSec` repair was already logged — once each. */
|
|
47348
|
+
groupingRepairLogged = /* @__PURE__ */ new Set();
|
|
47349
|
+
/** Rules whose class widening (D906) was already logged — once each. */
|
|
47350
|
+
classWideningLogged = /* @__PURE__ */ new Set();
|
|
47069
47351
|
constructor(deps) {
|
|
47070
47352
|
this.logger = deps.logger;
|
|
47071
47353
|
this.now = deps.now ?? (() => Date.now());
|
|
@@ -47100,20 +47382,29 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47100
47382
|
try {
|
|
47101
47383
|
const { records } = await this.kind.query({ limit: LOAD_LIMIT });
|
|
47102
47384
|
const next = /* @__PURE__ */ new Map();
|
|
47103
|
-
|
|
47385
|
+
const skippedRows = [];
|
|
47104
47386
|
for (const row of records) {
|
|
47105
|
-
const
|
|
47387
|
+
const parsed = parseStoredRule(row.id, row.data, (ruleId, projection) => {
|
|
47106
47388
|
this.pendingProjections.set(ruleId, projection);
|
|
47107
|
-
});
|
|
47108
|
-
if (
|
|
47109
|
-
|
|
47389
|
+
}, (ruleId, repair) => this.logGroupingRepair(ruleId, repair), (ruleId, before, after) => this.logClassWidening(ruleId, before, after));
|
|
47390
|
+
if (!parsed.ok) {
|
|
47391
|
+
const name = row.data["name"];
|
|
47392
|
+
skippedRows.push({
|
|
47393
|
+
ruleId: row.id,
|
|
47394
|
+
name: typeof name === "string" ? name : null,
|
|
47395
|
+
reason: parsed.reason
|
|
47396
|
+
});
|
|
47110
47397
|
continue;
|
|
47111
47398
|
}
|
|
47112
|
-
next.set(row.id, rule);
|
|
47399
|
+
next.set(row.id, parsed.rule);
|
|
47113
47400
|
}
|
|
47114
47401
|
this.byId.clear();
|
|
47115
47402
|
for (const [id, rule] of next) this.byId.set(id, rule);
|
|
47116
|
-
|
|
47403
|
+
this.skipped = skippedRows;
|
|
47404
|
+
if (skippedRows.length > 0) this.logger.warn("notification rule rows skipped as malformed", { meta: {
|
|
47405
|
+
skipped: skippedRows.length,
|
|
47406
|
+
rows: skippedRows.slice(0, SKIPPED_ROWS_NAMED_MAX)
|
|
47407
|
+
} });
|
|
47117
47408
|
this.loaded = true;
|
|
47118
47409
|
} catch (err) {
|
|
47119
47410
|
this.logger.warn("notification rules load failed — keeping rules already in memory", { meta: {
|
|
@@ -47125,6 +47416,27 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47125
47416
|
for (const id of warnLegacyAudioClassRules([...this.byId.values()], this.audioClassWarned, this.logger)) this.audioClassWarned.add(id);
|
|
47126
47417
|
this.logger.debug("notification rules loaded", { meta: { rules: this.byId.size } });
|
|
47127
47418
|
}
|
|
47419
|
+
/** Say, once per process per rule, that an instant rule was read as its whole class (D906). */
|
|
47420
|
+
logClassWidening(ruleId, before, after) {
|
|
47421
|
+
if (this.classWideningLogged.has(ruleId)) return;
|
|
47422
|
+
this.classWideningLogged.add(ruleId);
|
|
47423
|
+
const had = new Set(before.conditions.systemEvent?.kinds ?? []);
|
|
47424
|
+
this.logger.info("instant system rule read as its whole event class — both edges", { meta: {
|
|
47425
|
+
ruleId,
|
|
47426
|
+
rule: after.name,
|
|
47427
|
+
added: (after.conditions.systemEvent?.kinds ?? []).filter((kind) => !had.has(kind))
|
|
47428
|
+
} });
|
|
47429
|
+
}
|
|
47430
|
+
/** Say, once per process per rule, that an out-of-range stored `groupingSec` was repaired. */
|
|
47431
|
+
logGroupingRepair(ruleId, repair) {
|
|
47432
|
+
if (this.groupingRepairLogged.has(ruleId)) return;
|
|
47433
|
+
this.groupingRepairLogged.add(ruleId);
|
|
47434
|
+
this.logger.warn("rule carries an out-of-range groupingSec — repaired on read", { meta: {
|
|
47435
|
+
ruleId,
|
|
47436
|
+
from: repair.from,
|
|
47437
|
+
to: repair.to
|
|
47438
|
+
} });
|
|
47439
|
+
}
|
|
47128
47440
|
/** Log, once per process per rule, what `bodyToRule` projected from a retired field. */
|
|
47129
47441
|
logPendingProjections() {
|
|
47130
47442
|
for (const [ruleId, projection] of this.pendingProjections) {
|
|
@@ -47193,6 +47505,13 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47193
47505
|
* camera's ring) must ask this first: before the first load the empty list
|
|
47194
47506
|
* is not a fact, and a failed load leaves whatever was already mirrored.
|
|
47195
47507
|
*/
|
|
47508
|
+
/**
|
|
47509
|
+
* The rows the last SUCCESSFUL load could not parse — `null` until one has
|
|
47510
|
+
* succeeded, so "not read yet" never reads as "nothing broken" (D315).
|
|
47511
|
+
*/
|
|
47512
|
+
skippedRows() {
|
|
47513
|
+
return this.skipped;
|
|
47514
|
+
}
|
|
47196
47515
|
isLoaded() {
|
|
47197
47516
|
return this.loaded;
|
|
47198
47517
|
}
|
|
@@ -47222,7 +47541,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47222
47541
|
/** Create a new rule. `createdBy` is the SERVER-injected caller userId. */
|
|
47223
47542
|
async create(input, createdBy) {
|
|
47224
47543
|
const now = this.now();
|
|
47225
|
-
const rule =
|
|
47544
|
+
const rule = normalizeStoredRule({
|
|
47226
47545
|
...input,
|
|
47227
47546
|
id: this.newId(),
|
|
47228
47547
|
createdBy,
|
|
@@ -47267,7 +47586,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47267
47586
|
createdAt: existing.createdAt,
|
|
47268
47587
|
updatedAt: this.now()
|
|
47269
47588
|
};
|
|
47270
|
-
const updated =
|
|
47589
|
+
const updated = normalizeStoredRule(NcRuleSchema.parse(candidate));
|
|
47271
47590
|
await this.write(updated);
|
|
47272
47591
|
return updated;
|
|
47273
47592
|
}
|
|
@@ -47307,7 +47626,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47307
47626
|
id: ruleId
|
|
47308
47627
|
} : candidate);
|
|
47309
47628
|
if (!parsed.success) return existing;
|
|
47310
|
-
const next =
|
|
47629
|
+
const next = normalizeStoredRule(parsed.data);
|
|
47311
47630
|
const { id: _omit, ...data } = next;
|
|
47312
47631
|
await this.kind.put({
|
|
47313
47632
|
id: ruleId,
|
|
@@ -53336,6 +53655,104 @@ var NcZoneOwnershipCache = class {
|
|
|
53336
53655
|
}
|
|
53337
53656
|
};
|
|
53338
53657
|
//#endregion
|
|
53658
|
+
//#region src/notification-center/rule-health.ts
|
|
53659
|
+
var EMPTY_SNAPSHOT = {
|
|
53660
|
+
timelapse: [],
|
|
53661
|
+
summary: [],
|
|
53662
|
+
alarm: null,
|
|
53663
|
+
mutedDeviceIds: []
|
|
53664
|
+
};
|
|
53665
|
+
function fingerprintOf(reasons) {
|
|
53666
|
+
return reasons.map((r) => `${r.code}:${[...r.ids].sort().join(",")}`).join("|");
|
|
53667
|
+
}
|
|
53668
|
+
/** The deleted subjects a rule still names, from the prune's `kept*` records. */
|
|
53669
|
+
function deletedReasons(rule, prunes) {
|
|
53670
|
+
const snapshot = {
|
|
53671
|
+
...EMPTY_SNAPSHOT,
|
|
53672
|
+
rules: [rule]
|
|
53673
|
+
};
|
|
53674
|
+
const devices = referencedDeviceIds(snapshot);
|
|
53675
|
+
const targets = referencedTargetIds(snapshot);
|
|
53676
|
+
const deadDevices = /* @__PURE__ */ new Set();
|
|
53677
|
+
const deadTargets = /* @__PURE__ */ new Set();
|
|
53678
|
+
for (const p of prunes) {
|
|
53679
|
+
if (p.kind !== "rule" || p.ruleId !== rule.id) continue;
|
|
53680
|
+
if (p.outcome !== "kept" && p.outcome !== "kept-disabled") continue;
|
|
53681
|
+
if (p.subjectKind === "device" && devices.has(Number(p.subject))) deadDevices.add(p.subject);
|
|
53682
|
+
if (p.subjectKind === "target" && targets.has(p.subject)) deadTargets.add(p.subject);
|
|
53683
|
+
}
|
|
53684
|
+
const reasons = [];
|
|
53685
|
+
if (deadDevices.size > 0) reasons.push({
|
|
53686
|
+
code: "device-deleted",
|
|
53687
|
+
ids: [...deadDevices].sort()
|
|
53688
|
+
});
|
|
53689
|
+
if (deadTargets.size > 0) reasons.push({
|
|
53690
|
+
code: "target-deleted",
|
|
53691
|
+
ids: [...deadTargets].sort()
|
|
53692
|
+
});
|
|
53693
|
+
return reasons;
|
|
53694
|
+
}
|
|
53695
|
+
var NcRuleHealthMonitor = class {
|
|
53696
|
+
verdicts = /* @__PURE__ */ new Map();
|
|
53697
|
+
/** Judge every rule once; return the edges to announce. */
|
|
53698
|
+
evaluate(inputs) {
|
|
53699
|
+
const next = /* @__PURE__ */ new Map();
|
|
53700
|
+
const seen = /* @__PURE__ */ new Set();
|
|
53701
|
+
if (inputs.skipped !== null) for (const row of inputs.skipped) {
|
|
53702
|
+
seen.add(row.ruleId);
|
|
53703
|
+
const unparseable = {
|
|
53704
|
+
code: "unparseable",
|
|
53705
|
+
ids: [],
|
|
53706
|
+
detail: row.reason
|
|
53707
|
+
};
|
|
53708
|
+
next.set(row.ruleId, this.verdict(row.name, unparseable, []));
|
|
53709
|
+
}
|
|
53710
|
+
if (inputs.rules !== null) for (const rule of inputs.rules) {
|
|
53711
|
+
seen.add(rule.id);
|
|
53712
|
+
const previous = this.verdicts.get(rule.id);
|
|
53713
|
+
const deleted = inputs.prunes !== null ? deletedReasons(rule, inputs.prunes) : previous?.deleted ?? [];
|
|
53714
|
+
const unparseable = inputs.skipped !== null ? null : previous?.unparseable ?? null;
|
|
53715
|
+
next.set(rule.id, this.verdict(rule.name, unparseable, deleted));
|
|
53716
|
+
}
|
|
53717
|
+
const edges = [];
|
|
53718
|
+
const answeredAll = inputs.rules !== null && inputs.skipped !== null;
|
|
53719
|
+
for (const [ruleId, previous] of this.verdicts) {
|
|
53720
|
+
if (seen.has(ruleId)) continue;
|
|
53721
|
+
if (!answeredAll) next.set(ruleId, previous);
|
|
53722
|
+
}
|
|
53723
|
+
for (const [ruleId, verdict] of next) {
|
|
53724
|
+
const previous = this.verdicts.get(ruleId);
|
|
53725
|
+
const reasons = reasonsOf(verdict);
|
|
53726
|
+
if (reasons.length > 0) {
|
|
53727
|
+
if (previous?.fingerprint !== verdict.fingerprint) edges.push({
|
|
53728
|
+
kind: "broken",
|
|
53729
|
+
ruleId,
|
|
53730
|
+
ruleName: verdict.name,
|
|
53731
|
+
reasons
|
|
53732
|
+
});
|
|
53733
|
+
} else if (previous !== void 0 && reasonsOf(previous).length > 0) edges.push({
|
|
53734
|
+
kind: "repaired",
|
|
53735
|
+
ruleId,
|
|
53736
|
+
ruleName: verdict.name
|
|
53737
|
+
});
|
|
53738
|
+
}
|
|
53739
|
+
this.verdicts.clear();
|
|
53740
|
+
for (const [ruleId, verdict] of next) if (reasonsOf(verdict).length > 0) this.verdicts.set(ruleId, verdict);
|
|
53741
|
+
return edges;
|
|
53742
|
+
}
|
|
53743
|
+
verdict(name, unparseable, deleted) {
|
|
53744
|
+
return {
|
|
53745
|
+
name,
|
|
53746
|
+
unparseable,
|
|
53747
|
+
deleted,
|
|
53748
|
+
fingerprint: fingerprintOf([...unparseable !== null ? [unparseable] : [], ...deleted])
|
|
53749
|
+
};
|
|
53750
|
+
}
|
|
53751
|
+
};
|
|
53752
|
+
function reasonsOf(verdict) {
|
|
53753
|
+
return [...verdict.unparseable !== null ? [verdict.unparseable] : [], ...verdict.deleted];
|
|
53754
|
+
}
|
|
53755
|
+
//#endregion
|
|
53339
53756
|
//#region src/notification-center/index.ts
|
|
53340
53757
|
/**
|
|
53341
53758
|
* NotificationCenter — the P1 core of the Notification Center (spec
|
|
@@ -53402,7 +53819,7 @@ var NC_ALARM_ANNOUNCE_RULE_ID = "nc-alarm-announce";
|
|
|
53402
53819
|
* media (the SOURCE rule of a combined alarm row), and a zone overlay is
|
|
53403
53820
|
* frozen only when the rule asked for one AND fired on a zone.
|
|
53404
53821
|
*/
|
|
53405
|
-
function mediaPolicyFields(mediaRule, cropZoneIds) {
|
|
53822
|
+
function mediaPolicyFields(mediaRule, cropZoneIds, matchedZoneIds) {
|
|
53406
53823
|
const media = effectiveMediaPolicyForRule(mediaRule);
|
|
53407
53824
|
const image = media.image ?? "crop";
|
|
53408
53825
|
return {
|
|
@@ -53411,6 +53828,7 @@ function mediaPolicyFields(mediaRule, cropZoneIds) {
|
|
|
53411
53828
|
...media.showTriggerZone === true && cropZoneIds !== void 0 && cropZoneIds.length > 0 ? { mediaShowTriggerZone: true } : {},
|
|
53412
53829
|
...media.showSubjectPath === true && image !== "none" ? { mediaShowSubjectPath: true } : {},
|
|
53413
53830
|
...cropZoneIds !== void 0 ? { triggerZoneIds: [...cropZoneIds] } : {},
|
|
53831
|
+
...matchedZoneIds !== void 0 ? { matchedZoneIds: [...matchedZoneIds] } : {},
|
|
53414
53832
|
...media.profile !== void 0 ? { mediaProfile: media.profile } : {},
|
|
53415
53833
|
...media.gif === true ? { mediaGif: true } : {},
|
|
53416
53834
|
...media.clip === true ? { mediaClip: true } : {},
|
|
@@ -53916,6 +54334,21 @@ function groupOrderInput(fate, triggerZoneIds) {
|
|
|
53916
54334
|
* condition's ids, else an occupancy rule's one zone. A grouped member freezes the same
|
|
53917
54335
|
* ids a plain row of the rule would.
|
|
53918
54336
|
*/
|
|
54337
|
+
/**
|
|
54338
|
+
* The zones of `cropZoneIds` this subject actually triggered — what the zone
|
|
54339
|
+
* condition saw (membership plus the crossed zone, `zonesVisitedBy`), on the
|
|
54340
|
+
* rule whose media the row carries. On a combined alarm row the subject is the
|
|
54341
|
+
* alarm, so the source match's frozen zones answer. An occupancy rule's one
|
|
54342
|
+
* zone is the zone the count edge is ABOUT, so it is its own trigger.
|
|
54343
|
+
*/
|
|
54344
|
+
function matchedTriggerZoneIds(mediaRule, cropZoneIds, subject) {
|
|
54345
|
+
if (cropZoneIds === void 0) return void 0;
|
|
54346
|
+
if (mediaRule.conditions.zones === void 0) return [...cropZoneIds];
|
|
54347
|
+
const alarmSource = subject.systemEvent?.alarmSource;
|
|
54348
|
+
const visited = new Set(alarmSource !== void 0 ? alarmSource.zones ?? [] : subject.zones);
|
|
54349
|
+
if (alarmSource === void 0 && subject.crossing !== void 0) visited.add(subject.crossing.zoneId);
|
|
54350
|
+
return cropZoneIds.filter((id) => visited.has(id));
|
|
54351
|
+
}
|
|
53919
54352
|
function ruleCropZoneIds(rule) {
|
|
53920
54353
|
if (rule.conditions.zones !== void 0) return rule.conditions.zones.ids;
|
|
53921
54354
|
const zoneId = rule.conditions.occupancy?.zoneId;
|
|
@@ -54069,6 +54502,8 @@ var NotificationCenter = class {
|
|
|
54069
54502
|
* evaluating one.
|
|
54070
54503
|
*/
|
|
54071
54504
|
pruneLedger;
|
|
54505
|
+
/** D905: which rules cannot work, announced once per distinct reason. */
|
|
54506
|
+
ruleHealth = new NcRuleHealthMonitor();
|
|
54072
54507
|
/** The prune itself — see `device-prune-runner.ts`. */
|
|
54073
54508
|
pruner;
|
|
54074
54509
|
/**
|
|
@@ -57498,7 +57933,7 @@ var NotificationCenter = class {
|
|
|
57498
57933
|
const subject = incoming.subject;
|
|
57499
57934
|
const alarmSource = subject.systemEvent?.alarmSource;
|
|
57500
57935
|
const sourceRule = alarmSource !== void 0 ? this.rules.get(alarmSource.ruleId) : null;
|
|
57501
|
-
const media = sourceRule !== null ? mediaPolicyFields(sourceRule, ruleCropZoneIds(sourceRule)) : { media: "none" };
|
|
57936
|
+
const media = sourceRule !== null ? mediaPolicyFields(sourceRule, ruleCropZoneIds(sourceRule), matchedTriggerZoneIds(sourceRule, ruleCropZoneIds(sourceRule), subject)) : { media: "none" };
|
|
57502
57937
|
const mediaDeviceId = alarmSource?.deviceId ?? panel.deviceId;
|
|
57503
57938
|
const entries = targets.map((targetId) => ({
|
|
57504
57939
|
ruleId: NC_ALARM_ANNOUNCE_RULE_ID,
|
|
@@ -58704,6 +59139,7 @@ var NotificationCenter = class {
|
|
|
58704
59139
|
const renderedZones = notificationZones(rule, subject);
|
|
58705
59140
|
const confirm = frozenConfirmPolicy(rule, isSystemEvent);
|
|
58706
59141
|
const cropZoneIds = ruleCropZoneIds(mediaRule);
|
|
59142
|
+
const matchedZoneIds = matchedTriggerZoneIds(mediaRule, cropZoneIds, subject);
|
|
58707
59143
|
const allTargets = [...rule.targets, ...userTargets.filter((t) => !direct.has(t.targetId))];
|
|
58708
59144
|
const linkedOwners = !noMedia && subject.linkedStills !== void 0 ? linkedMediaOwners(subject.linkedStills, linkedCameraMode(mediaRule.media)) : void 0;
|
|
58709
59145
|
const groupV2 = extras?.groupV2 ?? (subject.systemEvent !== void 0 && !noMedia ? alarmIncidentGroup(subject.systemEvent, subject.timestamp) : void 0);
|
|
@@ -58714,7 +59150,7 @@ var NotificationCenter = class {
|
|
|
58714
59150
|
...origin === "synthetic" ? { synthetic: true } : {},
|
|
58715
59151
|
priority: rule.priority,
|
|
58716
59152
|
...rule.template !== void 0 ? { template: rule.template } : {},
|
|
58717
|
-
...noMedia ? { media: "none" } : mediaPolicyFields(mediaRule, cropZoneIds),
|
|
59153
|
+
...noMedia ? { media: "none" } : mediaPolicyFields(mediaRule, cropZoneIds, matchedZoneIds),
|
|
58718
59154
|
...confirm !== void 0 ? {
|
|
58719
59155
|
confirm,
|
|
58720
59156
|
...cooldown !== void 0 ? { cooldown } : {},
|
|
@@ -59359,6 +59795,51 @@ var NotificationCenter = class {
|
|
|
59359
59795
|
if (!this.pruneLedger.isLoaded()) await this.pruneLedger.load();
|
|
59360
59796
|
await this.pruner.reconcileDevices();
|
|
59361
59797
|
await this.pruner.reconcileTargets();
|
|
59798
|
+
this.announceRuleHealth();
|
|
59799
|
+
}
|
|
59800
|
+
/**
|
|
59801
|
+
* D905 — say which rules cannot work: a row the load skipped, or a rule still
|
|
59802
|
+
* naming a device / target the prune confirmed deleted. One edge per rule per
|
|
59803
|
+
* distinct set of reasons; a reading that did not answer changes nothing.
|
|
59804
|
+
*/
|
|
59805
|
+
announceRuleHealth() {
|
|
59806
|
+
const edges = this.ruleHealth.evaluate({
|
|
59807
|
+
rules: this.rules.isLoaded() ? this.rules.list() : null,
|
|
59808
|
+
skipped: this.rules.skippedRows(),
|
|
59809
|
+
prunes: this.pruneLedger.isLoaded() ? this.pruner.listPrunes() : null
|
|
59810
|
+
});
|
|
59811
|
+
for (const edge of edges) {
|
|
59812
|
+
const source = {
|
|
59813
|
+
type: "addon",
|
|
59814
|
+
id: "notification-center"
|
|
59815
|
+
};
|
|
59816
|
+
if (edge.kind === "broken") {
|
|
59817
|
+
const [first] = edge.reasons;
|
|
59818
|
+
if (first === void 0) continue;
|
|
59819
|
+
const offendingIds = [...new Set(edge.reasons.flatMap((r) => r.ids))];
|
|
59820
|
+
this.logger.warn("notification rule cannot work — announced", { meta: {
|
|
59821
|
+
ruleId: edge.ruleId,
|
|
59822
|
+
rule: edge.ruleName,
|
|
59823
|
+
reasons: edge.reasons
|
|
59824
|
+
} });
|
|
59825
|
+
this.deps.emitBusEvent?.(createEvent(EventCategory.NotificationRuleBroken, source, {
|
|
59826
|
+
ruleId: edge.ruleId,
|
|
59827
|
+
ruleName: edge.ruleName,
|
|
59828
|
+
reason: first.code,
|
|
59829
|
+
reasons: edge.reasons,
|
|
59830
|
+
offendingIds
|
|
59831
|
+
}));
|
|
59832
|
+
} else {
|
|
59833
|
+
this.logger.info("notification rule works again — announced", { meta: {
|
|
59834
|
+
ruleId: edge.ruleId,
|
|
59835
|
+
rule: edge.ruleName
|
|
59836
|
+
} });
|
|
59837
|
+
this.deps.emitBusEvent?.(createEvent(EventCategory.NotificationRuleRepaired, source, {
|
|
59838
|
+
ruleId: edge.ruleId,
|
|
59839
|
+
ruleName: edge.ruleName
|
|
59840
|
+
}));
|
|
59841
|
+
}
|
|
59842
|
+
}
|
|
59362
59843
|
}
|
|
59363
59844
|
/**
|
|
59364
59845
|
* `DeviceUnregistered` — a device was DELETED (only `removeDevice` emits it,
|
|
@@ -103428,6 +103909,8 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
|
|
|
103428
103909
|
EventCategory.StorageLocationLow,
|
|
103429
103910
|
EventCategory.StorageLocationCritical,
|
|
103430
103911
|
EventCategory.StorageLocationOk,
|
|
103912
|
+
EventCategory.NotificationRuleBroken,
|
|
103913
|
+
EventCategory.NotificationRuleRepaired,
|
|
103431
103914
|
EventCategory.AddonRunnerFailed,
|
|
103432
103915
|
EventCategory.AddonStarted,
|
|
103433
103916
|
EventCategory.AddonRestarted,
|
|
@@ -115458,6 +115941,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
115458
115941
|
nodeAddons: (nodeId) => api.nodes.getNodeAddons.query({ nodeId })
|
|
115459
115942
|
},
|
|
115460
115943
|
readAlerts: () => api.alerts.list.query({ limit: 5e3 }),
|
|
115944
|
+
emitBusEvent: (event) => this.ctx.eventBus.emit(event),
|
|
115461
115945
|
getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
|
|
115462
115946
|
recognitionOutlook: ({ deviceId, trackId, className, labelKind, now }) => recognitionOutlook({
|
|
115463
115947
|
className,
|