@camstack/addon-post-analysis 1.2.309 → 1.2.311

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 (14) hide show
  1. package/dist/{clip-model-registry-CrBn6Qvt.mjs → clip-model-registry-DPqDIZrG.mjs} +409 -200
  2. package/dist/{clip-model-registry-B3M4ZI17.js → clip-model-registry-X457TLXn.js} +456 -199
  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 +2 -2
  6. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Bkf0-pki.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D0JVXXRd.mjs} +2 -2
  7. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Ds6vT2FJ.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-B8qeS8Da.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B_KS96RA.mjs} +1 -1
  9. package/dist/pipeline-analytics/{hostInit-Dj0aaMhv.mjs → hostInit-Da_2XVg_.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +411 -34
  11. package/dist/pipeline-analytics/index.mjs +411 -34
  12. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  13. package/package.json +1 -1
  14. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DWg2WU0a.mjs +0 -26
@@ -1,4 +1,4 @@
1
- import { $ as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, $n as ruleDeviceCompatCapQueries, $t as TimelapseRuleInputSchema, A as EMBEDDING_REQUIREMENTS_FAILED, An as isDetectionMacroClass, Ar as sleep$1, At as NcTemplateVarDescriptorSchema, B as ListTrackRelationsInputSchema, Bn as memoizePinnedFfmpeg, Br as partialRecord, Bt as SCENE_DEFAULT_ANCHOR_THRESHOLD, C as DEFAULT_EVENT_COLOR, Cn as encodeVectorBase64, Cr as createEvent, Ct as NcSnoozeInputSchema, D as DeclaredDevices, Dn as gifDeriveSpeed, Dr as isDeviceScopedCap, Dt as NcTaxonomySchema, E as DETECTION_PIPELINE_CAP_NAME, En as failureContributionCapability, Er as hydrateSchema, Et as NcSystemEventKindSchema, F as EXPORT_DENSE_MAX_RANGES, Fn as isSystemDelivery, Fr as discriminatedUnion, Ft as RECORDING_SOURCE_CAMSTACK_ADDON, G as MAX_BIRTH_LATENCY_PROXY_MS, Gn as plateGalleryCapability, Gr as union, Gt as SCENE_DIVERGED, H as LockStateSchema, Hn as occupancyScope, Hr as strictObject, Ht as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, I as FIRST_LEVEL_MACRO_CLASSES, In as isSystemEventSummaryRule, Ir as literal, It as RebuildObjectEmbeddingsRefusalSchema, J as MOTION_CLOSE_AFTER_MS, Jn as readTimelapseGeneratedAt, Jt as SetTrackPurityInputSchema, K as MAX_TRACK_PURITIES, Kn as readDeviceStateFrom, Kr as unknown, Kt as SCENE_RESET_RECAPTURES, L as FULL_IMAGE_BBOX, Ln as isVectorIndexDimConflict, Lr as looseObject, Lt as RetrainStatusSchema, M as EVENT_DENSITY_BATCH_MAX, Mn as isKnownTemplateVar, Mr as _enum, Mt as OpsLogEntrySchema, N as EVENT_KIND_BY_CAP, Nn as isMediaOwnerType, Nr as array, O as DetectionSourceSchema, On as gifFromMp4Args, Or as nodePin, Ot as NcTemplatePreviewInputSchema, P as EVENT_OWNER_TYPES, Pn as isScheduleActive, Pr as boolean, Pt as RECORDING_EXPORT_MAX_READ_BYTES, Q as NC_AUDIO_CONFIRM_HITS_DEFAULT, Qn as ruleBelongsToAlarm$1, Qt as TRACK_RELATIONS_DEFAULT_LIMIT, R as FailureCounters, Rn as kebabToCamel, Rr as number, Rt as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, S as CoverStateSchema, Sr as CamProfileSchema, St as NcScheduleSchema, T as DETECTION_MACRO_CLASSES, Tn as faceGalleryCapability, Tr as defineCustomActions, Tt as NcSnoozeSuppressedSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Ur as string, Ut as SCENE_DEFAULT_QUIET_SECONDS, V as ListTrackRelationsResultSchema, Vn as notificationRulesCapability, Vr as record, Vt as SCENE_DEFAULT_CHECK_INTERVAL_SEC, W as MAX_BIRTH_DECISION_RECORDS, Wn as pipelineAnalyticsCapability, Wr as tuple, Wt as SCENE_DEFAULT_UNCOVERED_POLICY, X as MotionTriggeredBySchema, Xn as resolveLocationMode, Xt as SetTrackRelationInputSchema, Y as MediaFileKindEnum, Yn as resolveClipSearchMinScore, Yt as SetTrackPurityResultSchema, Z as NC_ALARM_SYSTEM_EVENT_KINDS, Zt as TIMELAPSE_DENSE_FLOOR_SEC, _ as BaseDevice, _n as describeSystemEventFamilyProblem, _r as evaluateSensorEdge, _t as NcRuleInputSchema, a as CLIP_EMBEDDING_STEP_ID, an as TrackSideSchema, ar as subKindsOf, at as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, b as ClearTrackRelationInputSchema, bn as embeddingRefusalOf, br as errMsg, bt as NcRuleTargetSchema, c as resolveClusterModelPin, cn as alarmPanelCapability, cr as systemEventFilterApplies, ct as NC_SNOOZE_MAX_MINUTES, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, dn as audioModeOf, dr as templateCarriesSummary, dt as NC_TAXONOMY, en as TimelapseRulePatchSchema, er as ruleKindOf, et as NC_CLIP_POST_ROLL_DEFAULT_SEC, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fn as buildEventKindDescriptor, fr as templateVarsFor, ft as NC_TEMPLATE_VARS, g as BIRTH_DECISION_DEFAULT_LIMIT, gn as describeSystemEventDeliveryProblem, gr as canonicalHash, gt as NcConditionsSchema, h as ArchivedDebugNoteSchema, hn as deriveRecordingMode, hr as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, ht as NcConditionDescriptorSchema, in as TrackRelationSchema, ir as storageOccupancyCapability, it as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as isEventOwnerType, jr as ZodObject, jt as OPS_LOG_DEFAULT_LIMIT, k as EMBEDDING_MODEL_FAILED, kn as isAudioClassName, kt as NcTemplatePreviewSchema, l as APPEARANCE_BIN_COUNT, ln as assertTimelapseCadences, lr as systemEventSummaryScheduleOf, lt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, m as AlarmArmModeSchema, mn as cosineSimilarity$1, mr as zoneAnalyticsCapability, mt as NcAlarmSettingsPatchSchema, nn as TrackPuritySchema, nr as sensorRaisedOf, nt as NC_CLIP_RETENTION_MAX_SEC, o as FACE_EMBEDDING_STEP_ID, on as TrackSourceSchema, or as systemEventDeliveryProblems, ot as NC_DEFAULT_SNOOZE_MINUTES, p as AUDIO_MACRO_LABELS, pn as consumableLowPercentOf, pr as vectorDimFromBase64, pt as NcAlarmConfigSchema, q as MAX_TRACK_RELATIONS, qn as readMotionTriggeredBy, qr as EventCategory, qt as SceneMonitorSchema, rn as TrackRelationKindSchema, rr as servedTemplateVars, rt as NC_CONDITION_CATALOG, s as readClusterModelPinStrict, sn as addonWidgetsSourceCapability, sr as systemEventFamilyProblems, st as NC_SENSOR_TRANSITION_KINDS, t as BUILTIN_CLIP_MODELS, tn as TimelapseRuleSchema, tr as sceneMonitorCapability, tt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, u as APPEARANCE_BLOCKS, un as audioMetricsCapability, ur as systemEventSummarySplitOf, ut as NC_TANK_SYSTEM_EVENT_KINDS, v as BirthDecisionRecordSchema, vr as isSourceCap, vt as NcRulePatchSchema, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wn as evictionPolicyOfLocation, wr as customAction, wt as NcSnoozeSchema, x as ClearTrackRelationResultSchema, xr as BaseAddon, xt as NcRuleUpdateInputSchema, y as COCO_TO_MACRO, yr as DeviceType, yt as NcRuleSchema, z as LabelAttributionSchema, zn as mayWriteToLocation, zr as object, zt as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS } from "../clip-model-registry-CrBn6Qvt.mjs";
1
+ import { $ as NC_ALARM_SYSTEM_EVENT_KINDS, $n as readDeviceStateFrom, $r as union, $t as SetTrackPurityInputSchema, A as EMBEDDING_REQUIREMENTS_FAILED, An as encodeVectorBase64, Ar as CamProfileSchema, At as NcSnoozeSuppressedSchema, B as ListTrackRelationsInputSchema, Bn as isMediaOwnerType, Br as _enum, Bt as RECORDING_SOURCE_CAMSTACK_ADDON, C as DEFAULT_EVENT_COLOR, Cn as describeSystemEventDeliveryProblem, Cr as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Ct as NcRulePatchSchema, D as DeclaredDevices, Dn as embeddingRefusalOf, Dr as DeviceType, Dt as NcScheduleSchema, E as DETECTION_PIPELINE_CAP_NAME, Er as isSourceCap, Et as NcRuleUpdateInputSchema, F as EXPORT_DENSE_MAX_RANGES, Fn as gifFromMp4Args, Fr as isDeviceScopedCap, Ft as NcTemplateVarDescriptorSchema, G as MAX_BIRTH_LATENCY_PROXY_MS, Gn as kebabToCamel, Gr as looseObject, Gt as SCENE_DEFAULT_ANCHOR_THRESHOLD, H as LockStateSchema, Hn as isSystemDelivery, Hr as boolean, Ht as RetrainStatusSchema, I as FIRST_LEVEL_MACRO_CLASSES, In as isAudioClassName, Ir as nodePin, It as OPS_LOG_DEFAULT_LIMIT, J as MOTION_CLOSE_AFTER_MS, Jn as notificationRulesCapability, Jr as partialRecord, Jt as SCENE_DEFAULT_QUIET_SECONDS, K as MAX_TRACK_PURITIES, Kn as mayWriteToLocation, Kr as number, Kt as SCENE_DEFAULT_CHECK_INTERVAL_SEC, L as FULL_IMAGE_BBOX, Ln as isDetectionMacroClass, Lt as OpsLogEntrySchema, M as EVENT_DENSITY_BATCH_MAX, Mn as faceGalleryCapability, Mr as customAction, Mt as NcTaxonomySchema, N as EVENT_KIND_BY_CAP, Nn as failureContributionCapability, Nr as defineCustomActions, Nt as NcTemplatePreviewInputSchema, O as DetectionSourceSchema, Or as errMsg, Ot as NcSnoozeInputSchema, P as EVENT_OWNER_TYPES, Pn as gifDeriveSpeed, Pr as hydrateSchema, Pt as NcTemplatePreviewSchema, Q as NC_ALARM_DELAY_MAX_SEC, Qn as plateGalleryCapability, Qr as tuple, Qt as SceneMonitorSchema, R as FailureCounters, Rn as isEventOwnerType, Rr as sleep$1, S as CoverStateSchema, Sn as deriveRecordingMode, Sr as zoneAnalyticsCapability, St as NcRuleInputSchema, T as DETECTION_MACRO_CLASSES, Tr as evaluateSensorEdge, Tt as NcRuleTargetSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Un as isSystemEventSummaryRule, Ur as discriminatedUnion, Ut as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, V as ListTrackRelationsResultSchema, Vn as isScheduleActive, Vr as array, Vt as RebuildObjectEmbeddingsRefusalSchema, W as MAX_BIRTH_DECISION_RECORDS, Wn as isVectorIndexDimConflict, Wr as literal, Wt as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, X as MotionTriggeredBySchema, Xr as strictObject, Xt as SCENE_DIVERGED, Y as MediaFileKindEnum, Yn as occupancyScope, Yr as record, Yt as SCENE_DEFAULT_UNCOVERED_POLICY, Z as NC_ALARM_DELAY_FIELDS, Zn as pipelineAnalyticsCapability, Zr as string, Zt as SCENE_RESET_RECAPTURES, _ as BaseDevice, _n as audioMetricsCapability, _r as systemEventSummaryScheduleOf, _t as NcAlarmSensorsInputSchema, a as CLIP_EMBEDDING_STEP_ID, an as TimelapseRulePatchSchema, at as NC_CLIP_RETENTION_MAX_SEC, b as ClearTrackRelationInputSchema, bn as consumableLowPercentOf, br as templateVarsFor, bt as NcConditionDescriptorSchema, c as resolveClusterModelPin, cn as TrackRelationKindSchema, cr as ruleKindOf, ct as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, dn as TrackSourceSchema, dr as servedTemplateVars, dt as NC_SNOOZE_MAX_MINUTES, ei as unknown, en as SetTrackPurityResultSchema, er as readMotionTriggeredBy, et as NC_ALARM_TRIGGERED_MAX_SEC, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fn as addonWidgetsSourceCapability, fr as storageOccupancyCapability, ft as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, g as BIRTH_DECISION_DEFAULT_LIMIT, gn as assertTimelapseCadences, gr as systemEventFilterApplies, gt as NcAlarmConfigSchema, h as ArchivedDebugNoteSchema, hn as applyNonBlockingEdit, hr as systemEventFamilyProblems, ht as NC_TEMPLATE_VARS, in as TimelapseRuleInputSchema, ir as resolveModeDelays, it as NC_CLIP_PRE_ROLL_DEFAULT_SEC, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as evictionPolicyOfLocation, jr as createEvent, jt as NcSystemEventKindSchema, k as EMBEDDING_MODEL_FAILED, kr as BaseAddon, kt as NcSnoozeSchema, l as APPEARANCE_BIN_COUNT, ln as TrackRelationSchema, lr as sceneMonitorCapability, lt as NC_DEFAULT_SNOOZE_MINUTES, m as AlarmArmModeSchema, mn as applyModeDelaysEdits, mr as systemEventDeliveryProblems, mt as NC_TAXONOMY, nn as TIMELAPSE_DENSE_FLOOR_SEC, nr as resolveClipSearchMinScore, nt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, o as FACE_EMBEDDING_STEP_ID, on as TimelapseRuleSchema, or as ruleBelongsToAlarm$1, ot as NC_CONDITION_CATALOG, p as AUDIO_MACRO_LABELS, pn as alarmPanelCapability, pr as subKindsOf, pt as NC_TANK_SYSTEM_EVENT_KINDS, q as MAX_TRACK_RELATIONS, qn as memoizePinnedFfmpeg, qr as object, qt as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, rn as TRACK_RELATIONS_DEFAULT_LIMIT, rr as resolveLocationMode, rt as NC_CLIP_POST_ROLL_DEFAULT_SEC, s as readClusterModelPinStrict, sn as TrackPuritySchema, sr as ruleDeviceCompatCapQueries, st as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, t as BUILTIN_CLIP_MODELS, ti as EventCategory, tn as SetTrackRelationInputSchema, tr as readTimelapseGeneratedAt, tt as NC_AUDIO_CONFIRM_HITS_DEFAULT, u as APPEARANCE_BLOCKS, un as TrackSideSchema, ur as sensorRaisedOf, ut as NC_SENSOR_TRANSITION_KINDS, v as BirthDecisionRecordSchema, vn as audioModeOf, vr as systemEventSummarySplitOf, vt as NcAlarmSensorsSchema, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wn as describeSystemEventFamilyProblem, wr as canonicalHash, wt as NcRuleSchema, x as ClearTrackRelationResultSchema, xn as cosineSimilarity$1, xr as vectorDimFromBase64, xt as NcConditionsSchema, y as COCO_TO_MACRO, yn as buildEventKindDescriptor, yr as templateCarriesSummary, yt as NcAlarmSettingsPatchSchema, z as LabelAttributionSchema, zn as isKnownTemplateVar, zr as ZodObject, zt as RECORDING_EXPORT_MAX_READ_BYTES } from "../clip-model-registry-DPqDIZrG.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";
@@ -1389,11 +1389,85 @@ var NcAlarmExclusions = class {
1389
1389
  }
1390
1390
  };
