@camstack/addon-post-analysis 1.2.621 → 1.2.623

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.
Files changed (15) hide show
  1. package/THIRD_PARTY_MODELS.md +2 -2
  2. package/dist/{clip-model-registry-BBzoIoKO.mjs → clip-model-registry-C58fyELd.mjs} +412 -66
  3. package/dist/{clip-model-registry-CcgzpELQ.js → clip-model-registry-KKT83z9U.js} +429 -65
  4. package/dist/embedding-encoder/index.js +1 -1
  5. package/dist/embedding-encoder/index.mjs +1 -1
  6. package/dist/pipeline-analytics/_stub.js +2 -2
  7. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CkNZAMox.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-_ahOxwNA.mjs} +3 -3
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-V5-s2hmZ.mjs +26 -0
  9. package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CssYIiFD.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B1HZu0i3.mjs} +1 -1
  10. package/dist/pipeline-analytics/{hostInit-wlnNaaYX.mjs → hostInit-CdRVx4vF.mjs} +3 -3
  11. package/dist/pipeline-analytics/index.js +888 -155
  12. package/dist/pipeline-analytics/index.mjs +888 -155
  13. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  14. package/package.json +1 -1
  15. 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 isDeviceScopedCap, $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 evaluateSensorEdge, 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 CamProfileSchema, Gn as buildEventKindDescriptor, Gr as normalizeNcMediaPolicy, Gt as NcSnoozeSuppressedSchema, H as LABEL_SUPPLY_MACRO, Hi as DeviceType, 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 createEvent, Jn as calendarEventLocalSpan, Jr as parseAlertEpisodePayload, Jt as NcTaxonomySchema, K as LockStateSchema, Ki as RECORD_MAX_DATA_BYTES, Kn as buildSubjectPath, Kr as notificationRulesCapability, Kt as NcStateSummarySectionIdSchema, L as EVENT_OWNER_TYPES, Li as zoneAnalyticsCapability, 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 hydrateSchema, Qn as coreBlockIdFromAddonId, Qr as plateGalleryCapability, Qt as OPS_LOG_DEFAULT_LIMIT, R as EXPORT_DENSE_MAX_RANGES, Ri as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, 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 errMsg, Un as audioMetricsCapability, Ur as memoizePinnedFfmpeg, Ut as NcSnoozeInputSchema, V as FailureCounters, Vi as isSourceCap, Vn as armRemovalReconcile, Vr as linkedStepOf, Vt as NcScheduleSchema, W as ListTrackRelationsInputSchema, Wi as BaseAddon, Wn as audioModeOf, Wr as ncMediaPolicyError, Wt as NcSnoozeSchema, X as MAX_TRACK_PURITIES, Xi as defineCustomActions, Xn as cloneAlarmSettings, Xr as pickFaceRescoreIntervalMinutes, Xt as NcTemplatePreviewSchema, Y as MAX_BIRTH_LATENCY_PROXY_MS, Yi as customAction, Yn as classifyRetrainReview, Yt as NcTemplatePreviewInputSchema, Z as MAX_TRACK_RELATIONS, Zi as defineRecordKinds, Zn as consumableLowPercentOf, Zr as pipelineAnalyticsCapability, Zt as NcTemplateVarDescriptorSchema, _ as BIRTH_DECISION_DEFAULT_LIMIT, _a as tuple, _i as sceneMonitorCapability, _n as SetTrackPurityInputSchema, _r as faceGalleryCapability, _t as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, aa as array, ai as resolveLocationMode, an as RetrainOriginSchema, ar as describeSystemEventDeliveryProblem, at as NC_ALARM_NOTIFY_FIELDS, b as BucketAccumulator, ba as EventCategory, bi as servedTemplateVars, bn as TIMELAPSE_DENSE_FLOOR_SEC, br as gifFromMp4Args, bt as NC_GROUPING_MAX_SEC, c as readClusterModelPinStrict, ca as literal, ci as resolveNcMediaImage, cn as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, ct as NC_AUDIO_CONFIRM_HITS_DEFAULT, d as APPEARANCE_BLOCKS, da as object, di as rootSupplyServes, dn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, dt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, ea as nodePin, ei as readDeviceStateFrom, er as createAlarmProfile, et as MotionTriggeredBySchema, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fa as partialRecord, fi as ruleAutoTriggersAlarm, fn as SCENE_DEFAULT_QUIET_SECONDS, ft as NC_CLIP_RETENTION_MAX_SEC, g as ArchivedDebugNoteSchema, ga as templateLiteral, gi as ruleKindOf, gn as SceneMonitorSchema, gr as evictionPolicyOfLocation, gt as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, h as AlarmArmModeSchema, ha as string, hi as ruleGroupingApplies, hn as SCENE_RESET_RECAPTURES, hr as eventKindLabel, ht as NC_CONFIRM_DEFAULT_TIMEOUT_MS, ia as _enum, 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 looseObject, ln as SCENE_DEFAULT_ANCHOR_THRESHOLD, lt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, m as AUDIO_MACRO_LABELS, ma as strictObject, mi as ruleDeviceCompatCapQueries, mn as SCENE_DIVERGED, mr as ensureAlarmProfiles, mt as NC_CONFIRM_DEFAULT_MAX_IMAGE_PX, na as sleep$1, 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 boolean, 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 record, 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_QUERY_MAX, qn as buildViewerLink, qr as occupancyScope, qt as NcSystemEventKindSchema, ra as ZodObject, ri as renameAlarmProfile, rn as RebuildObjectEmbeddingsRefusalSchema, rr as derivePostTriggeredAction, rt as NC_ALARM_DELAY_FIELDS, s as FACE_EMBEDDING_STEP_ID, sa as discriminatedUnion, 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, 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 number, 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 union, 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, xi as startMoverUnderClearance, xn as TRACK_RELATIONS_DEFAULT_LIMIT, xr as isAlarmControllableCap, xt as NC_GROUPING_MIN_SEC, y as BirthDecisionRecordSchema, ya as unknown, yi as sensorRaisedOf, yn as SetTrackRelationInputSchema, yr as gifDeriveSpeed, yt as NC_GROUPING_DEFAULT_SEC, z as FIRST_LEVEL_MACRO_CLASSES, zi as canonicalHash, zn as applyNonBlockingEdit, zr as isVectorIndexDimConflict, zt as NcRuleTargetSchema } from "../clip-model-registry-BBzoIoKO.mjs";
1
+ import { $ as MediaFileKindEnum, $i as defineCustomActions, $n as cosineSimilarity$1, $r as plateGalleryCapability, $t as OpsLogEntrySchema, A as DeclaredDevices, Ai as systemEventFamilyProblems, An as activateAlarmProfile, Ar as isEventKindLabelKey, At as NcAlarmSensorsInputSchema, B as FULL_IMAGE_BBOX, Bi as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Bn as applySecureOnArmEdit, Br as isVectorIndexDimConflict, Bt as NcRuleUpdateInputSchema, C as ClearTrackRelationResultSchema, Ca as EventCategory, Ci as storageOccupancyCapability, Cn as TimelapseRulePatchSchema, Cr as isAlertEpisodeOpen, Ct as NC_SNOOZE_MAX_MINUTES, D as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Di as summarizeReview, Dn as TrackRelationSchema, Dr as isDefaultAlarmProfileId, Dt as NC_TEMPLATE_VARS, E as DEFAULT_EVENT_COLOR, Ei as subjectPathSvgPoints, En as TrackRelationKindSchema, Er as isAudioClassName, Et as NC_TAXONOMY, F as EVENT_DENSITY_BATCH_MAX, Fi as templateVarsFor, Fn as alertEpisodeKind, Fr as isRetiredClearKey, Ft as NcRuleActionSchema, G as ListTrackRelationsResultSchema, Gi as errMsg, Gn as buildEventKindDescriptor, Gr as ncMediaPolicyError, Gt as NcSnoozeSuppressedSchema, H as LABEL_SUPPLY_MACRO, Hi as evaluateSensorEdge, Hn as assertTimelapseCadences, Hr as linkedStepOf, Ht as NcSharedSequenceSchema, I as EVENT_KIND_BY_CAP, Ii as vectorDimFromBase64, In as alertEpisodePayload, Ir as isRootSuppliableStepId, It as NcRuleInputSchema, J as MAX_BIRTH_DECISION_RECORDS, Ji as InvalidRecordError, Jn as calendarEventLocalSpan, Jr as occupancyScope, Jt as NcTaxonomySchema, K as LockStateSchema, Ki as BaseAddon, Kn as buildSubjectPath, Kr as normalizeNcMediaPolicy, Kt as NcStateSummarySectionIdSchema, L as EVENT_OWNER_TYPES, Li as viewerLinkBaseOf, Ln as applyBlockingEdit, Lr as isScheduleActive, Lt as NcRulePatchSchema, M as EMBEDDING_MODEL_FAILED, Mi as systemEventSummaryScheduleOf, Mn as alarmDeviceSecureOnArm, Mr as isKnownTemplateVar, Mt as NcAlarmSettingsPatchSchema, N as EMBEDDING_REQUIREMENTS_FAILED, Ni as systemEventSummarySplitOf, Nn as alarmPanelCapability, Nr as isLabelSupplyStepId, Nt as NcConditionDescriptorSchema, O as DETECTION_MACRO_CLASSES, Oi as syncActiveAlarmProfileSettings, On as TrackSideSchema, Or as isDetectionMacroClass, Ot as NcAlarmConfigSchema, P as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Pi as templateCarriesSummary, Pn as alertEpisodeId, Pr as isMediaOwnerType, Pt as NcConditionsSchema, Q as MOTION_CLOSE_AFTER_MS, Qi as customAction, Qn as coreBlockIdFromAddonId, Qr as pipelineAnalyticsCapability, Qt as OPS_LOG_DEFAULT_LIMIT, R as EXPORT_DENSE_MAX_RANGES, Ri as withSystemEventClasses, Rn as applyModeDelaysEdits, Rr as isSystemDelivery, Rt as NcRuleSchema, S as ClearTrackRelationInputSchema, Sa as unknown, Si as startMoverUnderClearance, Sn as TimelapseRuleInputSchema, Sr as isAlarmControllableCap, St as NC_SENSOR_TRANSITION_KINDS, T as DEFAULT_DETAIL_CROP_CONVENTION, Ti as subjectPathSampleFromBox, Tn as TrackPuritySchema, Tr as isAppLinkBase, Tt as NC_TANK_SYSTEM_EVENT_KINDS, U as LabelAttributionSchema, Ui as isSourceCap, Un as audioMetricsCapability, Ur as mayWriteToLocation, Ut as NcSnoozeInputSchema, V as FailureCounters, Vi as canonicalHash, Vn as armRemovalReconcile, Vr as kebabToCamel, Vt as NcScheduleSchema, W as ListTrackRelationsInputSchema, Wi as DeviceType, Wn as audioModeOf, Wr as memoizePinnedFfmpeg, Wt as NcSnoozeSchema, X as MAX_TRACK_PURITIES, Xi as RECORD_QUERY_MAX, Xn as cloneAlarmSettings, Xt as NcTemplatePreviewSchema, Y as MAX_BIRTH_LATENCY_PROXY_MS, Yi as RECORD_MAX_DATA_BYTES, Yn as classifyRetrainReview, Yr as parseAlertEpisodePayload, Yt as NcTemplatePreviewInputSchema, Z as MAX_TRACK_RELATIONS, Zi as createEvent, Zn as consumableLowPercentOf, Zr as pickFaceRescoreIntervalMinutes, Zt as NcTemplateVarDescriptorSchema, _ as BIRTH_DECISION_DEFAULT_LIMIT, _a as strictObject, _i as ruleKindOf, _n as SetTrackPurityInputSchema, _r as evictionPolicyOfLocation, _t as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, aa as sleep$1, ai as resolveClipSearchMinScore, an as RetrainOriginSchema, ar as describeSystemEventDeliveryProblem, at as NC_ALARM_NOTIFY_FIELDS, b as BucketAccumulator, ba as tuple, bi as sensorRaisedOf, bn as TIMELAPSE_DENSE_FLOOR_SEC, br as gifDeriveSpeed, bt as NC_GROUPING_MAX_SEC, c as readClusterModelPinStrict, ca as array, ci as resolveModeNotifyPolicy, cn as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, cr as deviceRemovalHookCapability, ct as NC_AUDIO_CONFIRM_HITS_DEFAULT, d as APPEARANCE_BLOCKS, da as literal, di as retrainRevisionConflictMessage, dn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, dr as embeddingRefusalOf, dt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, ea as defineRecordKinds, ei as projectSubjectPath, er as createAlarmProfile, et as MotionTriggeredBySchema, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fa as looseObject, fi as rootSupplyServes, fn as SCENE_DEFAULT_QUIET_SECONDS, ft as NC_CLIP_RETENTION_MAX_SEC, g as ArchivedDebugNoteSchema, ga as record, gi as ruleGroupingApplies, gn as SceneMonitorSchema, gr as eventKindLabel, gt as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, h as AlarmArmModeSchema, ha as partialRecord, hi as ruleDeviceCompatCapQueries, hn as SCENE_RESET_RECAPTURES, hr as ensureAlarmProfiles, ht as NC_CONFIRM_DEFAULT_TIMEOUT_MS, ii as renameAlarmProfile, in as RetrainDismissedSchema, ir as deriveRecordingMode, it as NC_ALARM_DELAY_MAX_SEC, j as DetectionSourceSchema, ji as systemEventFilterApplies, jn as addonWidgetsSourceCapability, jr as isEventOwnerType, jt as NcAlarmSensorsSchema, k as DETECTION_PIPELINE_CAP_NAME, ki as systemEventDeliveryProblems, kn as TrackSourceSchema, kr as isDrawableSubjectPath, kt as NcAlarmPostTriggeredActionSchema, l as resolveClusterModelPin, la as boolean, li as resolveNcMediaImage, ln as SCENE_DEFAULT_ANCHOR_THRESHOLD, lt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, m as AUDIO_MACRO_LABELS, ma as object, mi as ruleBelongsToAlarm$1, mn as SCENE_DIVERGED, mr as encodeVectorBase64, mt as NC_CONFIRM_DEFAULT_MAX_IMAGE_PX, na as isDeviceScopedCap, ni as readMotionTriggeredBy, 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 ZodObject, oi as resolveLocationMode, on as RetrainStatusSchema, or as describeSystemEventFamilyProblem, ot as NC_ALARM_SYSTEM_EVENT_KINDS, p as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, pa as number, pi as ruleAutoTriggersAlarm, pn as SCENE_DEFAULT_UNCOVERED_POLICY, pt as NC_CONDITION_CATALOG, q as MAX_ARCHIVED_DEBUG_NOTES, qi as CamProfileSchema, qn as buildViewerLink, qr as notificationRulesCapability, qt as NcSystemEventKindSchema, ra as nodePin, ri as readTimelapseGeneratedAt, rn as RebuildObjectEmbeddingsRefusalSchema, rr as derivePostTriggeredAction, rt as NC_ALARM_DELAY_FIELDS, s as FACE_EMBEDDING_STEP_ID, sa as _enum, si as resolveModeDelays, sn as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, sr as deviceAttentionKind, st as NC_ALARM_TRIGGERED_MAX_SEC, t as BUILTIN_CLIP_MODELS, ta as hydrateSchema, ti as readDeviceStateFrom, tn as RECORDING_EXPORT_MAX_READ_BYTES, tr as deleteAlarmProfile, tt as NC_ALARM_DEFAULT_PROFILE_ID, u as APPEARANCE_BIN_COUNT, ua as discriminatedUnion, un as SCENE_DEFAULT_CHECK_INTERVAL_SEC, ut as NC_CLIP_POST_ROLL_DEFAULT_SEC, v as BaseDevice, va as string, vi as sceneMonitorCapability, vn as SetTrackPurityResultSchema, vr as faceGalleryCapability, vt as NC_DEFAULT_SNOOZE_MINUTES, w as CoverStateSchema, wi as subKindsOf, wn as TimelapseRuleSchema, wr as isAlertEpisodeSeverity, wt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, x as COCO_TO_MACRO, xa as union, xi as servedTemplateVars, xn as TRACK_RELATIONS_DEFAULT_LIMIT, xr as gifFromMp4Args, xt as NC_GROUPING_MIN_SEC, y as BirthDecisionRecordSchema, ya as templateLiteral, yi as secureActuationFor, yn as SetTrackRelationInputSchema, yr as failureContributionCapability, yt as NC_GROUPING_DEFAULT_SEC, z as FIRST_LEVEL_MACRO_CLASSES, zi as zoneAnalyticsCapability, zn as applyNonBlockingEdit, zr as isSystemEventSummaryRule, zt as NcRuleTargetSchema } from "../clip-model-registry-C58fyELd.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,40 +5394,118 @@ async function notificationLinkEndpoints(api, logger) {
5394
5394
  */
