@camstack/addon-post-analysis 1.2.611 → 1.2.613

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