1391
1391
  //#endregion
1392
+ //#region src/notification-center/alarm/alarm-mode-delays.ts
1393
+ /**
1394
+ * The persisted per-mode delay overrides, read through a guard (D697).
1395
+ *
1396
+ * The panel's config schema keeps `mode` a plain string so a blob written by a later release
1397
+ * with a mode this one does not know cannot fail the whole config. This is where that string is
1398
+ * narrowed: an entry that cannot be trusted is dropped and REPORTED, never repaired. An entry of an unknown
1399
+ * mode is also handed back raw (`foreign`) so a write can keep it.
1400
+ */
1401
+ function inBounds(value, max) {
1402
+ return Number.isInteger(value) && value >= 0 && value <= max;
1403
+ }
1404
+ /** Read the persisted overrides through a guard, never a cast (like `nonBlockingFromPersisted`). */
1405
+ function modeDelaysFromPersisted(raw) {
1406
+ const seen = /* @__PURE__ */ new Set();
1407
+ const entries = [];
1408
+ const dropped = [];
1409
+ const foreign = [];
1410
+ for (const item of raw) {
1411
+ const mode = AlarmArmModeSchema.safeParse(item.mode);
1412
+ if (!mode.success) {
1413
+ dropped.push({
1414
+ mode: item.mode,
1415
+ why: "unknown-mode"
1416
+ });
1417
+ foreign.push(item);
1418
+ continue;
1419
+ }
1420
+ if (seen.has(mode.data)) {
1421
+ dropped.push({
1422
+ mode: item.mode,
1423
+ why: "duplicate-mode"
1424
+ });
1425
+ continue;
1426
+ }
1427
+ seen.add(mode.data);
1428
+ const kept = { mode: mode.data };
1429
+ const fields = [
1430
+ {
1431
+ field: "exitDelaySec",
1432
+ max: NC_ALARM_DELAY_MAX_SEC
1433
+ },
1434
+ {
1435
+ field: "entryDelaySec",
1436
+ max: NC_ALARM_DELAY_MAX_SEC
1437
+ },
1438
+ {
1439
+ field: "triggeredDurationSec",
1440
+ max: NC_ALARM_TRIGGERED_MAX_SEC
1441
+ }
1442
+ ];
1443
+ for (const { field, max } of fields) {
1444
+ const value = item[field];
1445
+ if (value === void 0) continue;
1446
+ if (!inBounds(value, max)) {
1447
+ dropped.push({
1448
+ mode: item.mode,
1449
+ why: "out-of-bounds",
1450
+ field
1451
+ });
1452
+ continue;
1453
+ }
1454
+ kept[field] = value;
1455
+ }
1456
+ if (kept.exitDelaySec !== void 0 || kept.entryDelaySec !== void 0 || kept.triggeredDurationSec !== void 0) entries.push(kept);
1457
+ }
1458
+ return {
1459
+ entries,
1460
+ dropped,
1461
+ foreign
1462
+ };
1463
+ }
1464
+ //#endregion
1392
1465
  //#region src/notification-center/alarm/alarm-state-machine.ts
