@camstack/addon-post-analysis 1.2.316 → 1.2.320

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (15) hide show
  1. package/dist/{clip-model-registry-Dkv8MUCO.mjs → clip-model-registry-C-IEAIQQ.mjs} +232 -51
  2. package/dist/{clip-model-registry-D-zzCBKK.js → clip-model-registry-C_aKxHpy.js} +261 -50
  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-BNvVKYGi.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D553qWb_.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-7e_3hsiC.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-C1V_UEyk.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-Cpm1aEUE.mjs → hostInit-ByDyL2zi.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +692 -95
  11. package/dist/pipeline-analytics/index.mjs +692 -95
  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-BAqKeExF.mjs +0 -26
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B_KS96RA.mjs +0 -26
@@ -1,4 +1,4 @@
1
- import { $ as NC_ALARM_SYSTEM_EVENT_KINDS, $n as notificationRulesCapability, $r as object, $t as SCENE_DEFAULT_UNCOVERED_POLICY, A as EMBEDDING_REQUIREMENTS_FAILED, Ar as canonicalHash, At as NcScheduleSchema, B as ListTrackRelationsInputSchema, Bn as isAudioClassName, Br as hydrateSchema, Bt as OPS_LOG_DEFAULT_LIMIT, C as DEFAULT_EVENT_COLOR, Cn as buildEventKindDescriptor, Cr as systemEventSummaryScheduleOf, Ct as NcConditionDescriptorSchema, D as DeclaredDevices, Dn as describeSystemEventDeliveryProblem, Dr as vectorDimFromBase64, Dt as NcRuleSchema, E as DETECTION_PIPELINE_CAP_NAME, En as deriveRecordingMode, Er as templateVarsFor, Et as NcRulePatchSchema, F as EXPORT_DENSE_MAX_RANGES, Fn as evictionPolicyOfLocation, Fr as BaseAddon, Ft as NcSystemEventKindSchema, G as MAX_BIRTH_LATENCY_PROXY_MS, Gn as isRetiredClearKey, Gr as ZodObject, Gt as RebuildObjectEmbeddingsRefusalSchema, H as LockStateSchema, Hn as isEventOwnerType, Hr as nodePin, I as FIRST_LEVEL_MACRO_CLASSES, In as faceGalleryCapability, Ir as CamProfileSchema, It as NcTaxonomySchema, J as MOTION_CLOSE_AFTER_MS, Jn as isSystemEventSummaryRule, Jr as boolean, Jt as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, K as MAX_TRACK_PURITIES, Kn as isScheduleActive, Kr as _enum, Kt as RetrainStatusSchema, L as FULL_IMAGE_BBOX, Ln as failureContributionCapability, Lr as createEvent, Lt as NcTemplatePreviewInputSchema, M as EVENT_DENSITY_BATCH_MAX, Mr as isSourceCap, Mt as NcSnoozeSchema, N as EVENT_KIND_BY_CAP, Nr as DeviceType, Nt as NcSnoozeSuppressedSchema, O as DetectionSourceSchema, On as describeSystemEventFamilyProblem, Or as zoneAnalyticsCapability, Ot as NcRuleTargetSchema, P as EVENT_OWNER_TYPES, Pn as encodeVectorBase64, Pr as errMsg, Pt as NcStateSummarySectionIdSchema, Q as NC_ALARM_DELAY_MAX_SEC, Qn as memoizePinnedFfmpeg, Qr as number, Qt as SCENE_DEFAULT_QUIET_SECONDS, R as FailureCounters, Rn as gifDeriveSpeed, Rr as customAction, Rt as NcTemplatePreviewSchema, S as CoverStateSchema, Sn as audioModeOf, Sr as systemEventFilterApplies, St as NcAlarmSettingsPatchSchema, T as DETECTION_MACRO_CLASSES, Tn as cosineSimilarity$1, Tr as templateCarriesSummary, Tt as NcRuleInputSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Un as isKnownTemplateVar, Ut as RECORDING_EXPORT_MAX_READ_BYTES, V as ListTrackRelationsResultSchema, Vn as isDetectionMacroClass, Vr as isDeviceScopedCap, Vt as OpsLogEntrySchema, W as MAX_BIRTH_DECISION_RECORDS, Wn as isMediaOwnerType, Wr as sleep$1, Wt as RECORDING_SOURCE_CAMSTACK_ADDON, X as MotionTriggeredBySchema, Xn as kebabToCamel, Xr as literal, Xt as SCENE_DEFAULT_CHECK_INTERVAL_SEC, Y as MediaFileKindEnum, Yn as isVectorIndexDimConflict, Yr as discriminatedUnion, Yt as SCENE_DEFAULT_ANCHOR_THRESHOLD, Z as NC_ALARM_DELAY_FIELDS, Zn as mayWriteToLocation, Zr as looseObject, Zt as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, _ as BaseDevice, _n as alarmPanelCapability, _r as servedTemplateVars, _t as NC_TAXONOMY, a as CLIP_EMBEDDING_STEP_ID, ai as tuple, an as SetTrackRelationInputSchema, ar as readMotionTriggeredBy, at as NC_CLIP_RETENTION_MAX_SEC, b as ClearTrackRelationInputSchema, bn as assertTimelapseCadences, br as systemEventDeliveryProblems, bt as NcAlarmSensorsInputSchema, c as resolveClusterModelPin, ci as EventCategory, cn as TimelapseRuleInputSchema, cr as resolveLocationMode, ct as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, dn as TrackPuritySchema, dr as ruleBelongsToAlarm$1, dt as NC_GROUPING_MAX_SEC, ei as partialRecord, en as SCENE_DIVERGED, er as occupancyScope, et as NC_ALARM_TRIGGERED_MAX_SEC, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fn as TrackRelationKindSchema, fr as ruleDeviceCompatCapQueries, ft as NC_GROUPING_MIN_SEC, g as BIRTH_DECISION_DEFAULT_LIMIT, gn as addonWidgetsSourceCapability, gr as sensorRaisedOf, gt as NC_TANK_SYSTEM_EVENT_KINDS, h as ArchivedDebugNoteSchema, hn as TrackSourceSchema, hr as sceneMonitorCapability, ht as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, ii as templateLiteral, in as SetTrackPurityResultSchema, ir as readDeviceStateFrom, it as NC_CLIP_PRE_ROLL_DEFAULT_SEC, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as embeddingRefusalOf, jr as evaluateSensorEdge, jt as NcSnoozeInputSchema, k as EMBEDDING_MODEL_FAILED, kr as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, kt as NcRuleUpdateInputSchema, l as APPEARANCE_BIN_COUNT, ln as TimelapseRulePatchSchema, lr as resolveModeDelays, lt as NC_DEFAULT_SNOOZE_MINUTES, m as AlarmArmModeSchema, mn as TrackSideSchema, mr as ruleKindOf, mt as NC_SNOOZE_MAX_MINUTES, ni as strictObject, nn as SceneMonitorSchema, nr as pipelineAnalyticsCapability, nt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, o as FACE_EMBEDDING_STEP_ID, oi as union, on as TIMELAPSE_DENSE_FLOOR_SEC, or as readTimelapseGeneratedAt, ot as NC_CONDITION_CATALOG, p as AUDIO_MACRO_LABELS, pn as TrackRelationSchema, pr as ruleGroupingApplies, pt as NC_SENSOR_TRANSITION_KINDS, q as MAX_TRACK_RELATIONS, qn as isSystemDelivery, qr as array, qt as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, ri as string, rn as SetTrackPurityInputSchema, rr as plateGalleryCapability, rt as NC_CLIP_POST_ROLL_DEFAULT_SEC, s as readClusterModelPinStrict, si as unknown, sn as TRACK_RELATIONS_DEFAULT_LIMIT, sr as resolveClipSearchMinScore, st as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, t as BUILTIN_CLIP_MODELS, ti as record, tn as SCENE_RESET_RECAPTURES, tt as NC_AUDIO_CONFIRM_HITS_DEFAULT, u as APPEARANCE_BLOCKS, un as TimelapseRuleSchema, ut as NC_GROUPING_DEFAULT_SEC, v as BirthDecisionRecordSchema, vn as applyModeDelaysEdits, vr as storageOccupancyCapability, vt as NC_TEMPLATE_VARS, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wn as consumableLowPercentOf, wr as systemEventSummarySplitOf, wt as NcConditionsSchema, x as ClearTrackRelationResultSchema, xn as audioMetricsCapability, xr as systemEventFamilyProblems, xt as NcAlarmSensorsSchema, y as COCO_TO_MACRO, yn as applyNonBlockingEdit, yr as subKindsOf, yt as NcAlarmConfigSchema, z as LabelAttributionSchema, zn as gifFromMp4Args, zr as defineCustomActions, zt as NcTemplateVarDescriptorSchema } from "../clip-model-registry-Dkv8MUCO.mjs";
1
+ import { $ as NC_ALARM_SYSTEM_EVENT_KINDS, $n as isVectorIndexDimConflict, $r as boolean, $t as SCENE_DEFAULT_UNCOVERED_POLICY, A as EMBEDDING_REQUIREMENTS_FAILED, An as describeSystemEventDeliveryProblem, Ar as templateCarriesSummary, At as NcScheduleSchema, B as ListTrackRelationsInputSchema, Bn as failureContributionCapability, Br as BaseAddon, Bt as OPS_LOG_DEFAULT_LIMIT, C as DEFAULT_EVENT_COLOR, Cn as assertTimelapseCadences, Cr as storageOccupancyCapability, Ct as NcConditionDescriptorSchema, D as DeclaredDevices, Dn as consumableLowPercentOf, Dr as systemEventFilterApplies, Dt as NcRuleSchema, E as DETECTION_PIPELINE_CAP_NAME, En as buildEventKindDescriptor, Er as systemEventFamilyProblems, Et as NcRulePatchSchema, F as EXPORT_DENSE_MAX_RANGES, Fr as canonicalHash, Ft as NcSystemEventKindSchema, G as MAX_BIRTH_LATENCY_PROXY_MS, Gn as isDetectionMacroClass, Gr as hydrateSchema, Gt as RebuildObjectEmbeddingsRefusalSchema, H as LockStateSchema, Hn as gifFromMp4Args, Hr as createEvent, I as FIRST_LEVEL_MACRO_CLASSES, Ir as evaluateSensorEdge, It as NcTaxonomySchema, J as MOTION_CLOSE_AFTER_MS, Jn as isMediaOwnerType, Jt as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, K as MAX_TRACK_PURITIES, Kn as isEventOwnerType, Kr as isDeviceScopedCap, Kt as RetrainStatusSchema, L as FULL_IMAGE_BBOX, Ln as encodeVectorBase64, Lr as isSourceCap, Lt as NcTemplatePreviewInputSchema, M as EVENT_DENSITY_BATCH_MAX, Mr as vectorDimFromBase64, Mt as NcSnoozeSchema, N as EVENT_KIND_BY_CAP, Nr as zoneAnalyticsCapability, Nt as NcSnoozeSuppressedSchema, O as DetectionSourceSchema, On as cosineSimilarity$1, Or as systemEventSummaryScheduleOf, Ot as NcRuleTargetSchema, P as EVENT_OWNER_TYPES, Pn as embeddingRefusalOf, Pr as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Pt as NcStateSummarySectionIdSchema, Q as NC_ALARM_DELAY_MAX_SEC, Qn as isSystemEventSummaryRule, Qr as array, Qt as SCENE_DEFAULT_QUIET_SECONDS, R as FailureCounters, Rn as evictionPolicyOfLocation, Rr as DeviceType, Rt as NcTemplatePreviewSchema, S as CoverStateSchema, Sn as applySecureOnArmEdit, Sr as servedTemplateVars, St as NcAlarmSettingsPatchSchema, T as DETECTION_MACRO_CLASSES, Tn as audioModeOf, Tr as systemEventDeliveryProblems, Tt as NcRuleInputSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Un as isAlarmControllableCap, Ur as customAction, Ut as RECORDING_EXPORT_MAX_READ_BYTES, V as ListTrackRelationsResultSchema, Vn as gifDeriveSpeed, Vr as CamProfileSchema, Vt as OpsLogEntrySchema, W as MAX_BIRTH_DECISION_RECORDS, Wn as isAudioClassName, Wr as defineCustomActions, Wt as RECORDING_SOURCE_CAMSTACK_ADDON, X as MotionTriggeredBySchema, Xn as isScheduleActive, Xr as ZodObject, Xt as SCENE_DEFAULT_CHECK_INTERVAL_SEC, Y as MediaFileKindEnum, Yn as isRetiredClearKey, Yr as sleep$1, Yt as SCENE_DEFAULT_ANCHOR_THRESHOLD, Z as NC_ALARM_DELAY_FIELDS, Zn as isSystemDelivery, Zr as _enum, Zt as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, _ as BaseDevice, _n as alarmDeviceSecureOnArm, _r as ruleGroupingApplies, _t as NC_TAXONOMY, a as CLIP_EMBEDDING_STEP_ID, ai as partialRecord, an as SetTrackRelationInputSchema, at as NC_CLIP_RETENTION_MAX_SEC, b as ClearTrackRelationInputSchema, bn as applyModeDelaysEdits, br as secureActuationFor, bt as NcAlarmSensorsInputSchema, c as resolveClusterModelPin, ci as string, cn as TimelapseRuleInputSchema, cr as readDeviceStateFrom, ct as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, di as union, dn as TrackPuritySchema, dr as resolveClipSearchMinScore, dt as NC_GROUPING_MAX_SEC, ei as discriminatedUnion, en as SCENE_DIVERGED, er as kebabToCamel, et as NC_ALARM_TRIGGERED_MAX_SEC, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fi as unknown, fn as TrackRelationKindSchema, fr as resolveLocationMode, ft as NC_GROUPING_MIN_SEC, g as BIRTH_DECISION_DEFAULT_LIMIT, gn as addonWidgetsSourceCapability, gr as ruleDeviceCompatCapQueries, gt as NC_TANK_SYSTEM_EVENT_KINDS, h as ArchivedDebugNoteSchema, hn as TrackSourceSchema, hr as ruleBelongsToAlarm$1, ht as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, ii as object, in as SetTrackPurityResultSchema, ir as occupancyScope, it as NC_CLIP_PRE_ROLL_DEFAULT_SEC, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as describeSystemEventFamilyProblem, jr as templateVarsFor, jt as NcSnoozeInputSchema, k as EMBEDDING_MODEL_FAILED, kn as deriveRecordingMode, kr as systemEventSummarySplitOf, kt as NcRuleUpdateInputSchema, l as APPEARANCE_BIN_COUNT, li as templateLiteral, ln as TimelapseRulePatchSchema, lr as readMotionTriggeredBy, lt as NC_DEFAULT_SNOOZE_MINUTES, m as AlarmArmModeSchema, mn as TrackSideSchema, mt as NC_SNOOZE_MAX_MINUTES, ni as looseObject, nn as SceneMonitorSchema, nr as memoizePinnedFfmpeg, nt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, o as FACE_EMBEDDING_STEP_ID, oi as record, on as TIMELAPSE_DENSE_FLOOR_SEC, or as pipelineAnalyticsCapability, ot as NC_CONDITION_CATALOG, p as AUDIO_MACRO_LABELS, pi as EventCategory, pn as TrackRelationSchema, pr as resolveModeDelays, pt as NC_SENSOR_TRANSITION_KINDS, q as MAX_TRACK_RELATIONS, qn as isKnownTemplateVar, qr as nodePin, qt as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, ri as number, rn as SetTrackPurityInputSchema, rr as notificationRulesCapability, rt as NC_CLIP_POST_ROLL_DEFAULT_SEC, s as readClusterModelPinStrict, si as strictObject, sn as TRACK_RELATIONS_DEFAULT_LIMIT, sr as plateGalleryCapability, st as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, t as BUILTIN_CLIP_MODELS, ti as literal, tn as SCENE_RESET_RECAPTURES, tr as mayWriteToLocation, tt as NC_AUDIO_CONFIRM_HITS_DEFAULT, u as APPEARANCE_BLOCKS, ui as tuple, un as TimelapseRuleSchema, ur as readTimelapseGeneratedAt, ut as NC_GROUPING_DEFAULT_SEC, v as BirthDecisionRecordSchema, vn as alarmPanelCapability, vr as ruleKindOf, vt as NC_TEMPLATE_VARS, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wn as audioMetricsCapability, wr as subKindsOf, wt as NcConditionsSchema, x as ClearTrackRelationResultSchema, xn as applyNonBlockingEdit, xr as sensorRaisedOf, xt as NcAlarmSensorsSchema, y as COCO_TO_MACRO, yn as applyBlockingEdit, yr as sceneMonitorCapability, yt as NcAlarmConfigSchema, z as LabelAttributionSchema, zn as faceGalleryCapability, zr as errMsg, zt as NcTemplateVarDescriptorSchema } from "../clip-model-registry-C-IEAIQQ.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";
@@ -597,7 +597,19 @@ var ARM_ANYWAY_TTL_MS = 5 * 6e4;
597
597
  var NcAlarmOpenDeviceSchema = object({
598
598
  deviceId: number().int(),
599
599
  name: string(),
600
- state: string()
600
+ state: string(),
601
+ /**
602
+ * Cover / lock / switch — default false so an older bridge answer still
603
+ * parses as a contact-style opening.
604
+ */
605
+ controllable: boolean().default(false),
606
+ /** Opening cap that produced the reading; used to actuate secure-on-arm. */
607
+ cap: _enum([
608
+ "contact",
609
+ "cover",
610
+ "lock-control",
611
+ "switch"
612
+ ]).optional()
601
613
  }).readonly();
