@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.
- package/dist/{clip-model-registry-Dkv8MUCO.mjs → clip-model-registry-C-IEAIQQ.mjs} +232 -51
- package/dist/{clip-model-registry-D-zzCBKK.js → clip-model-registry-C_aKxHpy.js} +261 -50
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- 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
- 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
- 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
- package/dist/pipeline-analytics/{hostInit-Cpm1aEUE.mjs → hostInit-ByDyL2zi.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +692 -95
- package/dist/pipeline-analytics/index.mjs +692 -95
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BAqKeExF.mjs +0 -26
- 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
|
|
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
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
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
|
-
*
|
|
673
|
-
*
|
|
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
|
|
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
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
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
|
|
1095
|
-
* about the opening — a reed switch on the frame says the window
|
|
1096
|
-
* when the motor's last reported word says `closed`.
|
|
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
|
|
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:
|
|
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 :
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
19992
|
-
const
|
|
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
|
-
|
|
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(
|
|
20162
|
+
dropped: deviceSubjects([...new Set(deadIds)]),
|
|
19997
20163
|
droppedKind: "device",
|
|
19998
|
-
...
|
|
19999
|
-
|
|
20000
|
-
|
|
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:
|
|
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
|
-
*
|
|
76194
|
-
* hub's wildcard param is percent-decoded by find-my-way before the
|
|
76195
|
-
* is proxied, so a `%2F` an owner id carried would arrive here as a
|
|
76196
|
-
* slash, and re-cutting on it would silently address a different owner.
|
|
76197
|
-
*
|
|
76198
|
-
*
|
|
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
|
|
76219
|
-
if (
|
|
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
|
|
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
|
-
|
|
82746
|
-
return
|
|
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)
|
|
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
|
-
|
|
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().
|
|
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
|
-
|
|
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)
|
|
114702
|
-
const gen =
|
|
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;
|