1393
1466
  /**
1394
- * Defaults an operator can change per panel. `home` has no exit delay on
1395
- * purpose — nobody is leaving — but the shared value is applied to every mode
1396
- * until per-mode delays are asked for.
1467
+ * The GLOBAL defaults an operator can change per panel — what a mode inherits
1468
+ * when it overrides nothing. A mode can override each of the three since D697
1469
+ * (`resolveModeDelays`): `home` has no exit delay on purpose — nobody is
1470
+ * leaving — and says so with its own entry, not by this shared value.
1397
1471
  *
1398
1472
  * `triggeredDurationSec: 0` = "sound until somebody disarms it". That is what
1399
1473
  * the panel did before the field existed, so an install that never opens the
@@ -1808,6 +1882,19 @@ var ncAlarmPanelSchema = object({
1808
1882
  nonBlocking: array(object({
1809
1883
  mode: string(),
1810
1884
  deviceIds: array(number().int())
1885
+ })).default([]),
1886
+ /**
1887
+ * Per arm mode, the delays that override the global ones (D697). Loose on purpose — `mode` is
1888
+ * a string and the numbers are unbounded here — and read back through
1889
+ * {@link modeDelaysFromPersisted}, never a cast: an entry this release cannot trust is dropped
1890
+ * and logged at boot, never the whole config. A blob written before D697 has no key and
1891
+ * parses to no overrides.
1892
+ */
1893
+ modeDelays: array(object({
1894
+ mode: string(),
1895
+ exitDelaySec: number().optional(),
1896
+ entryDelaySec: number().optional(),
1897
+ triggeredDurationSec: number().optional()
1811
1898
  })).default([])
