@camstack/addon-post-analysis 1.2.319 → 1.2.321
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-BjYpZuuI.js → clip-model-registry-BCx3qCL5.js} +341 -45
- package/dist/{clip-model-registry-DSqobJfx.mjs → clip-model-registry-Dnm9dfy0.mjs} +312 -46
- 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-CKHzDA_B.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CPcZN3mA.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CGUnHrR_.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-CVH5tT7I.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-DDDjJDA6.mjs → hostInit-Dt-jaGuG.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +934 -113
- package/dist/pipeline-analytics/index.mjs +934 -113
- 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-Bz37odzX.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-BCx3qCL5.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.
|
|
@@ -5308,7 +5448,7 @@ function isAdoptable(record, nowMs) {
|
|
|
5308
5448
|
* what `localDayMinute` does for the same input — one behaviour for a bad
|
|
5309
5449
|
* zone, not two.
|
|
5310
5450
|
*/
|
|
5311
|
-
function clockOf$
|
|
5451
|
+
function clockOf$6(atMs, timeZone) {
|
|
5312
5452
|
if (timeZone !== void 0) try {
|
|
5313
5453
|
return new Intl.DateTimeFormat("en-GB", {
|
|
5314
5454
|
timeZone,
|
|
@@ -5444,7 +5584,7 @@ var TimelapseRenderer = class {
|
|
|
5444
5584
|
includeAudio: false,
|
|
5445
5585
|
maxLifeMs: await this.ports.exportLifeMs(),
|
|
5446
5586
|
deleteAfterDownload: false,
|
|
5447
|
-
title: `${request.ruleName} ${clockOf$
|
|
5587
|
+
title: `${request.ruleName} ${clockOf$6(window.startMs, request.timezone)}→${clockOf$6(window.endMs, request.timezone)}`,
|
|
5448
5588
|
timelapse: {
|
|
5449
5589
|
everyMs,
|
|
5450
5590
|
outputFps: request.framerate,
|
|
@@ -5775,7 +5915,7 @@ var TimelapseRenderer = class {
|
|
|
5775
5915
|
...meta,
|
|
5776
5916
|
unservedWindowCount: verdict.unservedWindows.length,
|
|
5777
5917
|
unservedWindows: verdict.unservedWindows.slice(0, 12).map((w) => ({
|
|
5778
|
-
at: clockOf$
|
|
5918
|
+
at: clockOf$6(request.window.startMs + w.fromSec * 1e3, request.timezone),
|
|
5779
5919
|
fromSec: Math.round(w.fromSec),
|
|
5780
5920
|
toSec: Math.round(w.toSec),
|
|
5781
5921
|
expectedDenseFrames: w.expectedDenseFrames,
|
|
@@ -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
|
}
|
|
@@ -13755,7 +13900,7 @@ var NcPruneRecordSchema = require_clip_model_registry.object({
|
|
|
13755
13900
|
source: require_clip_model_registry._enum(["event", "reconcile"])
|
|
13756
13901
|
});
|
|
13757
13902
|
/** Rules × deleted subjects — small; a generous ceiling. */
|
|
13758
|
-
var LOAD_LIMIT$
|
|
13903
|
+
var LOAD_LIMIT$5 = 1e4;
|
|
13759
13904
|
function keyOf$1(row) {
|
|
13760
13905
|
return `${row.kind}:${row.ruleId}:${row.field}:${row.subject}`;
|
|
13761
13906
|
}
|
|
@@ -13774,7 +13919,7 @@ var NC_DEVICE_PRUNES_SPEC = {
|
|
|
13774
13919
|
return parsed.success ? parsed.data : null;
|
|
13775
13920
|
},
|
|
13776
13921
|
deviceIdOf: (row) => row.subjectKind === "device" ? Number(row.subject) : void 0,
|
|
13777
|
-
loadLimit: LOAD_LIMIT$
|
|
13922
|
+
loadLimit: LOAD_LIMIT$5
|
|
13778
13923
|
};
|
|
13779
13924
|
/** The outcomes that mean "this last reference was already dealt with". */
|
|
13780
13925
|
var KEPT_OUTCOMES = new Set(["kept-disabled", "kept"]);
|
|
@@ -13809,7 +13954,7 @@ var NcPruneLedger = class {
|
|
|
13809
13954
|
try {
|
|
13810
13955
|
const records = await this.store.query.query({
|
|
13811
13956
|
collection: NC_DEVICE_PRUNES_COLLECTION,
|
|
13812
|
-
filter: { limit: LOAD_LIMIT$
|
|
13957
|
+
filter: { limit: LOAD_LIMIT$5 }
|
|
13813
13958
|
});
|
|
13814
13959
|
const next = /* @__PURE__ */ new Map();
|
|
13815
13960
|
for (const record of records) {
|
|
@@ -17134,12 +17279,12 @@ var NcSummaryTemplateSchema = require_clip_model_registry.object({
|
|
|
17134
17279
|
title: require_clip_model_registry.string().max(500).optional(),
|
|
17135
17280
|
body: require_clip_model_registry.string().max(2e3).optional()
|
|
17136
17281
|
});
|
|
17137
|
-
var NameField = require_clip_model_registry.string().min(1).max(200);
|
|
17282
|
+
var NameField$1 = require_clip_model_registry.string().min(1).max(200);
|
|
17138
17283
|
/** MULTI-CAMERA is the point of the feature — a one-camera digest is a
|
|
17139
17284
|
* timelapse with extra steps, but it is allowed and it is cheap. */
|
|
17140
17285
|
var DeviceIdsField = require_clip_model_registry.array(require_clip_model_registry.number()).min(1).max(64);
|
|
17141
|
-
var TargetsField = require_clip_model_registry.array(require_clip_model_registry.NcRuleTargetSchema).min(1);
|
|
17142
|
-
var PriorityField = require_clip_model_registry.number().int().min(1).max(5);
|
|
17286
|
+
var TargetsField$1 = require_clip_model_registry.array(require_clip_model_registry.NcRuleTargetSchema).min(1);
|
|
17287
|
+
var PriorityField$1 = require_clip_model_registry.number().int().min(1).max(5);
|
|
17143
17288
|
/**
|
|
17144
17289
|
* The mosaic's tile ceiling. 9 (a 3×3 grid) is the shipped default: it is the
|
|
17145
17290
|
* most a phone lock-screen thumbnail can carry and still be read, and it is the
|
|
@@ -17304,7 +17449,7 @@ var NcSummaryAiSchema = require_clip_model_registry.object({
|
|
|
17304
17449
|
onFailure: require_clip_model_registry._enum(["deliver-without-text", "skip"]).optional()
|
|
17305
17450
|
});
|
|
17306
17451
|
var NcSummaryRuleInputSchema = require_clip_model_registry.object({
|
|
17307
|
-
name: NameField,
|
|
17452
|
+
name: NameField$1,
|
|
17308
17453
|
enabled: require_clip_model_registry.boolean().default(true),
|
|
17309
17454
|
deviceIds: DeviceIdsField,
|
|
17310
17455
|
window: NcSummaryWindowSchema,
|
|
@@ -17313,7 +17458,7 @@ var NcSummaryRuleInputSchema = require_clip_model_registry.object({
|
|
|
17313
17458
|
maxAiImages: MaxAiImagesField.default(NC_SUMMARY_DEFAULT_AI_IMAGES),
|
|
17314
17459
|
rank: NcSummaryRankSchema.default("importance"),
|
|
17315
17460
|
barrierSeconds: BarrierSecondsField.default(120),
|
|
17316
|
-
targets: TargetsField,
|
|
17461
|
+
targets: TargetsField$1,
|
|
17317
17462
|
template: NcSummaryTemplateSchema.optional(),
|
|
17318
17463
|
/** Burned onto the mosaic's title band — and ONLY when `captions` is `all`. */
|
|
17319
17464
|
captions: NcSummaryCaptionsSchema.default(NC_SUMMARY_DEFAULT_CAPTIONS),
|
|
@@ -17323,7 +17468,7 @@ var NcSummaryRuleInputSchema = require_clip_model_registry.object({
|
|
|
17323
17468
|
/** Deliver a window that matched NOTHING? Default no: a nightly "0 events"
|
|
17324
17469
|
* is a line nobody reads, and it costs the lines beside it their reader. */
|
|
17325
17470
|
deliverEmpty: require_clip_model_registry.boolean().default(false),
|
|
17326
|
-
priority: PriorityField.default(3),
|
|
17471
|
+
priority: PriorityField$1.default(3),
|
|
17327
17472
|
ai: NcSummaryAiSchema.optional()
|
|
17328
17473
|
});
|
|
17329
17474
|
/**
|
|
@@ -17335,7 +17480,7 @@ var NcSummaryRuleInputSchema = require_clip_model_registry.object({
|
|
|
17335
17480
|
* plain `.optional()`.
|
|
17336
17481
|
*/
|
|
17337
17482
|
var NcSummaryRulePatchSchema = require_clip_model_registry.object({
|
|
17338
|
-
name: NameField.optional(),
|
|
17483
|
+
name: NameField$1.optional(),
|
|
17339
17484
|
enabled: require_clip_model_registry.boolean().optional(),
|
|
17340
17485
|
deviceIds: DeviceIdsField.optional(),
|
|
17341
17486
|
window: NcSummaryWindowSchema.optional(),
|
|
@@ -17344,12 +17489,12 @@ var NcSummaryRulePatchSchema = require_clip_model_registry.object({
|
|
|
17344
17489
|
maxAiImages: MaxAiImagesField.optional(),
|
|
17345
17490
|
rank: NcSummaryRankSchema.optional(),
|
|
17346
17491
|
barrierSeconds: BarrierSecondsField.optional(),
|
|
17347
|
-
targets: TargetsField.optional(),
|
|
17492
|
+
targets: TargetsField$1.optional(),
|
|
17348
17493
|
template: NcSummaryTemplateSchema.nullable().optional(),
|
|
17349
17494
|
captions: NcSummaryCaptionsSchema.optional(),
|
|
17350
17495
|
captionText: require_clip_model_registry.string().max(200).optional(),
|
|
17351
17496
|
deliverEmpty: require_clip_model_registry.boolean().optional(),
|
|
17352
|
-
priority: PriorityField.optional(),
|
|
17497
|
+
priority: PriorityField$1.optional(),
|
|
17353
17498
|
ai: NcSummaryAiSchema.nullable().optional()
|
|
17354
17499
|
});
|
|
17355
17500
|
/**
|
|
@@ -17382,6 +17527,89 @@ function assertSummaryBudget(rule) {
|
|
|
17382
17527
|
if (rule.maxAiImages > rule.maxTiles) throw new Error(`maxAiImages (${rule.maxAiImages}) cannot exceed maxTiles (${rule.maxTiles}) — the model cannot judge frames the mosaic does not contain`);
|
|
17383
17528
|
}
|
|
17384
17529
|
//#endregion
|
|
17530
|
+
//#region src/notification-center/calendar-events/calendar-events-window.ts
|
|
17531
|
+
/** One day in milliseconds. */
|
|
17532
|
+
var CALENDAR_EVENTS_DAY_MS = 1440 * 60 * 1e3;
|
|
17533
|
+
/**
|
|
17534
|
+
* Floor / ceiling on the horizon days a rule may declare.
|
|
17535
|
+
* 1 day is the minimum useful digest; 30 keeps a single notification bounded.
|
|
17536
|
+
*/
|
|
17537
|
+
var CALENDAR_EVENTS_MIN_HORIZON_DAYS = 1;
|
|
17538
|
+
var CALENDAR_EVENTS_MAX_HORIZON_DAYS = 30;
|
|
17539
|
+
var CALENDAR_EVENTS_DEFAULT_HORIZON_DAYS = 7;
|
|
17540
|
+
/**
|
|
17541
|
+
* Build the producer window for one fire.
|
|
17542
|
+
*
|
|
17543
|
+
* `horizonDays` is clamped into the legal range rather than refused — a
|
|
17544
|
+
* stored rule that predates a tighter bound must still fire.
|
|
17545
|
+
*/
|
|
17546
|
+
function calendarEventsWindow(nowMs, horizonDays) {
|
|
17547
|
+
const days = Math.min(CALENDAR_EVENTS_MAX_HORIZON_DAYS, Math.max(CALENDAR_EVENTS_MIN_HORIZON_DAYS, Math.floor(horizonDays)));
|
|
17548
|
+
return {
|
|
17549
|
+
startMs: nowMs,
|
|
17550
|
+
endMs: nowMs + days * CALENDAR_EVENTS_DAY_MS,
|
|
17551
|
+
horizonDays: days
|
|
17552
|
+
};
|
|
17553
|
+
}
|
|
17554
|
+
//#endregion
|
|
17555
|
+
//#region src/notification-center/calendar-events/calendar-events-rule.ts
|
|
17556
|
+
/**
|
|
17557
|
+
* NcCalendarEventsRule — scheduled "upcoming calendar events" digest.
|
|
17558
|
+
*
|
|
17559
|
+
* Same ownership model as Summaries/Timelapses: lives in the addon (not
|
|
17560
|
+
* `@camstack/types`) so a field is live on the next post-analysis deploy.
|
|
17561
|
+
* Rides no `NcDelivery` member; reaches the outside world through
|
|
17562
|
+
* `nc.*CalendarEvents*` bridge actions.
|
|
17563
|
+
*
|
|
17564
|
+
* Calendar selection mirrors detection classes: `calendars` = Only,
|
|
17565
|
+
* `calendarsExclude` = Except, both empty = All.
|
|
17566
|
+
*/
|
|
17567
|
+
var NcCalendarEventsTemplateSchema = require_clip_model_registry.object({
|
|
17568
|
+
title: require_clip_model_registry.string().max(500).optional(),
|
|
17569
|
+
body: require_clip_model_registry.string().max(4e3).optional()
|
|
17570
|
+
});
|
|
17571
|
+
var NameField = require_clip_model_registry.string().min(1).max(200);
|
|
17572
|
+
var TargetsField = require_clip_model_registry.array(require_clip_model_registry.NcRuleTargetSchema).min(1);
|
|
17573
|
+
var PriorityField = require_clip_model_registry.number().int().min(1).max(5);
|
|
17574
|
+
var HorizonDaysField = require_clip_model_registry.number().int().min(CALENDAR_EVENTS_MIN_HORIZON_DAYS).max(CALENDAR_EVENTS_MAX_HORIZON_DAYS);
|
|
17575
|
+
var CalendarIdsField = require_clip_model_registry.array(require_clip_model_registry.string().min(1).max(200)).max(64);
|
|
17576
|
+
var NcCalendarEventsRuleInputSchema = require_clip_model_registry.object({
|
|
17577
|
+
name: NameField,
|
|
17578
|
+
enabled: require_clip_model_registry.boolean().default(true),
|
|
17579
|
+
schedule: require_clip_model_registry.NcScheduleSchema,
|
|
17580
|
+
/** Only these calendars. Empty/absent with no exclude = All. */
|
|
17581
|
+
calendars: CalendarIdsField.optional(),
|
|
17582
|
+
/** All except these. Ignored when `calendars` is non-empty. */
|
|
17583
|
+
calendarsExclude: CalendarIdsField.optional(),
|
|
17584
|
+
horizonDays: HorizonDaysField.default(CALENDAR_EVENTS_DEFAULT_HORIZON_DAYS),
|
|
17585
|
+
/** Deliver when the horizon holds no events? Default no. */
|
|
17586
|
+
deliverEmpty: require_clip_model_registry.boolean().default(false),
|
|
17587
|
+
targets: TargetsField,
|
|
17588
|
+
template: NcCalendarEventsTemplateSchema.optional(),
|
|
17589
|
+
priority: PriorityField.default(3)
|
|
17590
|
+
});
|
|
17591
|
+
var NcCalendarEventsRulePatchSchema = require_clip_model_registry.object({
|
|
17592
|
+
name: NameField.optional(),
|
|
17593
|
+
enabled: require_clip_model_registry.boolean().optional(),
|
|
17594
|
+
schedule: require_clip_model_registry.NcScheduleSchema.optional(),
|
|
17595
|
+
calendars: CalendarIdsField.nullable().optional(),
|
|
17596
|
+
calendarsExclude: CalendarIdsField.nullable().optional(),
|
|
17597
|
+
horizonDays: HorizonDaysField.optional(),
|
|
17598
|
+
deliverEmpty: require_clip_model_registry.boolean().optional(),
|
|
17599
|
+
targets: TargetsField.optional(),
|
|
17600
|
+
template: NcCalendarEventsTemplateSchema.nullable().optional(),
|
|
17601
|
+
priority: PriorityField.optional()
|
|
17602
|
+
});
|
|
17603
|
+
var NcCalendarEventsRuleSchema = NcCalendarEventsRuleInputSchema.extend({
|
|
17604
|
+
id: require_clip_model_registry.string(),
|
|
17605
|
+
ownerUserId: require_clip_model_registry.string().optional(),
|
|
17606
|
+
/** Epoch-ms of the schedule window end that was last delivered. */
|
|
17607
|
+
generatedAt: require_clip_model_registry.number().optional(),
|
|
17608
|
+
createdBy: require_clip_model_registry.string(),
|
|
17609
|
+
createdAt: require_clip_model_registry.number(),
|
|
17610
|
+
updatedAt: require_clip_model_registry.number()
|
|
17611
|
+
});
|
|
17612
|
+
//#endregion
|
|
17385
17613
|
//#region src/notification-center/unstored-media-id.ts
|
|
17386
17614
|
/**
|
|
17387
17615
|
* An id for media bytes that were NEVER STORED.
|
|
@@ -18542,6 +18770,22 @@ var ncActions = require_clip_model_registry.defineCustomActions({
|
|
|
18542
18770
|
kind: "mutation",
|
|
18543
18771
|
caller: "required"
|
|
18544
18772
|
}),
|
|
18773
|
+
"nc.listCalendarEventsRules": require_clip_model_registry.customAction(require_clip_model_registry.object({}), require_clip_model_registry.object({
|
|
18774
|
+
rules: require_clip_model_registry.array(NcCalendarEventsRuleSchema.extend({ readOnly: require_clip_model_registry.boolean() })),
|
|
18775
|
+
canAuthor: require_clip_model_registry.boolean()
|
|
18776
|
+
}), { caller: "required" }),
|
|
18777
|
+
"nc.upsertCalendarEventsRule": require_clip_model_registry.customAction(require_clip_model_registry.object({
|
|
18778
|
+
ruleId: require_clip_model_registry.string().optional(),
|
|
18779
|
+
rule: NcCalendarEventsRuleInputSchema.optional(),
|
|
18780
|
+
patch: NcCalendarEventsRulePatchSchema.optional()
|
|
18781
|
+
}), require_clip_model_registry.object({ rule: NcCalendarEventsRuleSchema }), {
|
|
18782
|
+
kind: "mutation",
|
|
18783
|
+
caller: "required"
|
|
18784
|
+
}),
|
|
18785
|
+
"nc.deleteCalendarEventsRule": require_clip_model_registry.customAction(require_clip_model_registry.object({ ruleId: require_clip_model_registry.string() }), require_clip_model_registry.object({ success: require_clip_model_registry.literal(true) }), {
|
|
18786
|
+
kind: "mutation",
|
|
18787
|
+
caller: "required"
|
|
18788
|
+
}),
|
|
18545
18789
|
/**
|
|
18546
18790
|
* Produce one digest window RIGHT NOW — the operator's Test button.
|
|
18547
18791
|
*
|
|
@@ -18828,6 +19072,11 @@ function makeNcActionHandlers(deps) {
|
|
|
18828
19072
|
if (sum === void 0) throw new Error("this node does not host the summary producer — use the hub");
|
|
18829
19073
|
return sum;
|
|
18830
19074
|
};
|
|
19075
|
+
const requireCalendarEvents = () => {
|
|
19076
|
+
const ce = deps.calendarEvents;
|
|
19077
|
+
if (ce === void 0) throw new Error("this node does not host calendar-events rules — use the hub");
|
|
19078
|
+
return ce;
|
|
19079
|
+
};
|
|
18831
19080
|
/** The artefact index, or an honest refusal. */
|
|
18832
19081
|
const requireArtifacts = () => {
|
|
18833
19082
|
const index = deps.artifacts;
|
|
@@ -18856,6 +19105,13 @@ function makeNcActionHandlers(deps) {
|
|
|
18856
19105
|
if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: summary rule not owned: ${ruleId}`);
|
|
18857
19106
|
return rule;
|
|
18858
19107
|
};
|
|
19108
|
+
const assertCalendarEventsEditable = (ruleId, caller, ce) => {
|
|
19109
|
+
const rule = ce.store.get(ruleId);
|
|
19110
|
+
if (rule === null) throw new Error(`forbidden: calendar-events rule not found: ${ruleId}`);
|
|
19111
|
+
if (caller.isAdmin) return rule;
|
|
19112
|
+
if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: calendar-events rule not owned: ${ruleId}`);
|
|
19113
|
+
return rule;
|
|
19114
|
+
};
|
|
18859
19115
|
/** A timelapse rule the caller may EDIT. Same admin bypass as `NcRule`. */
|
|
18860
19116
|
const assertTimelapseEditable = (ruleId, caller, tl) => {
|
|
18861
19117
|
const rule = tl.store.get(ruleId);
|
|
@@ -19397,6 +19653,53 @@ function makeNcActionHandlers(deps) {
|
|
|
19397
19653
|
...out.skippedByAi === true ? { skippedByAi: true } : {}
|
|
19398
19654
|
};
|
|
19399
19655
|
},
|
|
19656
|
+
"nc.listCalendarEventsRules": async (_input, caller) => {
|
|
19657
|
+
const c = requireCaller$1(caller);
|
|
19658
|
+
const ce = requireCalendarEvents();
|
|
19659
|
+
return {
|
|
19660
|
+
rules: (c.isAdmin ? ce.store.list() : ce.store.listForOwner(c.userId)).map((r) => ({
|
|
19661
|
+
...r,
|
|
19662
|
+
readOnly: !c.isAdmin && r.ownerUserId !== c.userId
|
|
19663
|
+
})),
|
|
19664
|
+
canAuthor: true
|
|
19665
|
+
};
|
|
19666
|
+
},
|
|
19667
|
+
"nc.upsertCalendarEventsRule": async (input, caller) => {
|
|
19668
|
+
const c = requireCaller$1(caller);
|
|
19669
|
+
const ce = requireCalendarEvents();
|
|
19670
|
+
if (input.ruleId === void 0) {
|
|
19671
|
+
if (input.rule === void 0) throw new Error("upsertCalendarEventsRule: rule or ruleId required");
|
|
19672
|
+
const parsed = NcCalendarEventsRuleInputSchema.parse(input.rule);
|
|
19673
|
+
await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
|
|
19674
|
+
const rule = await ce.store.create(parsed, c.userId, c.isAdmin ? void 0 : c.userId);
|
|
19675
|
+
deps.logger.info("calendar-events rule created", { meta: {
|
|
19676
|
+
ruleId: rule.id,
|
|
19677
|
+
owner: c.userId
|
|
19678
|
+
} });
|
|
19679
|
+
return { rule };
|
|
19680
|
+
}
|
|
19681
|
+
assertCalendarEventsEditable(input.ruleId, c, ce);
|
|
19682
|
+
if (input.patch === void 0) throw new Error("upsertCalendarEventsRule: patch required");
|
|
19683
|
+
const patch = NcCalendarEventsRulePatchSchema.parse(input.patch);
|
|
19684
|
+
if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
|
|
19685
|
+
const rule = await ce.store.update(input.ruleId, patch);
|
|
19686
|
+
deps.logger.info("calendar-events rule updated", { meta: {
|
|
19687
|
+
ruleId: rule.id,
|
|
19688
|
+
owner: c.userId
|
|
19689
|
+
} });
|
|
19690
|
+
return { rule };
|
|
19691
|
+
},
|
|
19692
|
+
"nc.deleteCalendarEventsRule": async (input, caller) => {
|
|
19693
|
+
const c = requireCaller$1(caller);
|
|
19694
|
+
const ce = requireCalendarEvents();
|
|
19695
|
+
assertCalendarEventsEditable(input.ruleId, c, ce);
|
|
19696
|
+
await ce.store.delete(input.ruleId);
|
|
19697
|
+
deps.logger.info("calendar-events rule deleted", { meta: {
|
|
19698
|
+
ruleId: input.ruleId,
|
|
19699
|
+
owner: c.userId
|
|
19700
|
+
} });
|
|
19701
|
+
return { success: true };
|
|
19702
|
+
},
|
|
19400
19703
|
"nc.getTexts": async (input) => {
|
|
19401
19704
|
const texts = requireTexts();
|
|
19402
19705
|
return input.language === void 0 ? texts.view() : texts.view(input.language);
|
|
@@ -20009,19 +20312,39 @@ function planSummaryDevices(rule, isDead, guard) {
|
|
|
20009
20312
|
notes: notes.notes
|
|
20010
20313
|
};
|
|
20011
20314
|
}
|
|
20315
|
+
function stripDeadFromLists(lists, isDead) {
|
|
20316
|
+
const dead = lists.flatMap((list) => list.deviceIds.filter(isDead));
|
|
20317
|
+
if (dead.length === 0) return {
|
|
20318
|
+
next: lists,
|
|
20319
|
+
dead
|
|
20320
|
+
};
|
|
20321
|
+
return {
|
|
20322
|
+
dead,
|
|
20323
|
+
next: lists.map((list) => ({
|
|
20324
|
+
...list,
|
|
20325
|
+
deviceIds: list.deviceIds.filter((id) => !isDead(id))
|
|
20326
|
+
}))
|
|
20327
|
+
};
|
|
20328
|
+
}
|
|
20012
20329
|
function planAlarmDevices(alarm, isDead) {
|
|
20013
20330
|
if (alarm === null) return null;
|
|
20014
|
-
const
|
|
20015
|
-
const
|
|
20331
|
+
const nonBlocking = stripDeadFromLists(alarm.nonBlocking, isDead);
|
|
20332
|
+
const blocking = stripDeadFromLists(alarm.blocking, isDead);
|
|
20333
|
+
const secureOnArmOff = stripDeadFromLists(alarm.secureOnArmOff, isDead);
|
|
20016
20334
|
const forgetExcluded = alarm.excluded.filter(isDead);
|
|
20017
|
-
|
|
20335
|
+
const deadIds = [
|
|
20336
|
+
...nonBlocking.dead,
|
|
20337
|
+
...blocking.dead,
|
|
20338
|
+
...secureOnArmOff.dead,
|
|
20339
|
+
...forgetExcluded
|
|
20340
|
+
];
|
|
20341
|
+
if (deadIds.length === 0) return null;
|
|
20018
20342
|
return {
|
|
20019
|
-
dropped: deviceSubjects([...new Set(
|
|
20343
|
+
dropped: deviceSubjects([...new Set(deadIds)]),
|
|
20020
20344
|
droppedKind: "device",
|
|
20021
|
-
...
|
|
20022
|
-
|
|
20023
|
-
|
|
20024
|
-
})) } : {},
|
|
20345
|
+
...nonBlocking.dead.length > 0 ? { nonBlocking: nonBlocking.next } : {},
|
|
20346
|
+
...blocking.dead.length > 0 ? { blocking: blocking.next } : {},
|
|
20347
|
+
...secureOnArmOff.dead.length > 0 ? { secureOnArmOff: secureOnArmOff.next } : {},
|
|
20025
20348
|
forgetExcluded
|
|
20026
20349
|
};
|
|
20027
20350
|
}
|
|
@@ -20063,6 +20386,8 @@ function referencedDeviceIds(snapshot) {
|
|
|
20063
20386
|
for (const rule of snapshot.timelapse) add(rule.deviceIds);
|
|
20064
20387
|
for (const rule of snapshot.summary) add(rule.deviceIds);
|
|
20065
20388
|
for (const list of snapshot.alarm?.nonBlocking ?? []) add(list.deviceIds);
|
|
20389
|
+
for (const list of snapshot.alarm?.blocking ?? []) add(list.deviceIds);
|
|
20390
|
+
for (const list of snapshot.alarm?.secureOnArmOff ?? []) add(list.deviceIds);
|
|
20066
20391
|
add(snapshot.alarm?.excluded);
|
|
20067
20392
|
add(snapshot.mutedDeviceIds);
|
|
20068
20393
|
return ids;
|
|
@@ -20407,6 +20732,8 @@ var NcDeletedSubjectPruner = class {
|
|
|
20407
20732
|
summary: this.writable("summary", this.deps.summary.list()),
|
|
20408
20733
|
alarm: panel !== null && settings !== void 0 ? {
|
|
20409
20734
|
nonBlocking: settings.nonBlocking,
|
|
20735
|
+
blocking: settings.blocking,
|
|
20736
|
+
secureOnArmOff: settings.secureOnArmOff,
|
|
20410
20737
|
announceTargets: settings.announceTargets,
|
|
20411
20738
|
excluded: panel.excludedDevices()
|
|
20412
20739
|
} : null,
|
|
@@ -20563,6 +20890,8 @@ var NcDeletedSubjectPruner = class {
|
|
|
20563
20890
|
...NO_SNAPSHOT,
|
|
20564
20891
|
alarm: {
|
|
20565
20892
|
nonBlocking: settings.nonBlocking,
|
|
20893
|
+
blocking: settings.blocking,
|
|
20894
|
+
secureOnArmOff: settings.secureOnArmOff,
|
|
20566
20895
|
announceTargets: settings.announceTargets,
|
|
20567
20896
|
excluded: panel.excludedDevices()
|
|
20568
20897
|
}
|
|
@@ -20571,6 +20900,8 @@ var NcDeletedSubjectPruner = class {
|
|
|
20571
20900
|
try {
|
|
20572
20901
|
const patch = {
|
|
20573
20902
|
...edit.nonBlocking !== void 0 ? { nonBlocking: edit.nonBlocking } : {},
|
|
20903
|
+
...edit.blocking !== void 0 ? { blocking: edit.blocking } : {},
|
|
20904
|
+
...edit.secureOnArmOff !== void 0 ? { secureOnArmOff: edit.secureOnArmOff } : {},
|
|
20574
20905
|
...edit.announceTargets !== void 0 ? { announceTargets: [...edit.announceTargets] } : {}
|
|
20575
20906
|
};
|
|
20576
20907
|
if (Object.keys(patch).length > 0) await panel.applySettings(patch);
|
|
@@ -28232,7 +28563,8 @@ var OUTBOX_RECORD_KINDS = new Set([
|
|
|
28232
28563
|
"audio-window",
|
|
28233
28564
|
"system-event",
|
|
28234
28565
|
"timelapse",
|
|
28235
|
-
"ai-summary"
|
|
28566
|
+
"ai-summary",
|
|
28567
|
+
"calendar-events"
|
|
28236
28568
|
]);
|
|
28237
28569
|
function isOutboxRecordKind(x) {
|
|
28238
28570
|
return typeof x === "string" && OUTBOX_RECORD_KINDS.has(x);
|
|
@@ -28585,7 +28917,7 @@ function buildRuleAiSystemPrompt(shape, language, maxWords, grouped = false) {
|
|
|
28585
28917
|
].join("");
|
|
28586
28918
|
}
|
|
28587
28919
|
/** `HH:MM` in the host timezone. */
|
|
28588
|
-
function clockOf$
|
|
28920
|
+
function clockOf$5(atMs) {
|
|
28589
28921
|
const d = new Date(atMs);
|
|
28590
28922
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
28591
28923
|
}
|
|
@@ -28623,7 +28955,7 @@ function buildRuleAiPrompt(input) {
|
|
|
28623
28955
|
if (shape === "detection-track") {
|
|
28624
28956
|
lines.push(`Camera: ${camera}.${located} Subject: ${subject}. The stills, in order:`);
|
|
28625
28957
|
for (const [i, frame] of input.frames.entries()) {
|
|
28626
|
-
const when = frame.atMs !== void 0 ? ` · ${clockOf$
|
|
28958
|
+
const when = frame.atMs !== void 0 ? ` · ${clockOf$5(frame.atMs)}` : "";
|
|
28627
28959
|
lines.push(`${String(i + 1)}.${when}`);
|
|
28628
28960
|
}
|
|
28629
28961
|
return lines.join("\n");
|
|
@@ -28920,7 +29252,7 @@ var NcRuleAiRowSchema = require_clip_model_registry.object({
|
|
|
28920
29252
|
updatedAt: require_clip_model_registry.number()
|
|
28921
29253
|
});
|
|
28922
29254
|
/** Operator-authored and tiny; a generous ceiling. */
|
|
28923
|
-
var LOAD_LIMIT$
|
|
29255
|
+
var LOAD_LIMIT$4 = 1e4;
|
|
28924
29256
|
var NC_RULE_AI_SPEC = {
|
|
28925
29257
|
collection: NC_RULE_AI_COLLECTION,
|
|
28926
29258
|
columns: [...NC_RULE_AI_COLUMNS],
|
|
@@ -28936,7 +29268,7 @@ var NC_RULE_AI_SPEC = {
|
|
|
28936
29268
|
const parsed = NcRuleAiRowSchema.safeParse(data["row"]);
|
|
28937
29269
|
return parsed.success ? parsed.data : null;
|
|
28938
29270
|
},
|
|
28939
|
-
loadLimit: LOAD_LIMIT$
|
|
29271
|
+
loadLimit: LOAD_LIMIT$4
|
|
28940
29272
|
};
|
|
28941
29273
|
var NcRuleAiStore = class {
|
|
28942
29274
|
ledger;
|
|
@@ -29273,7 +29605,7 @@ var NC_RULES_INDEXES = [{
|
|
|
29273
29605
|
columns: ["enabled"]
|
|
29274
29606
|
}];
|
|
29275
29607
|
/** Query cap — the rule set is operator-authored and tiny; a generous ceiling. */
|
|
29276
|
-
var LOAD_LIMIT$
|
|
29608
|
+
var LOAD_LIMIT$3 = 1e4;
|
|
29277
29609
|
function isRecord$3(value) {
|
|
29278
29610
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29279
29611
|
}
|
|
@@ -29322,7 +29654,7 @@ function makeRulesSpec(onProjection) {
|
|
|
29322
29654
|
if (projection.kind === "projected") onProjection?.(key, projection);
|
|
29323
29655
|
return parsed.data;
|
|
29324
29656
|
},
|
|
29325
|
-
loadLimit: LOAD_LIMIT$
|
|
29657
|
+
loadLimit: LOAD_LIMIT$3
|
|
29326
29658
|
};
|
|
29327
29659
|
}
|
|
29328
29660
|
/** The spec `declare` needs — no sink: declaring a collection projects nothing. */
|
|
@@ -30394,7 +30726,7 @@ var DEFAULT_PROMPT_EN = "What happened during this period? Summarise it for a ho
|
|
|
30394
30726
|
*/
|
|
30395
30727
|
var SummaryAnswerSchema = require_clip_model_registry.object({ summary: require_clip_model_registry.string() });
|
|
30396
30728
|
/** `HH:MM` in the host timezone — the two numbers a frame label needs. */
|
|
30397
|
-
function clockOf$
|
|
30729
|
+
function clockOf$4(atMs) {
|
|
30398
30730
|
const d = new Date(atMs);
|
|
30399
30731
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
30400
30732
|
}
|
|
@@ -30417,7 +30749,7 @@ function buildSummaryAiPrompt(input, frames) {
|
|
|
30417
30749
|
const roster = frames.map((f, i) => {
|
|
30418
30750
|
const location = plainLabel(f.location ?? "");
|
|
30419
30751
|
const where = location.length > 0 ? ` (${location})` : "";
|
|
30420
|
-
return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$
|
|
30752
|
+
return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$4(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
|
|
30421
30753
|
}).join("\n");
|
|
30422
30754
|
const described = /* @__PURE__ */ new Map();
|
|
30423
30755
|
for (const f of frames) {
|
|
@@ -31020,7 +31352,7 @@ var NO_DETECTIONS$1 = {
|
|
|
31020
31352
|
* has to check is a key somebody can get wrong. */
|
|
31021
31353
|
var BODY_SEPARATOR$1 = " · ";
|
|
31022
31354
|
/** `HH:MM` in the host timezone. */
|
|
31023
|
-
function clockOf$
|
|
31355
|
+
function clockOf$3(atMs) {
|
|
31024
31356
|
const d = new Date(atMs);
|
|
31025
31357
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
31026
31358
|
}
|
|
@@ -31034,8 +31366,8 @@ function clockOf$2(atMs) {
|
|
|
31034
31366
|
function summaryTemplateVars(input) {
|
|
31035
31367
|
return {
|
|
31036
31368
|
rule: input.ruleName,
|
|
31037
|
-
from: clockOf$
|
|
31038
|
-
to: clockOf$
|
|
31369
|
+
from: clockOf$3(input.window.startMs),
|
|
31370
|
+
to: clockOf$3(input.window.endMs),
|
|
31039
31371
|
time: ncClockText(input.texts.language, input.generatedAt),
|
|
31040
31372
|
events: String(input.tileCount),
|
|
31041
31373
|
cameras: String(input.cameraCount),
|
|
@@ -31079,7 +31411,7 @@ function composeSummaryBody(input) {
|
|
|
31079
31411
|
* string, for the reason the timelapse learned: a hand-rolled `n === 1` shipped
|
|
31080
31412
|
* "1 intervalli ripresi" to a live phone.
|
|
31081
31413
|
*/
|
|
31082
|
-
function derivedBody$
|
|
31414
|
+
function derivedBody$2(input, vars) {
|
|
31083
31415
|
const texts = input.texts;
|
|
31084
31416
|
const parts = [texts.text({
|
|
31085
31417
|
key: "summary.body.window",
|
|
@@ -31114,7 +31446,7 @@ function derivedBody$1(input, vars) {
|
|
|
31114
31446
|
}
|
|
31115
31447
|
/** Per-target params with the derived `clickUrl` filled in where absent. A
|
|
31116
31448
|
* value the operator set on the target WINS — he chose it. */
|
|
31117
|
-
function paramsFor$
|
|
31449
|
+
function paramsFor$2(target, clickUrl) {
|
|
31118
31450
|
if (clickUrl === void 0) return target.params;
|
|
31119
31451
|
if (target.params === void 0) return { clickUrl };
|
|
31120
31452
|
if (typeof target.params["clickUrl"] === "string") return target.params;
|
|
@@ -31130,11 +31462,11 @@ function summaryRecordId(ruleId, windowEndMs) {
|
|
|
31130
31462
|
/** The caption burned onto the mosaic's title band, with the same `{{var}}`
|
|
31131
31463
|
* vocabulary the title and body use. `''` is a real answer ("no caption"). */
|
|
31132
31464
|
function summaryCaptionText(input) {
|
|
31133
|
-
if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$
|
|
31465
|
+
if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$3(input.window.startMs)}→${clockOf$3(input.window.endMs)}`;
|
|
31134
31466
|
return renderTemplate(input.captionText, {
|
|
31135
31467
|
rule: input.ruleName,
|
|
31136
|
-
from: clockOf$
|
|
31137
|
-
to: clockOf$
|
|
31468
|
+
from: clockOf$3(input.window.startMs),
|
|
31469
|
+
to: clockOf$3(input.window.endMs)
|
|
31138
31470
|
}) ?? "";
|
|
31139
31471
|
}
|
|
31140
31472
|
/**
|
|
@@ -31152,7 +31484,7 @@ function buildSummaryOutboxInputs(input) {
|
|
|
31152
31484
|
const body = composeSummaryBody({
|
|
31153
31485
|
aiText: aiSentence(input),
|
|
31154
31486
|
renderedTemplateBody: renderTemplate(input.template?.body, vars),
|
|
31155
|
-
derivedBody: derivedBody$
|
|
31487
|
+
derivedBody: derivedBody$2(input, vars),
|
|
31156
31488
|
templateNamesAi: templatePlaceholders(input.template?.body ?? "").includes("ai")
|
|
31157
31489
|
});
|
|
31158
31490
|
const recordId = summaryRecordId(input.ruleId, input.window.endMs);
|
|
@@ -31171,7 +31503,7 @@ function buildSummaryOutboxInputs(input) {
|
|
|
31171
31503
|
},
|
|
31172
31504
|
media: "none",
|
|
31173
31505
|
...artifacts.length > 0 ? { artifacts } : {},
|
|
31174
|
-
...paramsFor$
|
|
31506
|
+
...paramsFor$2(target, input.clickUrl) !== void 0 ? { params: paramsFor$2(target, input.clickUrl) } : {},
|
|
31175
31507
|
subject: {
|
|
31176
31508
|
deviceId: input.primaryDeviceId,
|
|
31177
31509
|
className: "summary",
|
|
@@ -32066,7 +32398,7 @@ var NcSummaryProducer = class {
|
|
|
32066
32398
|
purpose: "summary",
|
|
32067
32399
|
tiles: tiles.map(({ candidate, jpeg }) => ({
|
|
32068
32400
|
jpeg,
|
|
32069
|
-
...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$
|
|
32401
|
+
...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$2(candidate.firstSeen)} · ${candidate.className}` } : {},
|
|
32070
32402
|
deviceId: candidate.deviceId,
|
|
32071
32403
|
sourceId: candidate.trackId
|
|
32072
32404
|
})),
|
|
@@ -32289,7 +32621,7 @@ var NcSummaryProducer = class {
|
|
|
32289
32621
|
}
|
|
32290
32622
|
};
|
|
32291
32623
|
/** `HH:MM` in the host timezone — the two numbers a tile label needs. */
|
|
32292
|
-
function clockOf$
|
|
32624
|
+
function clockOf$2(atMs) {
|
|
32293
32625
|
const d = new Date(atMs);
|
|
32294
32626
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
32295
32627
|
}
|
|
@@ -32365,7 +32697,7 @@ var NC_SUMMARY_RULES_INDEXES = [{
|
|
|
32365
32697
|
columns: ["enabled"]
|
|
32366
32698
|
}];
|
|
32367
32699
|
/** The rule set is operator-authored and tiny; a generous ceiling. */
|
|
32368
|
-
var LOAD_LIMIT$
|
|
32700
|
+
var LOAD_LIMIT$2 = 1e4;
|
|
32369
32701
|
/**
|
|
32370
32702
|
* The ledger contract for the digest rule set.
|
|
32371
32703
|
*
|
|
@@ -32395,7 +32727,7 @@ var NC_SUMMARY_RULES_SPEC = {
|
|
|
32395
32727
|
const parsed = NcSummaryRuleSchema.safeParse(data["rule"]);
|
|
32396
32728
|
return parsed.success ? parsed.data : null;
|
|
32397
32729
|
},
|
|
32398
|
-
loadLimit: LOAD_LIMIT$
|
|
32730
|
+
loadLimit: LOAD_LIMIT$2
|
|
32399
32731
|
};
|
|
32400
32732
|
/**
|
|
32401
32733
|
* The three-way CLEARABLE patch signal, one function per field: `undefined`
|
|
@@ -32406,7 +32738,7 @@ var NC_SUMMARY_RULES_SPEC = {
|
|
|
32406
32738
|
* cast here would be a cast on the exact path that decides whether a persisted
|
|
32407
32739
|
* rule keeps a field. Three literal destructurings are three provable ones.
|
|
32408
32740
|
*/
|
|
32409
|
-
function applyTemplatePatch$
|
|
32741
|
+
function applyTemplatePatch$2(merged, template) {
|
|
32410
32742
|
if (template === void 0) return merged;
|
|
32411
32743
|
if (template !== null) return {
|
|
32412
32744
|
...merged,
|
|
@@ -32552,7 +32884,7 @@ var NcSummaryStore = class {
|
|
|
32552
32884
|
createdAt: existing.createdAt,
|
|
32553
32885
|
updatedAt: this.now()
|
|
32554
32886
|
};
|
|
32555
|
-
return this.write(applyAiPatch(applyFiltersPatch(applyTemplatePatch$
|
|
32887
|
+
return this.write(applyAiPatch(applyFiltersPatch(applyTemplatePatch$2(merged, template), filters), ai));
|
|
32556
32888
|
}
|
|
32557
32889
|
async setEnabled(ruleId, enabled) {
|
|
32558
32890
|
return this.update(ruleId, { enabled });
|
|
@@ -32604,6 +32936,408 @@ var NcSummaryStore = class {
|
|
|
32604
32936
|
}
|
|
32605
32937
|
};
|
|
32606
32938
|
//#endregion
|
|
32939
|
+
//#region src/notification-center/calendar-events/calendar-events-store.ts
|
|
32940
|
+
/**
|
|
32941
|
+
* Durable calendar-events rule set — same DurableLedger mechanics as
|
|
32942
|
+
* Summaries, with a per-rule `generatedAt` stamp (one fire per schedule window).
|
|
32943
|
+
*/
|
|
32944
|
+
/**
|
|
32945
|
+
* @durable class=config owner=notification-center
|
|
32946
|
+
* write="an operator creates or edits a calendar-events rule, or the producer stamps a delivered window via markGenerated"
|
|
32947
|
+
* retention="none — a row goes only when the operator deletes the rule."
|
|
32948
|
+
*/
|
|
32949
|
+
var NC_CALENDAR_EVENTS_RULES_COLLECTION = "notification-center:calendar-events-rules";
|
|
32950
|
+
var NC_CALENDAR_EVENTS_RULES_COLUMNS = [
|
|
32951
|
+
{
|
|
32952
|
+
name: "id",
|
|
32953
|
+
type: "TEXT",
|
|
32954
|
+
primaryKey: true,
|
|
32955
|
+
notNull: true
|
|
32956
|
+
},
|
|
32957
|
+
{
|
|
32958
|
+
name: "name",
|
|
32959
|
+
type: "TEXT",
|
|
32960
|
+
notNull: true
|
|
32961
|
+
},
|
|
32962
|
+
{
|
|
32963
|
+
name: "enabled",
|
|
32964
|
+
type: "BOOLEAN",
|
|
32965
|
+
notNull: true
|
|
32966
|
+
},
|
|
32967
|
+
{
|
|
32968
|
+
name: "updatedAt",
|
|
32969
|
+
type: "INTEGER",
|
|
32970
|
+
notNull: true
|
|
32971
|
+
},
|
|
32972
|
+
{
|
|
32973
|
+
name: "rule",
|
|
32974
|
+
type: "JSON",
|
|
32975
|
+
notNull: true
|
|
32976
|
+
}
|
|
32977
|
+
];
|
|
32978
|
+
var NC_CALENDAR_EVENTS_RULES_INDEXES = [{
|
|
32979
|
+
name: "idx_nc_calendar_events_rules_enabled",
|
|
32980
|
+
columns: ["enabled"]
|
|
32981
|
+
}];
|
|
32982
|
+
var LOAD_LIMIT$1 = 1e4;
|
|
32983
|
+
var NC_CALENDAR_EVENTS_RULES_SPEC = {
|
|
32984
|
+
collection: NC_CALENDAR_EVENTS_RULES_COLLECTION,
|
|
32985
|
+
columns: [...NC_CALENDAR_EVENTS_RULES_COLUMNS],
|
|
32986
|
+
indexes: [...NC_CALENDAR_EVENTS_RULES_INDEXES],
|
|
32987
|
+
writeMode: "write-through",
|
|
32988
|
+
keyOf: (rule) => rule.id,
|
|
32989
|
+
toValue: (rule) => ({
|
|
32990
|
+
name: rule.name,
|
|
32991
|
+
enabled: rule.enabled,
|
|
32992
|
+
updatedAt: rule.updatedAt,
|
|
32993
|
+
rule
|
|
32994
|
+
}),
|
|
32995
|
+
fromRecord: (_key, data) => {
|
|
32996
|
+
const parsed = NcCalendarEventsRuleSchema.safeParse(data["rule"]);
|
|
32997
|
+
return parsed.success ? parsed.data : null;
|
|
32998
|
+
},
|
|
32999
|
+
loadLimit: LOAD_LIMIT$1
|
|
33000
|
+
};
|
|
33001
|
+
function applyTemplatePatch$1(merged, template) {
|
|
33002
|
+
if (template === void 0) return merged;
|
|
33003
|
+
if (template !== null) return {
|
|
33004
|
+
...merged,
|
|
33005
|
+
template
|
|
33006
|
+
};
|
|
33007
|
+
const { template: _cleared, ...rest } = merged;
|
|
33008
|
+
return rest;
|
|
33009
|
+
}
|
|
33010
|
+
function applyOptionalStringList(merged, key, value) {
|
|
33011
|
+
if (value === void 0) return merged;
|
|
33012
|
+
if (value !== null) return {
|
|
33013
|
+
...merged,
|
|
33014
|
+
[key]: [...value]
|
|
33015
|
+
};
|
|
33016
|
+
const { [key]: _cleared, ...rest } = merged;
|
|
33017
|
+
return rest;
|
|
33018
|
+
}
|
|
33019
|
+
var NcCalendarEventsStore = class {
|
|
33020
|
+
ledger;
|
|
33021
|
+
store;
|
|
33022
|
+
logger;
|
|
33023
|
+
now;
|
|
33024
|
+
newId;
|
|
33025
|
+
constructor(deps) {
|
|
33026
|
+
this.store = deps.store;
|
|
33027
|
+
this.ledger = new DurableLedger({
|
|
33028
|
+
spec: NC_CALENDAR_EVENTS_RULES_SPEC,
|
|
33029
|
+
store: deps.store,
|
|
33030
|
+
logger: deps.logger
|
|
33031
|
+
});
|
|
33032
|
+
this.logger = deps.logger;
|
|
33033
|
+
this.now = deps.now ?? (() => Date.now());
|
|
33034
|
+
this.newId = deps.newId ?? (() => (0, node_crypto.randomUUID)());
|
|
33035
|
+
}
|
|
33036
|
+
static async declare(store) {
|
|
33037
|
+
await DurableLedger.declare(store, NC_CALENDAR_EVENTS_RULES_SPEC);
|
|
33038
|
+
}
|
|
33039
|
+
async load() {
|
|
33040
|
+
await this.ledger.load();
|
|
33041
|
+
this.logger.debug("calendar-events rules loaded", { meta: { rules: this.ledger.size } });
|
|
33042
|
+
}
|
|
33043
|
+
list() {
|
|
33044
|
+
return this.ledger.snapshot().toSorted((a, b) => b.updatedAt - a.updatedAt);
|
|
33045
|
+
}
|
|
33046
|
+
listEnabled() {
|
|
33047
|
+
return this.list().filter((r) => r.enabled);
|
|
33048
|
+
}
|
|
33049
|
+
get(ruleId) {
|
|
33050
|
+
return this.ledger.get(ruleId) ?? null;
|
|
33051
|
+
}
|
|
33052
|
+
listForOwner(userId) {
|
|
33053
|
+
return this.list().filter((r) => r.ownerUserId === void 0 || r.ownerUserId === userId);
|
|
33054
|
+
}
|
|
33055
|
+
async create(input, createdBy, ownerUserId) {
|
|
33056
|
+
const now = this.now();
|
|
33057
|
+
const rule = NcCalendarEventsRuleSchema.parse({
|
|
33058
|
+
...NcCalendarEventsRuleInputSchema.parse(input),
|
|
33059
|
+
id: this.newId(),
|
|
33060
|
+
...ownerUserId !== void 0 ? { ownerUserId } : {},
|
|
33061
|
+
createdBy,
|
|
33062
|
+
createdAt: now,
|
|
33063
|
+
updatedAt: now
|
|
33064
|
+
});
|
|
33065
|
+
await this.ledger.put(rule);
|
|
33066
|
+
return rule;
|
|
33067
|
+
}
|
|
33068
|
+
async update(ruleId, patch) {
|
|
33069
|
+
const existing = this.ledger.get(ruleId);
|
|
33070
|
+
if (!existing) throw new Error(`calendar-events rule not found: ${ruleId}`);
|
|
33071
|
+
const { template, calendars, calendarsExclude, ...rest } = patch;
|
|
33072
|
+
const merged = {
|
|
33073
|
+
...existing,
|
|
33074
|
+
...rest,
|
|
33075
|
+
id: existing.id,
|
|
33076
|
+
...existing.ownerUserId !== void 0 ? { ownerUserId: existing.ownerUserId } : {},
|
|
33077
|
+
...existing.generatedAt !== void 0 ? { generatedAt: existing.generatedAt } : {},
|
|
33078
|
+
createdBy: existing.createdBy,
|
|
33079
|
+
createdAt: existing.createdAt,
|
|
33080
|
+
updatedAt: this.now()
|
|
33081
|
+
};
|
|
33082
|
+
return this.write(applyOptionalStringList(applyOptionalStringList(applyTemplatePatch$1(merged, template), "calendars", calendars), "calendarsExclude", calendarsExclude));
|
|
33083
|
+
}
|
|
33084
|
+
async setEnabled(ruleId, enabled) {
|
|
33085
|
+
return this.update(ruleId, { enabled });
|
|
33086
|
+
}
|
|
33087
|
+
async markGenerated(ruleId, windowEndMs) {
|
|
33088
|
+
const existing = this.ledger.get(ruleId);
|
|
33089
|
+
if (!existing) throw new Error(`calendar-events rule not found: ${ruleId}`);
|
|
33090
|
+
return this.write({
|
|
33091
|
+
...existing,
|
|
33092
|
+
generatedAt: Math.max(existing.generatedAt ?? 0, windowEndMs)
|
|
33093
|
+
});
|
|
33094
|
+
}
|
|
33095
|
+
/**
|
|
33096
|
+
* Idempotent delete — unknown ids are a no-op in RAM, still attempted on disk.
|
|
33097
|
+
*
|
|
33098
|
+
* Deliberately NOT {@link DurableLedger.forget}: that swallows a failed
|
|
33099
|
+
* durable delete. Mirror eviction first; durable half re-throws on failure
|
|
33100
|
+
* (same shape as Summaries).
|
|
33101
|
+
*/
|
|
33102
|
+
async delete(ruleId) {
|
|
33103
|
+
this.ledger.evict(ruleId);
|
|
33104
|
+
try {
|
|
33105
|
+
await this.store.delete.mutate({
|
|
33106
|
+
collection: NC_CALENDAR_EVENTS_RULES_COLLECTION,
|
|
33107
|
+
key: ruleId
|
|
33108
|
+
});
|
|
33109
|
+
} catch (err) {
|
|
33110
|
+
this.logger.warn("calendar-events rule durable delete failed", { meta: {
|
|
33111
|
+
ruleId,
|
|
33112
|
+
error: err instanceof Error ? err.message : String(err)
|
|
33113
|
+
} });
|
|
33114
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
33115
|
+
}
|
|
33116
|
+
}
|
|
33117
|
+
async write(rule) {
|
|
33118
|
+
const validated = NcCalendarEventsRuleSchema.parse(rule);
|
|
33119
|
+
await this.ledger.put(validated);
|
|
33120
|
+
return validated;
|
|
33121
|
+
}
|
|
33122
|
+
};
|
|
33123
|
+
//#endregion
|
|
33124
|
+
//#region src/notification-center/calendar-events/calendar-events-delivery.ts
|
|
33125
|
+
function calendarEventsRecordId(ruleId, windowEndMs) {
|
|
33126
|
+
return `calendar-events:${ruleId}:${windowEndMs}`;
|
|
33127
|
+
}
|
|
33128
|
+
/** `YYYY-MM-DD HH:MM` in the host timezone. */
|
|
33129
|
+
function clockOf$1(atMs) {
|
|
33130
|
+
const d = new Date(atMs);
|
|
33131
|
+
return `${`${String(d.getFullYear())}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`} ${`${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`}`;
|
|
33132
|
+
}
|
|
33133
|
+
function formatEventLine(event) {
|
|
33134
|
+
const when = event.allDay ? clockOf$1(event.startMs).slice(0, 10) : `${clockOf$1(event.startMs)}–${clockOf$1(event.endMs)}`;
|
|
33135
|
+
const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
|
|
33136
|
+
return `• ${when} ${event.title}${where}`;
|
|
33137
|
+
}
|
|
33138
|
+
function derivedBody$1(input) {
|
|
33139
|
+
if (input.events.length === 0) return `No events in the next ${String(input.horizonDays)} day(s).`;
|
|
33140
|
+
const lines = input.events.slice(0, 40).map(formatEventLine);
|
|
33141
|
+
const extra = input.events.length > 40 ? `\n…and ${String(input.events.length - 40)} more` : "";
|
|
33142
|
+
return lines.join("\n") + extra;
|
|
33143
|
+
}
|
|
33144
|
+
function templateVars(input) {
|
|
33145
|
+
return {
|
|
33146
|
+
rule: input.ruleName,
|
|
33147
|
+
count: String(input.events.length),
|
|
33148
|
+
horizonDays: String(input.horizonDays),
|
|
33149
|
+
from: clockOf$1(input.eventWindow.startMs),
|
|
33150
|
+
to: clockOf$1(input.eventWindow.endMs),
|
|
33151
|
+
events: derivedBody$1(input),
|
|
33152
|
+
calendars: String(input.calendarIds.length)
|
|
33153
|
+
};
|
|
33154
|
+
}
|
|
33155
|
+
function paramsFor$1(target) {
|
|
33156
|
+
const params = target.params;
|
|
33157
|
+
if (params === void 0) return void 0;
|
|
33158
|
+
const out = {};
|
|
33159
|
+
for (const [k, v] of Object.entries(params)) if (typeof v === "string") out[k] = v;
|
|
33160
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
33161
|
+
}
|
|
33162
|
+
/**
|
|
33163
|
+
* One outbox row per enabled target. Ready for `NcOutbox.enqueue`.
|
|
33164
|
+
*/
|
|
33165
|
+
function buildCalendarEventsOutboxInputs(input) {
|
|
33166
|
+
const vars = templateVars(input);
|
|
33167
|
+
const optedOut = new Set(input.disabledTargetIds ?? []);
|
|
33168
|
+
const title = renderTemplate(input.template?.title, vars) ?? `${input.ruleName}: ${String(input.events.length)} upcoming`;
|
|
33169
|
+
const body = renderTemplate(input.template?.body, vars) ?? derivedBody$1(input);
|
|
33170
|
+
const recordId = calendarEventsRecordId(input.ruleId, input.scheduleWindow.endMs);
|
|
33171
|
+
return input.targets.filter((t) => !optedOut.has(t.targetId)).map((target) => {
|
|
33172
|
+
const payload = {
|
|
33173
|
+
ruleName: input.ruleName,
|
|
33174
|
+
delivery: "calendar-events",
|
|
33175
|
+
priority: input.priority,
|
|
33176
|
+
template: {
|
|
33177
|
+
title,
|
|
33178
|
+
body
|
|
33179
|
+
},
|
|
33180
|
+
media: "none",
|
|
33181
|
+
...paramsFor$1(target) !== void 0 ? { params: paramsFor$1(target) } : {},
|
|
33182
|
+
subject: {
|
|
33183
|
+
deviceId: NC_SYSTEM_DEVICE_ID$1,
|
|
33184
|
+
className: "calendar-events",
|
|
33185
|
+
zones: [],
|
|
33186
|
+
timestamp: input.scheduleWindow.endMs
|
|
33187
|
+
}
|
|
33188
|
+
};
|
|
33189
|
+
return {
|
|
33190
|
+
ruleId: input.ruleId,
|
|
33191
|
+
targetId: target.targetId,
|
|
33192
|
+
deviceId: NC_SYSTEM_DEVICE_ID$1,
|
|
33193
|
+
recordKind: "calendar-events",
|
|
33194
|
+
recordId,
|
|
33195
|
+
payload
|
|
33196
|
+
};
|
|
33197
|
+
});
|
|
33198
|
+
}
|
|
33199
|
+
//#endregion
|
|
33200
|
+
//#region src/notification-center/calendar-events/calendar-events-selection.ts
|
|
33201
|
+
/**
|
|
33202
|
+
* Expand a rule's calendar scope against the known calendar ids.
|
|
33203
|
+
*
|
|
33204
|
+
* - All (both lists empty/absent) → every known id
|
|
33205
|
+
* - Only → the include list, intersected with known (unknown ids dropped)
|
|
33206
|
+
* - Except → known minus the exclude list
|
|
33207
|
+
*
|
|
33208
|
+
* An empty known set always yields empty — there is nothing to notify about.
|
|
33209
|
+
*/
|
|
33210
|
+
function expandCalendarScope(scope, knownIds) {
|
|
33211
|
+
const known = [...knownIds];
|
|
33212
|
+
if (known.length === 0) return [];
|
|
33213
|
+
const include = scope?.calendars ?? [];
|
|
33214
|
+
const exclude = scope?.calendarsExclude ?? [];
|
|
33215
|
+
if (include.length > 0) {
|
|
33216
|
+
const wanted = new Set(include);
|
|
33217
|
+
return known.filter((id) => wanted.has(id));
|
|
33218
|
+
}
|
|
33219
|
+
if (exclude.length > 0) {
|
|
33220
|
+
const banned = new Set(exclude);
|
|
33221
|
+
return known.filter((id) => !banned.has(id));
|
|
33222
|
+
}
|
|
33223
|
+
return known;
|
|
33224
|
+
}
|
|
33225
|
+
//#endregion
|
|
33226
|
+
//#region src/notification-center/calendar-events/calendar-events-producer.ts
|
|
33227
|
+
/** How far back a closed schedule window may still be owed. */
|
|
33228
|
+
var CALENDAR_EVENTS_LOOKBACK_MS = 2160 * 60 * 1e3;
|
|
33229
|
+
var NcCalendarEventsProducer = class {
|
|
33230
|
+
deps;
|
|
33231
|
+
inFlight = false;
|
|
33232
|
+
now;
|
|
33233
|
+
constructor(deps) {
|
|
33234
|
+
this.deps = deps;
|
|
33235
|
+
this.now = deps.now ?? (() => Date.now());
|
|
33236
|
+
}
|
|
33237
|
+
async tick() {
|
|
33238
|
+
if (this.inFlight) return {
|
|
33239
|
+
examined: 0,
|
|
33240
|
+
sent: 0,
|
|
33241
|
+
empty: 0,
|
|
33242
|
+
skipped: 0,
|
|
33243
|
+
failed: 0
|
|
33244
|
+
};
|
|
33245
|
+
this.inFlight = true;
|
|
33246
|
+
const report = {
|
|
33247
|
+
examined: 0,
|
|
33248
|
+
sent: 0,
|
|
33249
|
+
empty: 0,
|
|
33250
|
+
skipped: 0,
|
|
33251
|
+
failed: 0
|
|
33252
|
+
};
|
|
33253
|
+
try {
|
|
33254
|
+
const rules = this.deps.store.listEnabled();
|
|
33255
|
+
for (const rule of rules) {
|
|
33256
|
+
report.examined += 1;
|
|
33257
|
+
try {
|
|
33258
|
+
const outcome = await this.runRule(rule);
|
|
33259
|
+
if (outcome === "sent") report.sent += 1;
|
|
33260
|
+
else if (outcome === "empty") report.empty += 1;
|
|
33261
|
+
else report.skipped += 1;
|
|
33262
|
+
} catch (err) {
|
|
33263
|
+
report.failed += 1;
|
|
33264
|
+
this.deps.logger.warn("calendar-events rule failed", { meta: {
|
|
33265
|
+
ruleId: rule.id,
|
|
33266
|
+
error: err instanceof Error ? err.message : String(err)
|
|
33267
|
+
} });
|
|
33268
|
+
}
|
|
33269
|
+
}
|
|
33270
|
+
return report;
|
|
33271
|
+
} finally {
|
|
33272
|
+
this.inFlight = false;
|
|
33273
|
+
}
|
|
33274
|
+
}
|
|
33275
|
+
async runRule(rule) {
|
|
33276
|
+
const nowMs = this.now();
|
|
33277
|
+
const scheduleWindow = findLastClosedWindow({
|
|
33278
|
+
schedule: rule.schedule,
|
|
33279
|
+
nowMs,
|
|
33280
|
+
lookbackMs: CALENDAR_EVENTS_LOOKBACK_MS
|
|
33281
|
+
});
|
|
33282
|
+
if (scheduleWindow === null) return "skipped";
|
|
33283
|
+
if ((rule.generatedAt ?? 0) >= scheduleWindow.endMs) return "skipped";
|
|
33284
|
+
const known = await this.deps.calendars.listCalendars();
|
|
33285
|
+
const calendarIds = expandCalendarScope({
|
|
33286
|
+
calendars: rule.calendars,
|
|
33287
|
+
calendarsExclude: rule.calendarsExclude
|
|
33288
|
+
}, known.map((c) => c.id));
|
|
33289
|
+
if (calendarIds.length === 0) {
|
|
33290
|
+
this.deps.logger.info("calendar-events: no calendars in scope", { meta: { ruleId: rule.id } });
|
|
33291
|
+
if (!rule.deliverEmpty) {
|
|
33292
|
+
await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
|
|
33293
|
+
return "empty";
|
|
33294
|
+
}
|
|
33295
|
+
}
|
|
33296
|
+
const eventWindow = calendarEventsWindow(nowMs, rule.horizonDays);
|
|
33297
|
+
const events = calendarIds.length === 0 ? [] : await this.deps.calendars.listEvents({
|
|
33298
|
+
calendarIds,
|
|
33299
|
+
startMs: eventWindow.startMs,
|
|
33300
|
+
endMs: eventWindow.endMs
|
|
33301
|
+
});
|
|
33302
|
+
if (events.length === 0 && !rule.deliverEmpty) {
|
|
33303
|
+
this.deps.logger.info("calendar-events: empty horizon, not delivering", { meta: {
|
|
33304
|
+
ruleId: rule.id,
|
|
33305
|
+
horizonDays: eventWindow.horizonDays
|
|
33306
|
+
} });
|
|
33307
|
+
await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
|
|
33308
|
+
return "empty";
|
|
33309
|
+
}
|
|
33310
|
+
const sorted = [...events].toSorted((a, b) => a.startMs - b.startMs);
|
|
33311
|
+
await this.enqueue(rule, scheduleWindow, eventWindow, calendarIds, sorted);
|
|
33312
|
+
await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
|
|
33313
|
+
return "sent";
|
|
33314
|
+
}
|
|
33315
|
+
async enqueue(rule, scheduleWindow, eventWindow, calendarIds, events) {
|
|
33316
|
+
const inputs = buildCalendarEventsOutboxInputs({
|
|
33317
|
+
ruleId: rule.id,
|
|
33318
|
+
ruleName: rule.name,
|
|
33319
|
+
scheduleWindow,
|
|
33320
|
+
eventWindow: {
|
|
33321
|
+
startMs: eventWindow.startMs,
|
|
33322
|
+
endMs: eventWindow.endMs
|
|
33323
|
+
},
|
|
33324
|
+
horizonDays: eventWindow.horizonDays,
|
|
33325
|
+
events,
|
|
33326
|
+
calendarIds,
|
|
33327
|
+
targets: rule.targets,
|
|
33328
|
+
priority: rule.priority,
|
|
33329
|
+
...rule.template !== void 0 ? { template: rule.template } : {}
|
|
33330
|
+
});
|
|
33331
|
+
for (const input of inputs) await this.deps.outbox.enqueue(input);
|
|
33332
|
+
this.deps.logger.info("calendar-events delivered", { meta: {
|
|
33333
|
+
ruleId: rule.id,
|
|
33334
|
+
events: events.length,
|
|
33335
|
+
targets: inputs.length,
|
|
33336
|
+
windowEndMs: scheduleWindow.endMs
|
|
33337
|
+
} });
|
|
33338
|
+
}
|
|
33339
|
+
};
|
|
33340
|
+
//#endregion
|
|
32607
33341
|
//#region src/notification-center/text-catalog-editor.ts
|
|
32608
33342
|
var NcTextCatalogEditor = class {
|
|
32609
33343
|
store;
|
|
@@ -35573,6 +36307,8 @@ var NC_ALARM_SETTINGS_FALLBACK = {
|
|
|
35573
36307
|
...DEFAULT_DELAYS,
|
|
35574
36308
|
announceTargets: [],
|
|
35575
36309
|
nonBlocking: [],
|
|
36310
|
+
blocking: [],
|
|
36311
|
+
secureOnArmOff: [],
|
|
35576
36312
|
modeDelays: []
|
|
35577
36313
|
};
|
|
35578
36314
|
/**
|
|
@@ -35685,7 +36421,7 @@ function packagePhaseOf(ev) {
|
|
|
35685
36421
|
*/
|
|
35686
36422
|
function historyRecordKind(kind) {
|
|
35687
36423
|
if (kind === "audio-event" || kind === "audio-window") return "object-event";
|
|
35688
|
-
if (kind === "timelapse" || kind === "ai-summary") return "system-event";
|
|
36424
|
+
if (kind === "timelapse" || kind === "ai-summary" || kind === "calendar-events") return "system-event";
|
|
35689
36425
|
return kind;
|
|
35690
36426
|
}
|
|
35691
36427
|
/**
|
|
@@ -35700,7 +36436,8 @@ function historyRecordKind(kind) {
|
|
|
35700
36436
|
function cooldownSubjectKind(kind) {
|
|
35701
36437
|
switch (kind) {
|
|
35702
36438
|
case "timelapse":
|
|
35703
|
-
case "ai-summary":
|
|
36439
|
+
case "ai-summary":
|
|
36440
|
+
case "calendar-events": return "device-event";
|
|
35704
36441
|
default: return kind;
|
|
35705
36442
|
}
|
|
35706
36443
|
}
|
|
@@ -35800,7 +36537,7 @@ function outboxEntryToHistory(texts, entry) {
|
|
|
35800
36537
|
id: entry.id,
|
|
35801
36538
|
ruleId: entry.ruleId,
|
|
35802
36539
|
ruleName: entry.payload.ruleName,
|
|
35803
|
-
delivery: entry.payload.delivery === "timelapse" || entry.payload.delivery === "ai-summary" ? "system-event" : entry.payload.delivery,
|
|
36540
|
+
delivery: entry.payload.delivery === "timelapse" || entry.payload.delivery === "ai-summary" || entry.payload.delivery === "calendar-events" ? "system-event" : entry.payload.delivery,
|
|
35804
36541
|
targetId: entry.targetId,
|
|
35805
36542
|
deviceId: entry.deviceId,
|
|
35806
36543
|
recordKind: historyRecordKind(entry.recordKind),
|
|
@@ -36010,6 +36747,9 @@ var NotificationCenter = class {
|
|
|
36010
36747
|
*/
|
|
36011
36748
|
summaryRules;
|
|
36012
36749
|
summaryProducer = null;
|
|
36750
|
+
/** Calendar-events digest rules + producer (hub-only producer). */
|
|
36751
|
+
calendarEventsRules;
|
|
36752
|
+
calendarEventsProducer = null;
|
|
36013
36753
|
/**
|
|
36014
36754
|
* What the deleted-device / deleted-target prune did, and why — its
|
|
36015
36755
|
* disable-once guard and the source of both UIs' banner. Loaded on EVERY
|
|
@@ -37000,6 +37740,11 @@ var NotificationCenter = class {
|
|
|
37000
37740
|
logger: this.logger.child("summary"),
|
|
37001
37741
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
37002
37742
|
});
|
|
37743
|
+
this.calendarEventsRules = new NcCalendarEventsStore({
|
|
37744
|
+
store: deps.store,
|
|
37745
|
+
logger: this.logger.child("calendar-events"),
|
|
37746
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
37747
|
+
});
|
|
37003
37748
|
this.pruneLedger = new NcPruneLedger({
|
|
37004
37749
|
store: deps.store,
|
|
37005
37750
|
logger: this.logger.child("device-prune")
|
|
@@ -37046,6 +37791,14 @@ var NotificationCenter = class {
|
|
|
37046
37791
|
zoneOwner: this.zoneOwners.lookup()
|
|
37047
37792
|
});
|
|
37048
37793
|
}
|
|
37794
|
+
const calendarsPort = deps.calendars;
|
|
37795
|
+
if (calendarsPort !== void 0) this.calendarEventsProducer = new NcCalendarEventsProducer({
|
|
37796
|
+
store: this.calendarEventsRules,
|
|
37797
|
+
outbox: this.outbox,
|
|
37798
|
+
calendars: calendarsPort,
|
|
37799
|
+
logger: this.logger.child("calendar-events"),
|
|
37800
|
+
now: this.now
|
|
37801
|
+
});
|
|
37049
37802
|
}
|
|
37050
37803
|
/**
|
|
37051
37804
|
* The durable timelapse rule set — exposed for the `nc.*Timelapse*` bridge
|
|
@@ -37059,6 +37812,10 @@ var NotificationCenter = class {
|
|
|
37059
37812
|
get summaryStore() {
|
|
37060
37813
|
return this.summaryRules;
|
|
37061
37814
|
}
|
|
37815
|
+
/** The durable calendar-events rule set — exposed for the `nc.*CalendarEvents*` bridge. */
|
|
37816
|
+
get calendarEventsStore() {
|
|
37817
|
+
return this.calendarEventsRules;
|
|
37818
|
+
}
|
|
37062
37819
|
/**
|
|
37063
37820
|
* Produce one digest window for one rule, right now.
|
|
37064
37821
|
*
|
|
@@ -37132,6 +37889,7 @@ var NotificationCenter = class {
|
|
|
37132
37889
|
await NcDeviceMuteStore.declare(store);
|
|
37133
37890
|
await TimelapseStore.declare(store);
|
|
37134
37891
|
await NcSummaryStore.declare(store);
|
|
37892
|
+
await NcCalendarEventsStore.declare(store);
|
|
37135
37893
|
await NcRuleAiStore.declare(store);
|
|
37136
37894
|
await NcTextCatalogStore.declare(store);
|
|
37137
37895
|
await NcArtifactIndex.declare(store);
|
|
@@ -38577,6 +39335,9 @@ var NotificationCenter = class {
|
|
|
38577
39335
|
alarmArmGuard() {
|
|
38578
39336
|
return {
|
|
38579
39337
|
openings: (mode) => this.alarmOpenings(mode),
|
|
39338
|
+
secureOnArm: (devices) => {
|
|
39339
|
+
this.secureDevicesOnArm(devices);
|
|
39340
|
+
},
|
|
38580
39341
|
announceRefusal: (input) => this.announceArmRefused(input),
|
|
38581
39342
|
announceCheckFailed: (input) => this.announceArmCheckFailed(input)
|
|
38582
39343
|
};
|
|
@@ -38643,7 +39404,9 @@ var NotificationCenter = class {
|
|
|
38643
39404
|
if (verdict.kind === "open") open.push({
|
|
38644
39405
|
deviceId,
|
|
38645
39406
|
name: name(deviceId),
|
|
38646
|
-
state: verdict.state
|
|
39407
|
+
state: verdict.state,
|
|
39408
|
+
controllable: verdict.controllable,
|
|
39409
|
+
cap: verdict.cap
|
|
38647
39410
|
});
|
|
38648
39411
|
}
|
|
38649
39412
|
this.logger.info("alarm arm check", {
|
|
@@ -38662,6 +39425,47 @@ var NotificationCenter = class {
|
|
|
38662
39425
|
};
|
|
38663
39426
|
}
|
|
38664
39427
|
/**
|
|
39428
|
+
* Fire-and-forget secure actuation for controllable openings on arm.
|
|
39429
|
+
* Failures are WARN per device — they never refuse the arm.
|
|
39430
|
+
*/
|
|
39431
|
+
async secureDevicesOnArm(devices) {
|
|
39432
|
+
for (const device of devices) {
|
|
39433
|
+
const actuation = device.cap !== void 0 ? require_clip_model_registry.secureActuationFor(device.cap) : null;
|
|
39434
|
+
if (actuation === null) continue;
|
|
39435
|
+
try {
|
|
39436
|
+
await this.actuateWithAttribution({
|
|
39437
|
+
deviceId: device.deviceId,
|
|
39438
|
+
cap: actuation.cap,
|
|
39439
|
+
method: actuation.method,
|
|
39440
|
+
args: {
|
|
39441
|
+
deviceId: device.deviceId,
|
|
39442
|
+
...actuation.args
|
|
39443
|
+
}
|
|
39444
|
+
});
|
|
39445
|
+
this.logger.info("alarm secure-on-arm actuated", {
|
|
39446
|
+
tags: { deviceId: device.deviceId },
|
|
39447
|
+
meta: {
|
|
39448
|
+
name: device.name,
|
|
39449
|
+
state: device.state,
|
|
39450
|
+
cap: actuation.cap,
|
|
39451
|
+
method: actuation.method
|
|
39452
|
+
}
|
|
39453
|
+
});
|
|
39454
|
+
} catch (err) {
|
|
39455
|
+
this.logger.warn("alarm secure-on-arm actuation failed — arm continues", {
|
|
39456
|
+
tags: { deviceId: device.deviceId },
|
|
39457
|
+
meta: {
|
|
39458
|
+
name: device.name,
|
|
39459
|
+
state: device.state,
|
|
39460
|
+
cap: actuation.cap,
|
|
39461
|
+
method: actuation.method,
|
|
39462
|
+
error: String(err)
|
|
39463
|
+
}
|
|
39464
|
+
});
|
|
39465
|
+
}
|
|
39466
|
+
}
|
|
39467
|
+
}
|
|
39468
|
+
/**
|
|
38665
39469
|
* The refusal, as an ordinary notification.
|
|
38666
39470
|
*
|
|
38667
39471
|
* Same path every alarm announcement takes (D126: the alarm speaks through
|
|
@@ -40708,11 +41512,17 @@ var NotificationCenter = class {
|
|
|
40708
41512
|
await this.rules.load();
|
|
40709
41513
|
this.noteInertGroupedRules();
|
|
40710
41514
|
if (this.evaluationActive) this.deviceAlerts.tick();
|
|
40711
|
-
if (this.evaluationActive)
|
|
40712
|
-
this.
|
|
40713
|
-
|
|
41515
|
+
if (this.evaluationActive) {
|
|
41516
|
+
this.stateSummary.tick().catch((err) => {
|
|
41517
|
+
this.logger.warn("state summary tick failed", { meta: { error: String(err) } });
|
|
41518
|
+
});
|
|
41519
|
+
this.calendarEventsProducer?.tick().catch((err) => {
|
|
41520
|
+
this.logger.warn("calendar-events tick failed", { meta: { error: String(err) } });
|
|
41521
|
+
});
|
|
41522
|
+
}
|
|
40714
41523
|
await this.timelapseRules.load();
|
|
40715
41524
|
await this.summaryRules.load();
|
|
41525
|
+
await this.calendarEventsRules.load();
|
|
40716
41526
|
await this.ruleAi.load();
|
|
40717
41527
|
await this.reloadTexts();
|
|
40718
41528
|
this.refreshOccupancyWatch();
|
|
@@ -104595,6 +105405,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
104595
105405
|
store: center.summaryStore,
|
|
104596
105406
|
runNow: (input) => center.runSummaryNow(input)
|
|
104597
105407
|
},
|
|
105408
|
+
calendarEvents: { store: center.calendarEventsStore },
|
|
104598
105409
|
texts: center.textCatalogEditor,
|
|
104599
105410
|
previewTexts: center.textCatalog,
|
|
104600
105411
|
...this.ncArtifactIndex !== null ? { artifacts: this.ncArtifactIndex } : {},
|
|
@@ -105782,7 +106593,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
105782
106593
|
})).tracks;
|
|
105783
106594
|
},
|
|
105784
106595
|
...this.isPostProcessingNode ? { timelapse: this.buildTimelapsePorts(api, stores) } : {},
|
|
105785
|
-
...this.isPostProcessingNode ? { summary: this.buildSummaryPorts(api, stores) } : {}
|
|
106596
|
+
...this.isPostProcessingNode ? { summary: this.buildSummaryPorts(api, stores) } : {},
|
|
106597
|
+
...this.isPostProcessingNode ? { calendars: {
|
|
106598
|
+
listCalendars: async () => {
|
|
106599
|
+
return (await api.calendars.listCalendars.query()).map((c) => ({ id: c.id }));
|
|
106600
|
+
},
|
|
106601
|
+
listEvents: async (input) => api.calendars.listEvents.query({
|
|
106602
|
+
calendarIds: [...input.calendarIds],
|
|
106603
|
+
startMs: input.startMs,
|
|
106604
|
+
endMs: input.endMs
|
|
106605
|
+
})
|
|
106606
|
+
} } : {}
|
|
105786
106607
|
});
|
|
105787
106608
|
this.wireAlarmPanel(this.notificationCenter);
|
|
105788
106609
|
await this.serveNcActionPlane(this.notificationCenter);
|