@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
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_clip_model_registry = require("../clip-model-registry-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-C_aKxHpy.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_clip_model_registry.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -604,7 +604,19 @@ var ARM_ANYWAY_TTL_MS = 5 * 6e4;
|
|
|
604
604
|
var NcAlarmOpenDeviceSchema = require_clip_model_registry.object({
|
|
605
605
|
deviceId: require_clip_model_registry.number().int(),
|
|
606
606
|
name: require_clip_model_registry.string(),
|
|
607
|
-
state: require_clip_model_registry.string()
|
|
607
|
+
state: require_clip_model_registry.string(),
|
|
608
|
+
/**
|
|
609
|
+
* Cover / lock / switch — default false so an older bridge answer still
|
|
610
|
+
* parses as a contact-style opening.
|
|
611
|
+
*/
|
|
612
|
+
controllable: require_clip_model_registry.boolean().default(false),
|
|
613
|
+
/** Opening cap that produced the reading; used to actuate secure-on-arm. */
|
|
614
|
+
cap: require_clip_model_registry._enum([
|
|
615
|
+
"contact",
|
|
616
|
+
"cover",
|
|
617
|
+
"lock-control",
|
|
618
|
+
"switch"
|
|
619
|
+
]).optional()
|
|
608
620
|
}).readonly();
|
|
609
621
|
/** Why an Arm anyway was not honoured — see `NcAlarmPanelDevice.armAnyway`. */
|
|
610
622
|
var NcAlarmArmAnywayRejectionSchema = require_clip_model_registry._enum([
|
|
@@ -669,23 +681,22 @@ function alarmCoveredDevices(rules, panelDeviceId, mode) {
|
|
|
669
681
|
/**
|
|
670
682
|
* Refuse or arm — the whole decision, as one partition of the open devices.
|
|
671
683
|
*
|
|
672
|
-
* A device may be open at arm time
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
* confirming: they were never shown it, and excluding it would arm the alarm
|
|
677
|
-
* around an opening nobody agreed to.
|
|
684
|
+
* A device may be open at arm time when:
|
|
685
|
+
* - the mode TOLERATES it (`nonBlocking` for contacts),
|
|
686
|
+
* - it is controllable and NOT on the mode's `blocking` list (default),
|
|
687
|
+
* - or the operator confirmed it with Arm anyway (`acknowledged`).
|
|
678
688
|
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
* Arm anyway, whose lifecycle (one-shot, stale, expired) the panel owns.
|
|
689
|
+
* Anything else open refuses — including a device that opened AFTER the refusal
|
|
690
|
+
* the operator is confirming.
|
|
682
691
|
*
|
|
683
692
|
* Excluded is EVERY open device — tolerated and acknowledged alike — and the
|
|
684
693
|
* exclusion is two-phase (`alarm-exclusions.ts`): each one is watched again
|
|
685
694
|
* once it has closed and opened.
|
|
686
695
|
*/
|
|
687
696
|
function decideArm(input) {
|
|
697
|
+
const forceBlock = new Set(input.blocking ?? []);
|
|
688
698
|
const tolerated = new Set([...input.nonBlocking, ...input.acknowledged ?? []]);
|
|
699
|
+
for (const d of input.open) if (d.controllable && !forceBlock.has(d.deviceId)) tolerated.add(d.deviceId);
|
|
689
700
|
const blocking = input.open.filter((d) => !tolerated.has(d.deviceId));
|
|
690
701
|
if (blocking.length > 0) return {
|
|
691
702
|
kind: "refused",
|
|
@@ -1073,39 +1084,24 @@ var NcActionPlane = class {
|
|
|
1073
1084
|
* and never open):
|
|
1074
1085
|
*
|
|
1075
1086
|
* - `not-opening` — no opening slice at all: a camera named by an intrusion
|
|
1076
|
-
* rule
|
|
1087
|
+
* rule. It takes no part in the arm decision.
|
|
1077
1088
|
* - `unknown` — an opening cap's slice IS there, but its field is missing or
|
|
1078
1089
|
* is not a word that cap defines, or the slice is a provider's PLACEHOLDER
|
|
1079
|
-
* (`lastChangedAt === 0
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
* saw. It never blocks and is never armed as secure on a guess — and a
|
|
1084
|
-
* placeholder is never taken for a state, so neither the reconcile tick,
|
|
1085
|
-
* the arm check nor an exclusion lift acts on one.
|
|
1086
|
-
* - `reading` — open or secure, with the state word a person will read.
|
|
1087
|
-
* ANY non-zero `lastChangedAt` is a reading, however old — including `1`,
|
|
1088
|
-
* the stamp a provider writes on a value it REPLAYED after a respawn with
|
|
1089
|
-
* no upstream change time (Homematic `HM_SEEDED_CHANGED_AT`): the value is
|
|
1090
|
-
* known, only its change time is not. The transition gate
|
|
1091
|
-
* (`sensor-transition-gate.ts`, "A placeholder slice is not a state") reads
|
|
1092
|
-
* the same stamp as "not a fresh event"; `0` alone means "not read yet".
|
|
1090
|
+
* (`lastChangedAt === 0`). It never blocks and is never armed as secure on a
|
|
1091
|
+
* guess.
|
|
1092
|
+
* - `reading` — open or secure, with the state word a person will read and the
|
|
1093
|
+
* cap that produced it (so secure-on-arm knows what to actuate).
|
|
1093
1094
|
*
|
|
1094
1095
|
* A read that FAILED never reaches {@link alarmOpeningFrom}: the caller leaves
|
|
1095
1096
|
* that id out of its answer (see {@link readAlarmOpenings}).
|
|
1096
1097
|
*
|
|
1097
1098
|
* Anything short of fully secure is open: a cover stopped half-way, a lock that
|
|
1098
|
-
* jammed, a cover still closing when the arm is decided.
|
|
1099
|
+
* jammed, a cover still closing when the arm is decided, a switch that is on.
|
|
1099
1100
|
*
|
|
1100
1101
|
* PRECEDENCE, when one device carries more than one opening slice: `contact`
|
|
1101
|
-
* first, then `cover`, then `lock-control`. The contact is the
|
|
1102
|
-
* about the opening — a reed switch on the frame says the window
|
|
1103
|
-
* when the motor's last reported word says `closed`.
|
|
1104
|
-
* read falls through to the next; only when none of them reads is the device
|
|
1105
|
-
* `unknown`, naming the first unreadable cap.
|
|
1106
|
-
*
|
|
1107
|
-
* The vocabulary of each word is its cap's own schema (`CoverStateSchema`,
|
|
1108
|
-
* `LockStateSchema`) — a word the cap does not define is not guessed at.
|
|
1102
|
+
* first, then `cover`, then `lock-control`, then `switch`. The contact is the
|
|
1103
|
+
* physical truth about the opening — a reed switch on the frame says the window
|
|
1104
|
+
* is open even when the motor's last reported word says `closed`.
|
|
1109
1105
|
*/
|
|
1110
1106
|
/** The one state word, per cap, that counts as secure. Everything else is open. */
|
|
1111
1107
|
var COVER_SECURE = "closed";
|
|
@@ -1124,11 +1120,12 @@ function fieldOf(slice, field) {
|
|
|
1124
1120
|
function isPlaceholder(slice) {
|
|
1125
1121
|
return fieldOf(slice, "lastChangedAt") === 0;
|
|
1126
1122
|
}
|
|
1127
|
-
function reading(word, secure) {
|
|
1123
|
+
function reading(word, secure, cap) {
|
|
1128
1124
|
return {
|
|
1129
1125
|
kind: "reading",
|
|
1130
1126
|
reading: word === secure ? "secure" : "open",
|
|
1131
|
-
state: word
|
|
1127
|
+
state: word,
|
|
1128
|
+
cap
|
|
1132
1129
|
};
|
|
1133
1130
|
}
|
|
1134
1131
|
function readContact(runtime) {
|
|
@@ -1148,11 +1145,13 @@ function readContact(runtime) {
|
|
|
1148
1145
|
return raw ? {
|
|
1149
1146
|
kind: "reading",
|
|
1150
1147
|
reading: "open",
|
|
1151
|
-
state: "open"
|
|
1148
|
+
state: "open",
|
|
1149
|
+
cap: "contact"
|
|
1152
1150
|
} : {
|
|
1153
1151
|
kind: "reading",
|
|
1154
1152
|
reading: "secure",
|
|
1155
|
-
state: "closed"
|
|
1153
|
+
state: "closed",
|
|
1154
|
+
cap: "contact"
|
|
1156
1155
|
};
|
|
1157
1156
|
}
|
|
1158
1157
|
function readCover(runtime) {
|
|
@@ -1165,7 +1164,7 @@ function readCover(runtime) {
|
|
|
1165
1164
|
};
|
|
1166
1165
|
const raw = fieldOf(slice, "state");
|
|
1167
1166
|
const parsed = require_clip_model_registry.CoverStateSchema.safeParse(raw);
|
|
1168
|
-
return parsed.success ? reading(parsed.data, COVER_SECURE) : {
|
|
1167
|
+
return parsed.success ? reading(parsed.data, COVER_SECURE, "cover") : {
|
|
1169
1168
|
kind: "unknown",
|
|
1170
1169
|
cap: "cover",
|
|
1171
1170
|
raw
|
|
@@ -1181,19 +1180,46 @@ function readLock(runtime) {
|
|
|
1181
1180
|
};
|
|
1182
1181
|
const raw = fieldOf(slice, "state");
|
|
1183
1182
|
const parsed = require_clip_model_registry.LockStateSchema.safeParse(raw);
|
|
1184
|
-
return parsed.success ? reading(parsed.data, LOCK_SECURE) : {
|
|
1183
|
+
return parsed.success ? reading(parsed.data, LOCK_SECURE, "lock-control") : {
|
|
1185
1184
|
kind: "unknown",
|
|
1186
1185
|
cap: "lock-control",
|
|
1187
1186
|
raw
|
|
1188
1187
|
};
|
|
1189
1188
|
}
|
|
1189
|
+
function readSwitch(runtime) {
|
|
1190
|
+
const slice = sliceOf(runtime, "switch");
|
|
1191
|
+
if (slice === void 0) return { kind: "absent" };
|
|
1192
|
+
if (isPlaceholder(slice)) return {
|
|
1193
|
+
kind: "unknown",
|
|
1194
|
+
cap: "switch",
|
|
1195
|
+
raw: slice
|
|
1196
|
+
};
|
|
1197
|
+
const raw = fieldOf(slice, "on");
|
|
1198
|
+
if (typeof raw !== "boolean") return {
|
|
1199
|
+
kind: "unknown",
|
|
1200
|
+
cap: "switch",
|
|
1201
|
+
raw
|
|
1202
|
+
};
|
|
1203
|
+
return raw ? {
|
|
1204
|
+
kind: "reading",
|
|
1205
|
+
reading: "open",
|
|
1206
|
+
state: "on",
|
|
1207
|
+
cap: "switch"
|
|
1208
|
+
} : {
|
|
1209
|
+
kind: "reading",
|
|
1210
|
+
reading: "secure",
|
|
1211
|
+
state: "off",
|
|
1212
|
+
cap: "switch"
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1190
1215
|
/** D630's table over one device's runtime slices — see the header for the three answers. */
|
|
1191
1216
|
function alarmOpeningFrom(runtime) {
|
|
1192
1217
|
let firstUnknown = null;
|
|
1193
1218
|
for (const read of [
|
|
1194
1219
|
readContact(runtime),
|
|
1195
1220
|
readCover(runtime),
|
|
1196
|
-
readLock(runtime)
|
|
1221
|
+
readLock(runtime),
|
|
1222
|
+
readSwitch(runtime)
|
|
1197
1223
|
]) {
|
|
1198
1224
|
if (read.kind === "reading") return read;
|
|
1199
1225
|
if (read.kind === "unknown" && firstUnknown === null) firstUnknown = read;
|
|
@@ -1208,13 +1234,14 @@ function alarmOpeningFrom(runtime) {
|
|
|
1208
1234
|
var OPENING_CAP_BY_EVENT_KIND = {
|
|
1209
1235
|
contact: "contact",
|
|
1210
1236
|
cover: "cover",
|
|
1211
|
-
lock: "lock-control"
|
|
1237
|
+
lock: "lock-control",
|
|
1238
|
+
switch: "switch"
|
|
1212
1239
|
};
|
|
1213
1240
|
/**
|
|
1214
1241
|
* The same table, read off one sensor-event row: its `value` IS the slice of
|
|
1215
1242
|
* the cap its `kind` names, so the event path and the reconcile read answer in
|
|
1216
1243
|
* one vocabulary — a cover is secure only when `closed`, a lock only when
|
|
1217
|
-
* `locked`, whichever path saw it.
|
|
1244
|
+
* `locked`, a switch only when off, whichever path saw it.
|
|
1218
1245
|
*/
|
|
1219
1246
|
function alarmOpeningFromSensorEvent(kind, value) {
|
|
1220
1247
|
const cap = OPENING_CAP_BY_EVENT_KIND[kind];
|
|
@@ -1235,7 +1262,9 @@ function classifyCoveredOpening(opening) {
|
|
|
1235
1262
|
if (opening.kind === "not-opening" || opening.reading !== "open") return { kind: "skip" };
|
|
1236
1263
|
return {
|
|
1237
1264
|
kind: "open",
|
|
1238
|
-
state: opening.state
|
|
1265
|
+
state: opening.state,
|
|
1266
|
+
controllable: require_clip_model_registry.isAlarmControllableCap(opening.cap),
|
|
1267
|
+
cap: opening.cap
|
|
1239
1268
|
};
|
|
1240
1269
|
}
|
|
1241
1270
|
/**
|
|
@@ -1892,6 +1921,22 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
|
|
|
1892
1921
|
deviceIds: require_clip_model_registry.array(require_clip_model_registry.number().int())
|
|
1893
1922
|
})).default([]),
|
|
1894
1923
|
/**
|
|
1924
|
+
* Controllable openings that DO block when open (opt-in). Loose + guarded like
|
|
1925
|
+
* {@link nonBlocking}.
|
|
1926
|
+
*/
|
|
1927
|
+
blocking: require_clip_model_registry.array(require_clip_model_registry.object({
|
|
1928
|
+
mode: require_clip_model_registry.string(),
|
|
1929
|
+
deviceIds: require_clip_model_registry.array(require_clip_model_registry.number().int())
|
|
1930
|
+
})).default([]),
|
|
1931
|
+
/**
|
|
1932
|
+
* Controllable openings that must NOT secure on arm (opt-out). Loose + guarded
|
|
1933
|
+
* like {@link nonBlocking}.
|
|
1934
|
+
*/
|
|
1935
|
+
secureOnArmOff: require_clip_model_registry.array(require_clip_model_registry.object({
|
|
1936
|
+
mode: require_clip_model_registry.string(),
|
|
1937
|
+
deviceIds: require_clip_model_registry.array(require_clip_model_registry.number().int())
|
|
1938
|
+
})).default([]),
|
|
1939
|
+
/**
|
|
1895
1940
|
* Per arm mode, the delays that override the global ones (D697). Loose on purpose — `mode` is
|
|
1896
1941
|
* a string and the numbers are unbounded here — and read back through
|
|
1897
1942
|
* {@link modeDelaysFromPersisted}, never a cast: an entry this release cannot trust is dropped
|
|
@@ -1910,7 +1955,7 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
|
|
|
1910
1955
|
* per mode kept — the same "at most one list per mode" the settings schema
|
|
1911
1956
|
* enforces at the boundary. A type guard (`safeParse`), never a cast.
|
|
1912
1957
|
*/
|
|
1913
|
-
function
|
|
1958
|
+
function modeDeviceListFromPersisted(raw) {
|
|
1914
1959
|
const seen = /* @__PURE__ */ new Set();
|
|
1915
1960
|
const lists = [];
|
|
1916
1961
|
for (const entry of raw) {
|
|
@@ -2015,7 +2060,9 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2015
2060
|
...this.readDelays(),
|
|
2016
2061
|
announceArm: this.config.get("announceArm"),
|
|
2017
2062
|
announceTargets: [...this.config.get("announceTargets")],
|
|
2018
|
-
nonBlocking:
|
|
2063
|
+
nonBlocking: modeDeviceListFromPersisted(this.config.get("nonBlocking")),
|
|
2064
|
+
blocking: modeDeviceListFromPersisted(this.config.get("blocking")),
|
|
2065
|
+
secureOnArmOff: modeDeviceListFromPersisted(this.config.get("secureOnArmOff")),
|
|
2019
2066
|
modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
|
|
2020
2067
|
};
|
|
2021
2068
|
}
|
|
@@ -2042,30 +2089,73 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2042
2089
|
async applySettings(patch) {
|
|
2043
2090
|
const edits = patch.modeDelays ?? [];
|
|
2044
2091
|
const membership = patch.nonBlockingEdit;
|
|
2092
|
+
const blockingEdit = patch.blockingEdit;
|
|
2093
|
+
const secureEdit = patch.secureOnArmEdit;
|
|
2045
2094
|
for (const edit of edits) this.refuseUnofferedMode(edit.mode);
|
|
2046
2095
|
if (membership !== void 0) {
|
|
2047
2096
|
this.refuseUnofferedMode(membership.mode, membership.deviceId);
|
|
2048
|
-
const verdict = require_clip_model_registry.applyNonBlockingEdit(patch.nonBlocking !== void 0 ? patch.nonBlocking :
|
|
2097
|
+
const verdict = require_clip_model_registry.applyNonBlockingEdit(patch.nonBlocking !== void 0 ? patch.nonBlocking : modeDeviceListFromPersisted(this.config.get("nonBlocking")), membership);
|
|
2049
2098
|
if (verdict.kind === "refused") {
|
|
2050
2099
|
this.ctx.logger.info("alarm settings refused", {
|
|
2051
2100
|
tags: { deviceId: membership.deviceId },
|
|
2052
2101
|
meta: {
|
|
2053
2102
|
panelId: this.id,
|
|
2054
2103
|
reason: "list-full",
|
|
2055
|
-
mode: verdict.mode
|
|
2104
|
+
mode: verdict.mode,
|
|
2105
|
+
list: "nonBlocking"
|
|
2056
2106
|
}
|
|
2057
2107
|
});
|
|
2058
2108
|
throw new Error(`the ${verdict.mode} non-blocking list already holds ${verdict.max} devices — remove one first`);
|
|
2059
2109
|
}
|
|
2060
2110
|
}
|
|
2111
|
+
if (blockingEdit !== void 0) {
|
|
2112
|
+
this.refuseUnofferedMode(blockingEdit.mode, blockingEdit.deviceId);
|
|
2113
|
+
const verdict = require_clip_model_registry.applyBlockingEdit(patch.blocking !== void 0 ? patch.blocking : modeDeviceListFromPersisted(this.config.get("blocking")), blockingEdit);
|
|
2114
|
+
if (verdict.kind === "refused") {
|
|
2115
|
+
this.ctx.logger.info("alarm settings refused", {
|
|
2116
|
+
tags: { deviceId: blockingEdit.deviceId },
|
|
2117
|
+
meta: {
|
|
2118
|
+
panelId: this.id,
|
|
2119
|
+
reason: "list-full",
|
|
2120
|
+
mode: verdict.mode,
|
|
2121
|
+
list: "blocking"
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
throw new Error(`the ${verdict.mode} blocking list already holds ${verdict.max} devices — remove one first`);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
if (secureEdit !== void 0) {
|
|
2128
|
+
this.refuseUnofferedMode(secureEdit.mode, secureEdit.deviceId);
|
|
2129
|
+
const verdict = require_clip_model_registry.applySecureOnArmEdit(patch.secureOnArmOff !== void 0 ? patch.secureOnArmOff : modeDeviceListFromPersisted(this.config.get("secureOnArmOff")), secureEdit);
|
|
2130
|
+
if (verdict.kind === "refused") {
|
|
2131
|
+
this.ctx.logger.info("alarm settings refused", {
|
|
2132
|
+
tags: { deviceId: secureEdit.deviceId },
|
|
2133
|
+
meta: {
|
|
2134
|
+
panelId: this.id,
|
|
2135
|
+
reason: "list-full",
|
|
2136
|
+
mode: verdict.mode,
|
|
2137
|
+
list: "secureOnArmOff"
|
|
2138
|
+
}
|
|
2139
|
+
});
|
|
2140
|
+
throw new Error(`the ${verdict.mode} secure-on-arm-off list already holds ${verdict.max} devices — remove one first`);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2061
2143
|
const listWrites = [];
|
|
2062
2144
|
if (patch.nonBlocking !== void 0) listWrites.push(this.config.set("nonBlocking", patch.nonBlocking.map((l) => ({
|
|
2063
2145
|
mode: l.mode,
|
|
2064
2146
|
deviceIds: [...l.deviceIds]
|
|
2065
2147
|
}))));
|
|
2148
|
+
if (patch.blocking !== void 0) listWrites.push(this.config.set("blocking", patch.blocking.map((l) => ({
|
|
2149
|
+
mode: l.mode,
|
|
2150
|
+
deviceIds: [...l.deviceIds]
|
|
2151
|
+
}))));
|
|
2152
|
+
if (patch.secureOnArmOff !== void 0) listWrites.push(this.config.set("secureOnArmOff", patch.secureOnArmOff.map((l) => ({
|
|
2153
|
+
mode: l.mode,
|
|
2154
|
+
deviceIds: [...l.deviceIds]
|
|
2155
|
+
}))));
|
|
2066
2156
|
let membershipChanged = null;
|
|
2067
2157
|
if (membership !== void 0) {
|
|
2068
|
-
const result = require_clip_model_registry.applyNonBlockingEdit(
|
|
2158
|
+
const result = require_clip_model_registry.applyNonBlockingEdit(modeDeviceListFromPersisted(this.config.get("nonBlocking")), membership);
|
|
2069
2159
|
if (result.kind === "applied") {
|
|
2070
2160
|
membershipChanged = result.changed;
|
|
2071
2161
|
if (result.changed) listWrites.push(this.config.set("nonBlocking", result.lists.map((l) => ({
|
|
@@ -2074,6 +2164,26 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2074
2164
|
}))));
|
|
2075
2165
|
}
|
|
2076
2166
|
}
|
|
2167
|
+
if (blockingEdit !== void 0) {
|
|
2168
|
+
const result = require_clip_model_registry.applyBlockingEdit(modeDeviceListFromPersisted(this.config.get("blocking")), blockingEdit);
|
|
2169
|
+
if (result.kind === "applied" && result.changed) {
|
|
2170
|
+
membershipChanged = true;
|
|
2171
|
+
listWrites.push(this.config.set("blocking", result.lists.map((l) => ({
|
|
2172
|
+
mode: l.mode,
|
|
2173
|
+
deviceIds: [...l.deviceIds]
|
|
2174
|
+
}))));
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
if (secureEdit !== void 0) {
|
|
2178
|
+
const result = require_clip_model_registry.applySecureOnArmEdit(modeDeviceListFromPersisted(this.config.get("secureOnArmOff")), secureEdit);
|
|
2179
|
+
if (result.kind === "applied" && result.changed) {
|
|
2180
|
+
membershipChanged = true;
|
|
2181
|
+
listWrites.push(this.config.set("secureOnArmOff", result.lists.map((l) => ({
|
|
2182
|
+
mode: l.mode,
|
|
2183
|
+
deviceIds: [...l.deviceIds]
|
|
2184
|
+
}))));
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2077
2187
|
const persistedDelays = modeDelaysFromPersisted(this.config.get("modeDelays"));
|
|
2078
2188
|
const beforeDelays = persistedDelays.entries;
|
|
2079
2189
|
const afterDelays = edits.length > 0 ? require_clip_model_registry.applyModeDelaysEdits(beforeDelays, edits) : beforeDelays;
|
|
@@ -2212,7 +2322,7 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2212
2322
|
if (read.verdict.kind === "refused") return this.refuse(mode, read.verdict.blocking, now);
|
|
2213
2323
|
return this.settleArm(mode, read.verdict.excluded.map((device) => ({
|
|
2214
2324
|
device,
|
|
2215
|
-
why:
|
|
2325
|
+
why: this.exclusionWhyFor(mode, device)
|
|
2216
2326
|
})), now);
|
|
2217
2327
|
}
|
|
2218
2328
|
/**
|
|
@@ -2261,12 +2371,14 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2261
2371
|
return outcome;
|
|
2262
2372
|
}
|
|
2263
2373
|
const tolerated = this.nonBlockingFor(held.mode);
|
|
2374
|
+
const forceBlock = this.blockingFor(held.mode);
|
|
2264
2375
|
const unknown = new Set(read.openings.unknown);
|
|
2265
2376
|
if (read.verdict.kind === "refused") {
|
|
2266
2377
|
const plain = decideArm({
|
|
2267
2378
|
mode: held.mode,
|
|
2268
2379
|
open: read.openings.open,
|
|
2269
|
-
nonBlocking: tolerated
|
|
2380
|
+
nonBlocking: tolerated,
|
|
2381
|
+
blocking: forceBlock
|
|
2270
2382
|
});
|
|
2271
2383
|
const blockingNow = plain.kind === "refused" ? plain.blocking : read.verdict.blocking;
|
|
2272
2384
|
const shownIds = new Set(blockingNow.map((d) => d.deviceId));
|
|
@@ -2396,10 +2508,17 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2396
2508
|
error: String(err)
|
|
2397
2509
|
};
|
|
2398
2510
|
}
|
|
2511
|
+
const toSecure = openings.open.filter((d) => require_clip_model_registry.alarmDeviceSecureOnArm({
|
|
2512
|
+
controllable: d.controllable,
|
|
2513
|
+
deviceId: d.deviceId,
|
|
2514
|
+
secureOnArmOff: this.secureOnArmOffFor(mode)
|
|
2515
|
+
}));
|
|
2516
|
+
if (toSecure.length > 0) this.armGuard?.secureOnArm(toSecure);
|
|
2399
2517
|
const verdict = decideArm({
|
|
2400
2518
|
mode,
|
|
2401
2519
|
open: openings.open,
|
|
2402
2520
|
nonBlocking: this.nonBlockingFor(mode),
|
|
2521
|
+
blocking: this.blockingFor(mode),
|
|
2403
2522
|
...acknowledged !== void 0 ? { acknowledged } : {}
|
|
2404
2523
|
});
|
|
2405
2524
|
return {
|
|
@@ -2424,7 +2543,28 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2424
2543
|
}
|
|
2425
2544
|
/** This mode's non-blocking ids, from the persisted lists. */
|
|
2426
2545
|
nonBlockingFor(mode) {
|
|
2427
|
-
return
|
|
2546
|
+
return this.modeListIds("nonBlocking", mode);
|
|
2547
|
+
}
|
|
2548
|
+
/** Controllable openings that force-block when open. */
|
|
2549
|
+
blockingFor(mode) {
|
|
2550
|
+
return this.modeListIds("blocking", mode);
|
|
2551
|
+
}
|
|
2552
|
+
/** Controllable openings that opt out of secure-on-arm. */
|
|
2553
|
+
secureOnArmOffFor(mode) {
|
|
2554
|
+
return this.modeListIds("secureOnArmOff", mode);
|
|
2555
|
+
}
|
|
2556
|
+
modeListIds(key, mode) {
|
|
2557
|
+
return modeDeviceListFromPersisted(this.config.get(key)).find((l) => l.mode === mode)?.deviceIds ?? [];
|
|
2558
|
+
}
|
|
2559
|
+
/** Why an open device was armed around on a plain (non–Arm-anyway) arm. */
|
|
2560
|
+
exclusionWhyFor(mode, device) {
|
|
2561
|
+
if (this.nonBlockingFor(mode).includes(device.deviceId)) return "non-blocking";
|
|
2562
|
+
if (require_clip_model_registry.alarmDeviceSecureOnArm({
|
|
2563
|
+
controllable: device.controllable,
|
|
2564
|
+
deviceId: device.deviceId,
|
|
2565
|
+
secureOnArmOff: this.secureOnArmOffFor(mode)
|
|
2566
|
+
})) return "secure-on-arm";
|
|
2567
|
+
return "non-blocking";
|
|
2428
2568
|
}
|
|
2429
2569
|
/**
|
|
2430
2570
|
* Arm, with these exclusions.
|
|
@@ -8618,6 +8758,7 @@ function sameWord(a, b) {
|
|
|
8618
8758
|
* therefore moves the table with the arm.
|
|
8619
8759
|
*/
|
|
8620
8760
|
function alarmSensorStateOf(deviceId, opening, online) {
|
|
8761
|
+
const controllable = opening?.kind === "reading" ? require_clip_model_registry.isAlarmControllableCap(opening.cap) : false;
|
|
8621
8762
|
const verdict = classifyCoveredOpening(opening);
|
|
8622
8763
|
switch (verdict.kind) {
|
|
8623
8764
|
case "unknown": return {
|
|
@@ -8625,27 +8766,31 @@ function alarmSensorStateOf(deviceId, opening, online) {
|
|
|
8625
8766
|
reading: "unknown",
|
|
8626
8767
|
state: null,
|
|
8627
8768
|
unknownWhy: verdict.why,
|
|
8628
|
-
online
|
|
8769
|
+
online,
|
|
8770
|
+
controllable
|
|
8629
8771
|
};
|
|
8630
8772
|
case "open": return {
|
|
8631
8773
|
deviceId,
|
|
8632
8774
|
reading: "open",
|
|
8633
8775
|
state: verdict.state,
|
|
8634
8776
|
unknownWhy: null,
|
|
8635
|
-
online
|
|
8777
|
+
online,
|
|
8778
|
+
controllable: verdict.controllable
|
|
8636
8779
|
};
|
|
8637
8780
|
case "skip": return opening?.kind === "reading" ? {
|
|
8638
8781
|
deviceId,
|
|
8639
8782
|
reading: "closed",
|
|
8640
8783
|
state: opening.state,
|
|
8641
8784
|
unknownWhy: null,
|
|
8642
|
-
online
|
|
8785
|
+
online,
|
|
8786
|
+
controllable
|
|
8643
8787
|
} : {
|
|
8644
8788
|
deviceId,
|
|
8645
8789
|
reading: "not-opening",
|
|
8646
8790
|
state: null,
|
|
8647
8791
|
unknownWhy: null,
|
|
8648
|
-
online
|
|
8792
|
+
online,
|
|
8793
|
+
controllable: false
|
|
8649
8794
|
};
|
|
8650
8795
|
}
|
|
8651
8796
|
}
|
|
@@ -20009,19 +20154,39 @@ function planSummaryDevices(rule, isDead, guard) {
|
|
|
20009
20154
|
notes: notes.notes
|
|
20010
20155
|
};
|
|
20011
20156
|
}
|
|
20157
|
+
function stripDeadFromLists(lists, isDead) {
|
|
20158
|
+
const dead = lists.flatMap((list) => list.deviceIds.filter(isDead));
|
|
20159
|
+
if (dead.length === 0) return {
|
|
20160
|
+
next: lists,
|
|
20161
|
+
dead
|
|
20162
|
+
};
|
|
20163
|
+
return {
|
|
20164
|
+
dead,
|
|
20165
|
+
next: lists.map((list) => ({
|
|
20166
|
+
...list,
|
|
20167
|
+
deviceIds: list.deviceIds.filter((id) => !isDead(id))
|
|
20168
|
+
}))
|
|
20169
|
+
};
|
|
20170
|
+
}
|
|
20012
20171
|
function planAlarmDevices(alarm, isDead) {
|
|
20013
20172
|
if (alarm === null) return null;
|
|
20014
|
-
const
|
|
20015
|
-
const
|
|
20173
|
+
const nonBlocking = stripDeadFromLists(alarm.nonBlocking, isDead);
|
|
20174
|
+
const blocking = stripDeadFromLists(alarm.blocking, isDead);
|
|
20175
|
+
const secureOnArmOff = stripDeadFromLists(alarm.secureOnArmOff, isDead);
|
|
20016
20176
|
const forgetExcluded = alarm.excluded.filter(isDead);
|
|
20017
|
-
|
|
20177
|
+
const deadIds = [
|
|
20178
|
+
...nonBlocking.dead,
|
|
20179
|
+
...blocking.dead,
|
|
20180
|
+
...secureOnArmOff.dead,
|
|
20181
|
+
...forgetExcluded
|
|
20182
|
+
];
|
|
20183
|
+
if (deadIds.length === 0) return null;
|
|
20018
20184
|
return {
|
|
20019
|
-
dropped: deviceSubjects([...new Set(
|
|
20185
|
+
dropped: deviceSubjects([...new Set(deadIds)]),
|
|
20020
20186
|
droppedKind: "device",
|
|
20021
|
-
...
|
|
20022
|
-
|
|
20023
|
-
|
|
20024
|
-
})) } : {},
|
|
20187
|
+
...nonBlocking.dead.length > 0 ? { nonBlocking: nonBlocking.next } : {},
|
|
20188
|
+
...blocking.dead.length > 0 ? { blocking: blocking.next } : {},
|
|
20189
|
+
...secureOnArmOff.dead.length > 0 ? { secureOnArmOff: secureOnArmOff.next } : {},
|
|
20025
20190
|
forgetExcluded
|
|
20026
20191
|
};
|
|
20027
20192
|
}
|
|
@@ -20063,6 +20228,8 @@ function referencedDeviceIds(snapshot) {
|
|
|
20063
20228
|
for (const rule of snapshot.timelapse) add(rule.deviceIds);
|
|
20064
20229
|
for (const rule of snapshot.summary) add(rule.deviceIds);
|
|
20065
20230
|
for (const list of snapshot.alarm?.nonBlocking ?? []) add(list.deviceIds);
|
|
20231
|
+
for (const list of snapshot.alarm?.blocking ?? []) add(list.deviceIds);
|
|
20232
|
+
for (const list of snapshot.alarm?.secureOnArmOff ?? []) add(list.deviceIds);
|
|
20066
20233
|
add(snapshot.alarm?.excluded);
|
|
20067
20234
|
add(snapshot.mutedDeviceIds);
|
|
20068
20235
|
return ids;
|
|
@@ -20407,6 +20574,8 @@ var NcDeletedSubjectPruner = class {
|
|
|
20407
20574
|
summary: this.writable("summary", this.deps.summary.list()),
|
|
20408
20575
|
alarm: panel !== null && settings !== void 0 ? {
|
|
20409
20576
|
nonBlocking: settings.nonBlocking,
|
|
20577
|
+
blocking: settings.blocking,
|
|
20578
|
+
secureOnArmOff: settings.secureOnArmOff,
|
|
20410
20579
|
announceTargets: settings.announceTargets,
|
|
20411
20580
|
excluded: panel.excludedDevices()
|
|
20412
20581
|
} : null,
|
|
@@ -20563,6 +20732,8 @@ var NcDeletedSubjectPruner = class {
|
|
|
20563
20732
|
...NO_SNAPSHOT,
|
|
20564
20733
|
alarm: {
|
|
20565
20734
|
nonBlocking: settings.nonBlocking,
|
|
20735
|
+
blocking: settings.blocking,
|
|
20736
|
+
secureOnArmOff: settings.secureOnArmOff,
|
|
20566
20737
|
announceTargets: settings.announceTargets,
|
|
20567
20738
|
excluded: panel.excludedDevices()
|
|
20568
20739
|
}
|
|
@@ -20571,6 +20742,8 @@ var NcDeletedSubjectPruner = class {
|
|
|
20571
20742
|
try {
|
|
20572
20743
|
const patch = {
|
|
20573
20744
|
...edit.nonBlocking !== void 0 ? { nonBlocking: edit.nonBlocking } : {},
|
|
20745
|
+
...edit.blocking !== void 0 ? { blocking: edit.blocking } : {},
|
|
20746
|
+
...edit.secureOnArmOff !== void 0 ? { secureOnArmOff: edit.secureOnArmOff } : {},
|
|
20574
20747
|
...edit.announceTargets !== void 0 ? { announceTargets: [...edit.announceTargets] } : {}
|
|
20575
20748
|
};
|
|
20576
20749
|
if (Object.keys(patch).length > 0) await panel.applySettings(patch);
|
|
@@ -35573,6 +35746,8 @@ var NC_ALARM_SETTINGS_FALLBACK = {
|
|
|
35573
35746
|
...DEFAULT_DELAYS,
|
|
35574
35747
|
announceTargets: [],
|
|
35575
35748
|
nonBlocking: [],
|
|
35749
|
+
blocking: [],
|
|
35750
|
+
secureOnArmOff: [],
|
|
35576
35751
|
modeDelays: []
|
|
35577
35752
|
};
|
|
35578
35753
|
/**
|
|
@@ -38577,6 +38752,9 @@ var NotificationCenter = class {
|
|
|
38577
38752
|
alarmArmGuard() {
|
|
38578
38753
|
return {
|
|
38579
38754
|
openings: (mode) => this.alarmOpenings(mode),
|
|
38755
|
+
secureOnArm: (devices) => {
|
|
38756
|
+
this.secureDevicesOnArm(devices);
|
|
38757
|
+
},
|
|
38580
38758
|
announceRefusal: (input) => this.announceArmRefused(input),
|
|
38581
38759
|
announceCheckFailed: (input) => this.announceArmCheckFailed(input)
|
|
38582
38760
|
};
|
|
@@ -38643,7 +38821,9 @@ var NotificationCenter = class {
|
|
|
38643
38821
|
if (verdict.kind === "open") open.push({
|
|
38644
38822
|
deviceId,
|
|
38645
38823
|
name: name(deviceId),
|
|
38646
|
-
state: verdict.state
|
|
38824
|
+
state: verdict.state,
|
|
38825
|
+
controllable: verdict.controllable,
|
|
38826
|
+
cap: verdict.cap
|
|
38647
38827
|
});
|
|
38648
38828
|
}
|
|
38649
38829
|
this.logger.info("alarm arm check", {
|
|
@@ -38662,6 +38842,47 @@ var NotificationCenter = class {
|
|
|
38662
38842
|
};
|
|
38663
38843
|
}
|
|
38664
38844
|
/**
|
|
38845
|
+
* Fire-and-forget secure actuation for controllable openings on arm.
|
|
38846
|
+
* Failures are WARN per device — they never refuse the arm.
|
|
38847
|
+
*/
|
|
38848
|
+
async secureDevicesOnArm(devices) {
|
|
38849
|
+
for (const device of devices) {
|
|
38850
|
+
const actuation = device.cap !== void 0 ? require_clip_model_registry.secureActuationFor(device.cap) : null;
|
|
38851
|
+
if (actuation === null) continue;
|
|
38852
|
+
try {
|
|
38853
|
+
await this.actuateWithAttribution({
|
|
38854
|
+
deviceId: device.deviceId,
|
|
38855
|
+
cap: actuation.cap,
|
|
38856
|
+
method: actuation.method,
|
|
38857
|
+
args: {
|
|
38858
|
+
deviceId: device.deviceId,
|
|
38859
|
+
...actuation.args
|
|
38860
|
+
}
|
|
38861
|
+
});
|
|
38862
|
+
this.logger.info("alarm secure-on-arm actuated", {
|
|
38863
|
+
tags: { deviceId: device.deviceId },
|
|
38864
|
+
meta: {
|
|
38865
|
+
name: device.name,
|
|
38866
|
+
state: device.state,
|
|
38867
|
+
cap: actuation.cap,
|
|
38868
|
+
method: actuation.method
|
|
38869
|
+
}
|
|
38870
|
+
});
|
|
38871
|
+
} catch (err) {
|
|
38872
|
+
this.logger.warn("alarm secure-on-arm actuation failed — arm continues", {
|
|
38873
|
+
tags: { deviceId: device.deviceId },
|
|
38874
|
+
meta: {
|
|
38875
|
+
name: device.name,
|
|
38876
|
+
state: device.state,
|
|
38877
|
+
cap: actuation.cap,
|
|
38878
|
+
method: actuation.method,
|
|
38879
|
+
error: String(err)
|
|
38880
|
+
}
|
|
38881
|
+
});
|
|
38882
|
+
}
|
|
38883
|
+
}
|
|
38884
|
+
}
|
|
38885
|
+
/**
|
|
38665
38886
|
* The refusal, as an ordinary notification.
|
|
38666
38887
|
*
|
|
38667
38888
|
* Same path every alarm announcement takes (D126: the alarm speaks through
|
|
@@ -51362,6 +51583,11 @@ var MEDIA_ID_SEGMENT = "m";
|
|
|
51362
51583
|
/** Path segment that introduces an OWNER question ("best media of"). */
|
|
51363
51584
|
var MEDIA_OWNER_SEGMENT = "o";
|
|
51364
51585
|
/**
|
|
51586
|
+
* Path segment that introduces an event-owner GENERATION (D694) —
|
|
51587
|
+
* `/g/<deviceId>/<recordAt>` after the owner id.
|
|
51588
|
+
*/
|
|
51589
|
+
var MEDIA_GENERATION_SEGMENT = "g";
|
|
51590
|
+
/**
|
|
51365
51591
|
* The plane path for one media row.
|
|
51366
51592
|
*
|
|
51367
51593
|
* `prefix` is `/addon/<addonId>/event-media` — a pure function of the addon
|
|
@@ -51378,6 +51604,23 @@ function mediaFileUrl(prefix, mediaId) {
|
|
|
51378
51604
|
return `${prefix}/${MEDIA_ID_SEGMENT}/${mediaId}`;
|
|
51379
51605
|
}
|
|
51380
51606
|
/**
|
|
51607
|
+
* The plane path for "whatever media best represents this owner".
|
|
51608
|
+
*
|
|
51609
|
+
* The owner id IS encoded: a trackId is a UUID today but an owner id is an
|
|
51610
|
+
* arbitrary row key (the summary mosaic's is `summary-<ruleId>-<windowEndMs>`),
|
|
51611
|
+
* and one slash in it would silently re-cut the path.
|
|
51612
|
+
*
|
|
51613
|
+
* When `generation` is set and `ownerType` is an event table, the path carries
|
|
51614
|
+
* `/g/<deviceId>/<recordAt>` so a reused rowid cannot resolve another record's
|
|
51615
|
+
* crop. Generation is ignored (and not minted) for non-event owners — a track
|
|
51616
|
+
* id is random and needs none.
|
|
51617
|
+
*/
|
|
51618
|
+
function mediaOwnerUrl(prefix, ownerType, ownerId, generation) {
|
|
51619
|
+
const base = `${prefix}/${MEDIA_OWNER_SEGMENT}/${ownerType}/${encodeURIComponent(ownerId)}`;
|
|
51620
|
+
if (generation === void 0 || !require_clip_model_registry.isEventOwnerType(ownerType)) return base;
|
|
51621
|
+
return `${base}/${MEDIA_GENERATION_SEGMENT}/${generation.deviceId}/${generation.recordAt}`;
|
|
51622
|
+
}
|
|
51623
|
+
/**
|
|
51381
51624
|
* A stored row as the wire sees it: the ref, the address, and — transitionally
|
|
51382
51625
|
* — the bytes.
|
|
51383
51626
|
*
|
|
@@ -59230,6 +59473,12 @@ function openGzipStream(out) {
|
|
|
59230
59473
|
onceDrain: (listener) => {
|
|
59231
59474
|
gzip.once("drain", listener);
|
|
59232
59475
|
},
|
|
59476
|
+
flush: () => new Promise((resolve, reject) => {
|
|
59477
|
+
gzip.flush((err) => {
|
|
59478
|
+
if (err !== null && err !== void 0) reject(err);
|
|
59479
|
+
else resolve();
|
|
59480
|
+
});
|
|
59481
|
+
}),
|
|
59233
59482
|
finish: async () => {
|
|
59234
59483
|
gzip.end();
|
|
59235
59484
|
await closed;
|
|
@@ -59325,6 +59574,25 @@ var TapeSegmentWriter = class {
|
|
|
59325
59574
|
});
|
|
59326
59575
|
}
|
|
59327
59576
|
/**
|
|
59577
|
+
* Push the sink's pending gzip output to disk without ending the segment.
|
|
59578
|
+
* A quiet / silent camera otherwise keeps its tail only in the zlib
|
|
59579
|
+
* buffer until the next rotation (D681 follow-up). Never throws into the
|
|
59580
|
+
* caller's frame path: a flush fault is logged and swallowed here.
|
|
59581
|
+
*/
|
|
59582
|
+
async flushToDisk() {
|
|
59583
|
+
try {
|
|
59584
|
+
await this.deps.sink.flush();
|
|
59585
|
+
} catch (err) {
|
|
59586
|
+
this.deps.logger.warn("tracking tape segment flush failed", {
|
|
59587
|
+
tags: { deviceId: this.deps.deviceId },
|
|
59588
|
+
meta: {
|
|
59589
|
+
segmentOriginMs: this.deps.segmentOriginMs,
|
|
59590
|
+
error: require_clip_model_registry.errMsg(err)
|
|
59591
|
+
}
|
|
59592
|
+
});
|
|
59593
|
+
}
|
|
59594
|
+
}
|
|
59595
|
+
/**
|
|
59328
59596
|
* Always ends the segment's records (queue flush, lost-span gap, `end`
|
|
59329
59597
|
* record) before touching the sink, so a sink that cannot finish still
|
|
59330
59598
|
* leaves a decodable body behind it. `sink.finish()`'s rejection is caught
|
|
@@ -59651,6 +59919,21 @@ var TapeRecorder = class {
|
|
|
59651
59919
|
if (cam !== void 0) await this.closeSegment(deviceId, cam, "segment-end");
|
|
59652
59920
|
}
|
|
59653
59921
|
/**
|
|
59922
|
+
* Push every open segment's pending gzip output to disk without rotating
|
|
59923
|
+
* (D681 follow-up). A silent / quiet camera otherwise keeps its tail only
|
|
59924
|
+
* in the zlib buffer until the next frame or the 10-minute cut — a crash
|
|
59925
|
+
* loses it, and `bytesLastHour` lags behind what was already recorded.
|
|
59926
|
+
* Never throws: a per-camera flush fault is logged inside the writer.
|
|
59927
|
+
*/
|
|
59928
|
+
async flushOpenSegments() {
|
|
59929
|
+
const pending = [];
|
|
59930
|
+
for (const cam of this.cameras.values()) {
|
|
59931
|
+
const writer = cam.segment?.writer;
|
|
59932
|
+
if (writer !== void 0 && writer !== null) pending.push(writer.flushToDisk());
|
|
59933
|
+
}
|
|
59934
|
+
await Promise.all(pending);
|
|
59935
|
+
}
|
|
59936
|
+
/**
|
|
59654
59937
|
* Close the camera's open segment only if it holds time at or before
|
|
59655
59938
|
* `atMs` — what a promotion of a window ending at `atMs` needs on disk
|
|
59656
59939
|
* (Task 12 fix round, H1). A window wholly before the open segment leaves
|
|
@@ -60150,6 +60433,29 @@ var TapeShelf = class {
|
|
|
60150
60433
|
};
|
|
60151
60434
|
}
|
|
60152
60435
|
};
|
|
60436
|
+
/**
|
|
60437
|
+
* Log every refusal the sweep already measured (D391 / D681 follow-up).
|
|
60438
|
+
* `sweep()` itself never throws for `rootUnreadable` / per-camera `failed`:
|
|
60439
|
+
* those land on the report, and a caller that only `.catch`es would drop them
|
|
60440
|
+
* silently even when the shelf already warned at the `readdir` site. One
|
|
60441
|
+
* tagged line per refusal at the sweep boundary closes that gap.
|
|
60442
|
+
*/
|
|
60443
|
+
function logTapeSweepReport(report, logger) {
|
|
60444
|
+
if (report.rootUnreadable !== null) logger.warn("tracking tape sweep refused", { meta: {
|
|
60445
|
+
reason: report.rootUnreadable,
|
|
60446
|
+
scope: "root"
|
|
60447
|
+
} });
|
|
60448
|
+
for (const [deviceId, outcome] of report.perDevice) {
|
|
60449
|
+
if (!("failed" in outcome) || outcome.failed !== true) continue;
|
|
60450
|
+
logger.warn("tracking tape sweep refused", {
|
|
60451
|
+
tags: { deviceId },
|
|
60452
|
+
meta: {
|
|
60453
|
+
reason: outcome.reason,
|
|
60454
|
+
scope: "camera"
|
|
60455
|
+
}
|
|
60456
|
+
});
|
|
60457
|
+
}
|
|
60458
|
+
}
|
|
60153
60459
|
//#endregion
|
|
60154
60460
|
//#region src/pipeline-analytics/tracking-bench/recorder/tape-operation-queue.ts
|
|
60155
60461
|
/** Generous: a promotion reads its window's segments and walks back through
|
|
@@ -61203,6 +61509,17 @@ function shelfFigures(listing, now) {
|
|
|
61203
61509
|
...last !== void 0 ? { heldToMs: last.originMs } : {}
|
|
61204
61510
|
};
|
|
61205
61511
|
}
|
|
61512
|
+
/**
|
|
61513
|
+
* Default `debug.tape.status` (no `deviceIds`): drop the noise. An Off camera
|
|
61514
|
+
* with a measured empty shelf is not a cost row and is not promotable; keep
|
|
61515
|
+
* everything else — Recording / Refused / Failing / Unknown, Off with held
|
|
61516
|
+
* segments (still promotable after the switch went off), and Off whose shelf
|
|
61517
|
+
* could not be read (D393: never silently hide an unreadable camera). Explicit
|
|
61518
|
+
* `deviceIds` still return every asked-about camera, Off included (D62).
|
|
61519
|
+
*/
|
|
61520
|
+
function defaultTapeStatusCameras(rows) {
|
|
61521
|
+
return rows.filter((row) => row.state !== "off" || row.segmentsHeld === null || row.segmentsHeld > 0);
|
|
61522
|
+
}
|
|
61206
61523
|
function describeTapeStatus(input) {
|
|
61207
61524
|
return input.deviceIds.map((deviceId) => {
|
|
61208
61525
|
const shelf = {
|
|
@@ -73112,7 +73429,7 @@ var AnalyticsQueryFacade = class {
|
|
|
73112
73429
|
async getMotionEvents(input) {
|
|
73113
73430
|
const events = await (this.deps.eventStore()?.queryMotion(input) ?? Promise.resolve([]));
|
|
73114
73431
|
if (input.projection !== "slim" || this.deps.eventMediaBaseUrl() === null) return events;
|
|
73115
|
-
return this.withMediaUrl(events);
|
|
73432
|
+
return this.withMediaUrl(events, "motion");
|
|
73116
73433
|
}
|
|
73117
73434
|
async getEventDensity(input) {
|
|
73118
73435
|
return this.deps.eventStore()?.densityByKind(input) ?? [];
|
|
@@ -73169,12 +73486,12 @@ var AnalyticsQueryFacade = class {
|
|
|
73169
73486
|
async getObjectEvents(input) {
|
|
73170
73487
|
const events = await (this.deps.eventStore()?.queryObject(input) ?? Promise.resolve([]));
|
|
73171
73488
|
if (input.projection !== "slim" || this.deps.eventMediaBaseUrl() === null) return events;
|
|
73172
|
-
return this.withMediaUrl(events);
|
|
73489
|
+
return this.withMediaUrl(events, "object");
|
|
73173
73490
|
}
|
|
73174
73491
|
async getAudioEvents(input) {
|
|
73175
73492
|
const events = await (this.deps.eventStore()?.queryAudio(input) ?? Promise.resolve([]));
|
|
73176
73493
|
if (input.projection !== "slim" || this.deps.eventMediaBaseUrl() === null) return events;
|
|
73177
|
-
return this.withMediaUrl(events);
|
|
73494
|
+
return this.withMediaUrl(events, "audio");
|
|
73178
73495
|
}
|
|
73179
73496
|
async getEventMedia(input) {
|
|
73180
73497
|
const store = this.deps.mediaStore();
|
|
@@ -73441,13 +73758,20 @@ var AnalyticsQueryFacade = class {
|
|
|
73441
73758
|
*
|
|
73442
73759
|
* Only called when `eventMediaBaseUrl()` is non-null (guard sits in
|
|
73443
73760
|
* the callers), so the template literal is safe.
|
|
73761
|
+
*
|
|
73762
|
+
* The URL names the owner TABLE and the event's GENERATION (D482 + D694):
|
|
73763
|
+
* a bare `${base}/${id}` was both ambiguous across tables and unable to
|
|
73764
|
+
* survive a reused rowid. New stamps always carry `/o/<type>/<id>/g/…`.
|
|
73444
73765
|
*/
|
|
73445
|
-
withMediaUrl(events) {
|
|
73766
|
+
withMediaUrl(events, ownerType) {
|
|
73446
73767
|
const base = this.deps.eventMediaBaseUrl();
|
|
73447
73768
|
if (base === null) return events;
|
|
73448
73769
|
return events.map((e) => ({
|
|
73449
73770
|
...e,
|
|
73450
|
-
mediaUrl:
|
|
73771
|
+
mediaUrl: mediaOwnerUrl(base, ownerType, String(e.id), {
|
|
73772
|
+
deviceId: e.deviceId,
|
|
73773
|
+
recordAt: e.timestamp
|
|
73774
|
+
})
|
|
73451
73775
|
}));
|
|
73452
73776
|
}
|
|
73453
73777
|
};
|
|
@@ -76279,18 +76603,26 @@ function eventMediaEtag(media) {
|
|
|
76279
76603
|
}
|
|
76280
76604
|
/** Digits only — `Number('')` is 0 and `Number(' ')` is 0, so neither is used. */
|
|
76281
76605
|
var DIGITS = /^\d+$/;
|
|
76606
|
+
/** One run of digits, for composing the event-owner remainder pattern. */
|
|
76607
|
+
var DIGIT_RUN = "\\d+";
|
|
76608
|
+
/**
|
|
76609
|
+
* Event-owner remainder with an optional generation suffix (D694):
|
|
76610
|
+
* `<ownerId>` or `<ownerId>/g/<deviceId>/<recordAt>`. Owner ids for event
|
|
76611
|
+
* tables are decimal rowids; the generation segment is the literal `g`.
|
|
76612
|
+
*/
|
|
76613
|
+
var EVENT_OWNER_REMAINDER = new RegExp(`^(${DIGIT_RUN})(?:/${MEDIA_GENERATION_SEGMENT}/(${DIGIT_RUN})/(${DIGIT_RUN}))?$`);
|
|
76282
76614
|
/**
|
|
76283
76615
|
* Read a plane path. `null` for anything that is not one of the two forms —
|
|
76284
76616
|
* including the BARE id the plane used to accept, which is gone: a bare number
|
|
76285
76617
|
* could be a media id or an event id, and after D474 an event id could be one
|
|
76286
76618
|
* of three rows. Ambiguity at the door is what this record removes.
|
|
76287
76619
|
*
|
|
76288
|
-
*
|
|
76289
|
-
* hub's wildcard param is percent-decoded by find-my-way before the
|
|
76290
|
-
* is proxied, so a `%2F` an owner id carried would arrive here as a
|
|
76291
|
-
* slash, and re-cutting on it would silently address a different owner.
|
|
76292
|
-
*
|
|
76293
|
-
*
|
|
76620
|
+
* For non-event owners the id is the whole remainder rather than one segment:
|
|
76621
|
+
* the hub's wildcard param is percent-decoded by find-my-way before the
|
|
76622
|
+
* sub-path is proxied, so a `%2F` an owner id carried would arrive here as a
|
|
76623
|
+
* real slash, and re-cutting on it would silently address a different owner.
|
|
76624
|
+
* Event owners (decimal rowids) may end with `/g/<deviceId>/<recordAt>` —
|
|
76625
|
+
* that suffix is the generation, not part of the id (D694).
|
|
76294
76626
|
*/
|
|
76295
76627
|
function parseMediaAddress(rawPath) {
|
|
76296
76628
|
const path = rawPath.replace(/^\/+/, "");
|
|
@@ -76310,13 +76642,35 @@ function parseMediaAddress(rawPath) {
|
|
|
76310
76642
|
const typeSlash = rest.indexOf("/");
|
|
76311
76643
|
if (typeSlash <= 0) return null;
|
|
76312
76644
|
const ownerType = rest.slice(0, typeSlash);
|
|
76313
|
-
const
|
|
76314
|
-
if (
|
|
76645
|
+
const remainder = rest.slice(typeSlash + 1);
|
|
76646
|
+
if (remainder.length === 0) return null;
|
|
76315
76647
|
if (!require_clip_model_registry.isMediaOwnerType(ownerType)) return null;
|
|
76648
|
+
if (require_clip_model_registry.isEventOwnerType(ownerType)) {
|
|
76649
|
+
const match = EVENT_OWNER_REMAINDER.exec(remainder);
|
|
76650
|
+
if (match === null) return null;
|
|
76651
|
+
const ownerId = match[1];
|
|
76652
|
+
const deviceIdRaw = match[2];
|
|
76653
|
+
const recordAtRaw = match[3];
|
|
76654
|
+
if (ownerId === void 0) return null;
|
|
76655
|
+
if (deviceIdRaw !== void 0 && recordAtRaw !== void 0) return {
|
|
76656
|
+
kind: "owner",
|
|
76657
|
+
ownerType,
|
|
76658
|
+
ownerId,
|
|
76659
|
+
generation: {
|
|
76660
|
+
deviceId: Number(deviceIdRaw),
|
|
76661
|
+
recordAt: Number(recordAtRaw)
|
|
76662
|
+
}
|
|
76663
|
+
};
|
|
76664
|
+
return {
|
|
76665
|
+
kind: "owner",
|
|
76666
|
+
ownerType,
|
|
76667
|
+
ownerId
|
|
76668
|
+
};
|
|
76669
|
+
}
|
|
76316
76670
|
return {
|
|
76317
76671
|
kind: "owner",
|
|
76318
76672
|
ownerType,
|
|
76319
|
-
ownerId
|
|
76673
|
+
ownerId: remainder
|
|
76320
76674
|
};
|
|
76321
76675
|
}
|
|
76322
76676
|
/** Set by the HUB for a `csv_*` share token admitted on this plane; never by a client. */
|
|
@@ -78993,6 +79347,218 @@ function createRetrainExportHandler(deps) {
|
|
|
78993
79347
|
};
|
|
78994
79348
|
}
|
|
78995
79349
|
//#endregion
|
|
79350
|
+
//#region src/pipeline-analytics/retrain/retrain-collection-store.ts
|
|
79351
|
+
/**
|
|
79352
|
+
* @durable class=ledger owner=pipeline-analytics
|
|
79353
|
+
* write="one row per named training collection the operator creates. Names are
|
|
79354
|
+
* unique on nameKey (lower(trim)); empty names never become rows."
|
|
79355
|
+
* retention="none — operator-owned corpus metadata. Rows leave only when an
|
|
79356
|
+
* explicit delete API lands (not V1)."
|
|
79357
|
+
*/
|
|
79358
|
+
var RETRAIN_COLLECTIONS_COLLECTION = "pipeline-analytics:retrain-collections";
|
|
79359
|
+
var RETRAIN_COLLECTION_COLUMNS = [
|
|
79360
|
+
{
|
|
79361
|
+
name: "id",
|
|
79362
|
+
type: "TEXT",
|
|
79363
|
+
primaryKey: true,
|
|
79364
|
+
notNull: true
|
|
79365
|
+
},
|
|
79366
|
+
{
|
|
79367
|
+
name: "name",
|
|
79368
|
+
type: "TEXT",
|
|
79369
|
+
notNull: true
|
|
79370
|
+
},
|
|
79371
|
+
{
|
|
79372
|
+
name: "nameKey",
|
|
79373
|
+
type: "TEXT",
|
|
79374
|
+
notNull: true
|
|
79375
|
+
},
|
|
79376
|
+
{
|
|
79377
|
+
name: "createdAt",
|
|
79378
|
+
type: "INTEGER",
|
|
79379
|
+
notNull: true
|
|
79380
|
+
},
|
|
79381
|
+
{
|
|
79382
|
+
name: "updatedAt",
|
|
79383
|
+
type: "INTEGER",
|
|
79384
|
+
notNull: true
|
|
79385
|
+
}
|
|
79386
|
+
];
|
|
79387
|
+
var RETRAIN_COLLECTION_INDEXES = [{
|
|
79388
|
+
name: "idx_retrain_collections_name_key",
|
|
79389
|
+
columns: ["nameKey"],
|
|
79390
|
+
unique: true
|
|
79391
|
+
}, {
|
|
79392
|
+
name: "idx_retrain_collections_created",
|
|
79393
|
+
columns: ["createdAt"]
|
|
79394
|
+
}];
|
|
79395
|
+
function normalizeCollectionName(raw) {
|
|
79396
|
+
const name = raw.trim();
|
|
79397
|
+
if (name.length === 0) return null;
|
|
79398
|
+
return {
|
|
79399
|
+
name,
|
|
79400
|
+
nameKey: name.toLowerCase()
|
|
79401
|
+
};
|
|
79402
|
+
}
|
|
79403
|
+
var RetrainCollectionStore = class {
|
|
79404
|
+
store;
|
|
79405
|
+
logger;
|
|
79406
|
+
now;
|
|
79407
|
+
newId;
|
|
79408
|
+
countFrames;
|
|
79409
|
+
constructor(deps) {
|
|
79410
|
+
this.store = deps.store;
|
|
79411
|
+
this.logger = deps.logger;
|
|
79412
|
+
this.now = deps.now;
|
|
79413
|
+
this.newId = deps.newId;
|
|
79414
|
+
this.countFrames = deps.countFrames;
|
|
79415
|
+
}
|
|
79416
|
+
static async declare(store) {
|
|
79417
|
+
await store.declareCollection.mutate({
|
|
79418
|
+
collection: RETRAIN_COLLECTIONS_COLLECTION,
|
|
79419
|
+
columns: [...RETRAIN_COLLECTION_COLUMNS],
|
|
79420
|
+
indexes: [...RETRAIN_COLLECTION_INDEXES]
|
|
79421
|
+
});
|
|
79422
|
+
}
|
|
79423
|
+
async create(input) {
|
|
79424
|
+
const normalized = normalizeCollectionName(input.name);
|
|
79425
|
+
if (normalized === null) return {
|
|
79426
|
+
kind: "refused",
|
|
79427
|
+
reason: "empty-name"
|
|
79428
|
+
};
|
|
79429
|
+
if (await this.findByNameKey(normalized.nameKey) !== null) return {
|
|
79430
|
+
kind: "refused",
|
|
79431
|
+
reason: "duplicate-name"
|
|
79432
|
+
};
|
|
79433
|
+
const createdAt = this.now();
|
|
79434
|
+
const id = this.newId();
|
|
79435
|
+
await this.store.set.mutate({
|
|
79436
|
+
collection: RETRAIN_COLLECTIONS_COLLECTION,
|
|
79437
|
+
key: id,
|
|
79438
|
+
value: {
|
|
79439
|
+
name: normalized.name,
|
|
79440
|
+
nameKey: normalized.nameKey,
|
|
79441
|
+
createdAt,
|
|
79442
|
+
updatedAt: createdAt
|
|
79443
|
+
}
|
|
79444
|
+
});
|
|
79445
|
+
return {
|
|
79446
|
+
kind: "created",
|
|
79447
|
+
collection: {
|
|
79448
|
+
id,
|
|
79449
|
+
name: normalized.name,
|
|
79450
|
+
createdAt,
|
|
79451
|
+
frameCount: 0
|
|
79452
|
+
}
|
|
79453
|
+
};
|
|
79454
|
+
}
|
|
79455
|
+
async list() {
|
|
79456
|
+
let rows;
|
|
79457
|
+
try {
|
|
79458
|
+
rows = await this.store.query.query({
|
|
79459
|
+
collection: RETRAIN_COLLECTIONS_COLLECTION,
|
|
79460
|
+
filter: { orderBy: {
|
|
79461
|
+
field: "createdAt",
|
|
79462
|
+
direction: "desc"
|
|
79463
|
+
} }
|
|
79464
|
+
});
|
|
79465
|
+
} catch (err) {
|
|
79466
|
+
this.logger.warn("retrain collections list failed", { meta: { error: String(err) } });
|
|
79467
|
+
return [];
|
|
79468
|
+
}
|
|
79469
|
+
const out = [];
|
|
79470
|
+
for (const row of rows) {
|
|
79471
|
+
const name = typeof row.data["name"] === "string" ? row.data["name"] : "";
|
|
79472
|
+
const createdAt = Number(row.data["createdAt"] ?? 0);
|
|
79473
|
+
const frameCount = this.countFrames !== void 0 ? await this.countFrames(row.id) : 0;
|
|
79474
|
+
out.push({
|
|
79475
|
+
id: row.id,
|
|
79476
|
+
name,
|
|
79477
|
+
createdAt,
|
|
79478
|
+
frameCount
|
|
79479
|
+
});
|
|
79480
|
+
}
|
|
79481
|
+
return out;
|
|
79482
|
+
}
|
|
79483
|
+
async getById(id) {
|
|
79484
|
+
try {
|
|
79485
|
+
const row = await this.store.get.query({
|
|
79486
|
+
collection: RETRAIN_COLLECTIONS_COLLECTION,
|
|
79487
|
+
key: id
|
|
79488
|
+
});
|
|
79489
|
+
if (row === null || row === void 0) return null;
|
|
79490
|
+
const data = row;
|
|
79491
|
+
return {
|
|
79492
|
+
id,
|
|
79493
|
+
name: typeof data["name"] === "string" ? data["name"] : "",
|
|
79494
|
+
createdAt: Number(data["createdAt"] ?? 0),
|
|
79495
|
+
frameCount: this.countFrames !== void 0 ? await this.countFrames(id) : 0
|
|
79496
|
+
};
|
|
79497
|
+
} catch (err) {
|
|
79498
|
+
this.logger.warn("retrain collection get failed", { meta: {
|
|
79499
|
+
collectionId: id,
|
|
79500
|
+
error: String(err)
|
|
79501
|
+
} });
|
|
79502
|
+
return null;
|
|
79503
|
+
}
|
|
79504
|
+
}
|
|
79505
|
+
/**
|
|
79506
|
+
* Stub until still ingest lands. Validates the collection exists, then
|
|
79507
|
+
* refuses with a named reason so callers can wire UI against a stable shape.
|
|
79508
|
+
*/
|
|
79509
|
+
async addRetrainFrameFromRecording(input) {
|
|
79510
|
+
if (await this.getById(input.collectionId) === null) {
|
|
79511
|
+
this.logger.info("retrain collection addFrame refused: unknown-collection", {
|
|
79512
|
+
tags: { deviceId: input.deviceId },
|
|
79513
|
+
meta: {
|
|
79514
|
+
collectionId: input.collectionId,
|
|
79515
|
+
epochMs: input.epochMs
|
|
79516
|
+
}
|
|
79517
|
+
});
|
|
79518
|
+
return {
|
|
79519
|
+
kind: "refused",
|
|
79520
|
+
reason: "unknown-collection"
|
|
79521
|
+
};
|
|
79522
|
+
}
|
|
79523
|
+
this.logger.info("retrain collection addFrame stub: still-capture-unimplemented", {
|
|
79524
|
+
tags: { deviceId: input.deviceId },
|
|
79525
|
+
meta: {
|
|
79526
|
+
collectionId: input.collectionId,
|
|
79527
|
+
epochMs: input.epochMs
|
|
79528
|
+
}
|
|
79529
|
+
});
|
|
79530
|
+
return {
|
|
79531
|
+
kind: "refused",
|
|
79532
|
+
reason: "still-capture-unimplemented",
|
|
79533
|
+
detail: "Still ingest lands in plan Task 3"
|
|
79534
|
+
};
|
|
79535
|
+
}
|
|
79536
|
+
async findByNameKey(nameKey) {
|
|
79537
|
+
try {
|
|
79538
|
+
const row = (await this.store.query.query({
|
|
79539
|
+
collection: RETRAIN_COLLECTIONS_COLLECTION,
|
|
79540
|
+
filter: {
|
|
79541
|
+
where: { nameKey },
|
|
79542
|
+
limit: 1
|
|
79543
|
+
}
|
|
79544
|
+
}))[0];
|
|
79545
|
+
if (row === void 0) return null;
|
|
79546
|
+
return {
|
|
79547
|
+
id: row.id,
|
|
79548
|
+
name: typeof row.data["name"] === "string" ? row.data["name"] : "",
|
|
79549
|
+
createdAt: Number(row.data["createdAt"] ?? 0),
|
|
79550
|
+
frameCount: 0
|
|
79551
|
+
};
|
|
79552
|
+
} catch (err) {
|
|
79553
|
+
this.logger.warn("retrain collection findByNameKey failed", { meta: {
|
|
79554
|
+
nameKey,
|
|
79555
|
+
error: String(err)
|
|
79556
|
+
} });
|
|
79557
|
+
return null;
|
|
79558
|
+
}
|
|
79559
|
+
}
|
|
79560
|
+
};
|
|
79561
|
+
//#endregion
|
|
78996
79562
|
//#region src/pipeline-analytics/retrain/retrain-assist.ts
|
|
78997
79563
|
/** The whole frame, as a normalised rectangle. */
|
|
78998
79564
|
var WHOLE_FRAME = {
|
|
@@ -82402,7 +82968,8 @@ async function rankKeyEvents(candidates, options, peakLookup) {
|
|
|
82402
82968
|
...t.markForTrain !== void 0 ? { markForTrain: t.markForTrain } : {},
|
|
82403
82969
|
...t.retrainStatus !== void 0 ? { retrainStatus: t.retrainStatus } : {},
|
|
82404
82970
|
...t.debug !== void 0 ? { debug: t.debug } : {},
|
|
82405
|
-
...t.favourited !== void 0 ? { favourited: t.favourited } : {}
|
|
82971
|
+
...t.favourited !== void 0 ? { favourited: t.favourited } : {},
|
|
82972
|
+
...t.audioLabels !== void 0 && t.audioLabels.length > 0 ? { audioLabels: t.audioLabels } : {}
|
|
82406
82973
|
});
|
|
82407
82974
|
}
|
|
82408
82975
|
scored.sort((a, b) => b.importance - a.importance);
|
|
@@ -82829,16 +83396,19 @@ var MotionOnsetRegistry = class {
|
|
|
82829
83396
|
* - `resolveSearchThumbnailUrl` — the search-hit thumbnail resolver. It points
|
|
82830
83397
|
* a hit at the CLIP embedding row's OWN crop (the crop captured on the
|
|
82831
83398
|
* embedding's peak frame → the hit thumbnail IS the embedded crop), and only
|
|
82832
|
-
* falls back to the representative event's
|
|
82833
|
-
* crop yet, so a hit never renders blank.
|
|
83399
|
+
* falls back to the representative event's owner ladder when the embedding
|
|
83400
|
+
* row has no crop yet, so a hit never renders blank.
|
|
82834
83401
|
*/
|
|
82835
83402
|
function isClipObjectEmbedding(t, registry = require_clip_model_registry.BUILTIN_CLIP_MODELS) {
|
|
82836
83403
|
return Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && registry.resolve(t.embeddingModelId) !== null;
|
|
82837
83404
|
}
|
|
82838
83405
|
function resolveSearchThumbnailUrl(input) {
|
|
82839
83406
|
if (input.baseUrl === null) return void 0;
|
|
82840
|
-
|
|
82841
|
-
return
|
|
83407
|
+
if (input.embeddingMediaId !== void 0) return mediaFileUrl(input.baseUrl, input.embeddingMediaId);
|
|
83408
|
+
return mediaOwnerUrl(input.baseUrl, "object", input.eventId, {
|
|
83409
|
+
deviceId: input.deviceId,
|
|
83410
|
+
recordAt: input.recordAt
|
|
83411
|
+
});
|
|
82842
83412
|
}
|
|
82843
83413
|
//#endregion
|
|
82844
83414
|
//#region src/pipeline-analytics/pipeline/onboard-birth-authority.ts
|
|
@@ -100998,7 +101568,10 @@ function ofGeneration(files, scope) {
|
|
|
100998
101568
|
function makeNcOwnerMediaReader(deps) {
|
|
100999
101569
|
return async (ownerKind, ownerId, eventScope) => {
|
|
101000
101570
|
if (ownerKind === "track") return deps.media.listByOwner("track", await deps.resolveTrackOwnerId(ownerId));
|
|
101001
|
-
if (eventScope === void 0)
|
|
101571
|
+
if (eventScope === void 0) {
|
|
101572
|
+
deps.logger.error("object media read refused — NcEventMediaScope required", { meta: { ownerId } });
|
|
101573
|
+
return [];
|
|
101574
|
+
}
|
|
101002
101575
|
const onCamera = await deps.media.listByOwner("object", ownerId, eventScope.deviceId);
|
|
101003
101576
|
const kept = ofGeneration(onCamera, eventScope);
|
|
101004
101577
|
if (kept.length < onCamera.length) deps.logger.warn("event media of another record on a reused event id — not used", {
|
|
@@ -102091,6 +102664,10 @@ var NC_ARTIFACT_PRUNE_INTERVAL_MS = 36e5;
|
|
|
102091
102664
|
* (every camera switched off): the bound is 24 h, not "24 h after the next
|
|
102092
102665
|
* close" (D681). */
|
|
102093
102666
|
var TAPE_SWEEP_INTERVAL_MS = 36e5;
|
|
102667
|
+
/** How often open tape segments push their pending gzip output to disk
|
|
102668
|
+
* without rotating (D681 follow-up): a silent camera otherwise keeps its
|
|
102669
|
+
* tail only in the zlib buffer until the next frame or the 10-minute cut. */
|
|
102670
|
+
var TAPE_FLUSH_INTERVAL_MS = 6e4;
|
|
102094
102671
|
/** What a publish answers on a node whose artifact plane never came up: no
|
|
102095
102672
|
* link, no index row — and, exactly as before, bytes-only delivery. */
|
|
102096
102673
|
var NO_PUBLICATION = {
|
|
@@ -102624,6 +103201,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
102624
103201
|
ncArtifactReadDataPlane = null;
|
|
102625
103202
|
ncArtifactPruneTimer = null;
|
|
102626
103203
|
tapeSweepTimer = null;
|
|
103204
|
+
tapeFlushTimer = null;
|
|
102627
103205
|
/** Why `startTrackingTapes` threw on a node that was allowed to record —
|
|
102628
103206
|
* reported `failing` per switched-on camera, never as `recording`. */
|
|
102629
103207
|
tapeStartFailure = null;
|
|
@@ -104428,6 +105006,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
104428
105006
|
StationaryObjectRegistry.declare(api.settingsStore),
|
|
104429
105007
|
RetrainFrameStore.declare(api.settingsStore),
|
|
104430
105008
|
RetrainAnnotationStore.declare(api.settingsStore),
|
|
105009
|
+
RetrainCollectionStore.declare(api.settingsStore),
|
|
104431
105010
|
OpsLogStore.declare(api.settingsStore),
|
|
104432
105011
|
DebugNoteArchiveStore.declare(api.settingsStore),
|
|
104433
105012
|
TrackingTapeIndexStore.declare(api.settingsStore),
|
|
@@ -106876,6 +107455,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
106876
107455
|
clearInterval(this.tapeSweepTimer);
|
|
106877
107456
|
this.tapeSweepTimer = null;
|
|
106878
107457
|
}
|
|
107458
|
+
if (this.tapeFlushTimer !== null) {
|
|
107459
|
+
clearInterval(this.tapeFlushTimer);
|
|
107460
|
+
this.tapeFlushTimer = null;
|
|
107461
|
+
}
|
|
106879
107462
|
if (this.ncArtifactPruneTimer !== null) {
|
|
106880
107463
|
clearInterval(this.ncArtifactPruneTimer);
|
|
106881
107464
|
this.ncArtifactPruneTimer = null;
|
|
@@ -113649,10 +114232,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
113649
114232
|
});
|
|
113650
114233
|
const scored = [];
|
|
113651
114234
|
for (const { event, score } of bestEventByTrackId.values()) {
|
|
114235
|
+
const embeddingMediaId = event.trackId !== void 0 ? embeddingMediaIdByTrackId.get(event.trackId) : void 0;
|
|
113652
114236
|
const mediaUrl = resolveSearchThumbnailUrl({
|
|
113653
114237
|
baseUrl: this.eventMediaBaseUrl,
|
|
113654
114238
|
eventId: String(event.id),
|
|
113655
|
-
|
|
114239
|
+
deviceId: event.deviceId,
|
|
114240
|
+
recordAt: event.timestamp,
|
|
114241
|
+
...embeddingMediaId !== void 0 ? { embeddingMediaId } : {}
|
|
113656
114242
|
});
|
|
113657
114243
|
const keyFrameMediaId = event.trackId !== void 0 ? keyFrameMediaIdByTrackId.get(event.trackId) : void 0;
|
|
113658
114244
|
scored.push({
|
|
@@ -114039,7 +114625,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
114039
114625
|
logger
|
|
114040
114626
|
});
|
|
114041
114627
|
const sweep = (deviceId) => {
|
|
114042
|
-
shelf.sweep().
|
|
114628
|
+
shelf.sweep().then((report) => {
|
|
114629
|
+
logTapeSweepReport(report, logger);
|
|
114630
|
+
}).catch((err) => {
|
|
114043
114631
|
logger.warn("tracking tape sweep failed", {
|
|
114044
114632
|
...deviceId !== null ? { tags: { deviceId } } : {},
|
|
114045
114633
|
meta: { error: require_clip_model_registry.errMsg(err) }
|
|
@@ -114071,6 +114659,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
114071
114659
|
sweep(null);
|
|
114072
114660
|
this.tapeSweepTimer = setInterval(() => sweep(null), TAPE_SWEEP_INTERVAL_MS);
|
|
114073
114661
|
this.tapeSweepTimer.unref?.();
|
|
114662
|
+
this.tapeFlushTimer = setInterval(() => {
|
|
114663
|
+
this.tapeRecorder?.flushOpenSegments();
|
|
114664
|
+
}, TAPE_FLUSH_INTERVAL_MS);
|
|
114665
|
+
this.tapeFlushTimer.unref?.();
|
|
114074
114666
|
this.ctx.logger.info("tracking tapes ready", { meta: { seededProcessors: this.processorInputs.size } });
|
|
114075
114667
|
} catch (err) {
|
|
114076
114668
|
this.tapeStartFailure = require_clip_model_registry.errMsg(err);
|
|
@@ -114263,7 +114855,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
114263
114855
|
segments.set(deviceId, await this.tapeListingOf(shelf, deviceId));
|
|
114264
114856
|
}
|
|
114265
114857
|
const recorder = this.tapeRecorder;
|
|
114266
|
-
|
|
114858
|
+
const cameras = describeTapeStatus({
|
|
114267
114859
|
deviceIds,
|
|
114268
114860
|
switchOn,
|
|
114269
114861
|
switchUnreadable,
|
|
@@ -114272,7 +114864,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
114272
114864
|
startFailure: this.tapeStartFailure,
|
|
114273
114865
|
segments,
|
|
114274
114866
|
now: Date.now()
|
|
114275
|
-
})
|
|
114867
|
+
});
|
|
114868
|
+
return { cameras: input.deviceIds === void 0 ? defaultTapeStatusCameras(cameras) : cameras };
|
|
114276
114869
|
},
|
|
114277
114870
|
"debug.tape.promote": async (input) => {
|
|
114278
114871
|
const deps = this.tapePromotionDeps();
|
|
@@ -114721,7 +115314,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
114721
115314
|
* owner table, so the motion/object coin-flip is gone from here too.
|
|
114722
115315
|
*/
|
|
114723
115316
|
async readMediaByAddress(address, variant, preferKind) {
|
|
114724
|
-
const base = address.kind === "row" ? await this.readMediaById(address.mediaId) : await this.readOwnerThumbnail(address.ownerType, address.ownerId, preferKind);
|
|
115317
|
+
const base = address.kind === "row" ? await this.readMediaById(address.mediaId) : await this.readOwnerThumbnail(address.ownerType, address.ownerId, preferKind, address.generation);
|
|
114725
115318
|
if (base === null || variant === void 0) return base;
|
|
114726
115319
|
return this.applyThumbVariant(base, variant);
|
|
114727
115320
|
}
|
|
@@ -114779,7 +115372,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
114779
115372
|
* ~144 MB and allocated ~190 MB of base64 to deliver ~1.2 MB, all of it on
|
|
114780
115373
|
* the single-threaded post-analysis runner.
|
|
114781
115374
|
*/
|
|
114782
|
-
async readOwnerThumbnail(ownerType, rawOwnerId, preferKind) {
|
|
115375
|
+
async readOwnerThumbnail(ownerType, rawOwnerId, preferKind, addressGeneration) {
|
|
114783
115376
|
const ownerId = ownerType === "track" ? await resolveMediaOwnerId(ownerType, rawOwnerId, (id) => this.trackAliasStore?.resolve(id) ?? Promise.resolve(null), (failure) => this.aliasResolveFailureLogger?.(failure), "readOwnerThumbnail") : rawOwnerId;
|
|
114784
115377
|
const listInfo = (ownerKind, id, deviceId) => this.mediaStore?.listInfoByOwner(ownerKind, id, deviceId) ?? Promise.resolve([]);
|
|
114785
115378
|
const getTrackIdForEvent = (eventId) => require_clip_model_registry.isEventOwnerType(ownerType) ? this.eventStore?.getTrackIdForEvent(eventId, ownerType) ?? Promise.resolve(null) : Promise.resolve(null);
|
|
@@ -114793,8 +115386,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
114793
115386
|
};
|
|
114794
115387
|
};
|
|
114795
115388
|
let ownFiles = await listInfo(ownerType, ownerId);
|
|
114796
|
-
if (require_clip_model_registry.isEventOwnerType(ownerType)
|
|
114797
|
-
const gen =
|
|
115389
|
+
if (require_clip_model_registry.isEventOwnerType(ownerType)) {
|
|
115390
|
+
const gen = addressGeneration !== void 0 ? {
|
|
115391
|
+
deviceId: addressGeneration.deviceId,
|
|
115392
|
+
timestamp: addressGeneration.recordAt
|
|
115393
|
+
} : this.eventStore !== null ? await this.eventStore.eventGenerationOf(ownerType, ownerId) : null;
|
|
114798
115394
|
if (gen !== null) {
|
|
114799
115395
|
const onCamera = await listInfo(ownerType, ownerId, gen.deviceId);
|
|
114800
115396
|
const kept = onCamera.filter((f) => f.timestamp === gen.timestamp);
|
|
@@ -114806,7 +115402,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
114806
115402
|
recordAt: gen.timestamp,
|
|
114807
115403
|
refused: onCamera.length - kept.length,
|
|
114808
115404
|
kept: kept.length,
|
|
114809
|
-
via: "readOwnerThumbnail"
|
|
115405
|
+
via: "readOwnerThumbnail",
|
|
115406
|
+
scopedBy: addressGeneration !== void 0 ? "url" : "live-row"
|
|
114810
115407
|
}
|
|
114811
115408
|
});
|
|
114812
115409
|
ownFiles = kept;
|