1812
1899
  });
1813
1900
  /**
@@ -1878,7 +1965,6 @@ function machineFromPersisted(raw) {
1878
1965
  var NcAlarmPanelDevice = class extends BaseDevice {
1879
1966
  features = [];
1880
1967
  machine = DISARMED;
1881
- delays = DEFAULT_DELAYS;
1882
1968
  ticker = null;
1883
1969
  onPublish = null;
1884
1970
  /**
@@ -1909,7 +1995,7 @@ var NcAlarmPanelDevice = class extends BaseDevice {
1909
1995
  refusalSeq = 0;
1910
1996
  constructor(ctx) {
1911
1997
  super(ctx, ncAlarmPanelSchema, { type: DeviceType.AlarmPanel });
1912
- this.delays = this.readDelays();
1998
+ this.logDroppedModeDelays();
1913
1999
  this.restore();
1914
2000
  this.registerAlarmCap();
1915
2001
  this.publish();
@@ -1921,30 +2007,148 @@ var NcAlarmPanelDevice = class extends BaseDevice {
1921
2007
  ...this.readDelays(),
1922
2008
  announceArm: this.config.get("announceArm"),
1923
2009
  announceTargets: [...this.config.get("announceTargets")],
1924
- nonBlocking: nonBlockingFromPersisted(this.config.get("nonBlocking"))
2010
+ nonBlocking: nonBlockingFromPersisted(this.config.get("nonBlocking")),
2011
+ modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
1925
2012
  };
1926
2013
  }
1927
2014
  /**
1928
2015
  * Apply an editor's patch and return what the panel settled on.
1929
2016
  *
1930
2017
  * The new delays take effect on the NEXT transition, never retroactively: a
1931
- * machine already counting down keeps the instants it was given. Re-deriving
1932
- * `armedAt` from a delay the operator changed mid-exit would move the arm
1933
- * instant under somebody who is walking out of the door.
2018
+ * machine already counting down keeps the instants it was given (the exit
2019
+ * delay is read at arm, the entry delay and siren duration at trigger, and
2020
+ * the machine stores the instants, not the durations). Re-deriving `armedAt`
2021
+ * from a delay the operator changed mid-exit would move the arm instant under
2022
+ * somebody who is walking out of the door.
2023
+ *
2024
+ * Two phases, so a refused patch writes NOTHING:
2025
+ * 1. every refusal is checked first (a mode this panel does not offer, an
2026
+ * add to a full non-blocking list) — it logs why and throws;
2027
+ * 2. every LIST write is CALLED before the first `await`. `DeviceConfig.set`
2028
+ * replaces its in-memory value synchronously, so a list computed after an
2029
+ * await could be based on a list another call has since replaced — and a
2030
+ * whole-list write (the deleted-device prune sends one, computed
2031
+ * synchronously before calling: `device-prune-runner.ts` `applyAlarm`)
2032
+ * would erase the membership edit that landed in between.
1934
2033
  */