5395
5395
  var NC_TRACKS_AT_INSTANT_LIMIT = 25;
5396
5396
  /**
5397
- * Every source track with a position within `toleranceMs` of `timestamp`, its
5398
- * nearest box normalised, deduplicated by track id (a live copy and a
5399
- * persisted copy of one track are one track). Pure.
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
+ * A position this close to the instant is drawn as it is: one detection tick,
5410
+ * the same notion as {@link NC_TRACK_LIFE_SLACK_MS}. Anything farther is a
5411
+ * position of a MOVED subject, so it is either interpolated (a position on
5412
+ * each side inside the tolerance) or the track is left out — never drawn at a
5413
+ * later place than it was at the picture's instant.
5414
+ */
5415
+ var NC_BOX_EXACT_MS = NC_TRACK_LIFE_SLACK_MS;
5416
+ /** The box at `timestamp`: a near position as is, else the bracketing pair interpolated. */
5417
+ function boxAt(positions, timestamp, toleranceMs) {
5418
+ let nearest = null;
5419
+ let before = null;
5420
+ let after = null;
5421
+ for (const p of positions) {
5422
+ const d = p.timestamp - timestamp;
5423
+ if (nearest === null || Math.abs(d) < Math.abs(nearest.timestamp - timestamp)) nearest = p;
5424
+ if (d <= 0 && (before === null || p.timestamp > before.timestamp)) before = p;
5425
+ if (d >= 0 && (after === null || p.timestamp < after.timestamp)) after = p;
5426
+ }
5427
+ if (nearest === null) return null;
5428
+ const dt = nearest.timestamp - timestamp;
5429
+ if (Math.abs(dt) <= NC_BOX_EXACT_MS) return {
5430
+ bbox: nearest.bbox,
5431
+ score: nearest.score,
5432
+ dtMs: dt,
5433
+ interpolated: false
5434
+ };
5435
+ if (before === null || after === null || timestamp - before.timestamp > toleranceMs || after.timestamp - timestamp > toleranceMs) return null;
5436
+ const span = after.timestamp - before.timestamp;
5437
+ const f = span === 0 ? 0 : (timestamp - before.timestamp) / span;
5438
+ const lerp = (a, b) => a + (b - a) * f;
5439
+ return {
5440
+ bbox: {
5441
+ x: lerp(before.bbox.x, after.bbox.x),
5442
+ y: lerp(before.bbox.y, after.bbox.y),
5443
+ w: lerp(before.bbox.w, after.bbox.w),
5444
+ h: lerp(before.bbox.h, after.bbox.h)
5445
+ },
5446
+ score: nearest.score,
5447
+ dtMs: dt,
5448
+ interpolated: true
5449
+ };
5450
+ }
5451
+ /**
5452
+ * Every source track ALIVE at `timestamp` (± {@link NC_TRACK_LIFE_SLACK_MS})
5453
+ * with a box AT that instant: a trail position within {@link NC_BOX_EXACT_MS},
5454
+ * else the linear interpolation of the positions bracketing it (each within
5455
+ * `toleranceMs`). A track with neither is not drawn — `onDrop` names it.
5456
+ * Deduplicated by track id (a live copy and a persisted copy are one track).
5457
+ * Pure.
5400
5458
  */