602
614
  /** Why an Arm anyway was not honoured — see `NcAlarmPanelDevice.armAnyway`. */
603
615
  var NcAlarmArmAnywayRejectionSchema = _enum([
@@ -662,23 +674,22 @@ function alarmCoveredDevices(rules, panelDeviceId, mode) {
662
674
  /**
663
675
  * Refuse or arm — the whole decision, as one partition of the open devices.
664
676
  *
665
- * A device may be open at arm time for exactly two reasons: the mode TOLERATES
666
- * it (its non-blocking list), or the operator was shown it in a refusal and
667
- * pressed that refusal's Arm anyway (`acknowledged`). Anything else open
668
- * refuses — including a device that opened AFTER the refusal the operator is
669
- * confirming: they were never shown it, and excluding it would arm the alarm
670
- * around an opening nobody agreed to.
677
+ * A device may be open at arm time when:
678
+ * - the mode TOLERATES it (`nonBlocking` for contacts),
679
+ * - it is controllable and NOT on the mode's `blocking` list (default),
680
+ * - or the operator confirmed it with Arm anyway (`acknowledged`).
671
681
  *
672
- * There is no memory here. A plain arm pressed twice is the same question
673
- * twice and gets the same answer; the only way past a refusal is the explicit
674
- * Arm anyway, whose lifecycle (one-shot, stale, expired) the panel owns.
682
+ * Anything else open refuses — including a device that opened AFTER the refusal
683
+ * the operator is confirming.
675
684
  *
676
685
  * Excluded is EVERY open device — tolerated and acknowledged alike — and the
677
686
  * exclusion is two-phase (`alarm-exclusions.ts`): each one is watched again
678
687
  * once it has closed and opened.
679
688
  */
680
689
  function decideArm(input) {
690
+ const forceBlock = new Set(input.blocking ?? []);
681
691
  const tolerated = new Set([...input.nonBlocking, ...input.acknowledged ?? []]);
692
+ for (const d of input.open) if (d.controllable && !forceBlock.has(d.deviceId)) tolerated.add(d.deviceId);
682
693
  const blocking = input.open.filter((d) => !tolerated.has(d.deviceId));
683
694
  if (blocking.length > 0) return {
684
695
  kind: "refused",
@@ -1066,39 +1077,24 @@ var NcActionPlane = class {
1066
1077
  * and never open):
1067
1078
  *
1068
1079
  * - `not-opening` — no opening slice at all: a camera named by an intrusion
1069
- * rule, a switch. It takes no part in the arm decision.
1080
+ * rule. It takes no part in the arm decision.
1070
1081
  * - `unknown` — an opening cap's slice IS there, but its field is missing or
1071
1082
  * is not a word that cap defines, or the slice is a provider's PLACEHOLDER
1072
- * (`lastChangedAt === 0`: written by a constructor before anybody read the
1073
- * device — an HA lock's `unlocked`, an HA cover's `closed`, a binary
1074
- * sensor's `entryOpen: false`). It carries the cap and the raw value (for a
1075
- * placeholder, the whole slice) so the caller's log line can say what it
1076
- * saw. It never blocks and is never armed as secure on a guess — and a
1077
- * placeholder is never taken for a state, so neither the reconcile tick,
1078
- * the arm check nor an exclusion lift acts on one.
1079
- * - `reading` — open or secure, with the state word a person will read.
1080
- * ANY non-zero `lastChangedAt` is a reading, however old — including `1`,
1081
- * the stamp a provider writes on a value it REPLAYED after a respawn with
1082
- * no upstream change time (Homematic `HM_SEEDED_CHANGED_AT`): the value is
1083
- * known, only its change time is not. The transition gate
1084
- * (`sensor-transition-gate.ts`, "A placeholder slice is not a state") reads
1085
- * the same stamp as "not a fresh event"; `0` alone means "not read yet".
1083
+ * (`lastChangedAt === 0`). It never blocks and is never armed as secure on a
1084
+ * guess.
1085
+ * - `reading` — open or secure, with the state word a person will read and the
1086
+ * cap that produced it (so secure-on-arm knows what to actuate).
1086
1087
  *
1087
1088
  * A read that FAILED never reaches {@link alarmOpeningFrom}: the caller leaves
1088
1089
  * that id out of its answer (see {@link readAlarmOpenings}).
1089
1090
  *
1090
1091
  * Anything short of fully secure is open: a cover stopped half-way, a lock that
1091
- * jammed, a cover still closing when the arm is decided.
1092
+ * jammed, a cover still closing when the arm is decided, a switch that is on.
1092
1093
  *
1093
1094
  * PRECEDENCE, when one device carries more than one opening slice: `contact`
1094
- * first, then `cover`, then `lock-control`. The contact is the physical truth
1095
- * about the opening — a reed switch on the frame says the window is open even
1096
- * when the motor's last reported word says `closed`. A slice that cannot be
1097
- * read falls through to the next; only when none of them reads is the device
1098
- * `unknown`, naming the first unreadable cap.
1099
- *
1100
- * The vocabulary of each word is its cap's own schema (`CoverStateSchema`,
1101
- * `LockStateSchema`) — a word the cap does not define is not guessed at.
1095
+ * first, then `cover`, then `lock-control`, then `switch`. The contact is the
1096
+ * physical truth about the opening — a reed switch on the frame says the window
1097
+ * is open even when the motor's last reported word says `closed`.
1102
1098
  */
1103
1099
  /** The one state word, per cap, that counts as secure. Everything else is open. */
1104
1100
  var COVER_SECURE = "closed";
@@ -1117,11 +1113,12 @@ function fieldOf(slice, field) {
1117
1113
  function isPlaceholder(slice) {
1118
1114
  return fieldOf(slice, "lastChangedAt") === 0;
1119
1115
  }
1120
- function reading(word, secure) {
1116
+ function reading(word, secure, cap) {
1121
1117
  return {
1122
1118
  kind: "reading",
1123
1119
  reading: word === secure ? "secure" : "open",
1124
- state: word
1120
+ state: word,
1121
+ cap
1125
1122
  };
1126
1123
  }
1127
1124
  function readContact(runtime) {
@@ -1141,11 +1138,13 @@ function readContact(runtime) {
1141
1138
  return raw ? {
1142
1139
  kind: "reading",
1143
1140
  reading: "open",
1144
- state: "open"
1141
+ state: "open",
1142
+ cap: "contact"
1145
1143
  } : {
1146
1144
  kind: "reading",
1147
1145
  reading: "secure",
1148
- state: "closed"
1146
+ state: "closed",
1147
+ cap: "contact"
1149
1148
  };
1150
1149
  }
1151
1150
  function readCover(runtime) {
@@ -1158,7 +1157,7 @@ function readCover(runtime) {
1158
1157
  };
1159
1158
  const raw = fieldOf(slice, "state");
1160
1159
  const parsed = CoverStateSchema.safeParse(raw);
1161
- return parsed.success ? reading(parsed.data, COVER_SECURE) : {
1160
+ return parsed.success ? reading(parsed.data, COVER_SECURE, "cover") : {
1162
1161
  kind: "unknown",
1163
1162
  cap: "cover",
1164
1163
  raw
@@ -1174,19 +1173,46 @@ function readLock(runtime) {
1174
1173
  };
1175
1174
  const raw = fieldOf(slice, "state");
1176
1175
  const parsed = LockStateSchema.safeParse(raw);
1177
- return parsed.success ? reading(parsed.data, LOCK_SECURE) : {
1176
+ return parsed.success ? reading(parsed.data, LOCK_SECURE, "lock-control") : {
1178
1177
  kind: "unknown",
1179
1178
  cap: "lock-control",
1180
1179
  raw
1181
1180
  };
1182
1181
  }
1182
+ function readSwitch(runtime) {
1183
+ const slice = sliceOf(runtime, "switch");
1184
+ if (slice === void 0) return { kind: "absent" };
1185
+ if (isPlaceholder(slice)) return {
1186
+ kind: "unknown",
1187
+ cap: "switch",
1188
+ raw: slice
1189
+ };
1190
+ const raw = fieldOf(slice, "on");
1191
+ if (typeof raw !== "boolean") return {
1192
+ kind: "unknown",
1193
+ cap: "switch",
1194
+ raw
1195
+ };
1196
+ return raw ? {
1197
+ kind: "reading",
1198
+ reading: "open",
1199
+ state: "on",
1200
+ cap: "switch"
1201
+ } : {
1202
+ kind: "reading",
1203
+ reading: "secure",
1204
+ state: "off",
1205
+ cap: "switch"
1206
+ };
1207
+ }
1183
1208
  /** D630's table over one device's runtime slices — see the header for the three answers. */
1184
1209
  function alarmOpeningFrom(runtime) {
1185
1210
  let firstUnknown = null;
1186
1211
  for (const read of [
1187
1212
  readContact(runtime),
1188
1213
  readCover(runtime),
1189
- readLock(runtime)
1214
+ readLock(runtime),
1215
+ readSwitch(runtime)
1190
1216
  ]) {
1191
1217
  if (read.kind === "reading") return read;
1192
1218
  if (read.kind === "unknown" && firstUnknown === null) firstUnknown = read;
@@ -1201,13 +1227,14 @@ function alarmOpeningFrom(runtime) {
1201
1227
  var OPENING_CAP_BY_EVENT_KIND = {
1202
1228
  contact: "contact",
1203
1229
  cover: "cover",
1204
- lock: "lock-control"
1230
+ lock: "lock-control",
1231
+ switch: "switch"
1205
1232
  };
1206
1233
  /**
1207
1234
  * The same table, read off one sensor-event row: its `value` IS the slice of
1208
1235
  * the cap its `kind` names, so the event path and the reconcile read answer in
1209
1236
  * one vocabulary — a cover is secure only when `closed`, a lock only when
1210
- * `locked`, whichever path saw it.
1237
+ * `locked`, a switch only when off, whichever path saw it.
1211
1238
  */
1212
1239
  function alarmOpeningFromSensorEvent(kind, value) {
1213
1240
  const cap = OPENING_CAP_BY_EVENT_KIND[kind];
@@ -1228,7 +1255,9 @@ function classifyCoveredOpening(opening) {
1228
1255
  if (opening.kind === "not-opening" || opening.reading !== "open") return { kind: "skip" };
1229
1256
  return {
1230
1257
  kind: "open",
1231
- state: opening.state
1258
+ state: opening.state,
1259
+ controllable: isAlarmControllableCap(opening.cap),
1260
+ cap: opening.cap
1232
1261
  };
1233
1262
  }
1234
1263
  /**
@@ -1885,6 +1914,22 @@ var ncAlarmPanelSchema = object({
1885
1914
  deviceIds: array(number().int())
1886
1915
  })).default([]),
1887
1916
  /**
1917
+ * Controllable openings that DO block when open (opt-in). Loose + guarded like
1918
+ * {@link nonBlocking}.
1919
+ */
1920
+ blocking: array(object({
1921
+ mode: string(),
1922
+ deviceIds: array(number().int())
1923
+ })).default([]),
1924
+ /**
1925
+ * Controllable openings that must NOT secure on arm (opt-out). Loose + guarded
1926
+ * like {@link nonBlocking}.
1927
+ */
1928
+ secureOnArmOff: array(object({
1929
+ mode: string(),
1930
+ deviceIds: array(number().int())
1931
+ })).default([]),
1932
+ /**
1888
1933
  * Per arm mode, the delays that override the global ones (D697). Loose on purpose — `mode` is
1889
1934
  * a string and the numbers are unbounded here — and read back through
1890
1935
  * {@link modeDelaysFromPersisted}, never a cast: an entry this release cannot trust is dropped
@@ -1903,7 +1948,7 @@ var ncAlarmPanelSchema = object({
1903
1948
  * per mode kept — the same "at most one list per mode" the settings schema
1904
1949
  * enforces at the boundary. A type guard (`safeParse`), never a cast.
1905
1950
  */
1906
- function nonBlockingFromPersisted(raw) {
1951
+ function modeDeviceListFromPersisted(raw) {
1907
1952
  const seen = /* @__PURE__ */ new Set();
1908
1953
  const lists = [];
1909
1954
  for (const entry of raw) {
@@ -2008,7 +2053,9 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2008
2053
  ...this.readDelays(),
2009
2054
  announceArm: this.config.get("announceArm"),
2010
2055
  announceTargets: [...this.config.get("announceTargets")],
2011
- nonBlocking: nonBlockingFromPersisted(this.config.get("nonBlocking")),
2056
+ nonBlocking: modeDeviceListFromPersisted(this.config.get("nonBlocking")),
2057
+ blocking: modeDeviceListFromPersisted(this.config.get("blocking")),
2058
+ secureOnArmOff: modeDeviceListFromPersisted(this.config.get("secureOnArmOff")),
2012
2059
  modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
2013
2060
  };
2014
2061
  }
@@ -2035,30 +2082,73 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2035
2082
  async applySettings(patch) {
2036
2083
  const edits = patch.modeDelays ?? [];
2037
2084
  const membership = patch.nonBlockingEdit;
2085
+ const blockingEdit = patch.blockingEdit;
2086
+ const secureEdit = patch.secureOnArmEdit;
2038
2087
  for (const edit of edits) this.refuseUnofferedMode(edit.mode);
2039
2088
  if (membership !== void 0) {
2040
2089
  this.refuseUnofferedMode(membership.mode, membership.deviceId);
2041
- const verdict = applyNonBlockingEdit(patch.nonBlocking !== void 0 ? patch.nonBlocking : nonBlockingFromPersisted(this.config.get("nonBlocking")), membership);
2090
+ const verdict = applyNonBlockingEdit(patch.nonBlocking !== void 0 ? patch.nonBlocking : modeDeviceListFromPersisted(this.config.get("nonBlocking")), membership);
2042
2091
  if (verdict.kind === "refused") {
2043
2092
  this.ctx.logger.info("alarm settings refused", {
2044
2093
  tags: { deviceId: membership.deviceId },
2045
2094
  meta: {
2046
2095
  panelId: this.id,
2047
2096
  reason: "list-full",
2048
- mode: verdict.mode
2097
+ mode: verdict.mode,
2098
+ list: "nonBlocking"
2049
2099
  }
2050
2100
  });
2051
2101
  throw new Error(`the ${verdict.mode} non-blocking list already holds ${verdict.max} devices — remove one first`);
2052
2102
  }
2053
2103
  }
2104
+ if (blockingEdit !== void 0) {
2105
+ this.refuseUnofferedMode(blockingEdit.mode, blockingEdit.deviceId);
2106
+ const verdict = applyBlockingEdit(patch.blocking !== void 0 ? patch.blocking : modeDeviceListFromPersisted(this.config.get("blocking")), blockingEdit);
2107
+ if (verdict.kind === "refused") {
2108
+ this.ctx.logger.info("alarm settings refused", {
2109
+ tags: { deviceId: blockingEdit.deviceId },
2110
+ meta: {
2111
+ panelId: this.id,
2112
+ reason: "list-full",
2113
+ mode: verdict.mode,
2114
+ list: "blocking"
2115
+ }
2116
+ });
2117
+ throw new Error(`the ${verdict.mode} blocking list already holds ${verdict.max} devices — remove one first`);
2118
+ }
2119
+ }
2120
+ if (secureEdit !== void 0) {
2121
+ this.refuseUnofferedMode(secureEdit.mode, secureEdit.deviceId);
2122
+ const verdict = applySecureOnArmEdit(patch.secureOnArmOff !== void 0 ? patch.secureOnArmOff : modeDeviceListFromPersisted(this.config.get("secureOnArmOff")), secureEdit);
2123
+ if (verdict.kind === "refused") {
2124
+ this.ctx.logger.info("alarm settings refused", {
2125
+ tags: { deviceId: secureEdit.deviceId },
2126
+ meta: {
2127
+ panelId: this.id,
2128
+ reason: "list-full",
2129
+ mode: verdict.mode,
2130
+ list: "secureOnArmOff"
2131
+ }
2132
+ });
2133
+ throw new Error(`the ${verdict.mode} secure-on-arm-off list already holds ${verdict.max} devices — remove one first`);
2134
+ }
2135
+ }
2054
2136
  const listWrites = [];
2055
2137
  if (patch.nonBlocking !== void 0) listWrites.push(this.config.set("nonBlocking", patch.nonBlocking.map((l) => ({
2056
2138
  mode: l.mode,
2057
2139
  deviceIds: [...l.deviceIds]
2058
2140
  }))));
2141
+ if (patch.blocking !== void 0) listWrites.push(this.config.set("blocking", patch.blocking.map((l) => ({
2142
+ mode: l.mode,
2143
+ deviceIds: [...l.deviceIds]
2144
+ }))));
2145
+ if (patch.secureOnArmOff !== void 0) listWrites.push(this.config.set("secureOnArmOff", patch.secureOnArmOff.map((l) => ({
2146
+ mode: l.mode,
2147
+ deviceIds: [...l.deviceIds]
2148
+ }))));
2059
2149
  let membershipChanged = null;
2060
2150
  if (membership !== void 0) {
2061
- const result = applyNonBlockingEdit(nonBlockingFromPersisted(this.config.get("nonBlocking")), membership);
2151
+ const result = applyNonBlockingEdit(modeDeviceListFromPersisted(this.config.get("nonBlocking")), membership);
2062
2152
  if (result.kind === "applied") {
2063
2153
  membershipChanged = result.changed;
2064
2154
  if (result.changed) listWrites.push(this.config.set("nonBlocking", result.lists.map((l) => ({
@@ -2067,6 +2157,26 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2067
2157
  }))));
2068
2158
  }
2069
2159
  }
2160
+ if (blockingEdit !== void 0) {
2161
+ const result = applyBlockingEdit(modeDeviceListFromPersisted(this.config.get("blocking")), blockingEdit);
2162
+ if (result.kind === "applied" && result.changed) {
2163
+ membershipChanged = true;
2164
+ listWrites.push(this.config.set("blocking", result.lists.map((l) => ({
2165
+ mode: l.mode,
2166
+ deviceIds: [...l.deviceIds]
2167
+ }))));
2168
+ }
2169
+ }
2170
+ if (secureEdit !== void 0) {
2171
+ const result = applySecureOnArmEdit(modeDeviceListFromPersisted(this.config.get("secureOnArmOff")), secureEdit);
2172
+ if (result.kind === "applied" && result.changed) {
2173
+ membershipChanged = true;
2174
+ listWrites.push(this.config.set("secureOnArmOff", result.lists.map((l) => ({
2175
+ mode: l.mode,
2176
+ deviceIds: [...l.deviceIds]
2177
+ }))));
2178
+ }
2179
+ }
2070
2180
  const persistedDelays = modeDelaysFromPersisted(this.config.get("modeDelays"));
2071
2181
  const beforeDelays = persistedDelays.entries;
2072
2182
  const afterDelays = edits.length > 0 ? applyModeDelaysEdits(beforeDelays, edits) : beforeDelays;
@@ -2205,7 +2315,7 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2205
2315
  if (read.verdict.kind === "refused") return this.refuse(mode, read.verdict.blocking, now);
2206
2316
  return this.settleArm(mode, read.verdict.excluded.map((device) => ({
2207
2317
  device,
2208
- why: "non-blocking"
2318
+ why: this.exclusionWhyFor(mode, device)
2209
2319
  })), now);
2210
2320
  }
2211
2321
  /**
@@ -2254,12 +2364,14 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2254
2364
  return outcome;
2255
2365
  }
2256
2366
  const tolerated = this.nonBlockingFor(held.mode);
2367
+ const forceBlock = this.blockingFor(held.mode);
2257
2368
  const unknown = new Set(read.openings.unknown);
2258
2369
  if (read.verdict.kind === "refused") {
2259
2370
  const plain = decideArm({
2260
2371
  mode: held.mode,
2261
2372
  open: read.openings.open,
2262
- nonBlocking: tolerated
2373
+ nonBlocking: tolerated,
2374
+ blocking: forceBlock
2263
2375
  });
2264
2376
  const blockingNow = plain.kind === "refused" ? plain.blocking : read.verdict.blocking;
2265
2377
  const shownIds = new Set(blockingNow.map((d) => d.deviceId));
@@ -2389,10 +2501,17 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2389
2501
  error: String(err)
2390
2502
  };
2391
2503
  }
2504
+ const toSecure = openings.open.filter((d) => alarmDeviceSecureOnArm({
2505
+ controllable: d.controllable,
2506
+ deviceId: d.deviceId,
2507
+ secureOnArmOff: this.secureOnArmOffFor(mode)
2508
+ }));
2509
+ if (toSecure.length > 0) this.armGuard?.secureOnArm(toSecure);
2392
2510
  const verdict = decideArm({
2393
2511
  mode,
2394
2512
  open: openings.open,
2395
2513
  nonBlocking: this.nonBlockingFor(mode),
2514
+ blocking: this.blockingFor(mode),
2396
2515
  ...acknowledged !== void 0 ? { acknowledged } : {}
2397
2516
  });
2398
2517
  return {
@@ -2417,7 +2536,28 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2417
2536
  }
2418
2537
  /** This mode's non-blocking ids, from the persisted lists. */
2419
2538
  nonBlockingFor(mode) {
2420
- return nonBlockingFromPersisted(this.config.get("nonBlocking")).find((l) => l.mode === mode)?.deviceIds ?? [];
2539
+ return this.modeListIds("nonBlocking", mode);
2540
+ }
2541
+ /** Controllable openings that force-block when open. */
2542
+ blockingFor(mode) {
2543
+ return this.modeListIds("blocking", mode);
2544
+ }
2545
+ /** Controllable openings that opt out of secure-on-arm. */
2546
+ secureOnArmOffFor(mode) {
2547
+ return this.modeListIds("secureOnArmOff", mode);
2548
+ }
2549
+ modeListIds(key, mode) {
2550
+ return modeDeviceListFromPersisted(this.config.get(key)).find((l) => l.mode === mode)?.deviceIds ?? [];
2551
+ }
2552
+ /** Why an open device was armed around on a plain (non–Arm-anyway) arm. */
2553
+ exclusionWhyFor(mode, device) {
2554
+ if (this.nonBlockingFor(mode).includes(device.deviceId)) return "non-blocking";
2555
+ if (alarmDeviceSecureOnArm({
2556
+ controllable: device.controllable,
2557
+ deviceId: device.deviceId,
2558
+ secureOnArmOff: this.secureOnArmOffFor(mode)
2559
+ })) return "secure-on-arm";
2560
+ return "non-blocking";
2421
2561
  }
2422
2562
  /**
2423
2563
  * Arm, with these exclusions.
@@ -8610,6 +8750,7 @@ function sameWord(a, b) {
8610
8750
  * therefore moves the table with the arm.
8611
8751
  */
8612
8752
  function alarmSensorStateOf(deviceId, opening, online) {
8753
+ const controllable = opening?.kind === "reading" ? isAlarmControllableCap(opening.cap) : false;
8613
8754
  const verdict = classifyCoveredOpening(opening);
8614
8755
  switch (verdict.kind) {
8615
8756
  case "unknown": return {
@@ -8617,27 +8758,31 @@ function alarmSensorStateOf(deviceId, opening, online) {
8617
8758
  reading: "unknown",
8618
8759
  state: null,
8619
8760
  unknownWhy: verdict.why,
8620
- online
8761
+ online,
8762
+ controllable
8621
8763
  };
8622
8764
  case "open": return {
8623
8765
  deviceId,
8624
8766
  reading: "open",
8625
8767
  state: verdict.state,
8626
8768
  unknownWhy: null,
8627
- online
8769
+ online,
8770
+ controllable: verdict.controllable
8628
8771
  };
8629
8772
  case "skip": return opening?.kind === "reading" ? {
8630
8773
  deviceId,
8631
8774
  reading: "closed",
8632
8775
  state: opening.state,
8633
8776
  unknownWhy: null,
8634
- online
8777
+ online,
8778
+ controllable
8635
8779
  } : {
8636
8780
  deviceId,
8637
8781
  reading: "not-opening",
8638
8782
  state: null,
8639
8783
  unknownWhy: null,
8640
- online
8784
+ online,
8785
+ controllable: false
8641
8786
  };
8642
8787
  }
8643
8788
  }
@@ -19986,19 +20131,39 @@ function planSummaryDevices(rule, isDead, guard) {
19986
20131
  notes: notes.notes
19987
20132
  };
19988
20133
  }
20134
+ function stripDeadFromLists(lists, isDead) {
20135
+ const dead = lists.flatMap((list) => list.deviceIds.filter(isDead));
20136
+ if (dead.length === 0) return {
20137
+ next: lists,
20138
+ dead
20139
+ };
20140
+ return {
20141
+ dead,
20142
+ next: lists.map((list) => ({
20143
+ ...list,
20144
+ deviceIds: list.deviceIds.filter((id) => !isDead(id))
20145
+ }))
20146
+ };
20147
+ }
19989
20148
  function planAlarmDevices(alarm, isDead) {
19990
20149
  if (alarm === null) return null;
19991
- const nonBlockingDead = alarm.nonBlocking.flatMap((list) => list.deviceIds.filter(isDead));
19992
- const touched = nonBlockingDead.length > 0;
20150
+ const nonBlocking = stripDeadFromLists(alarm.nonBlocking, isDead);
20151
+ const blocking = stripDeadFromLists(alarm.blocking, isDead);
20152
+ const secureOnArmOff = stripDeadFromLists(alarm.secureOnArmOff, isDead);
19993
20153
  const forgetExcluded = alarm.excluded.filter(isDead);
19994
- if (!touched && forgetExcluded.length === 0) return null;
20154
+ const deadIds = [
20155
+ ...nonBlocking.dead,
20156
+ ...blocking.dead,
20157
+ ...secureOnArmOff.dead,
20158
+ ...forgetExcluded
20159
+ ];
20160
+ if (deadIds.length === 0) return null;
19995
20161
  return {
19996
- dropped: deviceSubjects([...new Set([...nonBlockingDead, ...forgetExcluded])]),
20162
+ dropped: deviceSubjects([...new Set(deadIds)]),
19997
20163
  droppedKind: "device",
19998
- ...touched ? { nonBlocking: alarm.nonBlocking.map((list) => ({
19999
- ...list,
20000
- deviceIds: list.deviceIds.filter((id) => !isDead(id))
20001
- })) } : {},
20164
+ ...nonBlocking.dead.length > 0 ? { nonBlocking: nonBlocking.next } : {},
20165
+ ...blocking.dead.length > 0 ? { blocking: blocking.next } : {},
20166
+ ...secureOnArmOff.dead.length > 0 ? { secureOnArmOff: secureOnArmOff.next } : {},
20002
20167
  forgetExcluded
20003
20168
  };
20004
20169
  }
@@ -20040,6 +20205,8 @@ function referencedDeviceIds(snapshot) {
20040
20205
  for (const rule of snapshot.timelapse) add(rule.deviceIds);
20041
20206
  for (const rule of snapshot.summary) add(rule.deviceIds);
20042
20207
  for (const list of snapshot.alarm?.nonBlocking ?? []) add(list.deviceIds);
20208
+ for (const list of snapshot.alarm?.blocking ?? []) add(list.deviceIds);
20209
+ for (const list of snapshot.alarm?.secureOnArmOff ?? []) add(list.deviceIds);
20043
20210
  add(snapshot.alarm?.excluded);
20044
20211
  add(snapshot.mutedDeviceIds);
20045
20212
  return ids;
@@ -20384,6 +20551,8 @@ var NcDeletedSubjectPruner = class {
20384
20551
  summary: this.writable("summary", this.deps.summary.list()),
20385
20552
  alarm: panel !== null && settings !== void 0 ? {
20386
20553
  nonBlocking: settings.nonBlocking,
20554
+ blocking: settings.blocking,
20555
+ secureOnArmOff: settings.secureOnArmOff,
20387
20556
  announceTargets: settings.announceTargets,
20388
20557
  excluded: panel.excludedDevices()
20389
20558
  } : null,
@@ -20540,6 +20709,8 @@ var NcDeletedSubjectPruner = class {
20540
20709
  ...NO_SNAPSHOT,
20541
20710
  alarm: {
20542
20711
  nonBlocking: settings.nonBlocking,
20712
+ blocking: settings.blocking,
20713
+ secureOnArmOff: settings.secureOnArmOff,
20543
20714
  announceTargets: settings.announceTargets,
20544
20715
  excluded: panel.excludedDevices()
20545
20716
  }
@@ -20548,6 +20719,8 @@ var NcDeletedSubjectPruner = class {
20548
20719
  try {
20549
20720
  const patch = {
20550
20721
  ...edit.nonBlocking !== void 0 ? { nonBlocking: edit.nonBlocking } : {},
20722
+ ...edit.blocking !== void 0 ? { blocking: edit.blocking } : {},
20723
+ ...edit.secureOnArmOff !== void 0 ? { secureOnArmOff: edit.secureOnArmOff } : {},
20551
20724
  ...edit.announceTargets !== void 0 ? { announceTargets: [...edit.announceTargets] } : {}
20552
20725
  };
20553
20726
  if (Object.keys(patch).length > 0) await panel.applySettings(patch);
@@ -35550,6 +35723,8 @@ var NC_ALARM_SETTINGS_FALLBACK = {
35550
35723
  ...DEFAULT_DELAYS,
35551
35724
  announceTargets: [],
35552
35725
  nonBlocking: [],
35726
+ blocking: [],
35727
+ secureOnArmOff: [],
35553
35728
  modeDelays: []
35554
35729
  };
35555
35730
  /**
@@ -38554,6 +38729,9 @@ var NotificationCenter = class {
38554
38729
  alarmArmGuard() {
38555
38730
  return {
38556
38731
  openings: (mode) => this.alarmOpenings(mode),
38732
+ secureOnArm: (devices) => {
38733
+ this.secureDevicesOnArm(devices);
38734
+ },
38557
38735
  announceRefusal: (input) => this.announceArmRefused(input),
38558
38736
  announceCheckFailed: (input) => this.announceArmCheckFailed(input)
38559
38737
  };
@@ -38620,7 +38798,9 @@ var NotificationCenter = class {
38620
38798
  if (verdict.kind === "open") open.push({
38621
38799
  deviceId,
38622
38800
  name: name(deviceId),
38623
- state: verdict.state
38801
+ state: verdict.state,
38802
+ controllable: verdict.controllable,
38803
+ cap: verdict.cap
38624
38804
  });
38625
38805
  }
38626
38806
  this.logger.info("alarm arm check", {
@@ -38639,6 +38819,47 @@ var NotificationCenter = class {
38639
38819
  };
38640
38820
  }
38641
38821
  /**
38822
+ * Fire-and-forget secure actuation for controllable openings on arm.
38823
+ * Failures are WARN per device — they never refuse the arm.
38824
+ */
38825
+ async secureDevicesOnArm(devices) {
38826
+ for (const device of devices) {
38827
+ const actuation = device.cap !== void 0 ? secureActuationFor(device.cap) : null;
38828
+ if (actuation === null) continue;
38829
+ try {
38830
+ await this.actuateWithAttribution({
38831
+ deviceId: device.deviceId,
38832
+ cap: actuation.cap,
38833
+ method: actuation.method,
38834
+ args: {
38835
+ deviceId: device.deviceId,
38836
+ ...actuation.args
38837
+ }
38838
+ });
38839
+ this.logger.info("alarm secure-on-arm actuated", {
38840
+ tags: { deviceId: device.deviceId },
38841
+ meta: {
38842
+ name: device.name,
38843
+ state: device.state,
38844
+ cap: actuation.cap,
38845
+ method: actuation.method
38846
+ }
38847
+ });
38848
+ } catch (err) {
38849
+ this.logger.warn("alarm secure-on-arm actuation failed — arm continues", {
38850
+ tags: { deviceId: device.deviceId },
38851
+ meta: {
38852
+ name: device.name,
38853
+ state: device.state,
38854
+ cap: actuation.cap,
38855
+ method: actuation.method,
38856
+ error: String(err)
38857
+ }
38858
+ });
38859
+ }
38860
+ }
38861
+ }
38862
+ /**
38642
38863
  * The refusal, as an ordinary notification.
38643
38864
  *
38644
38865
  * Same path every alarm announcement takes (D126: the alarm speaks through
@@ -51320,6 +51541,11 @@ var MEDIA_ID_SEGMENT = "m";
51320
51541
  /** Path segment that introduces an OWNER question ("best media of"). */
51321
51542
  var MEDIA_OWNER_SEGMENT = "o";
51322
51543
  /**
51544
+ * Path segment that introduces an event-owner GENERATION (D694) —
51545
+ * `/g/<deviceId>/<recordAt>` after the owner id.
51546
+ */
51547
+ var MEDIA_GENERATION_SEGMENT = "g";
51548
+ /**
51323
51549
  * The plane path for one media row.
51324
51550
  *
51325
51551
  * `prefix` is `/addon/<addonId>/event-media` — a pure function of the addon
@@ -51336,6 +51562,23 @@ function mediaFileUrl(prefix, mediaId) {
51336
51562
  return `${prefix}/${MEDIA_ID_SEGMENT}/${mediaId}`;
51337
51563
  }
51338
51564
  /**
51565
+ * The plane path for "whatever media best represents this owner".
51566
+ *
51567
+ * The owner id IS encoded: a trackId is a UUID today but an owner id is an
51568
+ * arbitrary row key (the summary mosaic's is `summary-<ruleId>-<windowEndMs>`),
51569
+ * and one slash in it would silently re-cut the path.
51570
+ *
51571
+ * When `generation` is set and `ownerType` is an event table, the path carries
51572
+ * `/g/<deviceId>/<recordAt>` so a reused rowid cannot resolve another record's
51573
+ * crop. Generation is ignored (and not minted) for non-event owners — a track
51574
+ * id is random and needs none.
51575
+ */
51576
+ function mediaOwnerUrl(prefix, ownerType, ownerId, generation) {
51577
+ const base = `${prefix}/${MEDIA_OWNER_SEGMENT}/${ownerType}/${encodeURIComponent(ownerId)}`;
51578
+ if (generation === void 0 || !isEventOwnerType(ownerType)) return base;
51579
+ return `${base}/${MEDIA_GENERATION_SEGMENT}/${generation.deviceId}/${generation.recordAt}`;
51580
+ }
51581
+ /**
51339
51582
  * A stored row as the wire sees it: the ref, the address, and — transitionally
51340
51583
  * — the bytes.
51341
51584
  *
@@ -59186,6 +59429,12 @@ function openGzipStream(out) {
59186
59429
  onceDrain: (listener) => {
59187
59430
  gzip.once("drain", listener);
59188
59431
  },
59432
+ flush: () => new Promise((resolve, reject) => {
59433
+ gzip.flush((err) => {
59434
+ if (err !== null && err !== void 0) reject(err);
59435
+ else resolve();
59436
+ });
59437
+ }),
59189
59438
  finish: async () => {
59190
59439
  gzip.end();
59191
59440
  await closed;
@@ -59281,6 +59530,25 @@ var TapeSegmentWriter = class {
59281
59530
  });
59282
59531
  }
59283
59532
  /**
59533
+ * Push the sink's pending gzip output to disk without ending the segment.
59534
+ * A quiet / silent camera otherwise keeps its tail only in the zlib
59535
+ * buffer until the next rotation (D681 follow-up). Never throws into the
59536
+ * caller's frame path: a flush fault is logged and swallowed here.
59537
+ */
59538
+ async flushToDisk() {
59539
+ try {
59540
+ await this.deps.sink.flush();
59541
+ } catch (err) {
59542
+ this.deps.logger.warn("tracking tape segment flush failed", {
59543
+ tags: { deviceId: this.deps.deviceId },
59544
+ meta: {
59545
+ segmentOriginMs: this.deps.segmentOriginMs,
59546
+ error: errMsg(err)
59547
+ }
59548
+ });
59549
+ }
59550
+ }
59551
+ /**
59284
59552
  * Always ends the segment's records (queue flush, lost-span gap, `end`
59285
59553
  * record) before touching the sink, so a sink that cannot finish still
59286
59554
  * leaves a decodable body behind it. `sink.finish()`'s rejection is caught
@@ -59607,6 +59875,21 @@ var TapeRecorder = class {
59607
59875
  if (cam !== void 0) await this.closeSegment(deviceId, cam, "segment-end");
59608
59876
  }
59609
59877
  /**
59878
+ * Push every open segment's pending gzip output to disk without rotating
59879
+ * (D681 follow-up). A silent / quiet camera otherwise keeps its tail only
59880
+ * in the zlib buffer until the next frame or the 10-minute cut — a crash
59881
+ * loses it, and `bytesLastHour` lags behind what was already recorded.
59882
+ * Never throws: a per-camera flush fault is logged inside the writer.
59883
+ */
59884
+ async flushOpenSegments() {
59885
+ const pending = [];
59886
+ for (const cam of this.cameras.values()) {
59887
+ const writer = cam.segment?.writer;
59888
+ if (writer !== void 0 && writer !== null) pending.push(writer.flushToDisk());
59889
+ }
59890
+ await Promise.all(pending);
59891
+ }
59892
+ /**
59610
59893
  * Close the camera's open segment only if it holds time at or before
59611
59894
  * `atMs` — what a promotion of a window ending at `atMs` needs on disk
59612
59895
  * (Task 12 fix round, H1). A window wholly before the open segment leaves
@@ -60106,6 +60389,29 @@ var TapeShelf = class {
60106
60389
  };
60107
60390
  }
60108
60391
  };
60392
+ /**
60393
+ * Log every refusal the sweep already measured (D391 / D681 follow-up).
60394
+ * `sweep()` itself never throws for `rootUnreadable` / per-camera `failed`:
60395
+ * those land on the report, and a caller that only `.catch`es would drop them
60396
+ * silently even when the shelf already warned at the `readdir` site. One
60397
+ * tagged line per refusal at the sweep boundary closes that gap.
60398
+ */
60399
+ function logTapeSweepReport(report, logger) {
60400
+ if (report.rootUnreadable !== null) logger.warn("tracking tape sweep refused", { meta: {
60401
+ reason: report.rootUnreadable,
60402
+ scope: "root"
60403
+ } });
60404
+ for (const [deviceId, outcome] of report.perDevice) {
60405
+ if (!("failed" in outcome) || outcome.failed !== true) continue;
60406
+ logger.warn("tracking tape sweep refused", {
60407
+ tags: { deviceId },
60408
+ meta: {
60409
+ reason: outcome.reason,
60410
+ scope: "camera"
60411
+ }
60412
+ });
60413
+ }
60414
+ }
60109
60415
  //#endregion
60110
60416
  //#region src/pipeline-analytics/tracking-bench/recorder/tape-operation-queue.ts
60111
60417
  /** Generous: a promotion reads its window's segments and walks back through
@@ -61159,6 +61465,17 @@ function shelfFigures(listing, now) {
61159
61465
  ...last !== void 0 ? { heldToMs: last.originMs } : {}
61160
61466
  };
61161
61467
  }
61468
+ /**
61469
+ * Default `debug.tape.status` (no `deviceIds`): drop the noise. An Off camera
61470
+ * with a measured empty shelf is not a cost row and is not promotable; keep
61471
+ * everything else — Recording / Refused / Failing / Unknown, Off with held
61472
+ * segments (still promotable after the switch went off), and Off whose shelf
61473
+ * could not be read (D393: never silently hide an unreadable camera). Explicit
61474
+ * `deviceIds` still return every asked-about camera, Off included (D62).
61475
+ */
61476
+ function defaultTapeStatusCameras(rows) {
61477
+ return rows.filter((row) => row.state !== "off" || row.segmentsHeld === null || row.segmentsHeld > 0);
61478
+ }
61162
61479
  function describeTapeStatus(input) {
61163
61480
  return input.deviceIds.map((deviceId) => {
61164
61481
  const shelf = {
@@ -73017,7 +73334,7 @@ var AnalyticsQueryFacade = class {
73017
73334
  async getMotionEvents(input) {
73018
73335
  const events = await (this.deps.eventStore()?.queryMotion(input) ?? Promise.resolve([]));
73019
73336
  if (input.projection !== "slim" || this.deps.eventMediaBaseUrl() === null) return events;
73020
- return this.withMediaUrl(events);
73337
+ return this.withMediaUrl(events, "motion");
73021
73338
  }
73022
73339
  async getEventDensity(input) {
73023
73340
  return this.deps.eventStore()?.densityByKind(input) ?? [];
@@ -73074,12 +73391,12 @@ var AnalyticsQueryFacade = class {
73074
73391
  async getObjectEvents(input) {
73075
73392
  const events = await (this.deps.eventStore()?.queryObject(input) ?? Promise.resolve([]));
73076
73393
  if (input.projection !== "slim" || this.deps.eventMediaBaseUrl() === null) return events;
73077
- return this.withMediaUrl(events);
73394
+ return this.withMediaUrl(events, "object");
73078
73395
  }
73079
73396
  async getAudioEvents(input) {
73080
73397
  const events = await (this.deps.eventStore()?.queryAudio(input) ?? Promise.resolve([]));
73081
73398
  if (input.projection !== "slim" || this.deps.eventMediaBaseUrl() === null) return events;
73082
- return this.withMediaUrl(events);
73399
+ return this.withMediaUrl(events, "audio");
73083
73400
  }
73084
73401
  async getEventMedia(input) {
73085
73402
  const store = this.deps.mediaStore();
@@ -73346,13 +73663,20 @@ var AnalyticsQueryFacade = class {
73346
73663
  *
73347
73664
  * Only called when `eventMediaBaseUrl()` is non-null (guard sits in
73348
73665
  * the callers), so the template literal is safe.
73666
+ *
73667
+ * The URL names the owner TABLE and the event's GENERATION (D482 + D694):
73668
+ * a bare `${base}/${id}` was both ambiguous across tables and unable to
73669
+ * survive a reused rowid. New stamps always carry `/o/<type>/<id>/g/…`.
73349
73670
  */
73350
- withMediaUrl(events) {
73671
+ withMediaUrl(events, ownerType) {
73351
73672
  const base = this.deps.eventMediaBaseUrl();
73352
73673
  if (base === null) return events;
73353
73674
  return events.map((e) => ({
73354
73675
  ...e,
73355
- mediaUrl: `${base}/${e.id}`
73676
+ mediaUrl: mediaOwnerUrl(base, ownerType, String(e.id), {
73677
+ deviceId: e.deviceId,
73678
+ recordAt: e.timestamp
73679
+ })
73356
73680
  }));
73357
73681
  }
73358
73682
  };
@@ -76184,18 +76508,26 @@ function eventMediaEtag(media) {
76184
76508
  }
76185
76509
  /** Digits only — `Number('')` is 0 and `Number(' ')` is 0, so neither is used. */
76186
76510
  var DIGITS = /^\d+$/;
76511
+ /** One run of digits, for composing the event-owner remainder pattern. */
76512
+ var DIGIT_RUN = "\\d+";
76513
+ /**
76514
+ * Event-owner remainder with an optional generation suffix (D694):
76515
+ * `<ownerId>` or `<ownerId>/g/<deviceId>/<recordAt>`. Owner ids for event
76516
+ * tables are decimal rowids; the generation segment is the literal `g`.
76517
+ */
76518
+ var EVENT_OWNER_REMAINDER = new RegExp(`^(${DIGIT_RUN})(?:/${MEDIA_GENERATION_SEGMENT}/(${DIGIT_RUN})/(${DIGIT_RUN}))?$`);
76187
76519
  /**
76188
76520
  * Read a plane path. `null` for anything that is not one of the two forms —
76189
76521
  * including the BARE id the plane used to accept, which is gone: a bare number
76190
76522
  * could be a media id or an event id, and after D474 an event id could be one
76191
76523
  * of three rows. Ambiguity at the door is what this record removes.
76192
76524
  *
76193
- * The owner id is taken as the whole remainder rather than one segment: the
76194
- * hub's wildcard param is percent-decoded by find-my-way before the sub-path
76195
- * is proxied, so a `%2F` an owner id carried would arrive here as a real
76196
- * slash, and re-cutting on it would silently address a different owner. It is
76197
- * only ever a DB query VALUE (`where: { ownerId }`) — never a filesystem path,
76198
- * which the row itself supplies.
76525
+ * For non-event owners the id is the whole remainder rather than one segment:
76526
+ * the hub's wildcard param is percent-decoded by find-my-way before the
76527
+ * sub-path is proxied, so a `%2F` an owner id carried would arrive here as a
76528
+ * real slash, and re-cutting on it would silently address a different owner.
76529
+ * Event owners (decimal rowids) may end with `/g/<deviceId>/<recordAt>` —
76530
+ * that suffix is the generation, not part of the id (D694).
76199
76531
  */
76200
76532
  function parseMediaAddress(rawPath) {
76201
76533
  const path = rawPath.replace(/^\/+/, "");
@@ -76215,13 +76547,35 @@ function parseMediaAddress(rawPath) {
76215
76547
  const typeSlash = rest.indexOf("/");
76216
76548
  if (typeSlash <= 0) return null;
76217
76549
  const ownerType = rest.slice(0, typeSlash);
76218
- const ownerId = rest.slice(typeSlash + 1);
76219
- if (ownerId.length === 0) return null;
76550
+ const remainder = rest.slice(typeSlash + 1);
76551
+ if (remainder.length === 0) return null;
76220
76552
  if (!isMediaOwnerType(ownerType)) return null;
76553
+ if (isEventOwnerType(ownerType)) {
76554
+ const match = EVENT_OWNER_REMAINDER.exec(remainder);
76555
+ if (match === null) return null;
76556
+ const ownerId = match[1];
76557
+ const deviceIdRaw = match[2];
76558
+ const recordAtRaw = match[3];
76559
+ if (ownerId === void 0) return null;
76560
+ if (deviceIdRaw !== void 0 && recordAtRaw !== void 0) return {
76561
+ kind: "owner",
76562
+ ownerType,
76563
+ ownerId,
76564
+ generation: {
76565
+ deviceId: Number(deviceIdRaw),
76566
+ recordAt: Number(recordAtRaw)
76567
+ }
76568
+ };
76569
+ return {
76570
+ kind: "owner",
76571
+ ownerType,
76572
+ ownerId
76573
+ };
76574
+ }
76221
76575
  return {
76222
76576
  kind: "owner",
76223
76577
  ownerType,
76224
- ownerId
76578
+ ownerId: remainder
76225
76579
  };
76226
76580
  }
76227
76581
  /** Set by the HUB for a `csv_*` share token admitted on this plane; never by a client. */
@@ -78898,6 +79252,218 @@ function createRetrainExportHandler(deps) {
78898
79252
  };
78899
79253
  }
78900
79254
  //#endregion
79255
+ //#region src/pipeline-analytics/retrain/retrain-collection-store.ts
79256
+ /**
79257
+ * @durable class=ledger owner=pipeline-analytics
79258
+ * write="one row per named training collection the operator creates. Names are
79259
+ * unique on nameKey (lower(trim)); empty names never become rows."
79260
+ * retention="none — operator-owned corpus metadata. Rows leave only when an
79261
+ * explicit delete API lands (not V1)."
79262
+ */
79263
+ var RETRAIN_COLLECTIONS_COLLECTION = "pipeline-analytics:retrain-collections";
79264
+ var RETRAIN_COLLECTION_COLUMNS = [
79265
+ {
79266
+ name: "id",
79267
+ type: "TEXT",
79268
+ primaryKey: true,
79269
+ notNull: true
79270
+ },
79271
+ {
79272
+ name: "name",
79273
+ type: "TEXT",
79274
+ notNull: true
79275
+ },
79276
+ {
79277
+ name: "nameKey",
79278
+ type: "TEXT",
79279
+ notNull: true
79280
+ },
79281
+ {
79282
+ name: "createdAt",
79283
+ type: "INTEGER",
79284
+ notNull: true
79285
+ },
79286
+ {
79287
+ name: "updatedAt",
79288
+ type: "INTEGER",
79289
+ notNull: true
79290
+ }
79291
+ ];
79292
+ var RETRAIN_COLLECTION_INDEXES = [{
79293
+ name: "idx_retrain_collections_name_key",
79294
+ columns: ["nameKey"],
79295
+ unique: true
79296
+ }, {
79297
+ name: "idx_retrain_collections_created",
79298
+ columns: ["createdAt"]
79299
+ }];
79300
+ function normalizeCollectionName(raw) {
79301
+ const name = raw.trim();
79302
+ if (name.length === 0) return null;
79303
+ return {
79304
+ name,
79305
+ nameKey: name.toLowerCase()
79306
+ };
79307
+ }
79308
+ var RetrainCollectionStore = class {
79309
+ store;
79310
+ logger;
79311
+ now;
79312
+ newId;
79313
+ countFrames;
79314
+ constructor(deps) {
79315
+ this.store = deps.store;
79316
+ this.logger = deps.logger;
79317
+ this.now = deps.now;
79318
+ this.newId = deps.newId;
79319
+ this.countFrames = deps.countFrames;
79320
+ }
79321
+ static async declare(store) {
79322
+ await store.declareCollection.mutate({
79323
+ collection: RETRAIN_COLLECTIONS_COLLECTION,
79324
+ columns: [...RETRAIN_COLLECTION_COLUMNS],
79325
+ indexes: [...RETRAIN_COLLECTION_INDEXES]
79326
+ });
79327
+ }
79328
+ async create(input) {
79329
+ const normalized = normalizeCollectionName(input.name);
79330
+ if (normalized === null) return {
79331
+ kind: "refused",
79332
+ reason: "empty-name"
79333
+ };
79334
+ if (await this.findByNameKey(normalized.nameKey) !== null) return {
79335
+ kind: "refused",
79336
+ reason: "duplicate-name"
79337
+ };
79338
+ const createdAt = this.now();
79339
+ const id = this.newId();
79340
+ await this.store.set.mutate({
79341
+ collection: RETRAIN_COLLECTIONS_COLLECTION,
79342
+ key: id,
79343
+ value: {
79344
+ name: normalized.name,
79345
+ nameKey: normalized.nameKey,
79346
+ createdAt,
79347
+ updatedAt: createdAt
79348
+ }
79349
+ });
79350
+ return {
79351
+ kind: "created",
79352
+ collection: {
79353
+ id,
79354
+ name: normalized.name,
79355
+ createdAt,
79356
+ frameCount: 0
79357
+ }
79358
+ };
79359
+ }
79360
+ async list() {
79361
+ let rows;
79362
+ try {
79363
+ rows = await this.store.query.query({
79364
+ collection: RETRAIN_COLLECTIONS_COLLECTION,
79365
+ filter: { orderBy: {
79366
+ field: "createdAt",
79367
+ direction: "desc"
79368
+ } }
79369
+ });
79370
+ } catch (err) {
79371
+ this.logger.warn("retrain collections list failed", { meta: { error: String(err) } });
79372
+ return [];
79373
+ }
79374
+ const out = [];
79375
+ for (const row of rows) {
79376
+ const name = typeof row.data["name"] === "string" ? row.data["name"] : "";
79377
+ const createdAt = Number(row.data["createdAt"] ?? 0);
79378
+ const frameCount = this.countFrames !== void 0 ? await this.countFrames(row.id) : 0;
79379
+ out.push({
79380
+ id: row.id,
79381
+ name,
79382
+ createdAt,
79383
+ frameCount
79384
+ });
79385
+ }
79386
+ return out;
79387
+ }
79388
+ async getById(id) {
79389
+ try {
79390
+ const row = await this.store.get.query({
79391
+ collection: RETRAIN_COLLECTIONS_COLLECTION,
79392
+ key: id
79393
+ });
79394
+ if (row === null || row === void 0) return null;
79395
+ const data = row;
79396
+ return {
79397
+ id,
79398
+ name: typeof data["name"] === "string" ? data["name"] : "",
79399
+ createdAt: Number(data["createdAt"] ?? 0),
79400
+ frameCount: this.countFrames !== void 0 ? await this.countFrames(id) : 0
79401
+ };
79402
+ } catch (err) {
79403
+ this.logger.warn("retrain collection get failed", { meta: {
79404
+ collectionId: id,
79405
+ error: String(err)
79406
+ } });
79407
+ return null;
79408
+ }
79409
+ }
79410
+ /**
79411
+ * Stub until still ingest lands. Validates the collection exists, then
79412
+ * refuses with a named reason so callers can wire UI against a stable shape.
79413
+ */
79414
+ async addRetrainFrameFromRecording(input) {
79415
+ if (await this.getById(input.collectionId) === null) {
79416
+ this.logger.info("retrain collection addFrame refused: unknown-collection", {
79417
+ tags: { deviceId: input.deviceId },
79418
+ meta: {
79419
+ collectionId: input.collectionId,
79420
+ epochMs: input.epochMs
79421
+ }
79422
+ });
79423
+ return {
79424
+ kind: "refused",
79425
+ reason: "unknown-collection"
79426
+ };
79427
+ }
79428
+ this.logger.info("retrain collection addFrame stub: still-capture-unimplemented", {
79429
+ tags: { deviceId: input.deviceId },
79430
+ meta: {
79431
+ collectionId: input.collectionId,
79432
+ epochMs: input.epochMs
79433
+ }
79434
+ });
79435
+ return {
79436
+ kind: "refused",
79437
+ reason: "still-capture-unimplemented",
79438
+ detail: "Still ingest lands in plan Task 3"
79439
+ };
79440
+ }
79441
+ async findByNameKey(nameKey) {
79442
+ try {
79443
+ const row = (await this.store.query.query({
79444
+ collection: RETRAIN_COLLECTIONS_COLLECTION,
79445
+ filter: {
79446
+ where: { nameKey },
79447
+ limit: 1
79448
+ }
79449
+ }))[0];
79450
+ if (row === void 0) return null;
79451
+ return {
79452
+ id: row.id,
79453
+ name: typeof row.data["name"] === "string" ? row.data["name"] : "",
79454
+ createdAt: Number(row.data["createdAt"] ?? 0),
79455
+ frameCount: 0
79456
+ };
79457
+ } catch (err) {
79458
+ this.logger.warn("retrain collection findByNameKey failed", { meta: {
79459
+ nameKey,
79460
+ error: String(err)
79461
+ } });
79462
+ return null;
79463
+ }
79464
+ }
79465
+ };
79466
+ //#endregion
78901
79467
  //#region src/pipeline-analytics/retrain/retrain-assist.ts
78902
79468
  /** The whole frame, as a normalised rectangle. */
78903
79469
  var WHOLE_FRAME = {
@@ -82307,7 +82873,8 @@ async function rankKeyEvents(candidates, options, peakLookup) {
82307
82873
  ...t.markForTrain !== void 0 ? { markForTrain: t.markForTrain } : {},
82308
82874
  ...t.retrainStatus !== void 0 ? { retrainStatus: t.retrainStatus } : {},
82309
82875
  ...t.debug !== void 0 ? { debug: t.debug } : {},
82310
- ...t.favourited !== void 0 ? { favourited: t.favourited } : {}
82876
+ ...t.favourited !== void 0 ? { favourited: t.favourited } : {},
82877
+ ...t.audioLabels !== void 0 && t.audioLabels.length > 0 ? { audioLabels: t.audioLabels } : {}
82311
82878
  });
82312
82879
  }
82313
82880
  scored.sort((a, b) => b.importance - a.importance);
@@ -82734,16 +83301,19 @@ var MotionOnsetRegistry = class {
82734
83301
  * - `resolveSearchThumbnailUrl` — the search-hit thumbnail resolver. It points
82735
83302
  * a hit at the CLIP embedding row's OWN crop (the crop captured on the
82736
83303
  * embedding's peak frame → the hit thumbnail IS the embedded crop), and only
82737
- * falls back to the representative event's crop when the embedding row has no
82738
- * crop yet, so a hit never renders blank.
83304
+ * falls back to the representative event's owner ladder when the embedding
83305
+ * row has no crop yet, so a hit never renders blank.
82739
83306
  */
82740
83307
  function isClipObjectEmbedding(t, registry = BUILTIN_CLIP_MODELS) {
82741
83308
  return Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && registry.resolve(t.embeddingModelId) !== null;
82742
83309
  }
82743
83310
  function resolveSearchThumbnailUrl(input) {
82744
83311
  if (input.baseUrl === null) return void 0;
82745
- const id = input.embeddingMediaKey ?? input.eventId;
82746
- return `${input.baseUrl}/${encodeURIComponent(id)}`;
83312
+ if (input.embeddingMediaId !== void 0) return mediaFileUrl(input.baseUrl, input.embeddingMediaId);
83313
+ return mediaOwnerUrl(input.baseUrl, "object", input.eventId, {
83314
+ deviceId: input.deviceId,
83315
+ recordAt: input.recordAt
83316
+ });
82747
83317
  }
82748
83318
  //#endregion
82749
83319
  //#region src/pipeline-analytics/pipeline/onboard-birth-authority.ts
@@ -100903,7 +101473,10 @@ function ofGeneration(files, scope) {
100903
101473
  function makeNcOwnerMediaReader(deps) {
100904
101474
  return async (ownerKind, ownerId, eventScope) => {
100905
101475
  if (ownerKind === "track") return deps.media.listByOwner("track", await deps.resolveTrackOwnerId(ownerId));
100906
- if (eventScope === void 0) return deps.media.listByOwner("object", ownerId);
101476
+ if (eventScope === void 0) {
101477
+ deps.logger.error("object media read refused — NcEventMediaScope required", { meta: { ownerId } });
101478
+ return [];
101479
+ }
100907
101480
  const onCamera = await deps.media.listByOwner("object", ownerId, eventScope.deviceId);
100908
101481
  const kept = ofGeneration(onCamera, eventScope);
100909
101482
  if (kept.length < onCamera.length) deps.logger.warn("event media of another record on a reused event id — not used", {
@@ -101996,6 +102569,10 @@ var NC_ARTIFACT_PRUNE_INTERVAL_MS = 36e5;
101996
102569
  * (every camera switched off): the bound is 24 h, not "24 h after the next
101997
102570
  * close" (D681). */
101998
102571
  var TAPE_SWEEP_INTERVAL_MS = 36e5;
102572
+ /** How often open tape segments push their pending gzip output to disk
102573
+ * without rotating (D681 follow-up): a silent camera otherwise keeps its
102574
+ * tail only in the zlib buffer until the next frame or the 10-minute cut. */
102575
+ var TAPE_FLUSH_INTERVAL_MS = 6e4;
101999
102576
  /** What a publish answers on a node whose artifact plane never came up: no
102000
102577
  * link, no index row — and, exactly as before, bytes-only delivery. */
102001
102578
  var NO_PUBLICATION = {
@@ -102529,6 +103106,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
102529
103106
  ncArtifactReadDataPlane = null;
102530
103107
  ncArtifactPruneTimer = null;
102531
103108
  tapeSweepTimer = null;
103109
+ tapeFlushTimer = null;
102532
103110
  /** Why `startTrackingTapes` threw on a node that was allowed to record —
102533
103111
  * reported `failing` per switched-on camera, never as `recording`. */
102534
103112
  tapeStartFailure = null;
@@ -104333,6 +104911,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
104333
104911
  StationaryObjectRegistry.declare(api.settingsStore),
104334
104912
  RetrainFrameStore.declare(api.settingsStore),
104335
104913
  RetrainAnnotationStore.declare(api.settingsStore),
104914
+ RetrainCollectionStore.declare(api.settingsStore),
104336
104915
  OpsLogStore.declare(api.settingsStore),
104337
104916
  DebugNoteArchiveStore.declare(api.settingsStore),
104338
104917
  TrackingTapeIndexStore.declare(api.settingsStore),
@@ -106781,6 +107360,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
106781
107360
  clearInterval(this.tapeSweepTimer);
106782
107361
  this.tapeSweepTimer = null;
106783
107362
  }
107363
+ if (this.tapeFlushTimer !== null) {
107364
+ clearInterval(this.tapeFlushTimer);
107365
+ this.tapeFlushTimer = null;
107366
+ }
106784
107367
  if (this.ncArtifactPruneTimer !== null) {
106785
107368
  clearInterval(this.ncArtifactPruneTimer);
106786
107369
  this.ncArtifactPruneTimer = null;
@@ -113554,10 +114137,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
113554
114137
  });
113555
114138
  const scored = [];
113556
114139
  for (const { event, score } of bestEventByTrackId.values()) {
114140
+ const embeddingMediaId = event.trackId !== void 0 ? embeddingMediaIdByTrackId.get(event.trackId) : void 0;
113557
114141
  const mediaUrl = resolveSearchThumbnailUrl({
113558
114142
  baseUrl: this.eventMediaBaseUrl,
113559
114143
  eventId: String(event.id),
113560
- ...event.trackId !== void 0 && embeddingMediaIdByTrackId.has(event.trackId) ? { embeddingMediaId: embeddingMediaIdByTrackId.get(event.trackId) } : {}
114144
+ deviceId: event.deviceId,
114145
+ recordAt: event.timestamp,
114146
+ ...embeddingMediaId !== void 0 ? { embeddingMediaId } : {}
113561
114147
  });
113562
114148
  const keyFrameMediaId = event.trackId !== void 0 ? keyFrameMediaIdByTrackId.get(event.trackId) : void 0;
113563
114149
  scored.push({
@@ -113944,7 +114530,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
113944
114530
  logger
113945
114531
  });
113946
114532
  const sweep = (deviceId) => {
113947
- shelf.sweep().catch((err) => {
114533
+ shelf.sweep().then((report) => {
114534
+ logTapeSweepReport(report, logger);
114535
+ }).catch((err) => {
113948
114536
  logger.warn("tracking tape sweep failed", {
113949
114537
  ...deviceId !== null ? { tags: { deviceId } } : {},
113950
114538
  meta: { error: errMsg(err) }
@@ -113976,6 +114564,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
113976
114564
  sweep(null);
113977
114565
  this.tapeSweepTimer = setInterval(() => sweep(null), TAPE_SWEEP_INTERVAL_MS);
113978
114566
  this.tapeSweepTimer.unref?.();
114567
+ this.tapeFlushTimer = setInterval(() => {
114568
+ this.tapeRecorder?.flushOpenSegments();
114569
+ }, TAPE_FLUSH_INTERVAL_MS);
114570
+ this.tapeFlushTimer.unref?.();
113979
114571
  this.ctx.logger.info("tracking tapes ready", { meta: { seededProcessors: this.processorInputs.size } });
113980
114572
  } catch (err) {
113981
114573
  this.tapeStartFailure = errMsg(err);
@@ -114168,7 +114760,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
114168
114760
  segments.set(deviceId, await this.tapeListingOf(shelf, deviceId));
114169
114761
  }
114170
114762
  const recorder = this.tapeRecorder;
114171
- return { cameras: describeTapeStatus({
114763
+ const cameras = describeTapeStatus({
114172
114764
  deviceIds,
114173
114765
  switchOn,
114174
114766
  switchUnreadable,
@@ -114177,7 +114769,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
114177
114769
  startFailure: this.tapeStartFailure,
114178
114770
  segments,
114179
114771
  now: Date.now()
114180
- }) };
114772
+ });
114773
+ return { cameras: input.deviceIds === void 0 ? defaultTapeStatusCameras(cameras) : cameras };
114181
114774
  },
114182
114775
  "debug.tape.promote": async (input) => {
114183
114776
  const deps = this.tapePromotionDeps();
@@ -114626,7 +115219,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
114626
115219
  * owner table, so the motion/object coin-flip is gone from here too.
114627
115220
  */
114628
115221
  async readMediaByAddress(address, variant, preferKind) {
114629
- const base = address.kind === "row" ? await this.readMediaById(address.mediaId) : await this.readOwnerThumbnail(address.ownerType, address.ownerId, preferKind);
115222
+ const base = address.kind === "row" ? await this.readMediaById(address.mediaId) : await this.readOwnerThumbnail(address.ownerType, address.ownerId, preferKind, address.generation);
114630
115223
  if (base === null || variant === void 0) return base;
114631
115224
  return this.applyThumbVariant(base, variant);
114632
115225
  }
@@ -114684,7 +115277,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
114684
115277
  * ~144 MB and allocated ~190 MB of base64 to deliver ~1.2 MB, all of it on
114685
115278
  * the single-threaded post-analysis runner.
114686
115279
  */
114687
- async readOwnerThumbnail(ownerType, rawOwnerId, preferKind) {
115280
+ async readOwnerThumbnail(ownerType, rawOwnerId, preferKind, addressGeneration) {
114688
115281
  const ownerId = ownerType === "track" ? await resolveMediaOwnerId(ownerType, rawOwnerId, (id) => this.trackAliasStore?.resolve(id) ?? Promise.resolve(null), (failure) => this.aliasResolveFailureLogger?.(failure), "readOwnerThumbnail") : rawOwnerId;
114689
115282
  const listInfo = (ownerKind, id, deviceId) => this.mediaStore?.listInfoByOwner(ownerKind, id, deviceId) ?? Promise.resolve([]);
114690
115283
  const getTrackIdForEvent = (eventId) => isEventOwnerType(ownerType) ? this.eventStore?.getTrackIdForEvent(eventId, ownerType) ?? Promise.resolve(null) : Promise.resolve(null);
@@ -114698,8 +115291,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
114698
115291
  };
114699
115292
  };
114700
115293
  let ownFiles = await listInfo(ownerType, ownerId);
114701
- if (isEventOwnerType(ownerType) && this.eventStore !== null) {
114702
- const gen = await this.eventStore.eventGenerationOf(ownerType, ownerId);
115294
+ if (isEventOwnerType(ownerType)) {
115295
+ const gen = addressGeneration !== void 0 ? {
115296
+ deviceId: addressGeneration.deviceId,
115297
+ timestamp: addressGeneration.recordAt
115298
+ } : this.eventStore !== null ? await this.eventStore.eventGenerationOf(ownerType, ownerId) : null;
114703
115299
  if (gen !== null) {
114704
115300
  const onCamera = await listInfo(ownerType, ownerId, gen.deviceId);
114705
115301
  const kept = onCamera.filter((f) => f.timestamp === gen.timestamp);
@@ -114711,7 +115307,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
114711
115307
  recordAt: gen.timestamp,
114712
115308
  refused: onCamera.length - kept.length,
114713
115309
  kept: kept.length,
114714
- via: "readOwnerThumbnail"
115310
+ via: "readOwnerThumbnail",
115311
+ scopedBy: addressGeneration !== void 0 ? "url" : "live-row"
114715
115312
  }
114716
115313
  });
114717
115314
  ownFiles = kept;