1935
2034
  async applySettings(patch) {
2035
+ const edits = patch.modeDelays ?? [];
2036
+ const membership = patch.nonBlockingEdit;
2037
+ for (const edit of edits) this.refuseUnofferedMode(edit.mode);
2038
+ if (membership !== void 0) {
2039
+ this.refuseUnofferedMode(membership.mode, membership.deviceId);
2040
+ const verdict = applyNonBlockingEdit(patch.nonBlocking !== void 0 ? patch.nonBlocking : nonBlockingFromPersisted(this.config.get("nonBlocking")), membership);
2041
+ if (verdict.kind === "refused") {
2042
+ this.ctx.logger.info("alarm settings refused", {
2043
+ tags: { deviceId: membership.deviceId },
2044
+ meta: {
2045
+ panelId: this.id,
2046
+ reason: "list-full",
2047
+ mode: verdict.mode
2048
+ }
2049
+ });
2050
+ throw new Error(`the ${verdict.mode} non-blocking list already holds ${verdict.max} devices — remove one first`);
2051
+ }
2052
+ }
2053
+ const listWrites = [];
2054
+ if (patch.nonBlocking !== void 0) listWrites.push(this.config.set("nonBlocking", patch.nonBlocking.map((l) => ({
2055
+ mode: l.mode,
2056
+ deviceIds: [...l.deviceIds]
2057
+ }))));
2058
+ let membershipChanged = null;
2059
+ if (membership !== void 0) {
2060
+ const result = applyNonBlockingEdit(nonBlockingFromPersisted(this.config.get("nonBlocking")), membership);
2061
+ if (result.kind === "applied") {
2062
+ membershipChanged = result.changed;
2063
+ if (result.changed) listWrites.push(this.config.set("nonBlocking", result.lists.map((l) => ({
2064
+ mode: l.mode,
2065
+ deviceIds: [...l.deviceIds]
2066
+ }))));
2067
+ }
2068
+ }
2069
+ const persistedDelays = modeDelaysFromPersisted(this.config.get("modeDelays"));
2070
+ const beforeDelays = persistedDelays.entries;
2071
+ const afterDelays = edits.length > 0 ? applyModeDelaysEdits(beforeDelays, edits) : beforeDelays;
2072
+ if (edits.length > 0) {
2073
+ for (const drop of persistedDelays.dropped) {
2074
+ if (drop.why === "unknown-mode") continue;
2075
+ this.ctx.logger.info("alarm per-mode delay discarded on write", {
2076
+ tags: { deviceId: this.id },
2077
+ meta: {
2078
+ mode: drop.mode,
2079
+ why: drop.why,
2080
+ field: drop.field
2081
+ }
2082
+ });
2083
+ }
2084
+ listWrites.push(this.config.set("modeDelays", [...afterDelays, ...persistedDelays.foreign]));
2085
+ }
2086
+ await Promise.all(listWrites);
2087
+ this.logModeDelayChanges(beforeDelays, afterDelays, edits);
1936
2088
  if (patch.exitDelaySec !== void 0) await this.config.set("exitDelaySec", patch.exitDelaySec);
1937
2089
  if (patch.entryDelaySec !== void 0) await this.config.set("entryDelaySec", patch.entryDelaySec);
1938
2090
  if (patch.triggeredDurationSec !== void 0) await this.config.set("triggeredDurationSec", patch.triggeredDurationSec);
1939
2091
  if (patch.announceArm !== void 0) await this.config.set("announceArm", patch.announceArm);
1940
2092
  if (patch.announceTargets !== void 0) await this.config.set("announceTargets", [...patch.announceTargets]);
1941
- if (patch.nonBlocking !== void 0) await this.config.set("nonBlocking", patch.nonBlocking.map((l) => ({
1942
- mode: l.mode,
1943
- deviceIds: [...l.deviceIds]
1944
- })));
1945
- this.delays = this.readDelays();
2093
+ if (membership !== void 0 && membershipChanged !== null) this.ctx.logger.info("alarm non-blocking list changed", {
2094
+ tags: { deviceId: membership.deviceId },
2095
+ meta: {
2096
+ panelId: this.id,
2097
+ mode: membership.mode,
2098
+ nonBlocking: membership.nonBlocking,
2099
+ changed: membershipChanged
2100
+ }
2101
+ });
1946
2102
  return this.settings();
1947
2103
  }
2104
+ /**
2105
+ * Refuse a mode this panel does not offer — logged by reason, then thrown. A membership edit
2106
+ * passes its SENSOR as `deviceId` (the tag); a per-mode delay edit defaults to the panel.
2107
+ */
2108
+ refuseUnofferedMode(mode, deviceId = this.id) {
2109
+ if (NC_ALARM_MODES.includes(mode)) return;
2110
+ this.ctx.logger.info("alarm settings refused", {
2111
+ tags: { deviceId },
2112
+ meta: {
2113
+ panelId: this.id,
2114
+ reason: "mode-not-offered",
2115
+ mode
2116
+ }
2117
+ });
2118
+ throw new Error(`alarm mode '${mode}' is not offered by this panel`);
2119
+ }
2120
+ /** One INFO line per mode-delay field an edit really changed (`null` = inherit). */
2121
+ logModeDelayChanges(before, after, edits) {
2122
+ for (const edit of edits) {
2123
+ const was = before.find((e) => e.mode === edit.mode);
2124
+ const now = after.find((e) => e.mode === edit.mode);
2125
+ for (const field of NC_ALARM_DELAY_FIELDS) {
2126
+ const from = was?.[field] ?? null;
2127
+ const to = now?.[field] ?? null;
2128
+ if (from === to) continue;
2129
+ this.ctx.logger.info("alarm mode delays changed", {
2130
+ tags: { deviceId: this.id },
2131
+ meta: {
2132
+ mode: edit.mode,
2133
+ field,
2134
+ from,
2135
+ to
2136
+ }
2137
+ });
2138
+ }
2139
+ }
2140
+ }
2141
+ /** One INFO line per persisted per-mode delay entry the guard refused to trust. */
2142
+ logDroppedModeDelays() {
2143
+ for (const drop of modeDelaysFromPersisted(this.config.get("modeDelays")).dropped) this.ctx.logger.info("alarm per-mode delay dropped at boot", {
2144
+ tags: { deviceId: this.id },
2145
+ meta: {
2146
+ mode: drop.mode,
2147
+ why: drop.why,
2148
+ field: drop.field
2149
+ }
2150
+ });
2151
+ }
1948
2152
  /** Wire the state mirror and the transition notifications. See
1949
2153
  * {@link NcAlarmPublishHook}. */
1950
2154
  setPublishHook(hook) {
@@ -2227,7 +2431,17 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2227
2431
  else this.exclusions.clear();
2228
2432
  this.persist();
2229
2433
  }
2230
- this.machine = arm(mode, this.delays, now);
2434
+ const resolved = this.delaysFor(mode);
2435
+ this.machine = arm(mode, resolved, now);
2436
+ this.ctx.logger.info("alarm delays applied", {
2437
+ tags: { deviceId: this.id },
2438
+ meta: {
2439
+ mode,
2440
+ phase: "arm",
2441
+ exitDelaySec: resolved.exitDelaySec,
2442
+ source: resolved.source.exitDelaySec
2443
+ }
2444
+ });
2231
2445
  this.attribution.clear();