5401
- function tracksAtInstant(sources, timestamp, toleranceMs, fallbackDims) {
5459
+ function tracksAtInstant(sources, timestamp, toleranceMs, fallbackDims, onDrop) {
5402
5460
  const out = /* @__PURE__ */ new Map();
5403
5461
  for (const track of sources) {
5404
5462
  if (out.has(track.trackId)) continue;
5405
- if (track.firstSeen > timestamp + toleranceMs || track.lastSeen < timestamp - toleranceMs) continue;
5463
+ if (track.firstSeen > timestamp + NC_TRACK_LIFE_SLACK_MS || track.lastSeen < timestamp - NC_TRACK_LIFE_SLACK_MS) {
5464
+ onDrop?.({
5465
+ trackId: track.trackId,
5466
+ className: track.className,
5467
+ reason: "outside-life"
5468
+ });
5469
+ continue;
5470
+ }
5406
5471
  const w = track.frameWidth ?? fallbackDims?.w ?? 0;
5407
5472
  const h = track.frameHeight ?? fallbackDims?.h ?? 0;
5408
- if (w <= 0 || h <= 0) continue;
5409
- let best = null;
5410
- let bestDelta = Number.POSITIVE_INFINITY;
5411
- for (const p of track.positions) {
5412
- const delta = Math.abs(p.timestamp - timestamp);
5413
- if (delta < bestDelta) {
5414
- bestDelta = delta;
5415
- best = p;
5416
- }
5473
+ if (w <= 0 || h <= 0) {
5474
+ onDrop?.({
5475
+ trackId: track.trackId,
5476
+ className: track.className,
5477
+ reason: "no-frame-dims"
5478
+ });
5479
+ continue;
5480
+ }
5481
+ const at = boxAt(track.positions, timestamp, toleranceMs);
5482
+ if (at === null) {
5483
+ const nearestDtMs = track.positions.reduce((best, p) => {
5484
+ const d = p.timestamp - timestamp;
5485
+ return best === void 0 || Math.abs(d) < Math.abs(best) ? d : best;
5486
+ }, void 0);
5487
+ onDrop?.({
5488
+ trackId: track.trackId,
5489
+ className: track.className,
5490
+ reason: "no-position-near-instant",
5491
+ ...nearestDtMs !== void 0 ? { nearestDtMs } : {}
5492
+ });
5493
+ continue;
5417
5494
  }
5418
- if (best === null || bestDelta > toleranceMs) continue;
5419
5495
  const label = track.subLabel ?? track.label;
5420
5496
  out.set(track.trackId, {
5421
5497
  trackId: track.trackId,
5422
5498
  className: track.className,
5423
5499
  ...label !== void 0 && label !== track.className ? { label } : {},
5424
- ...best.score !== void 0 ? { confidence: best.score } : {},
5500
+ ...at.score !== void 0 ? { confidence: at.score } : {},
5425
5501
  bbox: {
5426
- x: best.bbox.x / w,
5427
- y: best.bbox.y / h,
5428
- w: best.bbox.w / w,
5429
- h: best.bbox.h / h
5430
- }
5502
+ x: at.bbox.x / w,
5503
+ y: at.bbox.y / h,
5504
+ w: at.bbox.w / w,
5505
+ h: at.bbox.h / h
5506
+ },
5507
+ dtMs: at.dtMs,
5508
+ interpolated: at.interpolated
5431
5509
  });
5432
5510
  }
5433
5511
  return [...out.values()];
@@ -19329,11 +19407,16 @@ var DebugNoteArchiveStore = class {
19329
19407
  };
19330
19408
  //#endregion
19331
19409
  //#region src/pipeline-analytics/store/event-ledger-collection.ts
19410
+ /**
19411
+ * One event ledger (motion, audio, sensor, faces, plates …) over its
19412
+ * `ctx.records` kind. Every node has `ctx.records`, so there is no other path.
19413
+ */
19332
19414
  /** Matches {@link EventStore.LEDGER_PAGE_SIZE} — kept local to avoid a circular import. */
19333
19415
  var LEDGER_PAGE_SIZE = 500;
19334
19416
  var EventLedgerCollection = class EventLedgerCollection {
19335
19417
  collection;
19336
19418
  kind;
19419
+ records;
19337
19420
  autoIntegerId;
19338
19421
  legacyAtField;
19339
19422
  deviceIdMode;
@@ -19349,6 +19432,7 @@ var EventLedgerCollection = class EventLedgerCollection {
19349
19432
  this.logger = deps.logger;
19350
19433
  this.deriveBody = deps.deriveBody;
19351
19434
  this.indexFields = deps.definition.declaration.indexes.map((i) => i.fields);
19435
+ this.records = deps.records;
19352
19436
  const handle = deps.records.kind(deps.definition);
19353
19437
  this.kind = handle;
19354
19438
  }
@@ -19367,6 +19451,31 @@ var EventLedgerCollection = class EventLedgerCollection {
19367
19451
  }
19368
19452
  await this.kind.put(put);
19369
19453
  }
19454
+ /**
19455
+ * `upsertRow` as an op of {@link EventLedgerCollection.transact}. A row that
19456
+ * cannot be mapped THROWS here: inside a transaction a silently skipped write
19457
+ * would let the rest of the call commit without it.
19458
+ */
19459
+ async txUpsert(id, value) {
19460
+ const put = this.buildRecordPut(id, value);
19461
+ if (put === null) throw new InvalidRecordError(`${this.collection}/${id}: the record could not be mapped`);
19462
+ return this.kind.txPut(put);
19463
+ }
19464
+ /** `update` as an op of {@link EventLedgerCollection.transact}. */
19465
+ txUpdate(id, patch) {
19466
+ return this.kind.txPatch(id, this.derived(patch, "patch"));
19467
+ }
19468
+ /** `deleteKey` as an op of {@link EventLedgerCollection.transact}. */
19469
+ txDelete(id) {
19470
+ return this.kind.txDelete(id);
19471
+ }
19472
+ /**
19473
+ * Run ops (built by the `tx*` methods of ledgers whose kinds share a
19474
+ * `transactFamily`) in ONE transaction: all land or none does.
19475
+ */
19476
+ transact(ops) {
19477
+ return this.records.transact(ops);
19478
+ }
19370
19479
  async insert(record) {
19371
19480
  const idNum = this.autoIntegerId ? await this.kind.allocateId() : null;
19372
19481
  const id = idNum !== null ? String(idNum) : typeof record["id"] === "string" ? record["id"] : null;
@@ -20443,8 +20552,8 @@ var FACE_REF_COLUMNS = ["deviceId"];
20443
20552
  * D312) a predicate that silently matched nothing. The faces ledger therefore derives a
20444
20553
  * boolean `recognized` from every write that touches `recognizedIdentityId`
20445
20554
  * (`face-recognized.ts`), and the views are plain ranges over the `recognized_ts` /
20446
- * `assigned_recognized_ts` indexes. A row written before the field existed lacks it until
20447
- * `scripts/hub/backfill-faces-recognized.ts` has run.
20555
+ * `assigned_recognized_ts` indexes. Rows written before the field existed were
20556
+ * backfilled offline on the operator hubs on 2026-10-22 (the one-shot script is deleted).
20448
20557
  *
20449
20558
  * `assigned` is `BOOLEAN NOT NULL`; the backend serialises `false` to `0` at
20450
20559
  * the bind (`serializeColumnValue`) and decodes `0` back to `false` on read, so
@@ -22308,8 +22417,9 @@ var RetrainAnnotationStore = class {
22308
22417
  * The old rows' deletion, the new rows, the frame's `dismissed` set and its
22309
22418
  * bumped `revision` run under a per-frame commit lock, guarded by
22310
22419
  * `commit.fromRevision`: a concurrent save in this process leaves one winner
22311
- * and one `conflict` — never a blend. The steps are separate records writes,
22312
- * so a crash between them is NOT rolled back.
22420
+ * and one `conflict` — never a blend. The deletion, the new rows and the frame
22421
+ * row are ONE records `transact` (the four retrain kinds share a family): a
22422
+ * failure leaves the frame exactly as it was.
22313
22423
  *
22314
22424
  * Returns the persisted rows so the caller renders ids it can address.
22315
22425
  */
@@ -22426,21 +22536,14 @@ var RetrainAnnotationStore = class {
22426
22536
  }
22427
22537
  return stamped;
22428
22538
  }
22429
- /** Remove every annotation of a frame — the operator un-selecting it. */
22430
- async deleteForFrame(mediaKey) {
22431
- const rows = await this.listByFrame(mediaKey);
22432
- for (const row of rows) try {
22433
- await this.annotations.deleteKey(row.id);
22434
- } catch (err) {
22435
- this.logger.warn("retrain annotation delete failed", {
22436
- tags: { deviceId: row.deviceId },
22437
- meta: {
22438
- annotationId: row.id,
22439
- error: String(err)
22440
- }
22441
- });
22442
- }
22443
- return rows.length;
22539
+ /**
22540
+ * The delete ops of every annotation of a frame, for ONE transaction with
22541
+ * the frame row and its memberships (RetrainService.removeFrame). A failed
22542
+ * read THROWS: "no annotations" must never be the answer to an unreadable
22543
+ * store, because the frame row would then be deleted over live boxes.
22544
+ */
22545
+ async deleteOpsForFrame(mediaKey) {
22546
+ return (await this.annotations.query({ where: { mediaKey } })).map((row) => this.annotations.txDelete(row.id));
22444
22547
  }
22445
22548
  async replaceForFrameOnRecords(mediaKey, rows, commit, nextRevision) {
22446
22549
  const frame = await this.frames.get(mediaKey);
@@ -22453,12 +22556,14 @@ var RetrainAnnotationStore = class {
22453
22556
  kind: "conflict",
22454
22557
  currentRevision
22455
22558
  };
22456
- await this.annotations.deleteWhere({ where: { mediaKey } });
22457
- for (const row of rows) await this.annotations.upsertRow(row.id, this.toRecord(row));
22458
- await this.frames.update(mediaKey, {
22559
+ const ops = [];
22560
+ for (const existing of await this.annotations.query({ where: { mediaKey } })) ops.push(this.annotations.txDelete(existing.id));
22561
+ for (const row of rows) ops.push(await this.annotations.txUpsert(row.id, this.toRecord(row)));
22562
+ ops.push(await this.frames.txUpdate(mediaKey, {
22459
22563
  revision: nextRevision,
22460
22564
  ...commit.dismissed !== void 0 ? { dismissed: [...commit.dismissed] } : {}
22461
- });
22565
+ }));
22566
+ await this.frames.transact(ops);
22462
22567
  return {
22463
22568
  kind: "saved",
22464
22569
  rows,
@@ -22476,8 +22581,10 @@ var RetrainAnnotationStore = class {
22476
22581
  kind: "conflict",
22477
22582
  currentRevision
22478
22583
  };
22479
- for (const row of eligible) await this.annotations.update(row.id, { origin: origins.get(row.id) ?? null });
22480
- await this.frames.update(mediaKey, { revision: nextRevision });
22584
+ const ops = [];
22585
+ for (const row of eligible) ops.push(await this.annotations.txUpdate(row.id, { origin: origins.get(row.id) ?? null }));
22586
+ ops.push(await this.frames.txUpdate(mediaKey, { revision: nextRevision }));
22587
+ await this.frames.transact(ops);
22481
22588
  return {
22482
22589
  kind: "updated",
22483
22590
  updated: eligible.length,
@@ -22648,16 +22755,16 @@ var RetrainCollectionStore = class {
22648
22755
  }, counts.get(input.id) ?? 0)
22649
22756
  };
22650
22757
  }
22651
- /** Remove the collection and its memberships — members first, so a crash
22652
- * never leaves memberships pointing at a name that no longer exists. */
22758
+ /** Remove the collection and its memberships in ONE transaction: a failure
22759
+ * leaves the collection with all its members, never memberships pointing at
22760
+ * a name that no longer exists, nor a collection that silently lost some. */
22653
22761
  async delete(id) {
22654
22762
  if (await this.readRow(id) === null) return {
22655
22763
  deleted: false,
22656
22764
  memberFrameIds: []
22657
22765
  };
22658
22766
  const members = await this.memberships.query({ where: { collectionId: id } });
22659
- for (const m of members) await this.memberships.deleteKey(m.id);
22660
- await this.collections.deleteKey(id);
22767
+ await this.collections.transact([...members.map((m) => this.memberships.txDelete(m.id)), this.collections.txDelete(id)]);
22661
22768
  this.logger.info("retrain collection deleted", { meta: {
22662
22769
  collectionId: id,
22663
22770
  members: members.length
@@ -22686,11 +22793,29 @@ var RetrainCollectionStore = class {
22686
22793
  await this.memberships.deleteKey(key);
22687
22794
  return true;
22688
22795
  }
22689
- /** Remove a frame from EVERY collection — the frame itself is leaving. */
22690
- async forgetFrame(frameId) {
22691
- const rows = await this.membershipRows({ frameId });
22692
- for (const r of rows) await this.memberships.deleteKey(r.id);
22693
- return rows.length;
22796
+ /**
22797
+ * The delete ops of every membership of a frame, for ONE transaction with
22798
+ * the frame row and its annotations (RetrainService.removeFrame). A failed
22799
+ * read THROWS: the frame row must not be deleted over memberships nobody
22800
+ * could list.
22801
+ */
22802
+ async forgetFrameOps(frameId) {
22803
+ return (await this.memberships.query({ where: { frameId } })).map((r) => this.memberships.txDelete(r.id));
22804
+ }
22805
+ /**
22806
+ * The op that adds a frame to a collection, for ONE transaction with the
22807
+ * frame's own row (a still stored from the recording). `null` when it is
22808
+ * already a member: there is nothing to write.
22809
+ */
22810
+ async addFrameOp(input) {
22811
+ const key = membershipKey(input.collectionId, input.frameId);
22812
+ if (await this.memberships.get(key) !== null) return null;
22813
+ return this.memberships.txUpsert(key, {
22814
+ collectionId: input.collectionId,
22815
+ frameId: input.frameId,
22816
+ deviceId: input.deviceId,
22817
+ addedAt: this.now()
22818
+ });
22694
22819
  }
22695
22820
  /** The frames of a collection, in the order they were added. */
22696
22821
  async listFrameIds(collectionId) {
@@ -23282,7 +23407,7 @@ var RetrainFrameStore = class {
23282
23407
  * returned untouched; the blob lands before the row; a refusal is named.
23283
23408
  * `capturedAt` IS the instant — the camera saw this frame then.
23284
23409
  */
23285
- async storeRecordingStill(input) {
23410
+ async storeRecordingStill(input, withRow = []) {
23286
23411
  const frameId = recordingFrameId(input.deviceId, input.epochMs);
23287
23412
  const existing = await this.get(frameId);
23288
23413
  if (existing !== null) return {
@@ -23329,7 +23454,7 @@ var RetrainFrameStore = class {
23329
23454
  relativePath: path,
23330
23455
  data: input.bytes
23331
23456
  });
23332
- await this.ledger.upsertRow(frameId, {
23457
+ const row = await this.ledger.txUpsert(frameId, {
23333
23458
  deviceId: frame.deviceId,
23334
23459
  trackId: "",
23335
23460
  sourceMediaId: 0,
@@ -23346,6 +23471,7 @@ var RetrainFrameStore = class {
23346
23471
  origin: "recording",
23347
23472
  epochMs: input.epochMs
23348
23473
  });
23474
+ await this.ledger.transact([row, ...withRow]);
23349
23475
  };
23350
23476
  try {
23351
23477
  if (this.writeGate) await this.writeGate.run(persist);
@@ -23399,22 +23525,33 @@ var RetrainFrameStore = class {
23399
23525
  /**
23400
23526
  * Drop a copy and its blob — the operator un-selecting a frame.
23401
23527
  *
23402
- * The caller must have removed the frame's annotations first; this store does
23403
- * not cascade, because a store that deletes rows in another table on a
23404
- * best-effort write is how a half-deleted annotation set happens.
23528
+ * The ROW goes first, in one transaction with `withRow` (the frame's
23529
+ * annotations and collection memberships, which the caller builds): a failure
23530
+ * leaves the frame, its boxes and its memberships all in place, and throws.
23531
+ * The blob goes after: if that fails the row is already gone and the bytes
23532
+ * are reclaimable junk, logged, never a row pointing at missing media.
23405
23533
  */
23406
- async deleteFrame(frame) {
23534
+ async deleteFrame(frame, withRow = []) {
23407
23535
  const remove = async () => {
23536
+ try {
23537
+ await this.ledger.transact([...withRow, this.ledger.txDelete(frame.frameId)]);
23538
+ } catch (err) {
23539
+ this.logger.error("retrain frame delete failed; nothing was removed", {
23540
+ tags: { deviceId: frame.deviceId },
23541
+ meta: {
23542
+ frameId: frame.frameId,
23543
+ error: String(err)
23544
+ }
23545
+ });
23546
+ throw err;
23547
+ }
23408
23548
  try {
23409
23549
  await this.storage.delete({
23410
23550
  location: frame.locationId ?? LEGACY_RETRAIN_MEDIA_LOCATION,
23411
23551
  relativePath: framePath(frame)
23412
23552
  });
23413
- } catch {}
23414
- try {
23415
- await this.ledger.deleteKey(frame.frameId);
23416
23553
  } catch (err) {
23417
- this.logger.warn("RetrainFrameStore.deleteFrame row delete failed", {
23554
+ this.logger.warn("retrain frame removed but its blob could not be deleted", {
23418
23555
  tags: { deviceId: frame.deviceId },
23419
23556
  meta: {
23420
23557
  frameId: frame.frameId,
@@ -28392,7 +28529,9 @@ function incomingFromSystemEvent(event, lookup) {
28392
28529
  const blindForMs = numberField(data, "blindForMs");
28393
28530
  if (deviceId === void 0 || blindForMs === void 0) return null;
28394
28531
  const identity = lookup(deviceId);
28395
- const minutes = Math.max(1, Math.round(blindForMs / 6e4));
28532
+ const motionMs = numberField(data, "motionMs") ?? blindForMs;
28533
+ const sessions = numberField(data, "sessions") ?? 1;
28534
+ const minutes = Math.max(1, Math.round(motionMs / 6e4));
28396
28535
  return systemSubject({
28397
28536
  facts,
28398
28537
  kind: "detection-blind",
@@ -28401,13 +28540,39 @@ function incomingFromSystemEvent(event, lookup) {
28401
28540
  bodyKey: "system.detection-blind.body",
28402
28541
  textParams: {
28403
28542
  ...deviceTextParams(deviceId, identity),
28404
- minutes: String(minutes)
28543
+ minutes: String(minutes),
28544
+ sessions: String(sessions)
28405
28545
  },
28406
28546
  textCount: minutes,
28407
28547
  deviceId,
28408
28548
  ...identity !== void 0 ? { deviceType: identity.type } : {}
28409
28549
  });
28410
28550
  }
28551
+ case "pipeline.motion-noisy":
28552
+ case "pipeline.motion-calm": {
28553
+ const deviceId = numberField(data, "deviceId");
28554
+ const dutyPct = numberField(data, "dutyPct");
28555
+ const sessions = numberField(data, "sessions");
28556
+ const windowMs = numberField(data, "windowMs");
28557
+ if (deviceId === void 0 || dutyPct === void 0 || sessions === void 0 || windowMs === void 0) return null;
28558
+ const noisy = event.category === "pipeline.motion-noisy";
28559
+ const identity = lookup(deviceId);
28560
+ return systemSubject({
28561
+ facts,
28562
+ kind: noisy ? "motion-noisy" : "motion-calm",
28563
+ subject: `motion-noise:${deviceId}`,
28564
+ titleKey: noisy ? "system.motion-noisy.title" : "system.motion-calm.title",
28565
+ bodyKey: noisy ? "system.motion-noisy.body" : "system.motion-calm.body",
28566
+ textParams: {
28567
+ ...deviceTextParams(deviceId, identity),
28568
+ minutes: String(Math.max(1, Math.round(windowMs / 6e4))),
28569
+ duty: String(dutyPct),
28570
+ sessions: String(sessions)
28571
+ },
28572
+ deviceId,
28573
+ ...identity !== void 0 ? { deviceType: identity.type } : {}
28574
+ });
28575
+ }
28411
28576
  case "recording.health.stalled": {
28412
28577
  const deviceId = numberField(data, "deviceId");
28413
28578
  const idleMs = numberField(data, "idleMs");
@@ -28489,6 +28654,33 @@ function incomingFromSystemEvent(event, lookup) {
28489
28654
  ...nodeId !== void 0 ? { nodeId } : {}
28490
28655
  });
28491
28656
  }
28657
+ case "notification.rule-broken":
28658
+ case "notification.rule-repaired": {
28659
+ const ruleId = stringField(data, "ruleId");
28660
+ if (ruleId === void 0) return null;
28661
+ const broken = event.category === "notification.rule-broken";
28662
+ const kind = broken ? "rule-broken" : "rule-repaired";
28663
+ const ruleName = stringField(data, "ruleName") ?? ruleId;
28664
+ const reason = stringField(data, "reason") ?? "unknown";
28665
+ const ids = data["offendingIds"];
28666
+ const offendingIds = Array.isArray(ids) ? ids.filter((id) => typeof id === "string").join(", ") : "";
28667
+ return systemSubject({
28668
+ facts,
28669
+ kind,
28670
+ subject: `rule-health:${ruleId}`,
28671
+ titleKey: `system.${kind}.title`,
28672
+ bodyKey: broken ? `system.rule-broken.body.${reason}` : "system.rule-repaired.body",
28673
+ textParams: broken ? {
28674
+ ruleId,
28675
+ ruleName,
28676
+ brokenReason: reason,
28677
+ offendingIds
28678
+ } : {
28679
+ ruleId,
28680
+ ruleName
28681
+ }
28682
+ });
28683
+ }
28492
28684
  case "update.available": return incomingFromUpdateAvailable(facts, data);
28493
28685
  case "addon.updated": {
28494
28686
  const update = parseAddonUpdateComplete(data);
@@ -28498,7 +28690,7 @@ function incomingFromSystemEvent(event, lookup) {
28498
28690
  case "system.restarting": return incomingFromNodeRestarting(facts, data);
28499
28691
  case "recording.export.completed": return incomingFromExportCompleted(facts, data);
28500
28692
  case "backup.run-completed": return incomingFromBackupCompleted(facts, data);
28501
- case "alert.episode": return incomingFromAlertEpisode(facts);
28693
+ case "alert.episode": return incomingFromAlertEpisode(facts, lookup);
28502
28694
  default: return null;
28503
28695
  }
28504
28696
  }
@@ -28514,9 +28706,11 @@ function incomingFromSystemEvent(event, lookup) {
28514
28706
  * Whether an edge is notified at all (once per episode, reconciled from the
28515
28707
  * alerts store) is decided before this point, by `NcAlertEpisodeLedger`.
28516
28708
  */
28517
- function incomingFromAlertEpisode(facts) {
28709
+ function incomingFromAlertEpisode(facts, lookup) {
28518
28710
  const payload = parseAlertEpisodePayload(facts.data);
28519
28711
  if (payload === null) return null;
28712
+ const attention = incomingFromDeviceAttention(facts, payload, lookup);
28713
+ if (attention !== null) return attention;
28520
28714
  const kind = alertEpisodeKind(payload.severity, payload.phase);
28521
28715
  if (kind === null) return null;
28522
28716
  return systemSubject({
@@ -28537,6 +28731,38 @@ function incomingFromAlertEpisode(facts) {
28537
28731
  ...payload.nodeId !== void 0 ? { nodeId: payload.nodeId } : {}
28538
28732
  });
28539
28733
  }
28734
+ /**
28735
+ * A CRITICAL alert whose source is a DEVICE (D911) is announced in the devices
28736
+ * family — `device-attention-required` / `-resolved`, carrying the device id so
28737
+ * a rule's device filters apply — and NOT as `alert-critical`, so one alert
28738
+ * never notifies twice through two families. A device source whose id is not a
28739
+ * device id falls through to the generic alert kinds.
28740
+ */
28741
+ function incomingFromDeviceAttention(facts, payload, lookup) {
28742
+ if (payload.sourceType !== "device" || payload.sourceId === void 0) return null;
28743
+ const kind = deviceAttentionKind(payload.severity, payload.phase);
28744
+ if (kind === null) return null;
28745
+ const deviceId = Number(payload.sourceId);
28746
+ if (!Number.isInteger(deviceId)) return null;
28747
+ const identity = lookup?.(deviceId);
28748
+ return systemSubject({
28749
+ facts,
28750
+ kind,
28751
+ subject: `alert:${payload.episodeId}`,
28752
+ titleKey: `system.${kind}.title`,
28753
+ bodyKey: `system.${kind}.body`,
28754
+ textParams: {
28755
+ ...deviceTextParams(deviceId, identity),
28756
+ title: payload.title,
28757
+ message: payload.message,
28758
+ category: payload.category,
28759
+ actionUrl: payload.actionUrl ?? ""
28760
+ },
28761
+ deviceId,
28762
+ ...identity !== void 0 ? { deviceType: identity.type } : {},
28763
+ ...payload.nodeId !== void 0 ? { nodeId: payload.nodeId } : {}
28764
+ });
28765
+ }
28540
28766
  /** The shells whose update has its own sentence. Anything else gets the plain
28541
28767
  * one: a body that names a shell nobody observed is worse than a vague body. */
28542
28768
  var WRAPPER_BODY_KINDS = new Set([
@@ -30494,8 +30720,8 @@ var en_default = {
30494
30720
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
30495
30721
  "occupancy.op.occupied": "occupied",
30496
30722
  "occupancy.op.free": "free",
30497
- "system.device-online.title": "Online — {{deviceName}}",
30498
- "system.device-online.body": "Device {{deviceName}} is online",
30723
+ "system.device-online.title": "Back online — {{deviceName}}",
30724
+ "system.device-online.body": "Device {{deviceName}} is back online",
30499
30725
  "system.device-offline.title": "Offline — {{deviceName}}",
30500
30726
  "system.device-offline.body": "Device {{deviceName}} is offline",
30501
30727
  "system.device-disabled.title": "Switched off — {{deviceName}}",
@@ -30508,14 +30734,14 @@ var en_default = {
30508
30734
  "system.device-battery-normal.title": "Battery back to normal — {{deviceName}}",
30509
30735
  "system.device-battery-normal.body": "{{deviceName}} is back to {{percentage}}% battery",
30510
30736
  "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",
30737
+ "system.stream-online.title": "Stream back online — {{deviceName}}",
30738
+ "system.stream-online.body": "Stream for camera {{deviceName}} is back online",
30739
+ "system.stream-online.body.profiled": "{{profile}} stream for camera {{deviceName}} is back online",
30514
30740
  "system.stream-offline.title": "Stream offline — {{deviceName}}",
30515
30741
  "system.stream-offline.body": "Stream for camera {{deviceName}} is offline",
30516
30742
  "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",
30743
+ "system.node-online.title": "Node back online — {{nodeName}}",
30744
+ "system.node-online.body.agent": "Agent {{nodeName}} is back online",
30519
30745
  "system.node-online.body.agent.after": "Agent {{nodeName}} is back online after {{offlineFor}} offline",
30520
30746
  "system.node-offline.title": "Node offline — {{nodeName}}",
30521
30747
  "system.node-offline.body.agent": "Agent {{nodeName}} is offline",
@@ -30527,18 +30753,26 @@ var en_default = {
30527
30753
  "system.addon-crash-loop.body": "The {{addons}} addon on node {{nodeId}} crashed {{count}} times in a row and will NOT be restarted. Everything it provides is offline until it is repaired.",
30528
30754
  "system.detection-blind.title": "Detecting nothing — {{deviceName}}",
30529
30755
  "system.detection-blind.body": {
30530
- "one": "Camera {{deviceName}} has been analysed for {{minutes}} minute with no detection at all",
30531
- "other": "Camera {{deviceName}} has been analysed for {{minutes}} minutes with no detection at all"
30756
+ "one": "Camera {{deviceName}} moved for {{minutes}} minute over {{sessions}} sessions and the detector produced nothing at all, not even a weak candidate",
30757
+ "other": "Camera {{deviceName}} moved for {{minutes}} minutes over {{sessions}} sessions and the detector produced nothing at all, not even a weak candidate"
30532
30758
  },
30759
+ "system.motion-noisy.title": "Constant motion, no subject — {{deviceName}}",
30760
+ "system.motion-noisy.body": "Camera {{deviceName}} has been in motion {{duty}}% of the last {{minutes}} minutes ({{sessions}} sessions) without a single detection. The detector is fine — tune this camera's motion zones or sensitivity.",
30761
+ "system.motion-calm.title": "Motion calm again — {{deviceName}}",
30762
+ "system.motion-calm.body": "Camera {{deviceName}} is no longer in constant motion ({{duty}}% of the last {{minutes}} minutes, {{sessions}} sessions)",
30533
30763
  "system.recording-stalled.title": "Recording stalled — {{deviceName}}",
30534
30764
  "system.recording-stalled.body": {
30535
30765
  "one": "Camera {{deviceName}} has not written a recording segment for {{minutes}} minute",
30536
30766
  "other": "Camera {{deviceName}} has not written a recording segment for {{minutes}} minutes"
30537
30767
  },
30538
- "system.recording-healthy.title": "Recording healthy — {{deviceName}}",
30768
+ "system.recording-healthy.title": "Recording again — {{deviceName}}",
30539
30769
  "system.recording-healthy.body": "Camera {{deviceName}} is writing recording segments again",
30540
30770
  "system.recording-degraded.title": "Recording degraded — {{deviceName}}",
30541
30771
  "system.recording-degraded.body": "Camera {{deviceName}} exhausted its writer restart budget ({{message}})",
30772
+ "system.device-attention-required.title": "{{title}}",
30773
+ "system.device-attention-required.body": "{{message}}",
30774
+ "system.device-attention-resolved.title": "Resolved: {{title}}",
30775
+ "system.device-attention-resolved.body": "{{deviceName}} no longer needs attention",
30542
30776
  "system.storage-location-low.title": "Storage low — {{locationName}}",
30543
30777
  "system.storage-location-low.body": "Location {{locationName}} is down to {{freePercent}}% free ({{reason}})",
30544
30778
  "system.storage-location-critical.title": "Storage critical — {{locationName}}",
@@ -30796,7 +31030,13 @@ var en_default = {
30796
31030
  "calendar-events.line.timed": "• {{when}} {{title}}{{where}}",
30797
31031
  "calendar-events.line.allDay": "• All day {{title}}{{where}}",
30798
31032
  "calendar-events.where": " @ {{location}}",
30799
- "calendar-events.more": "…and {{count}} more"
31033
+ "calendar-events.more": "…and {{count}} more",
31034
+ "system.rule-broken.title": "Notification rule broken — {{ruleName}}",
31035
+ "system.rule-broken.body.unparseable": "The rule \"{{ruleName}}\" cannot be loaded and sends nothing. Open it and save it again.",
31036
+ "system.rule-broken.body.device-deleted": "The rule \"{{ruleName}}\" still names a deleted device ({{offendingIds}}) and was switched off. Pick the device again.",
31037
+ "system.rule-broken.body.target-deleted": "The rule \"{{ruleName}}\" still sends to a deleted destination ({{offendingIds}}) and was switched off. Pick a destination again.",
31038
+ "system.rule-repaired.title": "Notification rule works again — {{ruleName}}",
31039
+ "system.rule-repaired.body": "The rule \"{{ruleName}}\" loads and names nothing deleted any more."
30800
31040
  };
30801
31041
  //#endregion
30802
31042
  //#region src/notification-center/texts/it.json
@@ -30881,8 +31121,8 @@ var it_default = {
30881
31121
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
30882
31122
  "occupancy.op.occupied": "occupata",
30883
31123
  "occupancy.op.free": "libera",
30884
- "system.device-online.title": "Online — {{deviceName}}",
30885
- "system.device-online.body": "Il dispositivo {{deviceName}} è online",
31124
+ "system.device-online.title": "Di nuovo online — {{deviceName}}",
31125
+ "system.device-online.body": "Il dispositivo {{deviceName}} è di nuovo online",
30886
31126
  "system.device-offline.title": "Offline — {{deviceName}}",
30887
31127
  "system.device-offline.body": "Il dispositivo {{deviceName}} è offline",
30888
31128
  "system.device-disabled.title": "Spento — {{deviceName}}",
@@ -30895,14 +31135,14 @@ var it_default = {
30895
31135
  "system.device-battery-normal.title": "Batteria rientrata — {{deviceName}}",
30896
31136
  "system.device-battery-normal.body": "{{deviceName}} è risalito al {{percentage}}% di batteria",
30897
31137
  "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",
31138
+ "system.stream-online.title": "Stream di nuovo online — {{deviceName}}",
31139
+ "system.stream-online.body": "Lo stream della camera {{deviceName}} è di nuovo online",
31140
+ "system.stream-online.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è di nuovo online",
30901
31141
  "system.stream-offline.title": "Stream offline — {{deviceName}}",
30902
31142
  "system.stream-offline.body": "Lo stream della camera {{deviceName}} è offline",
30903
31143
  "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",
31144
+ "system.node-online.title": "Nodo di nuovo online — {{nodeName}}",
31145
+ "system.node-online.body.agent": "L'agent {{nodeName}} è di nuovo online",
30906
31146
  "system.node-online.body.agent.after": "L'agent {{nodeName}} è di nuovo online dopo {{offlineFor}} offline",
30907
31147
  "system.node-offline.title": "Nodo offline — {{nodeName}}",
30908
31148
  "system.node-offline.body.agent": "L'agent {{nodeName}} è offline",
@@ -30917,10 +31157,14 @@ var it_default = {
30917
31157
  "one": "La camera {{deviceName}} non scrive un segmento da {{minutes}} minuto",
30918
31158
  "other": "La camera {{deviceName}} non scrive un segmento da {{minutes}} minuti"
30919
31159
  },
30920
- "system.recording-healthy.title": "Registrazione ok — {{deviceName}}",
31160
+ "system.recording-healthy.title": "Registra di nuovo — {{deviceName}}",
30921
31161
  "system.recording-healthy.body": "La camera {{deviceName}} scrive di nuovo i segmenti",
30922
31162
  "system.recording-degraded.title": "Registrazione degradata — {{deviceName}}",
30923
31163
  "system.recording-degraded.body": "La camera {{deviceName}} ha esaurito i riavvii del writer ({{message}})",
31164
+ "system.device-attention-required.title": "{{title}}",
31165
+ "system.device-attention-required.body": "{{message}}",
31166
+ "system.device-attention-resolved.title": "Risolto: {{title}}",
31167
+ "system.device-attention-resolved.body": "{{deviceName}} non richiede più intervento",
30924
31168
  "system.storage-location-low.title": "Storage basso — {{locationName}}",
30925
31169
  "system.storage-location-low.body": "La location {{locationName}} è al {{freePercent}}% libero ({{reason}})",
30926
31170
  "system.storage-location-critical.title": "Storage critico — {{locationName}}",
@@ -30929,9 +31173,13 @@ var it_default = {
30929
31173
  "system.storage-location-ok.body": "La location {{locationName}} ha recuperato spazio libero",
30930
31174
  "system.detection-blind.title": "Senza detection — {{deviceName}}",
30931
31175
  "system.detection-blind.body": {
30932
- "one": "La camera {{deviceName}} è analizzata da {{minutes}} minuto senza alcun rilevamento",
30933
- "other": "La camera {{deviceName}} è analizzata da {{minutes}} minuti senza alcun rilevamento"
31176
+ "one": "La camera {{deviceName}} ha avuto movimento per {{minutes}} minuto in {{sessions}} sessioni e il detector non ha prodotto nulla, nemmeno un candidato debole",
31177
+ "other": "La camera {{deviceName}} ha avuto movimento per {{minutes}} minuti in {{sessions}} sessioni e il detector non ha prodotto nulla, nemmeno un candidato debole"
30934
31178
  },
31179
+ "system.motion-noisy.title": "Movimento continuo senza soggetto — {{deviceName}}",
31180
+ "system.motion-noisy.body": "La camera {{deviceName}} è stata in movimento il {{duty}}% degli ultimi {{minutes}} minuti ({{sessions}} sessioni) senza alcun rilevamento. Il detector funziona — regola le zone di movimento o la sensibilità di questa camera.",
31181
+ "system.motion-calm.title": "Movimento di nuovo calmo — {{deviceName}}",
31182
+ "system.motion-calm.body": "La camera {{deviceName}} non è più in movimento continuo ({{duty}}% degli ultimi {{minutes}} minuti, {{sessions}} sessioni)",
30935
31183
  "system.addon-update-available.title": {
30936
31184
  "one": "{{count}} aggiornamento addon disponibile",
30937
31185
  "other": "{{count}} aggiornamenti addon disponibili"
@@ -31183,7 +31431,13 @@ var it_default = {
31183
31431
  "calendar-events.line.timed": "• {{when}} {{title}}{{where}}",
31184
31432
  "calendar-events.line.allDay": "• Tutto il giorno {{title}}{{where}}",
31185
31433
  "calendar-events.where": " @ {{location}}",
31186
- "calendar-events.more": "…e altri {{count}}"
31434
+ "calendar-events.more": "…e altri {{count}}",
31435
+ "system.rule-broken.title": "Regola di notifica rotta — {{ruleName}}",
31436
+ "system.rule-broken.body.unparseable": "La regola \"{{ruleName}}\" non si carica e non invia nulla. Aprila e salvala di nuovo.",
31437
+ "system.rule-broken.body.device-deleted": "La regola \"{{ruleName}}\" usa ancora un dispositivo eliminato ({{offendingIds}}) ed è stata disattivata. Scegli di nuovo il dispositivo.",
31438
+ "system.rule-broken.body.target-deleted": "La regola \"{{ruleName}}\" invia ancora a una destinazione eliminata ({{offendingIds}}) ed è stata disattivata. Scegli di nuovo una destinazione.",
31439
+ "system.rule-repaired.title": "Regola di notifica di nuovo funzionante — {{ruleName}}",
31440
+ "system.rule-repaired.body": "La regola \"{{ruleName}}\" si carica e non usa più nulla di eliminato."
31187
31441
  };
31188
31442
  //#endregion
31189
31443
  //#region src/notification-center/text-catalog.ts
@@ -33454,6 +33708,10 @@ function systemEventKeys(kind) {
33454
33708
  titleKey: `system.${kind}.title`,
33455
33709
  bodyKey: `system.${kind}.body.cleanWater`
33456
33710
  };
33711
+ if (kind === "rule-broken") return {
33712
+ titleKey: `system.${kind}.title`,
33713
+ bodyKey: `system.${kind}.body.device-deleted`
33714
+ };
33457
33715
  return {
33458
33716
  titleKey: `system.${kind}.title`,
33459
33717
  bodyKey: `system.${kind}.body`
@@ -33481,6 +33739,7 @@ function systemEventSubject(kind, scope) {
33481
33739
  if (kind === "node-online" || kind === "node-offline") return `node:${scope.nodeId ?? "hub"}`;
33482
33740
  if (kind === "node-inference-unavailable") return `node-inference:${scope.nodeId ?? "hub"}`;
33483
33741
  if (kind === "detection-blind") return `detection-blind:${scope.deviceId ?? 0}`;
33742
+ if (kind === "motion-noisy" || kind === "motion-calm") return `motion-noise:${scope.deviceId ?? 0}`;
33484
33743
  if (kind === "addon-crash-loop") return `addon-crash:${scope.nodeId ?? "hub"}:${TEST_CRASHED_RUNNER}`;
33485
33744
  if (kind === "addon-update-available" || kind === "server-update-available") return `package:${scope.packageName ?? "@camstack/server"}`;
33486
33745
  if (kind === "wrapper-update-available") return `wrapper:${scope.nodeId ?? "hub"}`;
@@ -33626,6 +33885,8 @@ function planRuleTest(input) {
33626
33885
  seconds: "30",
33627
33886
  devices: "cuda:0",
33628
33887
  minutes: "15",
33888
+ duty: "55",
33889
+ sessions: "96",
33629
33890
  addons: TEST_CRASHED_RUNNER,
33630
33891
  list: "@camstack/addon-pipeline 1.0.1 (current 1.0.0)",
33631
33892
  nodes: "hub",
@@ -39694,6 +39955,99 @@ var NcRecordedCutMemo = class {
39694
39955
  }
39695
39956
  };
39696
39957
  //#endregion
39958
+ //#region src/notification-center/notification-label-layout.ts
39959
+ /**
39960
+ * Where each box's caption goes on a notification picture — pure geometry,
39961
+ * rects in, placements out (D897 follow-up, 2026-10-22).
39962
+ *
39963
+ * A caption drawn INSIDE its box used to cover the subject whole on a small
39964
+ * box, and a caption next to a crowd landed on a neighbour's box. The rule:
39965
+ *
39966
+ * 1. Try, in order: OUTSIDE above the box (inside the picture), OUTSIDE below
39967
+ * it, then INSIDE its top edge — the last only when the box is big enough
39968
+ * that the plate is a thin band of it: plate height ≤
39969
+ * {@link NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION} of the box height and plate
39970
+ * width ≤ the box width. A fifth of the height leaves the head and torso of
39971
+ * an upright person, or most of a vehicle, visible; anything taller is the
39972
+ * caption hiding what it names.
39973
+ * 2. A candidate is refused when its plate intersects ANY other box drawn on
39974
+ * the picture, or a plate already placed.
39975
+ * 3. The subject is placed first; the other tracks follow by score, highest
39976
+ * first, so a crowd never costs the subject its caption.
39977
+ * 4. The font never shrinks to make a plate fit: no fitting candidate means
39978
+ * no caption for that box. A box without its caption still carries its
39979
+ * outline and colour.
39980
+ *
39981
+ * Coordinates are the picture's own pixels, whatever it is (a scene or a crop).
39982
+ */
39983
+ /** The inside placement is allowed only when the plate is at most this fraction of the box height. */
39984
+ var NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION = .2;
39985
+ /** A caption is never drawn smaller than this, in output pixels. */
39986
+ var NC_LABEL_MIN_FONT_PX = 14;
39987
+ /** The dark plate a caption sits on, for this text at this font size. */
39988
+ function labelPlateSize(label, fontSize) {
39989
+ return {
39990
+ w: Math.round(label.length * fontSize * .62 + fontSize * .8),
39991
+ h: Math.round(fontSize * 1.35)
39992
+ };
39993
+ }
39994
+ /** Positive-area overlap; rectangles that only touch do not intersect. */
39995
+ function intersects(a, b) {
39996
+ 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;
39997
+ }
39998
+ function captionOrder(boxes) {
39999
+ const rank = (b) => b.role === "subject" ? 0 : 1;
40000
+ return boxes.map((_, i) => i).sort((i, j) => {
40001
+ const a = boxes[i];
40002
+ const b = boxes[j];
40003
+ if (a === void 0 || b === void 0) return 0;
40004
+ return rank(a) - rank(b) || (b.score ?? 0) - (a.score ?? 0) || i - j;
40005
+ });
40006
+ }
40007
+ function candidates(box, plate, width, height) {
40008
+ if (plate.w > width) return [];
40009
+ const x = Math.min(box.x, Math.max(0, width - plate.w));
40010
+ const out = [];
40011
+ if (box.y - plate.h >= 0) out.push({
40012
+ x,
40013
+ y: box.y - plate.h,
40014
+ ...plate,
40015
+ where: "above"
40016
+ });
40017
+ if (box.y + box.h + plate.h <= height) out.push({
40018
+ x,
40019
+ y: box.y + box.h,
40020
+ ...plate,
40021
+ where: "below"
40022
+ });
40023
+ if (plate.h <= box.h * NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION && plate.w <= box.w) out.push({
40024
+ x,
40025
+ y: box.y,
40026
+ ...plate,
40027
+ where: "inside"
40028
+ });
40029
+ return out;
40030
+ }
40031
+ /** Place every caption by the rule above. */
40032
+ function layoutBoxLabels(input) {
40033
+ const { width, height, boxes } = input;
40034
+ const fontSize = Math.max(NC_LABEL_MIN_FONT_PX, input.fontSize);
40035
+ const placements = /* @__PURE__ */ new Map();
40036
+ let dropped = 0;
40037
+ for (const i of captionOrder(boxes)) {
40038
+ const box = boxes[i];
40039
+ if (box === void 0 || box.label.length === 0) continue;
40040
+ 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)));
40041
+ if (chosen === void 0) dropped += 1;
40042
+ else placements.set(i, chosen);
40043
+ }
40044
+ return {
40045
+ placements: boxes.map((_, i) => placements.get(i) ?? null),
40046
+ placed: placements.size,
40047
+ dropped
40048
+ };
40049
+ }
40050
+ //#endregion
39697
40051
  //#region src/notification-center/trigger-zone-still.ts
39698
40052
  /**
39699
40053
  * How far a candidate's instant may sit from the nearest trail point before the
@@ -39816,6 +40170,15 @@ var NC_IMAGE_OTHER_COLOR = "#f2f2f2";
39816
40170
  var NC_IMAGE_ZONE_COLOR = "#29b6f6";
39817
40171
  /** Two tracks whose boxes overlap this much at one instant are the same subject (a fused id). */
39818
40172
  var SAME_SUBJECT_IOU = .8;
40173
+ /**
40174
+ * A box of the SUBJECT'S CLASS overlapping the subject this much at one
40175
+ * instant is the subject itself — an unfused piece L2 has not joined yet
40176
+ * (D683 fuses 10–20 s later; a push leaves ~6 s after the trigger). The
40177
+ * detector's per-class NMS (`NMS_IOU_DEFAULT` 0.45) keeps one of two
40178
+ * same-class boxes above 0.45 in a frame, so two REAL subjects of one class
40179
+ * cannot overlap past it; 0.5 leaves a margin over that.
40180
+ */
40181
+ var NC_IMAGE_SAME_CLASS_SUBJECT_IOU = .5;
39819
40182
  var JPEG_QUALITY = 85;
39820
40183
  /** The caption of a box: class (or label) and confidence — "dog 87%". Language-free. */
39821
40184
  function imageLabelText(name, confidence) {
@@ -39848,15 +40211,21 @@ function toPixels(box, width, height) {
39848
40211
  h: Math.max(1, Math.min(Math.round(box.h * height), height - y))
39849
40212
  };
39850
40213
  }
40214
+ /** {@link notificationOverlay}'s SVG alone. */
40215
+ function notificationOverlaySvg(input) {
40216
+ return notificationOverlay(input).svg;
40217
+ }
39851
40218
  /**
39852
- * The SVG drawn over a scene picture of `width` × `height`. The subject is
39853
- * drawn LAST so a crowd never hides it. Captions sit on a dark plate above
39854
- * the box (inside it when the box touches the top edge), white text.
40219
+ * The SVG drawn over a picture of `width` × `height`. The subject is drawn
40220
+ * LAST so a crowd never hides it. Captions sit on a dark plate, white text,
40221
+ * where `layoutBoxLabels` puts them — outside above, outside below, inside a
40222
+ * box only when it covers a thin band of it, never on another box or caption,
40223
+ * and nowhere at all rather than over the subject.
39855
40224
  */
39856
- function notificationOverlaySvg(input) {
40225
+ function notificationOverlay(input) {
39857
40226
  const { width, height } = input;
39858
40227
  const strokes = overlayStrokes(width, height);
39859
- const fontSize = Math.max(14, Math.round(height / 36));
40228
+ const fontSize = Math.max(NC_LABEL_MIN_FONT_PX, Math.round(height / 36));
39860
40229
  const parts = [];
39861
40230
  for (const zone of input.zones) {
39862
40231
  if (zone.length < 3) continue;
@@ -39868,19 +40237,29 @@ function notificationOverlaySvg(input) {
39868
40237
  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
40238
  }
39870
40239
  const ordered = [...input.boxes.filter((b) => b.role === "other"), ...input.boxes.filter((b) => b.role === "subject")];
39871
- for (const box of ordered) {
40240
+ const layout = layoutBoxLabels({
40241
+ width,
40242
+ height,
40243
+ fontSize,
40244
+ boxes: ordered
40245
+ });
40246
+ const plates = [];
40247
+ ordered.forEach((box, i) => {
39872
40248
  const subject = box.role === "subject";
39873
40249
  const color = subject ? NC_IMAGE_SUBJECT_COLOR : NC_IMAGE_OTHER_COLOR;
39874
40250
  const stroke = subject ? strokes.subject : strokes.other;
39875
40251
  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
- if (box.label.length === 0) continue;
39877
- const plateH = Math.round(fontSize * 1.35);
39878
- const plateW = Math.round(box.label.length * fontSize * .62 + fontSize * .8);
39879
- const plateY = box.y - plateH >= 0 ? box.y - plateH : box.y;
39880
- const plateX = Math.min(box.x, Math.max(0, width - plateW));
39881
- 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>`);
39882
- }
39883
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${String(width)}" height="${String(height)}">${parts.join("")}</svg>`;
40252
+ const plate = layout.placements[i];
40253
+ if (plate === null || plate === void 0) return;
40254
+ 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>`);
40255
+ });
40256
+ return {
40257
+ svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${String(width)}" height="${String(height)}">${[...parts, ...plates].join("")}</svg>`,
40258
+ labels: {
40259
+ placed: layout.placed,
40260
+ dropped: layout.dropped
40261
+ }
40262
+ };
39884
40263
  }
39885
40264
  /** The pixel rectangle a `crop` cuts from a `width` × `height` frame, or null when too small. */
39886
40265
  function subjectCropRect(box, width, height) {
@@ -39910,11 +40289,18 @@ function iou$4(a, b) {
39910
40289
  }
39911
40290
  /**
39912
40291
  * The tracks to draw thin: every track at the instant except the subject —
39913
- * the same id, or a box that is the subject's (a fused id reads under its
39914
- * survivor's name).
39915
- */
39916
- function otherTracks(tracks, subjectTrackId, subjectBox) {
39917
- return tracks.filter((t) => t.trackId !== subjectTrackId && (subjectBox === null || iou$4(t.bbox, subjectBox) < SAME_SUBJECT_IOU));
40292
+ * the same id, a box that is the subject's (a fused id reads under its
40293
+ * survivor's name), or a box of the subject's class over it past what NMS
40294
+ * lets two real subjects share (an unfused piece of the same subject).
40295
+ */
40296
+ function otherTracks(tracks, subjectTrackId, subjectBox, subjectClassName) {
40297
+ return tracks.filter((t) => {
40298
+ if (t.trackId === subjectTrackId) return false;
40299
+ if (subjectBox === null) return true;
40300
+ const overlap = iou$4(t.bbox, subjectBox);
40301
+ if (overlap >= SAME_SUBJECT_IOU) return false;
40302
+ return !(t.className === subjectClassName && overlap >= NC_IMAGE_SAME_CLASS_SUBJECT_IOU);
40303
+ });
39918
40304
  }
39919
40305
  /**
39920
40306
  * What picture a ROW asks for. A row written since D897 carries `mediaImage`;
@@ -39931,9 +40317,15 @@ function payloadImageMode(payload) {
39931
40317
  if (payload.media === "keyFrame") return "clean";
39932
40318
  return "crop";
39933
40319
  }
39934
- /** The zone ids a row's picture outlines: the zones that fired it, when it asked. */
40320
+ /**
40321
+ * The zone ids a row's picture outlines: the zone(s) THIS event triggered,
40322
+ * when it asked — never every zone the rule lists (a dog in Uscio drew Aiuola
40323
+ * too). A row that asked but froze no `matchedZoneIds` (written before the
40324
+ * field) draws none: an outline of a zone the subject never entered is a
40325
+ * picture that lies about its own reason.
40326
+ */
39935
40327
  function payloadZoneOverlayIds(payload) {
39936
- if (payload.mediaShowTriggerZone === true) return payload.triggerZoneIds ?? [];
40328
+ if (payload.mediaShowTriggerZone === true) return payload.matchedZoneIds ?? [];
39937
40329
  if (payload.mediaImage === void 0) return payload.mediaZoneIds ?? [];
39938
40330
  return [];
39939
40331
  }
@@ -40022,7 +40414,8 @@ async function renderNotificationImage(req) {
40022
40414
  const boxes = req.mode === "scene-boxes" ? [...req.others.map((o) => ({
40023
40415
  ...toPixels(o, outW, outH),
40024
40416
  role: "other",
40025
- label: o.label
40417
+ label: o.label,
40418
+ ...o.confidence !== void 0 ? { score: o.confidence } : {}
40026
40419
  })), ...req.subject !== null ? [{
40027
40420
  ...toPixels(req.subject, outW, outH),
40028
40421
  role: "subject",
@@ -40040,16 +40433,18 @@ async function renderNotificationImage(req) {
40040
40433
  height: outH,
40041
40434
  fit: "fill"
40042
40435
  });
40436
+ let labels;
40043
40437
  if (boxes.length > 0 || zones.length > 0 || paths.length > 0) {
40044
- const svg = notificationOverlaySvg({
40438
+ const overlay = notificationOverlay({
40045
40439
  width: outW,
40046
40440
  height: outH,
40047
40441
  boxes,
40048
40442
  zones,
40049
40443
  paths
40050
40444
  });
40445
+ if (boxes.length > 0) labels = overlay.labels;
40051
40446
  pipeline = pipeline.composite([{
40052
- input: Buffer.from(svg),
40447
+ input: Buffer.from(overlay.svg),
40053
40448
  top: 0,
40054
40449
  left: 0
40055
40450
  }]);
@@ -40059,7 +40454,8 @@ async function renderNotificationImage(req) {
40059
40454
  return {
40060
40455
  bytes: toArrayBufferBytes(out),
40061
40456
  name: `${rendered}.jpg`,
40062
- rendered
40457
+ rendered,
40458
+ ...labels !== void 0 ? { labels } : {}
40063
40459
  };
40064
40460
  }
40065
40461
  var NcNotificationImageStage = class {
@@ -40133,10 +40529,29 @@ var NcNotificationImageStage = class {
40133
40529
  ...subjectBox,
40134
40530
  label: imageLabelText(payload.subject.label ?? payload.subject.className, payload.subject.confidence)
40135
40531
  } : null;
40136
- const others = mode === "scene-boxes" ? otherTracks(await this.tracksAt(camera, source.timestamp), subjectTrackId, subjectBox).map((t) => ({
40532
+ const othersDrawn = mode === "scene-boxes" ? otherTracks(await this.tracksAt(camera, source.timestamp), subjectTrackId, subjectBox, payload.subject.className) : [];
40533
+ if (mode === "scene-boxes") this.deps.logger.info("scene-boxes: other boxes drawn", {
40534
+ tags: { deviceId: camera },
40535
+ meta: {
40536
+ ruleId: entry.ruleId,
40537
+ recordId: entry.recordId,
40538
+ instant: source.timestamp,
40539
+ drawn: othersDrawn.map((t) => `${t.trackId}/${t.className}/dt=${t.dtMs ?? "?"}ms/${t.interpolated === true ? "interpolated" : "exact"}`).join(",")
40540
+ }
40541
+ });
40542
+ const others = mode === "scene-boxes" ? othersDrawn.map((t) => ({
40137
40543
  ...t.bbox,
40138
- label: imageLabelText(t.label ?? t.className, t.confidence)
40544
+ label: imageLabelText(t.label ?? t.className, t.confidence),
40545
+ ...t.confidence !== void 0 ? { confidence: t.confidence } : {}
40139
40546
  })) : [];
40547
+ 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", {
40548
+ tags: { deviceId: camera },
40549
+ meta: {
40550
+ ruleId: entry.ruleId,
40551
+ recordId: entry.recordId,
40552
+ ruleZoneIds: (payload.triggerZoneIds ?? []).join(",")
40553
+ }
40554
+ });
40140
40555
  const zones = mode === "crop" || zoneIds.length === 0 ? [] : await this.zonePolygons(camera, zoneIds);
40141
40556
  try {
40142
40557
  const image = await this.render({
@@ -40149,6 +40564,7 @@ var NcNotificationImageStage = class {
40149
40564
  });
40150
40565
  const reason = image.rendered === mode ? void 0 : mode === "crop" ? "no-subject-box" : "no-tracks-at-instant";
40151
40566
  if (reason !== void 0) this.reportFallback(input, mode, reason);
40567
+ if (image.labels !== void 0) this.reportLabels(input, image.labels);
40152
40568
  return reason !== void 0 ? {
40153
40569
  ...image,
40154
40570
  reason
@@ -40197,6 +40613,19 @@ var NcNotificationImageStage = class {
40197
40613
  }
40198
40614
  });
40199
40615
  }
40616
+ /** ONE line per rendered picture: how many captions it carries and how many had no clear spot. */
40617
+ reportLabels(input, labels) {
40618
+ const camera = input.camera === "composite" ? input.entry.payload.subject.deviceId : input.camera;
40619
+ this.deps.logger.debug("notification picture captions laid out", {
40620
+ tags: { deviceId: camera },
40621
+ meta: {
40622
+ ruleId: input.entry.ruleId,
40623
+ recordId: input.entry.recordId,
40624
+ placed: labels.placed,
40625
+ dropped: labels.dropped
40626
+ }
40627
+ });
40628
+ }
40200
40629
  /** The subject's trail, read once per render for its box AND its path. */
40201
40630
  async subjectTrail(camera, trackId) {
40202
40631
  const read = this.deps.getSubjectTrail;
@@ -46890,6 +47319,27 @@ function isRecord(value) {
46890
47319
  return typeof value === "object" && value !== null && !Array.isArray(value);
46891
47320
  }
46892
47321
  /**
47322
+ * Incident 2026-10-22: a row stored with `groupingSec: 0` failed `NcRuleSchema` (min
47323
+ * {@link NC_GROUPING_MIN_SEC}) and the WHOLE rule was skipped as malformed on every load — a
47324
+ * system-event rule on which a window cannot even apply vanished from the engine and from
47325
+ * `listRules`. A non-positive (or non-numeric) window only ever meant "no window": dropped. A
47326
+ * positive one outside the range is clamped. One bad optional field never costs the rule.
47327
+ */
47328
+ function repairStoredGroupingSec(value) {
47329
+ if (value === void 0) return { kind: "unchanged" };
47330
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return {
47331
+ kind: "repaired",
47332
+ from: value,
47333
+ to: null
47334
+ };
47335
+ const clamped = clampGroupingSec(value);
47336
+ return clamped === value ? { kind: "unchanged" } : {
47337
+ kind: "repaired",
47338
+ from: value,
47339
+ to: clamped
47340
+ };
47341
+ }
47342
+ /**
46893
47343
  * Project a RAW stored rule value (`data['rule']`, before any Zod parse). A value that is not
46894
47344
  * an object, or has no string `delivery`, is returned untouched: the schema parse that follows
46895
47345
  * rejects it exactly as before, and a projection must never be what decides a row is valid.
@@ -46897,9 +47347,12 @@ function isRecord(value) {
46897
47347
  function projectRawStoredRule(raw) {
46898
47348
  if (!isRecord(raw) || typeof raw["delivery"] !== "string") return {
46899
47349
  candidate: raw,
46900
- projection: { kind: "unchanged" }
47350
+ projection: { kind: "unchanged" },
47351
+ groupingRepair: { kind: "unchanged" }
46901
47352
  };
46902
- const groupingSec = raw["groupingSec"];
47353
+ const groupingRepair = repairStoredGroupingSec(raw["groupingSec"]);
47354
+ const repaired = withRepairedGrouping(raw, groupingRepair);
47355
+ const groupingSec = repaired["groupingSec"];
46903
47356
  const projection = projectRetiredRuleFields({
46904
47357
  delivery: raw["delivery"],
46905
47358
  conditions: isRecord(raw["conditions"]) ? raw["conditions"] : {},
@@ -46908,15 +47361,25 @@ function projectRawStoredRule(raw) {
46908
47361
  ...typeof groupingSec === "number" ? { groupingSec } : {}
46909
47362
  });
46910
47363
  if (projection.kind === "unchanged" || projection.groupingSec === void 0) return {
46911
- candidate: raw,
46912
- projection
47364
+ candidate: repaired,
47365
+ projection,
47366
+ groupingRepair
46913
47367
  };
46914
47368
  return {
46915
47369
  candidate: {
46916
- ...raw,
47370
+ ...repaired,
46917
47371
  groupingSec: projection.groupingSec
46918
47372
  },
46919
- projection
47373
+ projection,
47374
+ groupingRepair
47375
+ };
47376
+ }
47377
+ function withRepairedGrouping(raw, repair) {
47378
+ if (repair.kind === "unchanged") return raw;
47379
+ const { groupingSec: _dropped, ...rest } = raw;
47380
+ return repair.to === null ? rest : {
47381
+ ...rest,
47382
+ groupingSec: repair.to
46920
47383
  };
46921
47384
  }
46922
47385
  /**
@@ -46969,16 +47432,44 @@ var LOAD_LIMIT = 1e4;
46969
47432
  * Parse a raw body object (as returned from storage — may contain retired keys)
46970
47433
  * into an `NcRule` applying the legacy field projection.
46971
47434
  */
46972
- function bodyToRule(id, raw, onProjection) {
46973
- const { candidate, projection } = projectRawStoredRule(raw);
47435
+ function bodyToRule(id, raw, onProjection, onGroupingRepair) {
47436
+ const parsed = parseStoredRule(id, raw, onProjection, onGroupingRepair);
47437
+ return parsed.ok ? parsed.rule : null;
47438
+ }
47439
+ function parseStoredRule(id, raw, onProjection, onGroupingRepair, onClassWidening) {
47440
+ const { candidate, projection, groupingRepair } = projectRawStoredRule(raw);
46974
47441
  const parsed = NcRuleSchema.safeParse({
46975
47442
  ...candidate,
46976
47443
  id
46977
47444
  });
46978
- if (!parsed.success) return null;
47445
+ if (!parsed.success) {
47446
+ const issue = parsed.error.issues[0];
47447
+ return {
47448
+ ok: false,
47449
+ reason: issue === void 0 ? "invalid" : `${issue.path.join(".") || "(root)"}: ${issue.message}`
47450
+ };
47451
+ }
46979
47452
  if (projection.kind === "projected") onProjection?.(id, projection);
46980
- return withNormalizedMedia(parsed.data);
47453
+ if (groupingRepair.kind === "repaired") onGroupingRepair?.(id, groupingRepair);
47454
+ const media = withNormalizedMedia(parsed.data);
47455
+ const rule = withSystemEventClasses(media);
47456
+ if (rule !== media) onClassWidening?.(id, media, rule);
47457
+ return {
47458
+ ok: true,
47459
+ rule
47460
+ };
47461
+ }
47462
+ /**
47463
+ * D906: what a rule is stored as — its media policy folded (D897) and, on an
47464
+ * INSTANT system rule, every paired class it touches completed with both
47465
+ * edges. Applied on every WRITE; the load applies the same through
47466
+ * {@link parseStoredRule}, which also says once that it widened a rule.
47467
+ */
47468
+ function normalizeStoredRule(rule) {
47469
+ return withSystemEventClasses(withNormalizedMedia(rule));
46981
47470
  }
47471
+ /** At most this many skipped rows are NAMED in the WARN line; the count is always whole. */
47472
+ var SKIPPED_ROWS_NAMED_MAX = 10;
46982
47473
  /**
46983
47474
  * D897: the rule with its media policy in the stored shape — `image` set, the
46984
47475
  * legacy `attach` / `frame` / `zoneCrop` folded into it. Applied on LOAD (a read
@@ -47055,6 +47546,8 @@ var NcRuleStore = class NcRuleStore {
47055
47546
  now;
47056
47547
  newId;
47057
47548
  identityIdsByName;
47549
+ /** Rows the last successful {@link load} skipped; `null` before one succeeded. */
47550
+ skipped = null;
47058
47551
  /** Has a {@link load} ever completed? See the note there. */
47059
47552
  loaded = false;
47060
47553
  /** Legacy detection rules naming sounds already reported (D690) — once each. */
@@ -47066,6 +47559,10 @@ var NcRuleStore = class NcRuleStore {
47066
47559
  pendingProjections = /* @__PURE__ */ new Map();
47067
47560
  /** Rules whose projection was already logged in this process (D692 §9) — once each. */
47068
47561
  projectionLogged = /* @__PURE__ */ new Set();
47562
+ /** Rules whose out-of-range `groupingSec` repair was already logged — once each. */
47563
+ groupingRepairLogged = /* @__PURE__ */ new Set();
47564
+ /** Rules whose class widening (D906) was already logged — once each. */
47565
+ classWideningLogged = /* @__PURE__ */ new Set();
47069
47566
  constructor(deps) {
47070
47567
  this.logger = deps.logger;
47071
47568
  this.now = deps.now ?? (() => Date.now());
@@ -47100,20 +47597,29 @@ var NcRuleStore = class NcRuleStore {
47100
47597
  try {
47101
47598
  const { records } = await this.kind.query({ limit: LOAD_LIMIT });
47102
47599
  const next = /* @__PURE__ */ new Map();
47103
- let skipped = 0;
47600
+ const skippedRows = [];
47104
47601
  for (const row of records) {
47105
- const rule = bodyToRule(row.id, row.data, (ruleId, projection) => {
47602
+ const parsed = parseStoredRule(row.id, row.data, (ruleId, projection) => {
47106
47603
  this.pendingProjections.set(ruleId, projection);
47107
- });
47108
- if (rule === null) {
47109
- skipped += 1;
47604
+ }, (ruleId, repair) => this.logGroupingRepair(ruleId, repair), (ruleId, before, after) => this.logClassWidening(ruleId, before, after));
47605
+ if (!parsed.ok) {
47606
+ const name = row.data["name"];
47607
+ skippedRows.push({
47608
+ ruleId: row.id,
47609
+ name: typeof name === "string" ? name : null,
47610
+ reason: parsed.reason
47611
+ });
47110
47612
  continue;
47111
47613
  }
47112
- next.set(row.id, rule);
47614
+ next.set(row.id, parsed.rule);
47113
47615
  }
47114
47616
  this.byId.clear();
47115
47617
  for (const [id, rule] of next) this.byId.set(id, rule);
47116
- if (skipped > 0) this.logger.warn("notification rule rows skipped as malformed", { meta: { skipped } });
47618
+ this.skipped = skippedRows;
47619
+ if (skippedRows.length > 0) this.logger.warn("notification rule rows skipped as malformed", { meta: {
47620
+ skipped: skippedRows.length,
47621
+ rows: skippedRows.slice(0, SKIPPED_ROWS_NAMED_MAX)
47622
+ } });
47117
47623
  this.loaded = true;
47118
47624
  } catch (err) {
47119
47625
  this.logger.warn("notification rules load failed — keeping rules already in memory", { meta: {
@@ -47125,6 +47631,27 @@ var NcRuleStore = class NcRuleStore {
47125
47631
  for (const id of warnLegacyAudioClassRules([...this.byId.values()], this.audioClassWarned, this.logger)) this.audioClassWarned.add(id);
47126
47632
  this.logger.debug("notification rules loaded", { meta: { rules: this.byId.size } });
47127
47633
  }
47634
+ /** Say, once per process per rule, that an instant rule was read as its whole class (D906). */
47635
+ logClassWidening(ruleId, before, after) {
47636
+ if (this.classWideningLogged.has(ruleId)) return;
47637
+ this.classWideningLogged.add(ruleId);
47638
+ const had = new Set(before.conditions.systemEvent?.kinds ?? []);
47639
+ this.logger.info("instant system rule read as its whole event class — both edges", { meta: {
47640
+ ruleId,
47641
+ rule: after.name,
47642
+ added: (after.conditions.systemEvent?.kinds ?? []).filter((kind) => !had.has(kind))
47643
+ } });
47644
+ }
47645
+ /** Say, once per process per rule, that an out-of-range stored `groupingSec` was repaired. */
47646
+ logGroupingRepair(ruleId, repair) {
47647
+ if (this.groupingRepairLogged.has(ruleId)) return;
47648
+ this.groupingRepairLogged.add(ruleId);
47649
+ this.logger.warn("rule carries an out-of-range groupingSec — repaired on read", { meta: {
47650
+ ruleId,
47651
+ from: repair.from,
47652
+ to: repair.to
47653
+ } });
47654
+ }
47128
47655
  /** Log, once per process per rule, what `bodyToRule` projected from a retired field. */
47129
47656
  logPendingProjections() {
47130
47657
  for (const [ruleId, projection] of this.pendingProjections) {
@@ -47193,6 +47720,13 @@ var NcRuleStore = class NcRuleStore {
47193
47720
  * camera's ring) must ask this first: before the first load the empty list
47194
47721
  * is not a fact, and a failed load leaves whatever was already mirrored.
47195
47722
  */
47723
+ /**
47724
+ * The rows the last SUCCESSFUL load could not parse — `null` until one has
47725
+ * succeeded, so "not read yet" never reads as "nothing broken" (D315).
47726
+ */
47727
+ skippedRows() {
47728
+ return this.skipped;
47729
+ }
47196
47730
  isLoaded() {
47197
47731
  return this.loaded;
47198
47732
  }
@@ -47222,7 +47756,7 @@ var NcRuleStore = class NcRuleStore {
47222
47756
  /** Create a new rule. `createdBy` is the SERVER-injected caller userId. */
47223
47757
  async create(input, createdBy) {
47224
47758
  const now = this.now();
47225
- const rule = withNormalizedMedia({
47759
+ const rule = normalizeStoredRule({
47226
47760
  ...input,
47227
47761
  id: this.newId(),
47228
47762
  createdBy,
@@ -47267,7 +47801,7 @@ var NcRuleStore = class NcRuleStore {
47267
47801
  createdAt: existing.createdAt,
47268
47802
  updatedAt: this.now()
47269
47803
  };
47270
- const updated = withNormalizedMedia(NcRuleSchema.parse(candidate));
47804
+ const updated = normalizeStoredRule(NcRuleSchema.parse(candidate));
47271
47805
  await this.write(updated);
47272
47806
  return updated;
47273
47807
  }
@@ -47307,7 +47841,7 @@ var NcRuleStore = class NcRuleStore {
47307
47841
  id: ruleId
47308
47842
  } : candidate);
47309
47843
  if (!parsed.success) return existing;
47310
- const next = withNormalizedMedia(parsed.data);
47844
+ const next = normalizeStoredRule(parsed.data);
47311
47845
  const { id: _omit, ...data } = next;
47312
47846
  await this.kind.put({
47313
47847
  id: ruleId,
@@ -53336,6 +53870,104 @@ var NcZoneOwnershipCache = class {
53336
53870
  }
53337
53871
  };
53338
53872
  //#endregion
53873
+ //#region src/notification-center/rule-health.ts
53874
+ var EMPTY_SNAPSHOT = {
53875
+ timelapse: [],
53876
+ summary: [],
53877
+ alarm: null,
53878
+ mutedDeviceIds: []
53879
+ };
53880
+ function fingerprintOf(reasons) {
53881
+ return reasons.map((r) => `${r.code}:${[...r.ids].sort().join(",")}`).join("|");
53882
+ }
53883
+ /** The deleted subjects a rule still names, from the prune's `kept*` records. */
53884
+ function deletedReasons(rule, prunes) {
53885
+ const snapshot = {
53886
+ ...EMPTY_SNAPSHOT,
53887
+ rules: [rule]
53888
+ };
53889
+ const devices = referencedDeviceIds(snapshot);
53890
+ const targets = referencedTargetIds(snapshot);
53891
+ const deadDevices = /* @__PURE__ */ new Set();
53892
+ const deadTargets = /* @__PURE__ */ new Set();
53893
+ for (const p of prunes) {
53894
+ if (p.kind !== "rule" || p.ruleId !== rule.id) continue;
53895
+ if (p.outcome !== "kept" && p.outcome !== "kept-disabled") continue;
53896
+ if (p.subjectKind === "device" && devices.has(Number(p.subject))) deadDevices.add(p.subject);
53897
+ if (p.subjectKind === "target" && targets.has(p.subject)) deadTargets.add(p.subject);
53898
+ }
53899
+ const reasons = [];
53900
+ if (deadDevices.size > 0) reasons.push({
53901
+ code: "device-deleted",
53902
+ ids: [...deadDevices].sort()
53903
+ });
53904
+ if (deadTargets.size > 0) reasons.push({
53905
+ code: "target-deleted",
53906
+ ids: [...deadTargets].sort()
53907
+ });
53908
+ return reasons;
53909
+ }
53910
+ var NcRuleHealthMonitor = class {
53911
+ verdicts = /* @__PURE__ */ new Map();
53912
+ /** Judge every rule once; return the edges to announce. */
53913
+ evaluate(inputs) {
53914
+ const next = /* @__PURE__ */ new Map();
53915
+ const seen = /* @__PURE__ */ new Set();
53916
+ if (inputs.skipped !== null) for (const row of inputs.skipped) {
53917
+ seen.add(row.ruleId);
53918
+ const unparseable = {
53919
+ code: "unparseable",
53920
+ ids: [],
53921
+ detail: row.reason
53922
+ };
53923
+ next.set(row.ruleId, this.verdict(row.name, unparseable, []));
53924
+ }
53925
+ if (inputs.rules !== null) for (const rule of inputs.rules) {
53926
+ seen.add(rule.id);
53927
+ const previous = this.verdicts.get(rule.id);
53928
+ const deleted = inputs.prunes !== null ? deletedReasons(rule, inputs.prunes) : previous?.deleted ?? [];
53929
+ const unparseable = inputs.skipped !== null ? null : previous?.unparseable ?? null;
53930
+ next.set(rule.id, this.verdict(rule.name, unparseable, deleted));
53931
+ }
53932
+ const edges = [];
53933
+ const answeredAll = inputs.rules !== null && inputs.skipped !== null;
53934
+ for (const [ruleId, previous] of this.verdicts) {
53935
+ if (seen.has(ruleId)) continue;
53936
+ if (!answeredAll) next.set(ruleId, previous);
53937
+ }
53938
+ for (const [ruleId, verdict] of next) {
53939
+ const previous = this.verdicts.get(ruleId);
53940
+ const reasons = reasonsOf(verdict);
53941
+ if (reasons.length > 0) {
53942
+ if (previous?.fingerprint !== verdict.fingerprint) edges.push({
53943
+ kind: "broken",
53944
+ ruleId,
53945
+ ruleName: verdict.name,
53946
+ reasons
53947
+ });
53948
+ } else if (previous !== void 0 && reasonsOf(previous).length > 0) edges.push({
53949
+ kind: "repaired",
53950
+ ruleId,
53951
+ ruleName: verdict.name
53952
+ });
53953
+ }
53954
+ this.verdicts.clear();
53955
+ for (const [ruleId, verdict] of next) if (reasonsOf(verdict).length > 0) this.verdicts.set(ruleId, verdict);
53956
+ return edges;
53957
+ }
53958
+ verdict(name, unparseable, deleted) {
53959
+ return {
53960
+ name,
53961
+ unparseable,
53962
+ deleted,
53963
+ fingerprint: fingerprintOf([...unparseable !== null ? [unparseable] : [], ...deleted])
53964
+ };
53965
+ }
53966
+ };
53967
+ function reasonsOf(verdict) {
53968
+ return [...verdict.unparseable !== null ? [verdict.unparseable] : [], ...verdict.deleted];
53969
+ }
53970
+ //#endregion
53339
53971
  //#region src/notification-center/index.ts
53340
53972
  /**
53341
53973
  * NotificationCenter — the P1 core of the Notification Center (spec
@@ -53402,7 +54034,7 @@ var NC_ALARM_ANNOUNCE_RULE_ID = "nc-alarm-announce";
53402
54034
  * media (the SOURCE rule of a combined alarm row), and a zone overlay is
53403
54035
  * frozen only when the rule asked for one AND fired on a zone.
53404
54036
  */
53405
- function mediaPolicyFields(mediaRule, cropZoneIds) {
54037
+ function mediaPolicyFields(mediaRule, cropZoneIds, matchedZoneIds) {
53406
54038
  const media = effectiveMediaPolicyForRule(mediaRule);
53407
54039
  const image = media.image ?? "crop";
53408
54040
  return {
@@ -53411,6 +54043,7 @@ function mediaPolicyFields(mediaRule, cropZoneIds) {
53411
54043
  ...media.showTriggerZone === true && cropZoneIds !== void 0 && cropZoneIds.length > 0 ? { mediaShowTriggerZone: true } : {},
53412
54044
  ...media.showSubjectPath === true && image !== "none" ? { mediaShowSubjectPath: true } : {},
53413
54045
  ...cropZoneIds !== void 0 ? { triggerZoneIds: [...cropZoneIds] } : {},
54046
+ ...matchedZoneIds !== void 0 ? { matchedZoneIds: [...matchedZoneIds] } : {},
53414
54047
  ...media.profile !== void 0 ? { mediaProfile: media.profile } : {},
53415
54048
  ...media.gif === true ? { mediaGif: true } : {},
53416
54049
  ...media.clip === true ? { mediaClip: true } : {},
@@ -53916,6 +54549,21 @@ function groupOrderInput(fate, triggerZoneIds) {
53916
54549
  * condition's ids, else an occupancy rule's one zone. A grouped member freezes the same
53917
54550
  * ids a plain row of the rule would.
53918
54551
  */
54552
+ /**
54553
+ * The zones of `cropZoneIds` this subject actually triggered — what the zone
54554
+ * condition saw (membership plus the crossed zone, `zonesVisitedBy`), on the
54555
+ * rule whose media the row carries. On a combined alarm row the subject is the
54556
+ * alarm, so the source match's frozen zones answer. An occupancy rule's one
54557
+ * zone is the zone the count edge is ABOUT, so it is its own trigger.
54558
+ */
54559
+ function matchedTriggerZoneIds(mediaRule, cropZoneIds, subject) {
54560
+ if (cropZoneIds === void 0) return void 0;
54561
+ if (mediaRule.conditions.zones === void 0) return [...cropZoneIds];
54562
+ const alarmSource = subject.systemEvent?.alarmSource;
54563
+ const visited = new Set(alarmSource !== void 0 ? alarmSource.zones ?? [] : subject.zones);
54564
+ if (alarmSource === void 0 && subject.crossing !== void 0) visited.add(subject.crossing.zoneId);
54565
+ return cropZoneIds.filter((id) => visited.has(id));
54566
+ }
53919
54567
  function ruleCropZoneIds(rule) {
53920
54568
  if (rule.conditions.zones !== void 0) return rule.conditions.zones.ids;
53921
54569
  const zoneId = rule.conditions.occupancy?.zoneId;
@@ -54069,6 +54717,8 @@ var NotificationCenter = class {
54069
54717
  * evaluating one.
54070
54718
  */
54071
54719
  pruneLedger;
54720
+ /** D905: which rules cannot work, announced once per distinct reason. */
54721
+ ruleHealth = new NcRuleHealthMonitor();
54072
54722
  /** The prune itself — see `device-prune-runner.ts`. */
54073
54723
  pruner;
54074
54724
  /**
@@ -57498,7 +58148,7 @@ var NotificationCenter = class {
57498
58148
  const subject = incoming.subject;
57499
58149
  const alarmSource = subject.systemEvent?.alarmSource;
57500
58150
  const sourceRule = alarmSource !== void 0 ? this.rules.get(alarmSource.ruleId) : null;
57501
- const media = sourceRule !== null ? mediaPolicyFields(sourceRule, ruleCropZoneIds(sourceRule)) : { media: "none" };
58151
+ const media = sourceRule !== null ? mediaPolicyFields(sourceRule, ruleCropZoneIds(sourceRule), matchedTriggerZoneIds(sourceRule, ruleCropZoneIds(sourceRule), subject)) : { media: "none" };
57502
58152
  const mediaDeviceId = alarmSource?.deviceId ?? panel.deviceId;
57503
58153
  const entries = targets.map((targetId) => ({
57504
58154
  ruleId: NC_ALARM_ANNOUNCE_RULE_ID,
@@ -58704,6 +59354,7 @@ var NotificationCenter = class {
58704
59354
  const renderedZones = notificationZones(rule, subject);
58705
59355
  const confirm = frozenConfirmPolicy(rule, isSystemEvent);
58706
59356
  const cropZoneIds = ruleCropZoneIds(mediaRule);
59357
+ const matchedZoneIds = matchedTriggerZoneIds(mediaRule, cropZoneIds, subject);
58707
59358
  const allTargets = [...rule.targets, ...userTargets.filter((t) => !direct.has(t.targetId))];
58708
59359
  const linkedOwners = !noMedia && subject.linkedStills !== void 0 ? linkedMediaOwners(subject.linkedStills, linkedCameraMode(mediaRule.media)) : void 0;
58709
59360
  const groupV2 = extras?.groupV2 ?? (subject.systemEvent !== void 0 && !noMedia ? alarmIncidentGroup(subject.systemEvent, subject.timestamp) : void 0);
@@ -58714,7 +59365,7 @@ var NotificationCenter = class {
58714
59365
  ...origin === "synthetic" ? { synthetic: true } : {},
58715
59366
  priority: rule.priority,
58716
59367
  ...rule.template !== void 0 ? { template: rule.template } : {},
58717
- ...noMedia ? { media: "none" } : mediaPolicyFields(mediaRule, cropZoneIds),
59368
+ ...noMedia ? { media: "none" } : mediaPolicyFields(mediaRule, cropZoneIds, matchedZoneIds),
58718
59369
  ...confirm !== void 0 ? {
58719
59370
  confirm,
58720
59371
  ...cooldown !== void 0 ? { cooldown } : {},
@@ -59359,6 +60010,51 @@ var NotificationCenter = class {
59359
60010
  if (!this.pruneLedger.isLoaded()) await this.pruneLedger.load();
59360
60011
  await this.pruner.reconcileDevices();
59361
60012
  await this.pruner.reconcileTargets();
60013
+ this.announceRuleHealth();
60014
+ }
60015
+ /**
60016
+ * D905 — say which rules cannot work: a row the load skipped, or a rule still
60017
+ * naming a device / target the prune confirmed deleted. One edge per rule per
60018
+ * distinct set of reasons; a reading that did not answer changes nothing.
60019
+ */
60020
+ announceRuleHealth() {
60021
+ const edges = this.ruleHealth.evaluate({
60022
+ rules: this.rules.isLoaded() ? this.rules.list() : null,
60023
+ skipped: this.rules.skippedRows(),
60024
+ prunes: this.pruneLedger.isLoaded() ? this.pruner.listPrunes() : null
60025
+ });
60026
+ for (const edge of edges) {
60027
+ const source = {
60028
+ type: "addon",
60029
+ id: "notification-center"
60030
+ };
60031
+ if (edge.kind === "broken") {
60032
+ const [first] = edge.reasons;
60033
+ if (first === void 0) continue;
60034
+ const offendingIds = [...new Set(edge.reasons.flatMap((r) => r.ids))];
60035
+ this.logger.warn("notification rule cannot work — announced", { meta: {
60036
+ ruleId: edge.ruleId,
60037
+ rule: edge.ruleName,
60038
+ reasons: edge.reasons
60039
+ } });
60040
+ this.deps.emitBusEvent?.(createEvent(EventCategory.NotificationRuleBroken, source, {
60041
+ ruleId: edge.ruleId,
60042
+ ruleName: edge.ruleName,
60043
+ reason: first.code,
60044
+ reasons: edge.reasons,
60045
+ offendingIds
60046
+ }));
60047
+ } else {
60048
+ this.logger.info("notification rule works again — announced", { meta: {
60049
+ ruleId: edge.ruleId,
60050
+ rule: edge.ruleName
60051
+ } });
60052
+ this.deps.emitBusEvent?.(createEvent(EventCategory.NotificationRuleRepaired, source, {
60053
+ ruleId: edge.ruleId,
60054
+ ruleName: edge.ruleName
60055
+ }));
60056
+ }
60057
+ }
59362
60058
  }
59363
60059
  /**
59364
60060
  * `DeviceUnregistered` — a device was DELETED (only `removeDevice` emits it,
@@ -87156,7 +87852,12 @@ var RetrainCollectionService = class {
87156
87852
  reason: "unknown-collection"
87157
87853
  };
87158
87854
  }
87159
- const stored = await this.storeStill(input.deviceId, input.epochMs);
87855
+ const membershipOp = await this.deps.collections.addFrameOp({
87856
+ collectionId: input.collectionId,
87857
+ frameId: recordingFrameId(input.deviceId, input.epochMs),
87858
+ deviceId: input.deviceId
87859
+ });
87860
+ const stored = await this.storeStill(input.deviceId, input.epochMs, membershipOp === null ? [] : [membershipOp]);
87160
87861
  if (stored.kind === "refused") {
87161
87862
  this.deps.logger.info("retrain collection add refused", {
87162
87863
  tags,
@@ -87168,7 +87869,7 @@ var RetrainCollectionService = class {
87168
87869
  });
87169
87870
  return stored;
87170
87871
  }
87171
- await this.deps.collections.addFrame({
87872
+ if (stored.reused) await this.deps.collections.addFrame({
87172
87873
  collectionId: input.collectionId,
87173
87874
  frameId: stored.frame.frameId,
87174
87875
  deviceId: input.deviceId
@@ -87233,7 +87934,7 @@ var RetrainCollectionService = class {
87233
87934
  const ids = await this.deps.collections.listFrameIds(input.collectionId);
87234
87935
  return this.deps.retrain.toWireFrames(await this.deps.frames.getMany(ids));
87235
87936
  }
87236
- async storeStill(deviceId, epochMs) {
87937
+ async storeStill(deviceId, epochMs, withRow) {
87237
87938
  const existing = await this.deps.frames.get(recordingFrameId(deviceId, epochMs));
87238
87939
  if (existing !== null) return {
87239
87940
  kind: "stored",
@@ -87253,7 +87954,7 @@ var RetrainCollectionService = class {
87253
87954
  deviceId,
87254
87955
  epochMs,
87255
87956
  bytes: still.jpeg
87256
- });
87957
+ }, withRow);
87257
87958
  return stored.kind === "copied" ? {
87258
87959
  kind: "stored",
87259
87960
  frame: stored.frame,
@@ -89576,9 +90277,9 @@ var RetrainService = class {
89576
90277
  ...this.frameUrls(updated.frameId)
89577
90278
  };
89578
90279
  }
89579
- /** Un-select: annotations first, then the copy. Never the other way round —
89580
- * an annotation whose frame is gone is unopenable and unfixable. The frame's
89581
- * collection memberships go with it. */
90280
+ /** Un-select: the frame row, its annotations and its collection memberships
90281
+ * leave in one transaction (an annotation whose frame is gone is unopenable
90282
+ * and unfixable, so they never part); the blob is deleted after the row. */
89582
90283
  async deselectFrame(input) {
89583
90284
  const frame = await this.deps.frames.get(input.frameId);
89584
90285
  if (frame === null) {
@@ -89597,11 +90298,16 @@ var RetrainService = class {
89597
90298
  removedAnnotations: await this.removeFrame(frame)
89598
90299
  };
89599
90300
  }
89600
- /** Remove a frame from the dataset: annotations, memberships, then the copy. */
90301
+ /**
90302
+ * Remove a frame from the dataset. The frame row, its annotations and its
90303
+ * collection memberships leave in ONE transaction (a failure removes nothing
90304
+ * and throws); the blob is deleted after the row, never before.
90305
+ */
89601
90306
  async removeFrame(frame) {
89602
- const removedAnnotations = await this.deps.annotations.deleteForFrame(frame.frameId);
89603
- await this.deps.memberships?.forgetFrame(frame.frameId);
89604
- await this.deps.frames.deleteFrame(frame);
90307
+ const annotationOps = await this.deps.annotations.deleteOpsForFrame(frame.frameId);
90308
+ const membershipOps = await this.deps.memberships?.forgetFrameOps(frame.frameId) ?? [];
90309
+ await this.deps.frames.deleteFrame(frame, [...annotationOps, ...membershipOps]);
90310
+ const removedAnnotations = annotationOps.length;
89605
90311
  this.deps.logger.info("retrain frame removed from the dataset", {
89606
90312
  tags: { deviceId: frame.deviceId },
89607
90313
  meta: {
@@ -89795,7 +90501,8 @@ var RetrainService = class {
89795
90501
  * The guarded replace. A caller that names the revision it saw gets exactly
89796
90502
  * one attempt: a different current revision is a conflict, never a retry —
89797
90503
  * the point is that it must look again. A caller that names none asked for
89798
- * last-write-wins, so a race lost between our read and the transaction is
90504
+ * last-write-wins, so a race lost between our revision read and the records
90505
+ * `transact` that commits the save (a per-frame lock serialises the two) is
89799
90506
  * retried on the fresh revision, a bounded number of times.
89800
90507
  */
89801
90508
  async replaceGuarded(frame, input, drafts, dismissed) {
@@ -103422,12 +104129,16 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
103422
104129
  EventCategory.WorkerOffline,
103423
104130
  EventCategory.PipelineNodeInferenceUnavailable,
103424
104131
  EventCategory.PipelineDetectionBlind,
104132
+ EventCategory.PipelineMotionNoisy,
104133
+ EventCategory.PipelineMotionCalm,
103425
104134
  EventCategory.RecordingHealthStalled,
103426
104135
  EventCategory.RecordingHealthOk,
103427
104136
  EventCategory.RecordingHealthDegraded,
103428
104137
  EventCategory.StorageLocationLow,
103429
104138
  EventCategory.StorageLocationCritical,
103430
104139
  EventCategory.StorageLocationOk,
104140
+ EventCategory.NotificationRuleBroken,
104141
+ EventCategory.NotificationRuleRepaired,
103431
104142
  EventCategory.AddonRunnerFailed,
103432
104143
  EventCategory.AddonStarted,
103433
104144
  EventCategory.AddonRestarted,
@@ -106438,6 +107149,18 @@ Object.freeze({
106438
107149
  "empty"
106439
107150
  ]
106440
107151
  },
107152
+ {
107153
+ "path": "filterCleaningDue",
107154
+ "valueType": "boolean"
107155
+ },
107156
+ {
107157
+ "path": "machineCareDue",
107158
+ "valueType": "boolean"
107159
+ },
107160
+ {
107161
+ "path": "machineCareRemainingRuns",
107162
+ "valueType": "number"
107163
+ },
106441
107164
  {
106442
107165
  "path": "halfLoad",
106443
107166
  "valueType": "boolean"
@@ -115458,6 +116181,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
115458
116181
  nodeAddons: (nodeId) => api.nodes.getNodeAddons.query({ nodeId })
115459
116182
  },
115460
116183
  readAlerts: () => api.alerts.list.query({ limit: 5e3 }),
116184
+ emitBusEvent: (event) => this.ctx.eventBus.emit(event),
115461
116185
  getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
115462
116186
  recognitionOutlook: ({ deviceId, trackId, className, labelKind, now }) => recognitionOutlook({
115463
116187
  className,
@@ -115731,7 +116455,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
115731
116455
  since: timestamp - toleranceMs,
115732
116456
  limit: NC_TRACKS_AT_INSTANT_LIMIT
115733
116457
  });
115734
- return tracksAtInstant([...live, ...persisted.tracks], timestamp, toleranceMs, this.lastFrameDimsByDevice.get(deviceId));
116458
+ return tracksAtInstant([...live, ...persisted.tracks], timestamp, toleranceMs, this.lastFrameDimsByDevice.get(deviceId), (drop) => this.ctx.logger.info("scene-boxes: other track not drawn", {
116459
+ tags: { deviceId },
116460
+ meta: {
116461
+ trackId: drop.trackId,
116462
+ className: drop.className,
116463
+ reason: drop.reason,
116464
+ instant: timestamp,
116465
+ ...drop.nearestDtMs !== void 0 ? { nearestDtMs: drop.nearestDtMs } : {}
116466
+ }
116467
+ }));
115735
116468
  },
115736
116469
  getDeviceName,
115737
116470
  getDeviceLocation,