2232
2446
  this.publish(now);
2233
2447
  for (const { device, why } of exclusions ?? []) this.ctx.logger.info("alarm armed with an open device excluded", {
@@ -2327,7 +2541,9 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2327
2541
  this.announceIgnoredTrigger(source, now);
2328
2542
  return;
2329
2543
  }
2330
- const next = trigger(this.machine, this.delays, now);
2544
+ const armed = armModeOf(this.machine.target, NC_ALARM_MODES);
2545
+ const resolved = this.delaysFor(armed);
2546
+ const next = trigger(this.machine, resolved, now);
2331
2547
  if (next === this.machine) {
2332
2548
  const state = stateAt(this.machine, now);
2333
2549
  this.ctx.logger.info(ignoredTriggerMessage(state), {
@@ -2345,6 +2561,19 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2345
2561
  }
2346
2562
  });
2347
2563
  this.machine = next;
2564
+ this.ctx.logger.info("alarm delays applied", {
2565
+ tags: { deviceId: this.id },
2566
+ meta: {
2567
+ mode: armed,
2568
+ phase: "trigger",
2569
+ entryDelaySec: resolved.entryDelaySec,
2570
+ triggeredDurationSec: resolved.triggeredDurationSec,
2571
+ source: {
2572
+ entry: resolved.source.entryDelaySec,
2573
+ siren: resolved.source.triggeredDurationSec
2574
+ }
2575
+ }
2576
+ });
2348
2577
  this.publish(now);
2349
2578
  }
2350
2579
  });
@@ -2480,7 +2709,32 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2480
2709
  });
2481
2710
  }
2482
2711
  }
2483
- /** The three durations, read from the persisted config. */
2712
+ /** The global durations plus the per-mode overrides, read from the persisted config NOW. */
2713
+ delaySettings() {
2714
+ return {
2715
+ ...this.readDelays(),
2716
+ modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
2717
+ };
2718
+ }
2719
+ /**
2720
+ * What `mode` runs on, read at the transition that uses it — never cached. `null` (no armed
2721
+ * mode to look at) is the global values, every field `default`.
2722
+ */
2723
+ delaysFor(mode) {
2724
+ const settings = this.delaySettings();
2725
+ if (mode === null) return {
2726
+ exitDelaySec: settings.exitDelaySec,
2727
+ entryDelaySec: settings.entryDelaySec,
2728
+ triggeredDurationSec: settings.triggeredDurationSec,
2729
+ source: {
2730
+ exitDelaySec: "default",
2731
+ entryDelaySec: "default",
2732
+ triggeredDurationSec: "default"
2733
+ }
2734
+ };
2735
+ return resolveModeDelays(settings, mode);
2736
+ }
2737
+ /** The three global durations, read from the persisted config. */
2484
2738
  readDelays() {
2485
2739
  return {
2486
2740
  exitDelaySec: this.config.get("exitDelaySec"),
@@ -8345,6 +8599,79 @@ function sameWord(a, b) {
8345
8599
  return a !== void 0 && a.trim().toLowerCase() === b.trim().toLowerCase();
8346
8600
  }
8347
8601
  //#endregion
8602
+ //#region src/notification-center/alarm/alarm-sensors.ts
8603
+ /**
8604
+ * One id's state as the table shows it — spec §3.4's hub table, nothing else.
8605
+ *
8606
+ * Built ON `classifyCoveredOpening`, the arm check's classifier: what is
8607
+ * unknown, open or skipped is decided there, and this only splits a skip into
8608
+ * `closed` / `not-opening` for the table. A later change to the classifier
8609
+ * therefore moves the table with the arm.
8610
+ */
8611
+ function alarmSensorStateOf(deviceId, opening, online) {
8612
+ const verdict = classifyCoveredOpening(opening);
8613
+ switch (verdict.kind) {
8614
+ case "unknown": return {
8615
+ deviceId,
8616
+ reading: "unknown",
8617
+ state: null,
8618
+ unknownWhy: verdict.why,
8619
+ online
8620
+ };
8621
+ case "open": return {
8622
+ deviceId,
8623
+ reading: "open",
8624
+ state: verdict.state,
8625
+ unknownWhy: null,
8626
+ online
8627
+ };
8628
+ case "skip": return opening?.kind === "reading" ? {
8629
+ deviceId,
8630
+ reading: "closed",
8631
+ state: opening.state,
8632
+ unknownWhy: null,
8633
+ online
8634
+ } : {
8635
+ deviceId,
8636
+ reading: "not-opening",
8637
+ state: null,
8638
+ unknownWhy: null,
8639
+ online
8640
+ };
8641
+ }
8642
+ }
8643
+ /** The asked ids, de-duplicated and ascending. */
8644
+ function alarmSensorReadIds(ids) {
8645
+ return [...new Set(ids)].toSorted((a, b) => a - b);
8646
+ }
8647
+ /**
8648
+ * The sensors read's health, logged on CHANGE only (the call is polled every
8649
+ * 5-15 s by every open tab). "Wholesale failure" = ids were asked and not one
8650
+ * was answered (the device manager is unreachable): WARN once when it starts,
8651
+ * ONE INFO when answers return. A partial answer is not a health change — those
8652
+ * ids already leave their own debug line and show as `unknown`.
8653
+ */
8654
+ var NcAlarmSensorsReadLog = class {
8655
+ failing = false;
8656
+ next(asked, answered) {
8657
+ const wholesale = asked > 0 && answered === 0;
8658
+ if (wholesale === this.failing) return null;
8659
+ this.failing = wholesale;
8660
+ return wholesale ? {
8661
+ level: "warn",
8662
+ message: "alarm sensors read: no asked device answered — every row is unknown",
8663
+ meta: { asked }
8664
+ } : {
8665
+ level: "info",
8666
+ message: "alarm sensors read: devices answer again",
8667
+ meta: {
8668
+ asked,
8669
+ answered
8670
+ }
8671
+ };
8672
+ }
8673
+ };
8674
+ //#endregion
8348
8675
  //#region src/notification-center/alarm/alarm-live.ts
8349
8676
  /** Every covered id across all modes, ascending, once — what the one read asks for. */
8350
8677
  function alarmLiveReadIds(covered) {
@@ -17004,7 +17331,8 @@ var ncActions = defineCustomActions({
17004
17331
  * `getStatus`; this one opens up once it filters to the caller's grants.
17005
17332
  * The config pair is the admin's, as `getAlarmConfig` / `setAlarmConfig` are
17006
17333
  * on the cap: it carries the delays, the recipients and the non-blocking
17007
- * lists.
17334
+ * lists. `getAlarmSensors` is the admin's too: it names which of the asked
17335
+ * devices are open.
17008
17336
  */
17009
17337
  "nc.getAlarmStatus": customAction(object({}), NcAlarmStatusSchema, {
17010
17338
  auth: "admin",
@@ -17045,6 +17373,10 @@ var ncActions = defineCustomActions({
17045
17373
  auth: "admin",
17046
17374
  caller: "required"
17047
17375
  }),
17376
+ "nc.getAlarmSensors": customAction(NcAlarmSensorsInputSchema, NcAlarmSensorsSchema, {
17377
+ auth: "admin",
17378
+ caller: "required"
17379
+ }),
17048
17380
  "nc.setAlarmConfig": customAction(object({ patch: NcAlarmSettingsPatchSchema }), NcAlarmConfigSchema, {
17049
17381
  kind: "mutation",
17050
17382
  auth: "admin",
@@ -17837,6 +18169,10 @@ function makeNcActionHandlers(deps) {
17837
18169
  requireAdmin(caller);
17838
18170
  return requireAlarm("nc.getAlarmConfig").getConfig();
17839
18171
  },
18172
+ "nc.getAlarmSensors": async (input, caller) => {
18173
+ requireAdmin(caller);
18174
+ return requireAlarm("nc.getAlarmSensors").getSensors(input);
18175
+ },
17840
18176
  "nc.setAlarmConfig": async (input, caller) => {
17841
18177
  requireAdmin(caller);
17842
18178
  return requireAlarm("nc.setAlarmConfig").setConfig(input.patch);
@@ -32671,7 +33007,8 @@ function consumablesSliceOf(event) {
32671
33007
  var NC_ALARM_SETTINGS_FALLBACK = {
32672
33008
  ...DEFAULT_DELAYS,
32673
33009
  announceTargets: [],
32674
- nonBlocking: []
33010
+ nonBlocking: [],
33011
+ modeDelays: []
32675
33012
  };
32676
33013
  /**
32677
33014
  * How often the "matched NO rule" report may fire per device. Long enough that
@@ -33053,6 +33390,7 @@ var NotificationCenter = class NotificationCenter {
33053
33390
  alarmPanel = null;
33054
33391
  /** The live view's read, logged on change only — see `alarm-live-read-log.ts`. */
33055
33392
  alarmLiveReadLog = new NcAlarmLiveReadLog();
33393
+ alarmSensorsReadLog = new NcAlarmSensorsReadLog();
33056
33394
  /**
33057
33395
  * The words the panel answers the `deviceState` gate with — its state, plus
33058
33396
  * the armed mode a `pending`/`triggered` belongs to (D630). Refreshed on
@@ -35371,6 +35709,7 @@ var NotificationCenter = class NotificationCenter {
35371
35709
  return { success: true };
35372
35710
  },
35373
35711
  getAlarmConfig: async () => this.alarmConfig(),
35712
+ getAlarmSensors: async (input) => this.alarmSensors(input),
35374
35713
  setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch)
35375
35714
  };
35376
35715
  }
@@ -35598,6 +35937,7 @@ var NotificationCenter = class NotificationCenter {
35598
35937
  });
35599
35938
  },
35600
35939
  getConfig: () => this.alarmConfig(),
35940
+ getSensors: (input) => this.alarmSensors(input),
35601
35941
  setConfig: (patch) => this.setAlarmConfig(patch)
35602
35942
  };
35603
35943
  }
@@ -35611,7 +35951,8 @@ var NotificationCenter = class NotificationCenter {
35611
35951
  exitDelaySec: settings.exitDelaySec,
35612
35952
  entryDelaySec: settings.entryDelaySec,
35613
35953
  triggeredDurationSec: settings.triggeredDurationSec,
35614
- announceTargets: settings.announceTargets.length
35954
+ announceTargets: settings.announceTargets.length,
35955
+ modeDelays: settings.modeDelays.length
35615
35956
  }
35616
35957
  });
35617
35958
  return this.alarmConfig();
@@ -35667,6 +36008,40 @@ var NotificationCenter = class NotificationCenter {
35667
36008
  name: this.deviceNameFromDirectory()
35668
36009
  });
35669
36010
  }
36011
+ /**
36012
+ * The opening states of the asked ids, for the alarm editors' sensor table.
36013
+ *
36014
+ * A separate read from `getAlarmConfig` on purpose: that one is polled every
36015
+ * 2–15 s and the household set needs a whole-fleet bindings pass, while the
36016
+ * client already holds the ids. Cost: one `loadRuntimeState` RPC per id, 8 in
36017
+ * flight (`readAlarmOpenings`); the client sends at most `NC_ALARM_SENSORS_MAX`
36018
+ * ids per call and polls every 15 s, every 5 s only while a countdown or the
36019
+ * siren runs. No log line per call (it is polled): one WARN/INFO per change of
36020
+ * wholesale read health (`NcAlarmSensorsReadLog`); a per-id read failure keeps
36021
+ * `readAlarmOpenings`' own debug line, and shows as `unknown` (D49).
36022
+ *
36023
+ * An id with no persisted device (removed) or whose provider never wrote an
36024
+ * opening slice reads `not-opening`, exactly as the arm check sees it — NOT
36025
+ * "verified not an opening". The client shows "Removed" from its fleet read.
36026
+ */
36027
+ async alarmSensors(input) {
36028
+ const read = this.deps.readDeviceOpenings;
36029
+ if (read === void 0) throw new Error("the opening reader is not wired on this node");
36030
+ const ids = alarmSensorReadIds(input.deviceIds);
36031
+ const openings = await read(ids);
36032
+ const line = this.alarmSensorsReadLog.next(ids.length, openings.size);
36033
+ if (line !== null) {
36034
+ const panelId = this.alarmPanel?.deviceId;
36035
+ this.logger[line.level](line.message, {
36036
+ ...panelId === void 0 ? {} : { tags: { deviceId: panelId } },
36037
+ meta: line.meta
36038
+ });
36039
+ }
36040
+ return {
36041
+ sensors: ids.map((id) => alarmSensorStateOf(id, openings.get(id), this.deviceDirectory.get(id)?.online ?? null)),
36042
+ readAt: Date.now()
36043
+ };
36044
+ }
35670
36045
  /** Settings + the DERIVED coverage + the live panel. Read together — see the cap doc. */
35671
36046
  async alarmConfig() {
35672
36047
  const panel = this.alarmPanel;
@@ -92144,8 +92519,8 @@ function buildAudioLabels(input) {
92144
92519
  * Mapped sensor/control caps that a CAMERA must NOT contribute for ITSELF.
92145
92520
  *
92146
92521
  * A sensor cap bound on a camera makes the camera both the source and the
92147
- * subject — a doorbell wrapper on camera 615, a native Reolink doorbell, a
92148
- * camera-side button. Those have no other route into the per-camera event
92522
+ * subject — a composed doorbell on camera 615 (D671), a native Reolink
92523
+ * doorbell, a camera-side button. Those have no other route into the per-camera event
92149
92524
  * substrate, so the camera includes itself.
92150
92525
  *
92151
92526
  * `motion` is the exception, and today the ONLY one: camera motion already
@@ -92414,8 +92789,8 @@ var LinkedCamerasCache = class {
92414
92789
  * THE camera-set derivation for a sensor-cap state change — one function, so
92415
92790
  * every consumer of a device-event attributes it to the same cameras.
92416
92791
  *
92417
- * It is shared on purpose. The self-camera half (a doorbell wrapper bound on
92418
- * camera 615 makes 615 both source and subject) was added to the history-row
92792
+ * It is shared on purpose. The self-camera half (a doorbell bound on camera
92793
+ * 615 makes 615 both source and subject) was added to the history-row
92419
92794
  * ingest on 2026-08-07 and NOT to the synthetic-track projection beside it,
92420
92795
  * which kept resolving `camerasFor` alone. The result: camera 615's real
92421
92796
  * doorbell press on 2026-08-08 06:59Z produced a `SensorEvent` row and no
@@ -92503,8 +92878,8 @@ function isSliceRecord(slice) {
92503
92878
  /**
92504
92879
  * One `DeviceStateChanged` → N history rows, one per camera the event belongs
92505
92880
  * to: the emitting device's LINKED cameras, plus the emitting device ITSELF
92506
- * when it is a camera carrying the sensor cap (a doorbell wrapper bound on
92507
- * camera 615 — see `SELF_CAMERA_EXCLUDED_CAPS` for the one cap that is not
92881
+ * when it is a camera carrying the sensor cap (a doorbell bound on camera
92882
+ * 615 — see `SELF_CAMERA_EXCLUDED_CAPS` for the one cap that is not
92508
92883
  * self-ingested). The EVENT_KIND_BY_CAP gate exits first so non-sensor cap
92509
92884
  * churn costs one map lookup. Returns the number of rows inserted (0 when
92510
92885
  * unmapped, or when no camera takes a row for the device — a non-camera
@@ -93596,9 +93971,10 @@ var MotionEventSnapshotCapturer = class {
93596
93971
  *
93597
93972
  * **What this fixes.** The projection used to resolve its cameras with
93598
93973
  * `camerasFor` alone — LINKED cameras only — while the history-row ingest
93599
- * beside it had gained the self-camera half on 2026-08-07. A doorbell wrapper
93600
- * is bound on the CAMERA (the virtual-doorbell builtin writes the `doorbell`
93601
- * slice onto camera 615 itself), so `camerasFor(615)` answered `[]` and the
93974
+ * beside it had gained the self-camera half on 2026-08-07. A doorbell can be
93975
+ * served on the CAMERA itself (a composed doorbell, D671, writes the `doorbell`
93976
+ * slice onto camera 615; Reolink registers it at camera level), so
93977
+ * `camerasFor(615)` answered `[]` and the
93602
93978
  * projection returned on a `debug` line nobody runs at. Camera 615's real
93603
93979
  * press at 2026-08-08 06:59Z therefore produced a `SensorEvent` row, a HomeKit
93604
93980
  * ring, an Alexa post and a matched notification rule — and no track. Both
@@ -93746,16 +94122,17 @@ async function projectSensorMarkers(deps, data, timestamp) {
93746
94122
  * ## Why the predicate is imported and not written here
93747
94123
  *
93748
94124
  * `evaluateSensorEdge` (`@camstack/types`) is already THE edge predicate: the
93749
- * virtual doorbell feeds it (`builtins/doorbell/trigger-engine.ts`) and so does
93750
- * the recorder's sensor trigger (`addon-pipeline` `recorder/sensor-trigger.ts`).
94125
+ * recorder's sensor trigger feeds it (`addon-pipeline` `recorder/sensor-trigger.ts`)
94126
+ * and so does the orchestrator's camera-sensors bridge
94127
+ * (`addon-pipeline-orchestrator` `camera-sensors/source.ts`).
93751
94128
  * Post-analysis was the third consumer of the same bus event and the only one
93752
94129
  * that did not. A second predicate here would diverge on the first cap added —
93753
94130
  * the exact failure [D62](../../../../../docs/decisions/adr-0062.md) names.
93754
94131
  *
93755
94132
  * ## Where it deliberately differs from the other two
93756
94133
  *
93757
- * The doorbell and the recorder want the RISING edge only. A timeline marker
93758
- * is not a doorbell: a door CLOSING is an event an operator wants to see, and
94134
+ * The recorder and the camera-sensors bridge want the RISING edge only. A
94135
+ * timeline marker is not a doorbell: a door CLOSING is an event an operator wants to see, and
93759
94136
  * the taxonomy has one `contact` kind for both directions. So this gate accepts
93760
94137
  * `edge: 'rising'` **and** the `falling-edge` verdict, and rejects only the
93761
94138
  * verdicts that mean "nothing moved" — which is precisely the 30.
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Bkf0-pki.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D0JVXXRd.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }