@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as NC_ALARM_SYSTEM_EVENT_KINDS, $n as
|
|
1
|
+
import { $ as NC_ALARM_SYSTEM_EVENT_KINDS, $n as isVectorIndexDimConflict, $r as boolean, $t as SCENE_DEFAULT_UNCOVERED_POLICY, A as EMBEDDING_REQUIREMENTS_FAILED, An as describeSystemEventDeliveryProblem, Ar as templateCarriesSummary, At as NcScheduleSchema, B as ListTrackRelationsInputSchema, Bn as failureContributionCapability, Br as BaseAddon, Bt as OPS_LOG_DEFAULT_LIMIT, C as DEFAULT_EVENT_COLOR, Cn as assertTimelapseCadences, Cr as storageOccupancyCapability, Ct as NcConditionDescriptorSchema, D as DeclaredDevices, Dn as consumableLowPercentOf, Dr as systemEventFilterApplies, Dt as NcRuleSchema, E as DETECTION_PIPELINE_CAP_NAME, En as buildEventKindDescriptor, Er as systemEventFamilyProblems, Et as NcRulePatchSchema, F as EXPORT_DENSE_MAX_RANGES, Fr as canonicalHash, Ft as NcSystemEventKindSchema, G as MAX_BIRTH_LATENCY_PROXY_MS, Gn as isDetectionMacroClass, Gr as hydrateSchema, Gt as RebuildObjectEmbeddingsRefusalSchema, H as LockStateSchema, Hn as gifFromMp4Args, Hr as createEvent, I as FIRST_LEVEL_MACRO_CLASSES, Ir as evaluateSensorEdge, It as NcTaxonomySchema, J as MOTION_CLOSE_AFTER_MS, Jn as isMediaOwnerType, Jt as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, K as MAX_TRACK_PURITIES, Kn as isEventOwnerType, Kr as isDeviceScopedCap, Kt as RetrainStatusSchema, L as FULL_IMAGE_BBOX, Ln as encodeVectorBase64, Lr as isSourceCap, Lt as NcTemplatePreviewInputSchema, M as EVENT_DENSITY_BATCH_MAX, Mr as vectorDimFromBase64, Mt as NcSnoozeSchema, N as EVENT_KIND_BY_CAP, Nr as zoneAnalyticsCapability, Nt as NcSnoozeSuppressedSchema, O as DetectionSourceSchema, On as cosineSimilarity$1, Or as systemEventSummaryScheduleOf, Ot as NcRuleTargetSchema, P as EVENT_OWNER_TYPES, Pn as embeddingRefusalOf, Pr as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Pt as NcStateSummarySectionIdSchema, Q as NC_ALARM_DELAY_MAX_SEC, Qn as isSystemEventSummaryRule, Qr as array, Qt as SCENE_DEFAULT_QUIET_SECONDS, R as FailureCounters, Rn as evictionPolicyOfLocation, Rr as DeviceType, Rt as NcTemplatePreviewSchema, S as CoverStateSchema, Sn as applySecureOnArmEdit, Sr as servedTemplateVars, St as NcAlarmSettingsPatchSchema, T as DETECTION_MACRO_CLASSES, Tn as audioModeOf, Tr as systemEventDeliveryProblems, Tt as NcRuleInputSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Un as isAlarmControllableCap, Ur as customAction, Ut as RECORDING_EXPORT_MAX_READ_BYTES, V as ListTrackRelationsResultSchema, Vn as gifDeriveSpeed, Vr as CamProfileSchema, Vt as OpsLogEntrySchema, W as MAX_BIRTH_DECISION_RECORDS, Wn as isAudioClassName, Wr as defineCustomActions, Wt as RECORDING_SOURCE_CAMSTACK_ADDON, X as MotionTriggeredBySchema, Xn as isScheduleActive, Xr as ZodObject, Xt as SCENE_DEFAULT_CHECK_INTERVAL_SEC, Y as MediaFileKindEnum, Yn as isRetiredClearKey, Yr as sleep$1, Yt as SCENE_DEFAULT_ANCHOR_THRESHOLD, Z as NC_ALARM_DELAY_FIELDS, Zn as isSystemDelivery, Zr as _enum, Zt as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, _ as BaseDevice, _n as alarmDeviceSecureOnArm, _r as ruleGroupingApplies, _t as NC_TAXONOMY, a as CLIP_EMBEDDING_STEP_ID, ai as partialRecord, an as SetTrackRelationInputSchema, at as NC_CLIP_RETENTION_MAX_SEC, b as ClearTrackRelationInputSchema, bn as applyModeDelaysEdits, br as secureActuationFor, bt as NcAlarmSensorsInputSchema, c as resolveClusterModelPin, ci as string, cn as TimelapseRuleInputSchema, cr as readDeviceStateFrom, ct as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, di as union, dn as TrackPuritySchema, dr as resolveClipSearchMinScore, dt as NC_GROUPING_MAX_SEC, ei as discriminatedUnion, en as SCENE_DIVERGED, er as kebabToCamel, et as NC_ALARM_TRIGGERED_MAX_SEC, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fi as unknown, fn as TrackRelationKindSchema, fr as resolveLocationMode, ft as NC_GROUPING_MIN_SEC, g as BIRTH_DECISION_DEFAULT_LIMIT, gn as addonWidgetsSourceCapability, gr as ruleDeviceCompatCapQueries, gt as NC_TANK_SYSTEM_EVENT_KINDS, h as ArchivedDebugNoteSchema, hn as TrackSourceSchema, hr as ruleBelongsToAlarm$1, ht as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, ii as object, in as SetTrackPurityResultSchema, ir as occupancyScope, it as NC_CLIP_PRE_ROLL_DEFAULT_SEC, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as describeSystemEventFamilyProblem, jr as templateVarsFor, jt as NcSnoozeInputSchema, k as EMBEDDING_MODEL_FAILED, kn as deriveRecordingMode, kr as systemEventSummarySplitOf, kt as NcRuleUpdateInputSchema, l as APPEARANCE_BIN_COUNT, li as templateLiteral, ln as TimelapseRulePatchSchema, lr as readMotionTriggeredBy, lt as NC_DEFAULT_SNOOZE_MINUTES, m as AlarmArmModeSchema, mn as TrackSideSchema, mt as NC_SNOOZE_MAX_MINUTES, ni as looseObject, nn as SceneMonitorSchema, nr as memoizePinnedFfmpeg, nt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, o as FACE_EMBEDDING_STEP_ID, oi as record, on as TIMELAPSE_DENSE_FLOOR_SEC, or as pipelineAnalyticsCapability, ot as NC_CONDITION_CATALOG, p as AUDIO_MACRO_LABELS, pi as EventCategory, pn as TrackRelationSchema, pr as resolveModeDelays, pt as NC_SENSOR_TRANSITION_KINDS, q as MAX_TRACK_RELATIONS, qn as isKnownTemplateVar, qr as nodePin, qt as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, ri as number, rn as SetTrackPurityInputSchema, rr as notificationRulesCapability, rt as NC_CLIP_POST_ROLL_DEFAULT_SEC, s as readClusterModelPinStrict, si as strictObject, sn as TRACK_RELATIONS_DEFAULT_LIMIT, sr as plateGalleryCapability, st as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, t as BUILTIN_CLIP_MODELS, ti as literal, tn as SCENE_RESET_RECAPTURES, tr as mayWriteToLocation, tt as NC_AUDIO_CONFIRM_HITS_DEFAULT, u as APPEARANCE_BLOCKS, ui as tuple, un as TimelapseRuleSchema, ur as readTimelapseGeneratedAt, ut as NC_GROUPING_DEFAULT_SEC, v as BirthDecisionRecordSchema, vn as alarmPanelCapability, vr as ruleKindOf, vt as NC_TEMPLATE_VARS, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wn as audioMetricsCapability, wr as subKindsOf, wt as NcConditionsSchema, x as ClearTrackRelationResultSchema, xn as applyNonBlockingEdit, xr as sensorRaisedOf, xt as NcAlarmSensorsSchema, y as COCO_TO_MACRO, yn as applyBlockingEdit, yr as sceneMonitorCapability, yt as NcAlarmConfigSchema, z as LabelAttributionSchema, zn as faceGalleryCapability, zr as errMsg, zt as NcTemplateVarDescriptorSchema } from "../clip-model-registry-Dnm9dfy0.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs$1 from "node:fs";
|
|
4
4
|
import { createWriteStream, promises } from "node:fs";
|
|
@@ -597,7 +597,19 @@ var ARM_ANYWAY_TTL_MS = 5 * 6e4;
|
|
|
597
597
|
var NcAlarmOpenDeviceSchema = object({
|
|
598
598
|
deviceId: number().int(),
|
|
599
599
|
name: string(),
|
|
600
|
-
state: string()
|
|
600
|
+
state: string(),
|
|
601
|
+
/**
|
|
602
|
+
* Cover / lock / switch — default false so an older bridge answer still
|
|
603
|
+
* parses as a contact-style opening.
|
|
604
|
+
*/
|
|
605
|
+
controllable: boolean().default(false),
|
|
606
|
+
/** Opening cap that produced the reading; used to actuate secure-on-arm. */
|
|
607
|
+
cap: _enum([
|
|
608
|
+
"contact",
|
|
609
|
+
"cover",
|
|
610
|
+
"lock-control",
|
|
611
|
+
"switch"
|
|
612
|
+
]).optional()
|
|
601
613
|
}).readonly();
|
|
602
614
|
/** Why an Arm anyway was not honoured — see `NcAlarmPanelDevice.armAnyway`. */
|
|
603
615
|
var NcAlarmArmAnywayRejectionSchema = _enum([
|
|
@@ -662,23 +674,22 @@ function alarmCoveredDevices(rules, panelDeviceId, mode) {
|
|
|
662
674
|
/**
|
|
663
675
|
* Refuse or arm — the whole decision, as one partition of the open devices.
|
|
664
676
|
*
|
|
665
|
-
* A device may be open at arm time
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
* confirming: they were never shown it, and excluding it would arm the alarm
|
|
670
|
-
* around an opening nobody agreed to.
|
|
677
|
+
* A device may be open at arm time when:
|
|
678
|
+
* - the mode TOLERATES it (`nonBlocking` for contacts),
|
|
679
|
+
* - it is controllable and NOT on the mode's `blocking` list (default),
|
|
680
|
+
* - or the operator confirmed it with Arm anyway (`acknowledged`).
|
|
671
681
|
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
* Arm anyway, whose lifecycle (one-shot, stale, expired) the panel owns.
|
|
682
|
+
* Anything else open refuses — including a device that opened AFTER the refusal
|
|
683
|
+
* the operator is confirming.
|
|
675
684
|
*
|
|
676
685
|
* Excluded is EVERY open device — tolerated and acknowledged alike — and the
|
|
677
686
|
* exclusion is two-phase (`alarm-exclusions.ts`): each one is watched again
|
|
678
687
|
* once it has closed and opened.
|
|
679
688
|
*/
|
|
680
689
|
function decideArm(input) {
|
|
690
|
+
const forceBlock = new Set(input.blocking ?? []);
|
|
681
691
|
const tolerated = new Set([...input.nonBlocking, ...input.acknowledged ?? []]);
|
|
692
|
+
for (const d of input.open) if (d.controllable && !forceBlock.has(d.deviceId)) tolerated.add(d.deviceId);
|
|
682
693
|
const blocking = input.open.filter((d) => !tolerated.has(d.deviceId));
|
|
683
694
|
if (blocking.length > 0) return {
|
|
684
695
|
kind: "refused",
|
|
@@ -1066,39 +1077,24 @@ var NcActionPlane = class {
|
|
|
1066
1077
|
* and never open):
|
|
1067
1078
|
*
|
|
1068
1079
|
* - `not-opening` — no opening slice at all: a camera named by an intrusion
|
|
1069
|
-
* rule
|
|
1080
|
+
* rule. It takes no part in the arm decision.
|
|
1070
1081
|
* - `unknown` — an opening cap's slice IS there, but its field is missing or
|
|
1071
1082
|
* is not a word that cap defines, or the slice is a provider's PLACEHOLDER
|
|
1072
|
-
* (`lastChangedAt === 0
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
* saw. It never blocks and is never armed as secure on a guess — and a
|
|
1077
|
-
* placeholder is never taken for a state, so neither the reconcile tick,
|
|
1078
|
-
* the arm check nor an exclusion lift acts on one.
|
|
1079
|
-
* - `reading` — open or secure, with the state word a person will read.
|
|
1080
|
-
* ANY non-zero `lastChangedAt` is a reading, however old — including `1`,
|
|
1081
|
-
* the stamp a provider writes on a value it REPLAYED after a respawn with
|
|
1082
|
-
* no upstream change time (Homematic `HM_SEEDED_CHANGED_AT`): the value is
|
|
1083
|
-
* known, only its change time is not. The transition gate
|
|
1084
|
-
* (`sensor-transition-gate.ts`, "A placeholder slice is not a state") reads
|
|
1085
|
-
* the same stamp as "not a fresh event"; `0` alone means "not read yet".
|
|
1083
|
+
* (`lastChangedAt === 0`). It never blocks and is never armed as secure on a
|
|
1084
|
+
* guess.
|
|
1085
|
+
* - `reading` — open or secure, with the state word a person will read and the
|
|
1086
|
+
* cap that produced it (so secure-on-arm knows what to actuate).
|
|
1086
1087
|
*
|
|
1087
1088
|
* A read that FAILED never reaches {@link alarmOpeningFrom}: the caller leaves
|
|
1088
1089
|
* that id out of its answer (see {@link readAlarmOpenings}).
|
|
1089
1090
|
*
|
|
1090
1091
|
* Anything short of fully secure is open: a cover stopped half-way, a lock that
|
|
1091
|
-
* jammed, a cover still closing when the arm is decided.
|
|
1092
|
+
* jammed, a cover still closing when the arm is decided, a switch that is on.
|
|
1092
1093
|
*
|
|
1093
1094
|
* PRECEDENCE, when one device carries more than one opening slice: `contact`
|
|
1094
|
-
* first, then `cover`, then `lock-control`. The contact is the
|
|
1095
|
-
* about the opening — a reed switch on the frame says the window
|
|
1096
|
-
* when the motor's last reported word says `closed`.
|
|
1097
|
-
* read falls through to the next; only when none of them reads is the device
|
|
1098
|
-
* `unknown`, naming the first unreadable cap.
|
|
1099
|
-
*
|
|
1100
|
-
* The vocabulary of each word is its cap's own schema (`CoverStateSchema`,
|
|
1101
|
-
* `LockStateSchema`) — a word the cap does not define is not guessed at.
|
|
1095
|
+
* first, then `cover`, then `lock-control`, then `switch`. The contact is the
|
|
1096
|
+
* physical truth about the opening — a reed switch on the frame says the window
|
|
1097
|
+
* is open even when the motor's last reported word says `closed`.
|
|
1102
1098
|
*/
|
|
1103
1099
|
/** The one state word, per cap, that counts as secure. Everything else is open. */
|
|
1104
1100
|
var COVER_SECURE = "closed";
|
|
@@ -1117,11 +1113,12 @@ function fieldOf(slice, field) {
|
|
|
1117
1113
|
function isPlaceholder(slice) {
|
|
1118
1114
|
return fieldOf(slice, "lastChangedAt") === 0;
|
|
1119
1115
|
}
|
|
1120
|
-
function reading(word, secure) {
|
|
1116
|
+
function reading(word, secure, cap) {
|
|
1121
1117
|
return {
|
|
1122
1118
|
kind: "reading",
|
|
1123
1119
|
reading: word === secure ? "secure" : "open",
|
|
1124
|
-
state: word
|
|
1120
|
+
state: word,
|
|
1121
|
+
cap
|
|
1125
1122
|
};
|
|
1126
1123
|
}
|
|
1127
1124
|
function readContact(runtime) {
|
|
@@ -1141,11 +1138,13 @@ function readContact(runtime) {
|
|
|
1141
1138
|
return raw ? {
|
|
1142
1139
|
kind: "reading",
|
|
1143
1140
|
reading: "open",
|
|
1144
|
-
state: "open"
|
|
1141
|
+
state: "open",
|
|
1142
|
+
cap: "contact"
|
|
1145
1143
|
} : {
|
|
1146
1144
|
kind: "reading",
|
|
1147
1145
|
reading: "secure",
|
|
1148
|
-
state: "closed"
|
|
1146
|
+
state: "closed",
|
|
1147
|
+
cap: "contact"
|
|
1149
1148
|
};
|
|
1150
1149
|
}
|
|
1151
1150
|
function readCover(runtime) {
|
|
@@ -1158,7 +1157,7 @@ function readCover(runtime) {
|
|
|
1158
1157
|
};
|
|
1159
1158
|
const raw = fieldOf(slice, "state");
|
|
1160
1159
|
const parsed = CoverStateSchema.safeParse(raw);
|
|
1161
|
-
return parsed.success ? reading(parsed.data, COVER_SECURE) : {
|
|
1160
|
+
return parsed.success ? reading(parsed.data, COVER_SECURE, "cover") : {
|
|
1162
1161
|
kind: "unknown",
|
|
1163
1162
|
cap: "cover",
|
|
1164
1163
|
raw
|
|
@@ -1174,19 +1173,46 @@ function readLock(runtime) {
|
|
|
1174
1173
|
};
|
|
1175
1174
|
const raw = fieldOf(slice, "state");
|
|
1176
1175
|
const parsed = LockStateSchema.safeParse(raw);
|
|
1177
|
-
return parsed.success ? reading(parsed.data, LOCK_SECURE) : {
|
|
1176
|
+
return parsed.success ? reading(parsed.data, LOCK_SECURE, "lock-control") : {
|
|
1178
1177
|
kind: "unknown",
|
|
1179
1178
|
cap: "lock-control",
|
|
1180
1179
|
raw
|
|
1181
1180
|
};
|
|
1182
1181
|
}
|
|
1182
|
+
function readSwitch(runtime) {
|
|
1183
|
+
const slice = sliceOf(runtime, "switch");
|
|
1184
|
+
if (slice === void 0) return { kind: "absent" };
|
|
1185
|
+
if (isPlaceholder(slice)) return {
|
|
1186
|
+
kind: "unknown",
|
|
1187
|
+
cap: "switch",
|
|
1188
|
+
raw: slice
|
|
1189
|
+
};
|
|
1190
|
+
const raw = fieldOf(slice, "on");
|
|
1191
|
+
if (typeof raw !== "boolean") return {
|
|
1192
|
+
kind: "unknown",
|
|
1193
|
+
cap: "switch",
|
|
1194
|
+
raw
|
|
1195
|
+
};
|
|
1196
|
+
return raw ? {
|
|
1197
|
+
kind: "reading",
|
|
1198
|
+
reading: "open",
|
|
1199
|
+
state: "on",
|
|
1200
|
+
cap: "switch"
|
|
1201
|
+
} : {
|
|
1202
|
+
kind: "reading",
|
|
1203
|
+
reading: "secure",
|
|
1204
|
+
state: "off",
|
|
1205
|
+
cap: "switch"
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1183
1208
|
/** D630's table over one device's runtime slices — see the header for the three answers. */
|
|
1184
1209
|
function alarmOpeningFrom(runtime) {
|
|
1185
1210
|
let firstUnknown = null;
|
|
1186
1211
|
for (const read of [
|
|
1187
1212
|
readContact(runtime),
|
|
1188
1213
|
readCover(runtime),
|
|
1189
|
-
readLock(runtime)
|
|
1214
|
+
readLock(runtime),
|
|
1215
|
+
readSwitch(runtime)
|
|
1190
1216
|
]) {
|
|
1191
1217
|
if (read.kind === "reading") return read;
|
|
1192
1218
|
if (read.kind === "unknown" && firstUnknown === null) firstUnknown = read;
|
|
@@ -1201,13 +1227,14 @@ function alarmOpeningFrom(runtime) {
|
|
|
1201
1227
|
var OPENING_CAP_BY_EVENT_KIND = {
|
|
1202
1228
|
contact: "contact",
|
|
1203
1229
|
cover: "cover",
|
|
1204
|
-
lock: "lock-control"
|
|
1230
|
+
lock: "lock-control",
|
|
1231
|
+
switch: "switch"
|
|
1205
1232
|
};
|
|
1206
1233
|
/**
|
|
1207
1234
|
* The same table, read off one sensor-event row: its `value` IS the slice of
|
|
1208
1235
|
* the cap its `kind` names, so the event path and the reconcile read answer in
|
|
1209
1236
|
* one vocabulary — a cover is secure only when `closed`, a lock only when
|
|
1210
|
-
* `locked`, whichever path saw it.
|
|
1237
|
+
* `locked`, a switch only when off, whichever path saw it.
|
|
1211
1238
|
*/
|
|
1212
1239
|
function alarmOpeningFromSensorEvent(kind, value) {
|
|
1213
1240
|
const cap = OPENING_CAP_BY_EVENT_KIND[kind];
|
|
@@ -1228,7 +1255,9 @@ function classifyCoveredOpening(opening) {
|
|
|
1228
1255
|
if (opening.kind === "not-opening" || opening.reading !== "open") return { kind: "skip" };
|
|
1229
1256
|
return {
|
|
1230
1257
|
kind: "open",
|
|
1231
|
-
state: opening.state
|
|
1258
|
+
state: opening.state,
|
|
1259
|
+
controllable: isAlarmControllableCap(opening.cap),
|
|
1260
|
+
cap: opening.cap
|
|
1232
1261
|
};
|
|
1233
1262
|
}
|
|
1234
1263
|
/**
|
|
@@ -1885,6 +1914,22 @@ var ncAlarmPanelSchema = object({
|
|
|
1885
1914
|
deviceIds: array(number().int())
|
|
1886
1915
|
})).default([]),
|
|
1887
1916
|
/**
|
|
1917
|
+
* Controllable openings that DO block when open (opt-in). Loose + guarded like
|
|
1918
|
+
* {@link nonBlocking}.
|
|
1919
|
+
*/
|
|
1920
|
+
blocking: array(object({
|
|
1921
|
+
mode: string(),
|
|
1922
|
+
deviceIds: array(number().int())
|
|
1923
|
+
})).default([]),
|
|
1924
|
+
/**
|
|
1925
|
+
* Controllable openings that must NOT secure on arm (opt-out). Loose + guarded
|
|
1926
|
+
* like {@link nonBlocking}.
|
|
1927
|
+
*/
|
|
1928
|
+
secureOnArmOff: array(object({
|
|
1929
|
+
mode: string(),
|
|
1930
|
+
deviceIds: array(number().int())
|
|
1931
|
+
})).default([]),
|
|
1932
|
+
/**
|
|
1888
1933
|
* Per arm mode, the delays that override the global ones (D697). Loose on purpose — `mode` is
|
|
1889
1934
|
* a string and the numbers are unbounded here — and read back through
|
|
1890
1935
|
* {@link modeDelaysFromPersisted}, never a cast: an entry this release cannot trust is dropped
|
|
@@ -1903,7 +1948,7 @@ var ncAlarmPanelSchema = object({
|
|
|
1903
1948
|
* per mode kept — the same "at most one list per mode" the settings schema
|
|
1904
1949
|
* enforces at the boundary. A type guard (`safeParse`), never a cast.
|
|
1905
1950
|
*/
|
|
1906
|
-
function
|
|
1951
|
+
function modeDeviceListFromPersisted(raw) {
|
|
1907
1952
|
const seen = /* @__PURE__ */ new Set();
|
|
1908
1953
|
const lists = [];
|
|
1909
1954
|
for (const entry of raw) {
|
|
@@ -2008,7 +2053,9 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2008
2053
|
...this.readDelays(),
|
|
2009
2054
|
announceArm: this.config.get("announceArm"),
|
|
2010
2055
|
announceTargets: [...this.config.get("announceTargets")],
|
|
2011
|
-
nonBlocking:
|
|
2056
|
+
nonBlocking: modeDeviceListFromPersisted(this.config.get("nonBlocking")),
|
|
2057
|
+
blocking: modeDeviceListFromPersisted(this.config.get("blocking")),
|
|
2058
|
+
secureOnArmOff: modeDeviceListFromPersisted(this.config.get("secureOnArmOff")),
|
|
2012
2059
|
modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
|
|
2013
2060
|
};
|
|
2014
2061
|
}
|
|
@@ -2035,30 +2082,73 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2035
2082
|
async applySettings(patch) {
|
|
2036
2083
|
const edits = patch.modeDelays ?? [];
|
|
2037
2084
|
const membership = patch.nonBlockingEdit;
|
|
2085
|
+
const blockingEdit = patch.blockingEdit;
|
|
2086
|
+
const secureEdit = patch.secureOnArmEdit;
|
|
2038
2087
|
for (const edit of edits) this.refuseUnofferedMode(edit.mode);
|
|
2039
2088
|
if (membership !== void 0) {
|
|
2040
2089
|
this.refuseUnofferedMode(membership.mode, membership.deviceId);
|
|
2041
|
-
const verdict = applyNonBlockingEdit(patch.nonBlocking !== void 0 ? patch.nonBlocking :
|
|
2090
|
+
const verdict = applyNonBlockingEdit(patch.nonBlocking !== void 0 ? patch.nonBlocking : modeDeviceListFromPersisted(this.config.get("nonBlocking")), membership);
|
|
2042
2091
|
if (verdict.kind === "refused") {
|
|
2043
2092
|
this.ctx.logger.info("alarm settings refused", {
|
|
2044
2093
|
tags: { deviceId: membership.deviceId },
|
|
2045
2094
|
meta: {
|
|
2046
2095
|
panelId: this.id,
|
|
2047
2096
|
reason: "list-full",
|
|
2048
|
-
mode: verdict.mode
|
|
2097
|
+
mode: verdict.mode,
|
|
2098
|
+
list: "nonBlocking"
|
|
2049
2099
|
}
|
|
2050
2100
|
});
|
|
2051
2101
|
throw new Error(`the ${verdict.mode} non-blocking list already holds ${verdict.max} devices — remove one first`);
|
|
2052
2102
|
}
|
|
2053
2103
|
}
|
|
2104
|
+
if (blockingEdit !== void 0) {
|
|
2105
|
+
this.refuseUnofferedMode(blockingEdit.mode, blockingEdit.deviceId);
|
|
2106
|
+
const verdict = applyBlockingEdit(patch.blocking !== void 0 ? patch.blocking : modeDeviceListFromPersisted(this.config.get("blocking")), blockingEdit);
|
|
2107
|
+
if (verdict.kind === "refused") {
|
|
2108
|
+
this.ctx.logger.info("alarm settings refused", {
|
|
2109
|
+
tags: { deviceId: blockingEdit.deviceId },
|
|
2110
|
+
meta: {
|
|
2111
|
+
panelId: this.id,
|
|
2112
|
+
reason: "list-full",
|
|
2113
|
+
mode: verdict.mode,
|
|
2114
|
+
list: "blocking"
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
throw new Error(`the ${verdict.mode} blocking list already holds ${verdict.max} devices — remove one first`);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
if (secureEdit !== void 0) {
|
|
2121
|
+
this.refuseUnofferedMode(secureEdit.mode, secureEdit.deviceId);
|
|
2122
|
+
const verdict = applySecureOnArmEdit(patch.secureOnArmOff !== void 0 ? patch.secureOnArmOff : modeDeviceListFromPersisted(this.config.get("secureOnArmOff")), secureEdit);
|
|
2123
|
+
if (verdict.kind === "refused") {
|
|
2124
|
+
this.ctx.logger.info("alarm settings refused", {
|
|
2125
|
+
tags: { deviceId: secureEdit.deviceId },
|
|
2126
|
+
meta: {
|
|
2127
|
+
panelId: this.id,
|
|
2128
|
+
reason: "list-full",
|
|
2129
|
+
mode: verdict.mode,
|
|
2130
|
+
list: "secureOnArmOff"
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
throw new Error(`the ${verdict.mode} secure-on-arm-off list already holds ${verdict.max} devices — remove one first`);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2054
2136
|
const listWrites = [];
|
|
2055
2137
|
if (patch.nonBlocking !== void 0) listWrites.push(this.config.set("nonBlocking", patch.nonBlocking.map((l) => ({
|
|
2056
2138
|
mode: l.mode,
|
|
2057
2139
|
deviceIds: [...l.deviceIds]
|
|
2058
2140
|
}))));
|
|
2141
|
+
if (patch.blocking !== void 0) listWrites.push(this.config.set("blocking", patch.blocking.map((l) => ({
|
|
2142
|
+
mode: l.mode,
|
|
2143
|
+
deviceIds: [...l.deviceIds]
|
|
2144
|
+
}))));
|
|
2145
|
+
if (patch.secureOnArmOff !== void 0) listWrites.push(this.config.set("secureOnArmOff", patch.secureOnArmOff.map((l) => ({
|
|
2146
|
+
mode: l.mode,
|
|
2147
|
+
deviceIds: [...l.deviceIds]
|
|
2148
|
+
}))));
|
|
2059
2149
|
let membershipChanged = null;
|
|
2060
2150
|
if (membership !== void 0) {
|
|
2061
|
-
const result = applyNonBlockingEdit(
|
|
2151
|
+
const result = applyNonBlockingEdit(modeDeviceListFromPersisted(this.config.get("nonBlocking")), membership);
|
|
2062
2152
|
if (result.kind === "applied") {
|
|
2063
2153
|
membershipChanged = result.changed;
|
|
2064
2154
|
if (result.changed) listWrites.push(this.config.set("nonBlocking", result.lists.map((l) => ({
|
|
@@ -2067,6 +2157,26 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2067
2157
|
}))));
|
|
2068
2158
|
}
|
|
2069
2159
|
}
|
|
2160
|
+
if (blockingEdit !== void 0) {
|
|
2161
|
+
const result = applyBlockingEdit(modeDeviceListFromPersisted(this.config.get("blocking")), blockingEdit);
|
|
2162
|
+
if (result.kind === "applied" && result.changed) {
|
|
2163
|
+
membershipChanged = true;
|
|
2164
|
+
listWrites.push(this.config.set("blocking", result.lists.map((l) => ({
|
|
2165
|
+
mode: l.mode,
|
|
2166
|
+
deviceIds: [...l.deviceIds]
|
|
2167
|
+
}))));
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
if (secureEdit !== void 0) {
|
|
2171
|
+
const result = applySecureOnArmEdit(modeDeviceListFromPersisted(this.config.get("secureOnArmOff")), secureEdit);
|
|
2172
|
+
if (result.kind === "applied" && result.changed) {
|
|
2173
|
+
membershipChanged = true;
|
|
2174
|
+
listWrites.push(this.config.set("secureOnArmOff", result.lists.map((l) => ({
|
|
2175
|
+
mode: l.mode,
|
|
2176
|
+
deviceIds: [...l.deviceIds]
|
|
2177
|
+
}))));
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2070
2180
|
const persistedDelays = modeDelaysFromPersisted(this.config.get("modeDelays"));
|
|
2071
2181
|
const beforeDelays = persistedDelays.entries;
|
|
2072
2182
|
const afterDelays = edits.length > 0 ? applyModeDelaysEdits(beforeDelays, edits) : beforeDelays;
|
|
@@ -2205,7 +2315,7 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2205
2315
|
if (read.verdict.kind === "refused") return this.refuse(mode, read.verdict.blocking, now);
|
|
2206
2316
|
return this.settleArm(mode, read.verdict.excluded.map((device) => ({
|
|
2207
2317
|
device,
|
|
2208
|
-
why:
|
|
2318
|
+
why: this.exclusionWhyFor(mode, device)
|
|
2209
2319
|
})), now);
|
|
2210
2320
|
}
|
|
2211
2321
|
/**
|
|
@@ -2254,12 +2364,14 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2254
2364
|
return outcome;
|
|
2255
2365
|
}
|
|
2256
2366
|
const tolerated = this.nonBlockingFor(held.mode);
|
|
2367
|
+
const forceBlock = this.blockingFor(held.mode);
|
|
2257
2368
|
const unknown = new Set(read.openings.unknown);
|
|
2258
2369
|
if (read.verdict.kind === "refused") {
|
|
2259
2370
|
const plain = decideArm({
|
|
2260
2371
|
mode: held.mode,
|
|
2261
2372
|
open: read.openings.open,
|
|
2262
|
-
nonBlocking: tolerated
|
|
2373
|
+
nonBlocking: tolerated,
|
|
2374
|
+
blocking: forceBlock
|
|
2263
2375
|
});
|
|
2264
2376
|
const blockingNow = plain.kind === "refused" ? plain.blocking : read.verdict.blocking;
|
|
2265
2377
|
const shownIds = new Set(blockingNow.map((d) => d.deviceId));
|
|
@@ -2389,10 +2501,17 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2389
2501
|
error: String(err)
|
|
2390
2502
|
};
|
|
2391
2503
|
}
|
|
2504
|
+
const toSecure = openings.open.filter((d) => alarmDeviceSecureOnArm({
|
|
2505
|
+
controllable: d.controllable,
|
|
2506
|
+
deviceId: d.deviceId,
|
|
2507
|
+
secureOnArmOff: this.secureOnArmOffFor(mode)
|
|
2508
|
+
}));
|
|
2509
|
+
if (toSecure.length > 0) this.armGuard?.secureOnArm(toSecure);
|
|
2392
2510
|
const verdict = decideArm({
|
|
2393
2511
|
mode,
|
|
2394
2512
|
open: openings.open,
|
|
2395
2513
|
nonBlocking: this.nonBlockingFor(mode),
|
|
2514
|
+
blocking: this.blockingFor(mode),
|
|
2396
2515
|
...acknowledged !== void 0 ? { acknowledged } : {}
|
|
2397
2516
|
});
|
|
2398
2517
|
return {
|
|
@@ -2417,7 +2536,28 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
2417
2536
|
}
|
|
2418
2537
|
/** This mode's non-blocking ids, from the persisted lists. */
|
|
2419
2538
|
nonBlockingFor(mode) {
|
|
2420
|
-
return
|
|
2539
|
+
return this.modeListIds("nonBlocking", mode);
|
|
2540
|
+
}
|
|
2541
|
+
/** Controllable openings that force-block when open. */
|
|
2542
|
+
blockingFor(mode) {
|
|
2543
|
+
return this.modeListIds("blocking", mode);
|
|
2544
|
+
}
|
|
2545
|
+
/** Controllable openings that opt out of secure-on-arm. */
|
|
2546
|
+
secureOnArmOffFor(mode) {
|
|
2547
|
+
return this.modeListIds("secureOnArmOff", mode);
|
|
2548
|
+
}
|
|
2549
|
+
modeListIds(key, mode) {
|
|
2550
|
+
return modeDeviceListFromPersisted(this.config.get(key)).find((l) => l.mode === mode)?.deviceIds ?? [];
|
|
2551
|
+
}
|
|
2552
|
+
/** Why an open device was armed around on a plain (non–Arm-anyway) arm. */
|
|
2553
|
+
exclusionWhyFor(mode, device) {
|
|
2554
|
+
if (this.nonBlockingFor(mode).includes(device.deviceId)) return "non-blocking";
|
|
2555
|
+
if (alarmDeviceSecureOnArm({
|
|
2556
|
+
controllable: device.controllable,
|
|
2557
|
+
deviceId: device.deviceId,
|
|
2558
|
+
secureOnArmOff: this.secureOnArmOffFor(mode)
|
|
2559
|
+
})) return "secure-on-arm";
|
|
2560
|
+
return "non-blocking";
|
|
2421
2561
|
}
|
|
2422
2562
|
/**
|
|
2423
2563
|
* Arm, with these exclusions.
|
|
@@ -5300,7 +5440,7 @@ function isAdoptable(record, nowMs) {
|
|
|
5300
5440
|
* what `localDayMinute` does for the same input — one behaviour for a bad
|
|
5301
5441
|
* zone, not two.
|
|
5302
5442
|
*/
|
|
5303
|
-
function clockOf$
|
|
5443
|
+
function clockOf$6(atMs, timeZone) {
|
|
5304
5444
|
if (timeZone !== void 0) try {
|
|
5305
5445
|
return new Intl.DateTimeFormat("en-GB", {
|
|
5306
5446
|
timeZone,
|
|
@@ -5436,7 +5576,7 @@ var TimelapseRenderer = class {
|
|
|
5436
5576
|
includeAudio: false,
|
|
5437
5577
|
maxLifeMs: await this.ports.exportLifeMs(),
|
|
5438
5578
|
deleteAfterDownload: false,
|
|
5439
|
-
title: `${request.ruleName} ${clockOf$
|
|
5579
|
+
title: `${request.ruleName} ${clockOf$6(window.startMs, request.timezone)}→${clockOf$6(window.endMs, request.timezone)}`,
|
|
5440
5580
|
timelapse: {
|
|
5441
5581
|
everyMs,
|
|
5442
5582
|
outputFps: request.framerate,
|
|
@@ -5767,7 +5907,7 @@ var TimelapseRenderer = class {
|
|
|
5767
5907
|
...meta,
|
|
5768
5908
|
unservedWindowCount: verdict.unservedWindows.length,
|
|
5769
5909
|
unservedWindows: verdict.unservedWindows.slice(0, 12).map((w) => ({
|
|
5770
|
-
at: clockOf$
|
|
5910
|
+
at: clockOf$6(request.window.startMs + w.fromSec * 1e3, request.timezone),
|
|
5771
5911
|
fromSec: Math.round(w.fromSec),
|
|
5772
5912
|
toSec: Math.round(w.toSec),
|
|
5773
5913
|
expectedDenseFrames: w.expectedDenseFrames,
|
|
@@ -8610,6 +8750,7 @@ function sameWord(a, b) {
|
|
|
8610
8750
|
* therefore moves the table with the arm.
|
|
8611
8751
|
*/
|
|
8612
8752
|
function alarmSensorStateOf(deviceId, opening, online) {
|
|
8753
|
+
const controllable = opening?.kind === "reading" ? isAlarmControllableCap(opening.cap) : false;
|
|
8613
8754
|
const verdict = classifyCoveredOpening(opening);
|
|
8614
8755
|
switch (verdict.kind) {
|
|
8615
8756
|
case "unknown": return {
|
|
@@ -8617,27 +8758,31 @@ function alarmSensorStateOf(deviceId, opening, online) {
|
|
|
8617
8758
|
reading: "unknown",
|
|
8618
8759
|
state: null,
|
|
8619
8760
|
unknownWhy: verdict.why,
|
|
8620
|
-
online
|
|
8761
|
+
online,
|
|
8762
|
+
controllable
|
|
8621
8763
|
};
|
|
8622
8764
|
case "open": return {
|
|
8623
8765
|
deviceId,
|
|
8624
8766
|
reading: "open",
|
|
8625
8767
|
state: verdict.state,
|
|
8626
8768
|
unknownWhy: null,
|
|
8627
|
-
online
|
|
8769
|
+
online,
|
|
8770
|
+
controllable: verdict.controllable
|
|
8628
8771
|
};
|
|
8629
8772
|
case "skip": return opening?.kind === "reading" ? {
|
|
8630
8773
|
deviceId,
|
|
8631
8774
|
reading: "closed",
|
|
8632
8775
|
state: opening.state,
|
|
8633
8776
|
unknownWhy: null,
|
|
8634
|
-
online
|
|
8777
|
+
online,
|
|
8778
|
+
controllable
|
|
8635
8779
|
} : {
|
|
8636
8780
|
deviceId,
|
|
8637
8781
|
reading: "not-opening",
|
|
8638
8782
|
state: null,
|
|
8639
8783
|
unknownWhy: null,
|
|
8640
|
-
online
|
|
8784
|
+
online,
|
|
8785
|
+
controllable: false
|
|
8641
8786
|
};
|
|
8642
8787
|
}
|
|
8643
8788
|
}
|
|
@@ -13747,7 +13892,7 @@ var NcPruneRecordSchema = object({
|
|
|
13747
13892
|
source: _enum(["event", "reconcile"])
|
|
13748
13893
|
});
|
|
13749
13894
|
/** Rules × deleted subjects — small; a generous ceiling. */
|
|
13750
|
-
var LOAD_LIMIT$
|
|
13895
|
+
var LOAD_LIMIT$5 = 1e4;
|
|
13751
13896
|
function keyOf$1(row) {
|
|
13752
13897
|
return `${row.kind}:${row.ruleId}:${row.field}:${row.subject}`;
|
|
13753
13898
|
}
|
|
@@ -13766,7 +13911,7 @@ var NC_DEVICE_PRUNES_SPEC = {
|
|
|
13766
13911
|
return parsed.success ? parsed.data : null;
|
|
13767
13912
|
},
|
|
13768
13913
|
deviceIdOf: (row) => row.subjectKind === "device" ? Number(row.subject) : void 0,
|
|
13769
|
-
loadLimit: LOAD_LIMIT$
|
|
13914
|
+
loadLimit: LOAD_LIMIT$5
|
|
13770
13915
|
};
|
|
13771
13916
|
/** The outcomes that mean "this last reference was already dealt with". */
|
|
13772
13917
|
var KEPT_OUTCOMES = new Set(["kept-disabled", "kept"]);
|
|
@@ -13801,7 +13946,7 @@ var NcPruneLedger = class {
|
|
|
13801
13946
|
try {
|
|
13802
13947
|
const records = await this.store.query.query({
|
|
13803
13948
|
collection: NC_DEVICE_PRUNES_COLLECTION,
|
|
13804
|
-
filter: { limit: LOAD_LIMIT$
|
|
13949
|
+
filter: { limit: LOAD_LIMIT$5 }
|
|
13805
13950
|
});
|
|
13806
13951
|
const next = /* @__PURE__ */ new Map();
|
|
13807
13952
|
for (const record of records) {
|
|
@@ -17126,12 +17271,12 @@ var NcSummaryTemplateSchema = object({
|
|
|
17126
17271
|
title: string().max(500).optional(),
|
|
17127
17272
|
body: string().max(2e3).optional()
|
|
17128
17273
|
});
|
|
17129
|
-
var NameField = string().min(1).max(200);
|
|
17274
|
+
var NameField$1 = string().min(1).max(200);
|
|
17130
17275
|
/** MULTI-CAMERA is the point of the feature — a one-camera digest is a
|
|
17131
17276
|
* timelapse with extra steps, but it is allowed and it is cheap. */
|
|
17132
17277
|
var DeviceIdsField = array(number()).min(1).max(64);
|
|
17133
|
-
var TargetsField = array(NcRuleTargetSchema).min(1);
|
|
17134
|
-
var PriorityField = number().int().min(1).max(5);
|
|
17278
|
+
var TargetsField$1 = array(NcRuleTargetSchema).min(1);
|
|
17279
|
+
var PriorityField$1 = number().int().min(1).max(5);
|
|
17135
17280
|
/**
|
|
17136
17281
|
* The mosaic's tile ceiling. 9 (a 3×3 grid) is the shipped default: it is the
|
|
17137
17282
|
* most a phone lock-screen thumbnail can carry and still be read, and it is the
|
|
@@ -17296,7 +17441,7 @@ var NcSummaryAiSchema = object({
|
|
|
17296
17441
|
onFailure: _enum(["deliver-without-text", "skip"]).optional()
|
|
17297
17442
|
});
|
|
17298
17443
|
var NcSummaryRuleInputSchema = object({
|
|
17299
|
-
name: NameField,
|
|
17444
|
+
name: NameField$1,
|
|
17300
17445
|
enabled: boolean().default(true),
|
|
17301
17446
|
deviceIds: DeviceIdsField,
|
|
17302
17447
|
window: NcSummaryWindowSchema,
|
|
@@ -17305,7 +17450,7 @@ var NcSummaryRuleInputSchema = object({
|
|
|
17305
17450
|
maxAiImages: MaxAiImagesField.default(NC_SUMMARY_DEFAULT_AI_IMAGES),
|
|
17306
17451
|
rank: NcSummaryRankSchema.default("importance"),
|
|
17307
17452
|
barrierSeconds: BarrierSecondsField.default(120),
|
|
17308
|
-
targets: TargetsField,
|
|
17453
|
+
targets: TargetsField$1,
|
|
17309
17454
|
template: NcSummaryTemplateSchema.optional(),
|
|
17310
17455
|
/** Burned onto the mosaic's title band — and ONLY when `captions` is `all`. */
|
|
17311
17456
|
captions: NcSummaryCaptionsSchema.default(NC_SUMMARY_DEFAULT_CAPTIONS),
|
|
@@ -17315,7 +17460,7 @@ var NcSummaryRuleInputSchema = object({
|
|
|
17315
17460
|
/** Deliver a window that matched NOTHING? Default no: a nightly "0 events"
|
|
17316
17461
|
* is a line nobody reads, and it costs the lines beside it their reader. */
|
|
17317
17462
|
deliverEmpty: boolean().default(false),
|
|
17318
|
-
priority: PriorityField.default(3),
|
|
17463
|
+
priority: PriorityField$1.default(3),
|
|
17319
17464
|
ai: NcSummaryAiSchema.optional()
|
|
17320
17465
|
});
|
|
17321
17466
|
/**
|
|
@@ -17327,7 +17472,7 @@ var NcSummaryRuleInputSchema = object({
|
|
|
17327
17472
|
* plain `.optional()`.
|
|
17328
17473
|
*/
|
|
17329
17474
|
var NcSummaryRulePatchSchema = object({
|
|
17330
|
-
name: NameField.optional(),
|
|
17475
|
+
name: NameField$1.optional(),
|
|
17331
17476
|
enabled: boolean().optional(),
|
|
17332
17477
|
deviceIds: DeviceIdsField.optional(),
|
|
17333
17478
|
window: NcSummaryWindowSchema.optional(),
|
|
@@ -17336,12 +17481,12 @@ var NcSummaryRulePatchSchema = object({
|
|
|
17336
17481
|
maxAiImages: MaxAiImagesField.optional(),
|
|
17337
17482
|
rank: NcSummaryRankSchema.optional(),
|
|
17338
17483
|
barrierSeconds: BarrierSecondsField.optional(),
|
|
17339
|
-
targets: TargetsField.optional(),
|
|
17484
|
+
targets: TargetsField$1.optional(),
|
|
17340
17485
|
template: NcSummaryTemplateSchema.nullable().optional(),
|
|
17341
17486
|
captions: NcSummaryCaptionsSchema.optional(),
|
|
17342
17487
|
captionText: string().max(200).optional(),
|
|
17343
17488
|
deliverEmpty: boolean().optional(),
|
|
17344
|
-
priority: PriorityField.optional(),
|
|
17489
|
+
priority: PriorityField$1.optional(),
|
|
17345
17490
|
ai: NcSummaryAiSchema.nullable().optional()
|
|
17346
17491
|
});
|
|
17347
17492
|
/**
|
|
@@ -17374,6 +17519,89 @@ function assertSummaryBudget(rule) {
|
|
|
17374
17519
|
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`);
|
|
17375
17520
|
}
|
|
17376
17521
|
//#endregion
|
|
17522
|
+
//#region src/notification-center/calendar-events/calendar-events-window.ts
|
|
17523
|
+
/** One day in milliseconds. */
|
|
17524
|
+
var CALENDAR_EVENTS_DAY_MS = 1440 * 60 * 1e3;
|
|
17525
|
+
/**
|
|
17526
|
+
* Floor / ceiling on the horizon days a rule may declare.
|
|
17527
|
+
* 1 day is the minimum useful digest; 30 keeps a single notification bounded.
|
|
17528
|
+
*/
|
|
17529
|
+
var CALENDAR_EVENTS_MIN_HORIZON_DAYS = 1;
|
|
17530
|
+
var CALENDAR_EVENTS_MAX_HORIZON_DAYS = 30;
|
|
17531
|
+
var CALENDAR_EVENTS_DEFAULT_HORIZON_DAYS = 7;
|
|
17532
|
+
/**
|
|
17533
|
+
* Build the producer window for one fire.
|
|
17534
|
+
*
|
|
17535
|
+
* `horizonDays` is clamped into the legal range rather than refused — a
|
|
17536
|
+
* stored rule that predates a tighter bound must still fire.
|
|
17537
|
+
*/
|
|
17538
|
+
function calendarEventsWindow(nowMs, horizonDays) {
|
|
17539
|
+
const days = Math.min(CALENDAR_EVENTS_MAX_HORIZON_DAYS, Math.max(CALENDAR_EVENTS_MIN_HORIZON_DAYS, Math.floor(horizonDays)));
|
|
17540
|
+
return {
|
|
17541
|
+
startMs: nowMs,
|
|
17542
|
+
endMs: nowMs + days * CALENDAR_EVENTS_DAY_MS,
|
|
17543
|
+
horizonDays: days
|
|
17544
|
+
};
|
|
17545
|
+
}
|
|
17546
|
+
//#endregion
|
|
17547
|
+
//#region src/notification-center/calendar-events/calendar-events-rule.ts
|
|
17548
|
+
/**
|
|
17549
|
+
* NcCalendarEventsRule — scheduled "upcoming calendar events" digest.
|
|
17550
|
+
*
|
|
17551
|
+
* Same ownership model as Summaries/Timelapses: lives in the addon (not
|
|
17552
|
+
* `@camstack/types`) so a field is live on the next post-analysis deploy.
|
|
17553
|
+
* Rides no `NcDelivery` member; reaches the outside world through
|
|
17554
|
+
* `nc.*CalendarEvents*` bridge actions.
|
|
17555
|
+
*
|
|
17556
|
+
* Calendar selection mirrors detection classes: `calendars` = Only,
|
|
17557
|
+
* `calendarsExclude` = Except, both empty = All.
|
|
17558
|
+
*/
|
|
17559
|
+
var NcCalendarEventsTemplateSchema = object({
|
|
17560
|
+
title: string().max(500).optional(),
|
|
17561
|
+
body: string().max(4e3).optional()
|
|
17562
|
+
});
|
|
17563
|
+
var NameField = string().min(1).max(200);
|
|
17564
|
+
var TargetsField = array(NcRuleTargetSchema).min(1);
|
|
17565
|
+
var PriorityField = number().int().min(1).max(5);
|
|
17566
|
+
var HorizonDaysField = number().int().min(CALENDAR_EVENTS_MIN_HORIZON_DAYS).max(CALENDAR_EVENTS_MAX_HORIZON_DAYS);
|
|
17567
|
+
var CalendarIdsField = array(string().min(1).max(200)).max(64);
|
|
17568
|
+
var NcCalendarEventsRuleInputSchema = object({
|
|
17569
|
+
name: NameField,
|
|
17570
|
+
enabled: boolean().default(true),
|
|
17571
|
+
schedule: NcScheduleSchema,
|
|
17572
|
+
/** Only these calendars. Empty/absent with no exclude = All. */
|
|
17573
|
+
calendars: CalendarIdsField.optional(),
|
|
17574
|
+
/** All except these. Ignored when `calendars` is non-empty. */
|
|
17575
|
+
calendarsExclude: CalendarIdsField.optional(),
|
|
17576
|
+
horizonDays: HorizonDaysField.default(CALENDAR_EVENTS_DEFAULT_HORIZON_DAYS),
|
|
17577
|
+
/** Deliver when the horizon holds no events? Default no. */
|
|
17578
|
+
deliverEmpty: boolean().default(false),
|
|
17579
|
+
targets: TargetsField,
|
|
17580
|
+
template: NcCalendarEventsTemplateSchema.optional(),
|
|
17581
|
+
priority: PriorityField.default(3)
|
|
17582
|
+
});
|
|
17583
|
+
var NcCalendarEventsRulePatchSchema = object({
|
|
17584
|
+
name: NameField.optional(),
|
|
17585
|
+
enabled: boolean().optional(),
|
|
17586
|
+
schedule: NcScheduleSchema.optional(),
|
|
17587
|
+
calendars: CalendarIdsField.nullable().optional(),
|
|
17588
|
+
calendarsExclude: CalendarIdsField.nullable().optional(),
|
|
17589
|
+
horizonDays: HorizonDaysField.optional(),
|
|
17590
|
+
deliverEmpty: boolean().optional(),
|
|
17591
|
+
targets: TargetsField.optional(),
|
|
17592
|
+
template: NcCalendarEventsTemplateSchema.nullable().optional(),
|
|
17593
|
+
priority: PriorityField.optional()
|
|
17594
|
+
});
|
|
17595
|
+
var NcCalendarEventsRuleSchema = NcCalendarEventsRuleInputSchema.extend({
|
|
17596
|
+
id: string(),
|
|
17597
|
+
ownerUserId: string().optional(),
|
|
17598
|
+
/** Epoch-ms of the schedule window end that was last delivered. */
|
|
17599
|
+
generatedAt: number().optional(),
|
|
17600
|
+
createdBy: string(),
|
|
17601
|
+
createdAt: number(),
|
|
17602
|
+
updatedAt: number()
|
|
17603
|
+
});
|
|
17604
|
+
//#endregion
|
|
17377
17605
|
//#region src/notification-center/unstored-media-id.ts
|
|
17378
17606
|
/**
|
|
17379
17607
|
* An id for media bytes that were NEVER STORED.
|
|
@@ -18519,6 +18747,22 @@ var ncActions = defineCustomActions({
|
|
|
18519
18747
|
kind: "mutation",
|
|
18520
18748
|
caller: "required"
|
|
18521
18749
|
}),
|
|
18750
|
+
"nc.listCalendarEventsRules": customAction(object({}), object({
|
|
18751
|
+
rules: array(NcCalendarEventsRuleSchema.extend({ readOnly: boolean() })),
|
|
18752
|
+
canAuthor: boolean()
|
|
18753
|
+
}), { caller: "required" }),
|
|
18754
|
+
"nc.upsertCalendarEventsRule": customAction(object({
|
|
18755
|
+
ruleId: string().optional(),
|
|
18756
|
+
rule: NcCalendarEventsRuleInputSchema.optional(),
|
|
18757
|
+
patch: NcCalendarEventsRulePatchSchema.optional()
|
|
18758
|
+
}), object({ rule: NcCalendarEventsRuleSchema }), {
|
|
18759
|
+
kind: "mutation",
|
|
18760
|
+
caller: "required"
|
|
18761
|
+
}),
|
|
18762
|
+
"nc.deleteCalendarEventsRule": customAction(object({ ruleId: string() }), object({ success: literal(true) }), {
|
|
18763
|
+
kind: "mutation",
|
|
18764
|
+
caller: "required"
|
|
18765
|
+
}),
|
|
18522
18766
|
/**
|
|
18523
18767
|
* Produce one digest window RIGHT NOW — the operator's Test button.
|
|
18524
18768
|
*
|
|
@@ -18805,6 +19049,11 @@ function makeNcActionHandlers(deps) {
|
|
|
18805
19049
|
if (sum === void 0) throw new Error("this node does not host the summary producer — use the hub");
|
|
18806
19050
|
return sum;
|
|
18807
19051
|
};
|
|
19052
|
+
const requireCalendarEvents = () => {
|
|
19053
|
+
const ce = deps.calendarEvents;
|
|
19054
|
+
if (ce === void 0) throw new Error("this node does not host calendar-events rules — use the hub");
|
|
19055
|
+
return ce;
|
|
19056
|
+
};
|
|
18808
19057
|
/** The artefact index, or an honest refusal. */
|
|
18809
19058
|
const requireArtifacts = () => {
|
|
18810
19059
|
const index = deps.artifacts;
|
|
@@ -18833,6 +19082,13 @@ function makeNcActionHandlers(deps) {
|
|
|
18833
19082
|
if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: summary rule not owned: ${ruleId}`);
|
|
18834
19083
|
return rule;
|
|
18835
19084
|
};
|
|
19085
|
+
const assertCalendarEventsEditable = (ruleId, caller, ce) => {
|
|
19086
|
+
const rule = ce.store.get(ruleId);
|
|
19087
|
+
if (rule === null) throw new Error(`forbidden: calendar-events rule not found: ${ruleId}`);
|
|
19088
|
+
if (caller.isAdmin) return rule;
|
|
19089
|
+
if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: calendar-events rule not owned: ${ruleId}`);
|
|
19090
|
+
return rule;
|
|
19091
|
+
};
|
|
18836
19092
|
/** A timelapse rule the caller may EDIT. Same admin bypass as `NcRule`. */
|
|
18837
19093
|
const assertTimelapseEditable = (ruleId, caller, tl) => {
|
|
18838
19094
|
const rule = tl.store.get(ruleId);
|
|
@@ -19374,6 +19630,53 @@ function makeNcActionHandlers(deps) {
|
|
|
19374
19630
|
...out.skippedByAi === true ? { skippedByAi: true } : {}
|
|
19375
19631
|
};
|
|
19376
19632
|
},
|
|
19633
|
+
"nc.listCalendarEventsRules": async (_input, caller) => {
|
|
19634
|
+
const c = requireCaller$1(caller);
|
|
19635
|
+
const ce = requireCalendarEvents();
|
|
19636
|
+
return {
|
|
19637
|
+
rules: (c.isAdmin ? ce.store.list() : ce.store.listForOwner(c.userId)).map((r) => ({
|
|
19638
|
+
...r,
|
|
19639
|
+
readOnly: !c.isAdmin && r.ownerUserId !== c.userId
|
|
19640
|
+
})),
|
|
19641
|
+
canAuthor: true
|
|
19642
|
+
};
|
|
19643
|
+
},
|
|
19644
|
+
"nc.upsertCalendarEventsRule": async (input, caller) => {
|
|
19645
|
+
const c = requireCaller$1(caller);
|
|
19646
|
+
const ce = requireCalendarEvents();
|
|
19647
|
+
if (input.ruleId === void 0) {
|
|
19648
|
+
if (input.rule === void 0) throw new Error("upsertCalendarEventsRule: rule or ruleId required");
|
|
19649
|
+
const parsed = NcCalendarEventsRuleInputSchema.parse(input.rule);
|
|
19650
|
+
await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
|
|
19651
|
+
const rule = await ce.store.create(parsed, c.userId, c.isAdmin ? void 0 : c.userId);
|
|
19652
|
+
deps.logger.info("calendar-events rule created", { meta: {
|
|
19653
|
+
ruleId: rule.id,
|
|
19654
|
+
owner: c.userId
|
|
19655
|
+
} });
|
|
19656
|
+
return { rule };
|
|
19657
|
+
}
|
|
19658
|
+
assertCalendarEventsEditable(input.ruleId, c, ce);
|
|
19659
|
+
if (input.patch === void 0) throw new Error("upsertCalendarEventsRule: patch required");
|
|
19660
|
+
const patch = NcCalendarEventsRulePatchSchema.parse(input.patch);
|
|
19661
|
+
if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
|
|
19662
|
+
const rule = await ce.store.update(input.ruleId, patch);
|
|
19663
|
+
deps.logger.info("calendar-events rule updated", { meta: {
|
|
19664
|
+
ruleId: rule.id,
|
|
19665
|
+
owner: c.userId
|
|
19666
|
+
} });
|
|
19667
|
+
return { rule };
|
|
19668
|
+
},
|
|
19669
|
+
"nc.deleteCalendarEventsRule": async (input, caller) => {
|
|
19670
|
+
const c = requireCaller$1(caller);
|
|
19671
|
+
const ce = requireCalendarEvents();
|
|
19672
|
+
assertCalendarEventsEditable(input.ruleId, c, ce);
|
|
19673
|
+
await ce.store.delete(input.ruleId);
|
|
19674
|
+
deps.logger.info("calendar-events rule deleted", { meta: {
|
|
19675
|
+
ruleId: input.ruleId,
|
|
19676
|
+
owner: c.userId
|
|
19677
|
+
} });
|
|
19678
|
+
return { success: true };
|
|
19679
|
+
},
|
|
19377
19680
|
"nc.getTexts": async (input) => {
|
|
19378
19681
|
const texts = requireTexts();
|
|
19379
19682
|
return input.language === void 0 ? texts.view() : texts.view(input.language);
|
|
@@ -19986,19 +20289,39 @@ function planSummaryDevices(rule, isDead, guard) {
|
|
|
19986
20289
|
notes: notes.notes
|
|
19987
20290
|
};
|
|
19988
20291
|
}
|
|
20292
|
+
function stripDeadFromLists(lists, isDead) {
|
|
20293
|
+
const dead = lists.flatMap((list) => list.deviceIds.filter(isDead));
|
|
20294
|
+
if (dead.length === 0) return {
|
|
20295
|
+
next: lists,
|
|
20296
|
+
dead
|
|
20297
|
+
};
|
|
20298
|
+
return {
|
|
20299
|
+
dead,
|
|
20300
|
+
next: lists.map((list) => ({
|
|
20301
|
+
...list,
|
|
20302
|
+
deviceIds: list.deviceIds.filter((id) => !isDead(id))
|
|
20303
|
+
}))
|
|
20304
|
+
};
|
|
20305
|
+
}
|
|
19989
20306
|
function planAlarmDevices(alarm, isDead) {
|
|
19990
20307
|
if (alarm === null) return null;
|
|
19991
|
-
const
|
|
19992
|
-
const
|
|
20308
|
+
const nonBlocking = stripDeadFromLists(alarm.nonBlocking, isDead);
|
|
20309
|
+
const blocking = stripDeadFromLists(alarm.blocking, isDead);
|
|
20310
|
+
const secureOnArmOff = stripDeadFromLists(alarm.secureOnArmOff, isDead);
|
|
19993
20311
|
const forgetExcluded = alarm.excluded.filter(isDead);
|
|
19994
|
-
|
|
20312
|
+
const deadIds = [
|
|
20313
|
+
...nonBlocking.dead,
|
|
20314
|
+
...blocking.dead,
|
|
20315
|
+
...secureOnArmOff.dead,
|
|
20316
|
+
...forgetExcluded
|
|
20317
|
+
];
|
|
20318
|
+
if (deadIds.length === 0) return null;
|
|
19995
20319
|
return {
|
|
19996
|
-
dropped: deviceSubjects([...new Set(
|
|
20320
|
+
dropped: deviceSubjects([...new Set(deadIds)]),
|
|
19997
20321
|
droppedKind: "device",
|
|
19998
|
-
...
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
})) } : {},
|
|
20322
|
+
...nonBlocking.dead.length > 0 ? { nonBlocking: nonBlocking.next } : {},
|
|
20323
|
+
...blocking.dead.length > 0 ? { blocking: blocking.next } : {},
|
|
20324
|
+
...secureOnArmOff.dead.length > 0 ? { secureOnArmOff: secureOnArmOff.next } : {},
|
|
20002
20325
|
forgetExcluded
|
|
20003
20326
|
};
|
|
20004
20327
|
}
|
|
@@ -20040,6 +20363,8 @@ function referencedDeviceIds(snapshot) {
|
|
|
20040
20363
|
for (const rule of snapshot.timelapse) add(rule.deviceIds);
|
|
20041
20364
|
for (const rule of snapshot.summary) add(rule.deviceIds);
|
|
20042
20365
|
for (const list of snapshot.alarm?.nonBlocking ?? []) add(list.deviceIds);
|
|
20366
|
+
for (const list of snapshot.alarm?.blocking ?? []) add(list.deviceIds);
|
|
20367
|
+
for (const list of snapshot.alarm?.secureOnArmOff ?? []) add(list.deviceIds);
|
|
20043
20368
|
add(snapshot.alarm?.excluded);
|
|
20044
20369
|
add(snapshot.mutedDeviceIds);
|
|
20045
20370
|
return ids;
|
|
@@ -20384,6 +20709,8 @@ var NcDeletedSubjectPruner = class {
|
|
|
20384
20709
|
summary: this.writable("summary", this.deps.summary.list()),
|
|
20385
20710
|
alarm: panel !== null && settings !== void 0 ? {
|
|
20386
20711
|
nonBlocking: settings.nonBlocking,
|
|
20712
|
+
blocking: settings.blocking,
|
|
20713
|
+
secureOnArmOff: settings.secureOnArmOff,
|
|
20387
20714
|
announceTargets: settings.announceTargets,
|
|
20388
20715
|
excluded: panel.excludedDevices()
|
|
20389
20716
|
} : null,
|
|
@@ -20540,6 +20867,8 @@ var NcDeletedSubjectPruner = class {
|
|
|
20540
20867
|
...NO_SNAPSHOT,
|
|
20541
20868
|
alarm: {
|
|
20542
20869
|
nonBlocking: settings.nonBlocking,
|
|
20870
|
+
blocking: settings.blocking,
|
|
20871
|
+
secureOnArmOff: settings.secureOnArmOff,
|
|
20543
20872
|
announceTargets: settings.announceTargets,
|
|
20544
20873
|
excluded: panel.excludedDevices()
|
|
20545
20874
|
}
|
|
@@ -20548,6 +20877,8 @@ var NcDeletedSubjectPruner = class {
|
|
|
20548
20877
|
try {
|
|
20549
20878
|
const patch = {
|
|
20550
20879
|
...edit.nonBlocking !== void 0 ? { nonBlocking: edit.nonBlocking } : {},
|
|
20880
|
+
...edit.blocking !== void 0 ? { blocking: edit.blocking } : {},
|
|
20881
|
+
...edit.secureOnArmOff !== void 0 ? { secureOnArmOff: edit.secureOnArmOff } : {},
|
|
20551
20882
|
...edit.announceTargets !== void 0 ? { announceTargets: [...edit.announceTargets] } : {}
|
|
20552
20883
|
};
|
|
20553
20884
|
if (Object.keys(patch).length > 0) await panel.applySettings(patch);
|
|
@@ -28209,7 +28540,8 @@ var OUTBOX_RECORD_KINDS = new Set([
|
|
|
28209
28540
|
"audio-window",
|
|
28210
28541
|
"system-event",
|
|
28211
28542
|
"timelapse",
|
|
28212
|
-
"ai-summary"
|
|
28543
|
+
"ai-summary",
|
|
28544
|
+
"calendar-events"
|
|
28213
28545
|
]);
|
|
28214
28546
|
function isOutboxRecordKind(x) {
|
|
28215
28547
|
return typeof x === "string" && OUTBOX_RECORD_KINDS.has(x);
|
|
@@ -28562,7 +28894,7 @@ function buildRuleAiSystemPrompt(shape, language, maxWords, grouped = false) {
|
|
|
28562
28894
|
].join("");
|
|
28563
28895
|
}
|
|
28564
28896
|
/** `HH:MM` in the host timezone. */
|
|
28565
|
-
function clockOf$
|
|
28897
|
+
function clockOf$5(atMs) {
|
|
28566
28898
|
const d = new Date(atMs);
|
|
28567
28899
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
28568
28900
|
}
|
|
@@ -28600,7 +28932,7 @@ function buildRuleAiPrompt(input) {
|
|
|
28600
28932
|
if (shape === "detection-track") {
|
|
28601
28933
|
lines.push(`Camera: ${camera}.${located} Subject: ${subject}. The stills, in order:`);
|
|
28602
28934
|
for (const [i, frame] of input.frames.entries()) {
|
|
28603
|
-
const when = frame.atMs !== void 0 ? ` · ${clockOf$
|
|
28935
|
+
const when = frame.atMs !== void 0 ? ` · ${clockOf$5(frame.atMs)}` : "";
|
|
28604
28936
|
lines.push(`${String(i + 1)}.${when}`);
|
|
28605
28937
|
}
|
|
28606
28938
|
return lines.join("\n");
|
|
@@ -28897,7 +29229,7 @@ var NcRuleAiRowSchema = object({
|
|
|
28897
29229
|
updatedAt: number()
|
|
28898
29230
|
});
|
|
28899
29231
|
/** Operator-authored and tiny; a generous ceiling. */
|
|
28900
|
-
var LOAD_LIMIT$
|
|
29232
|
+
var LOAD_LIMIT$4 = 1e4;
|
|
28901
29233
|
var NC_RULE_AI_SPEC = {
|
|
28902
29234
|
collection: NC_RULE_AI_COLLECTION,
|
|
28903
29235
|
columns: [...NC_RULE_AI_COLUMNS],
|
|
@@ -28913,7 +29245,7 @@ var NC_RULE_AI_SPEC = {
|
|
|
28913
29245
|
const parsed = NcRuleAiRowSchema.safeParse(data["row"]);
|
|
28914
29246
|
return parsed.success ? parsed.data : null;
|
|
28915
29247
|
},
|
|
28916
|
-
loadLimit: LOAD_LIMIT$
|
|
29248
|
+
loadLimit: LOAD_LIMIT$4
|
|
28917
29249
|
};
|
|
28918
29250
|
var NcRuleAiStore = class {
|
|
28919
29251
|
ledger;
|
|
@@ -29250,7 +29582,7 @@ var NC_RULES_INDEXES = [{
|
|
|
29250
29582
|
columns: ["enabled"]
|
|
29251
29583
|
}];
|
|
29252
29584
|
/** Query cap — the rule set is operator-authored and tiny; a generous ceiling. */
|
|
29253
|
-
var LOAD_LIMIT$
|
|
29585
|
+
var LOAD_LIMIT$3 = 1e4;
|
|
29254
29586
|
function isRecord$3(value) {
|
|
29255
29587
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29256
29588
|
}
|
|
@@ -29299,7 +29631,7 @@ function makeRulesSpec(onProjection) {
|
|
|
29299
29631
|
if (projection.kind === "projected") onProjection?.(key, projection);
|
|
29300
29632
|
return parsed.data;
|
|
29301
29633
|
},
|
|
29302
|
-
loadLimit: LOAD_LIMIT$
|
|
29634
|
+
loadLimit: LOAD_LIMIT$3
|
|
29303
29635
|
};
|
|
29304
29636
|
}
|
|
29305
29637
|
/** The spec `declare` needs — no sink: declaring a collection projects nothing. */
|
|
@@ -30371,7 +30703,7 @@ var DEFAULT_PROMPT_EN = "What happened during this period? Summarise it for a ho
|
|
|
30371
30703
|
*/
|
|
30372
30704
|
var SummaryAnswerSchema = object({ summary: string() });
|
|
30373
30705
|
/** `HH:MM` in the host timezone — the two numbers a frame label needs. */
|
|
30374
|
-
function clockOf$
|
|
30706
|
+
function clockOf$4(atMs) {
|
|
30375
30707
|
const d = new Date(atMs);
|
|
30376
30708
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
30377
30709
|
}
|
|
@@ -30394,7 +30726,7 @@ function buildSummaryAiPrompt(input, frames) {
|
|
|
30394
30726
|
const roster = frames.map((f, i) => {
|
|
30395
30727
|
const location = plainLabel(f.location ?? "");
|
|
30396
30728
|
const where = location.length > 0 ? ` (${location})` : "";
|
|
30397
|
-
return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$
|
|
30729
|
+
return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$4(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
|
|
30398
30730
|
}).join("\n");
|
|
30399
30731
|
const described = /* @__PURE__ */ new Map();
|
|
30400
30732
|
for (const f of frames) {
|
|
@@ -30997,7 +31329,7 @@ var NO_DETECTIONS$1 = {
|
|
|
30997
31329
|
* has to check is a key somebody can get wrong. */
|
|
30998
31330
|
var BODY_SEPARATOR$1 = " · ";
|
|
30999
31331
|
/** `HH:MM` in the host timezone. */
|
|
31000
|
-
function clockOf$
|
|
31332
|
+
function clockOf$3(atMs) {
|
|
31001
31333
|
const d = new Date(atMs);
|
|
31002
31334
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
31003
31335
|
}
|
|
@@ -31011,8 +31343,8 @@ function clockOf$2(atMs) {
|
|
|
31011
31343
|
function summaryTemplateVars(input) {
|
|
31012
31344
|
return {
|
|
31013
31345
|
rule: input.ruleName,
|
|
31014
|
-
from: clockOf$
|
|
31015
|
-
to: clockOf$
|
|
31346
|
+
from: clockOf$3(input.window.startMs),
|
|
31347
|
+
to: clockOf$3(input.window.endMs),
|
|
31016
31348
|
time: ncClockText(input.texts.language, input.generatedAt),
|
|
31017
31349
|
events: String(input.tileCount),
|
|
31018
31350
|
cameras: String(input.cameraCount),
|
|
@@ -31056,7 +31388,7 @@ function composeSummaryBody(input) {
|
|
|
31056
31388
|
* string, for the reason the timelapse learned: a hand-rolled `n === 1` shipped
|
|
31057
31389
|
* "1 intervalli ripresi" to a live phone.
|
|
31058
31390
|
*/
|
|
31059
|
-
function derivedBody$
|
|
31391
|
+
function derivedBody$2(input, vars) {
|
|
31060
31392
|
const texts = input.texts;
|
|
31061
31393
|
const parts = [texts.text({
|
|
31062
31394
|
key: "summary.body.window",
|
|
@@ -31091,7 +31423,7 @@ function derivedBody$1(input, vars) {
|
|
|
31091
31423
|
}
|
|
31092
31424
|
/** Per-target params with the derived `clickUrl` filled in where absent. A
|
|
31093
31425
|
* value the operator set on the target WINS — he chose it. */
|
|
31094
|
-
function paramsFor$
|
|
31426
|
+
function paramsFor$2(target, clickUrl) {
|
|
31095
31427
|
if (clickUrl === void 0) return target.params;
|
|
31096
31428
|
if (target.params === void 0) return { clickUrl };
|
|
31097
31429
|
if (typeof target.params["clickUrl"] === "string") return target.params;
|
|
@@ -31107,11 +31439,11 @@ function summaryRecordId(ruleId, windowEndMs) {
|
|
|
31107
31439
|
/** The caption burned onto the mosaic's title band, with the same `{{var}}`
|
|
31108
31440
|
* vocabulary the title and body use. `''` is a real answer ("no caption"). */
|
|
31109
31441
|
function summaryCaptionText(input) {
|
|
31110
|
-
if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$
|
|
31442
|
+
if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$3(input.window.startMs)}→${clockOf$3(input.window.endMs)}`;
|
|
31111
31443
|
return renderTemplate(input.captionText, {
|
|
31112
31444
|
rule: input.ruleName,
|
|
31113
|
-
from: clockOf$
|
|
31114
|
-
to: clockOf$
|
|
31445
|
+
from: clockOf$3(input.window.startMs),
|
|
31446
|
+
to: clockOf$3(input.window.endMs)
|
|
31115
31447
|
}) ?? "";
|
|
31116
31448
|
}
|
|
31117
31449
|
/**
|
|
@@ -31129,7 +31461,7 @@ function buildSummaryOutboxInputs(input) {
|
|
|
31129
31461
|
const body = composeSummaryBody({
|
|
31130
31462
|
aiText: aiSentence(input),
|
|
31131
31463
|
renderedTemplateBody: renderTemplate(input.template?.body, vars),
|
|
31132
|
-
derivedBody: derivedBody$
|
|
31464
|
+
derivedBody: derivedBody$2(input, vars),
|
|
31133
31465
|
templateNamesAi: templatePlaceholders(input.template?.body ?? "").includes("ai")
|
|
31134
31466
|
});
|
|
31135
31467
|
const recordId = summaryRecordId(input.ruleId, input.window.endMs);
|
|
@@ -31148,7 +31480,7 @@ function buildSummaryOutboxInputs(input) {
|
|
|
31148
31480
|
},
|
|
31149
31481
|
media: "none",
|
|
31150
31482
|
...artifacts.length > 0 ? { artifacts } : {},
|
|
31151
|
-
...paramsFor$
|
|
31483
|
+
...paramsFor$2(target, input.clickUrl) !== void 0 ? { params: paramsFor$2(target, input.clickUrl) } : {},
|
|
31152
31484
|
subject: {
|
|
31153
31485
|
deviceId: input.primaryDeviceId,
|
|
31154
31486
|
className: "summary",
|
|
@@ -32043,7 +32375,7 @@ var NcSummaryProducer = class {
|
|
|
32043
32375
|
purpose: "summary",
|
|
32044
32376
|
tiles: tiles.map(({ candidate, jpeg }) => ({
|
|
32045
32377
|
jpeg,
|
|
32046
|
-
...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$
|
|
32378
|
+
...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$2(candidate.firstSeen)} · ${candidate.className}` } : {},
|
|
32047
32379
|
deviceId: candidate.deviceId,
|
|
32048
32380
|
sourceId: candidate.trackId
|
|
32049
32381
|
})),
|
|
@@ -32266,7 +32598,7 @@ var NcSummaryProducer = class {
|
|
|
32266
32598
|
}
|
|
32267
32599
|
};
|
|
32268
32600
|
/** `HH:MM` in the host timezone — the two numbers a tile label needs. */
|
|
32269
|
-
function clockOf$
|
|
32601
|
+
function clockOf$2(atMs) {
|
|
32270
32602
|
const d = new Date(atMs);
|
|
32271
32603
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
32272
32604
|
}
|
|
@@ -32342,7 +32674,7 @@ var NC_SUMMARY_RULES_INDEXES = [{
|
|
|
32342
32674
|
columns: ["enabled"]
|
|
32343
32675
|
}];
|
|
32344
32676
|
/** The rule set is operator-authored and tiny; a generous ceiling. */
|
|
32345
|
-
var LOAD_LIMIT$
|
|
32677
|
+
var LOAD_LIMIT$2 = 1e4;
|
|
32346
32678
|
/**
|
|
32347
32679
|
* The ledger contract for the digest rule set.
|
|
32348
32680
|
*
|
|
@@ -32372,7 +32704,7 @@ var NC_SUMMARY_RULES_SPEC = {
|
|
|
32372
32704
|
const parsed = NcSummaryRuleSchema.safeParse(data["rule"]);
|
|
32373
32705
|
return parsed.success ? parsed.data : null;
|
|
32374
32706
|
},
|
|
32375
|
-
loadLimit: LOAD_LIMIT$
|
|
32707
|
+
loadLimit: LOAD_LIMIT$2
|
|
32376
32708
|
};
|
|
32377
32709
|
/**
|
|
32378
32710
|
* The three-way CLEARABLE patch signal, one function per field: `undefined`
|
|
@@ -32383,7 +32715,7 @@ var NC_SUMMARY_RULES_SPEC = {
|
|
|
32383
32715
|
* cast here would be a cast on the exact path that decides whether a persisted
|
|
32384
32716
|
* rule keeps a field. Three literal destructurings are three provable ones.
|
|
32385
32717
|
*/
|
|
32386
|
-
function applyTemplatePatch$
|
|
32718
|
+
function applyTemplatePatch$2(merged, template) {
|
|
32387
32719
|
if (template === void 0) return merged;
|
|
32388
32720
|
if (template !== null) return {
|
|
32389
32721
|
...merged,
|
|
@@ -32529,7 +32861,7 @@ var NcSummaryStore = class {
|
|
|
32529
32861
|
createdAt: existing.createdAt,
|
|
32530
32862
|
updatedAt: this.now()
|
|
32531
32863
|
};
|
|
32532
|
-
return this.write(applyAiPatch(applyFiltersPatch(applyTemplatePatch$
|
|
32864
|
+
return this.write(applyAiPatch(applyFiltersPatch(applyTemplatePatch$2(merged, template), filters), ai));
|
|
32533
32865
|
}
|
|
32534
32866
|
async setEnabled(ruleId, enabled) {
|
|
32535
32867
|
return this.update(ruleId, { enabled });
|
|
@@ -32581,6 +32913,408 @@ var NcSummaryStore = class {
|
|
|
32581
32913
|
}
|
|
32582
32914
|
};
|
|
32583
32915
|
//#endregion
|
|
32916
|
+
//#region src/notification-center/calendar-events/calendar-events-store.ts
|
|
32917
|
+
/**
|
|
32918
|
+
* Durable calendar-events rule set — same DurableLedger mechanics as
|
|
32919
|
+
* Summaries, with a per-rule `generatedAt` stamp (one fire per schedule window).
|
|
32920
|
+
*/
|
|
32921
|
+
/**
|
|
32922
|
+
* @durable class=config owner=notification-center
|
|
32923
|
+
* write="an operator creates or edits a calendar-events rule, or the producer stamps a delivered window via markGenerated"
|
|
32924
|
+
* retention="none — a row goes only when the operator deletes the rule."
|
|
32925
|
+
*/
|
|
32926
|
+
var NC_CALENDAR_EVENTS_RULES_COLLECTION = "notification-center:calendar-events-rules";
|
|
32927
|
+
var NC_CALENDAR_EVENTS_RULES_COLUMNS = [
|
|
32928
|
+
{
|
|
32929
|
+
name: "id",
|
|
32930
|
+
type: "TEXT",
|
|
32931
|
+
primaryKey: true,
|
|
32932
|
+
notNull: true
|
|
32933
|
+
},
|
|
32934
|
+
{
|
|
32935
|
+
name: "name",
|
|
32936
|
+
type: "TEXT",
|
|
32937
|
+
notNull: true
|
|
32938
|
+
},
|
|
32939
|
+
{
|
|
32940
|
+
name: "enabled",
|
|
32941
|
+
type: "BOOLEAN",
|
|
32942
|
+
notNull: true
|
|
32943
|
+
},
|
|
32944
|
+
{
|
|
32945
|
+
name: "updatedAt",
|
|
32946
|
+
type: "INTEGER",
|
|
32947
|
+
notNull: true
|
|
32948
|
+
},
|
|
32949
|
+
{
|
|
32950
|
+
name: "rule",
|
|
32951
|
+
type: "JSON",
|
|
32952
|
+
notNull: true
|
|
32953
|
+
}
|
|
32954
|
+
];
|
|
32955
|
+
var NC_CALENDAR_EVENTS_RULES_INDEXES = [{
|
|
32956
|
+
name: "idx_nc_calendar_events_rules_enabled",
|
|
32957
|
+
columns: ["enabled"]
|
|
32958
|
+
}];
|
|
32959
|
+
var LOAD_LIMIT$1 = 1e4;
|
|
32960
|
+
var NC_CALENDAR_EVENTS_RULES_SPEC = {
|
|
32961
|
+
collection: NC_CALENDAR_EVENTS_RULES_COLLECTION,
|
|
32962
|
+
columns: [...NC_CALENDAR_EVENTS_RULES_COLUMNS],
|
|
32963
|
+
indexes: [...NC_CALENDAR_EVENTS_RULES_INDEXES],
|
|
32964
|
+
writeMode: "write-through",
|
|
32965
|
+
keyOf: (rule) => rule.id,
|
|
32966
|
+
toValue: (rule) => ({
|
|
32967
|
+
name: rule.name,
|
|
32968
|
+
enabled: rule.enabled,
|
|
32969
|
+
updatedAt: rule.updatedAt,
|
|
32970
|
+
rule
|
|
32971
|
+
}),
|
|
32972
|
+
fromRecord: (_key, data) => {
|
|
32973
|
+
const parsed = NcCalendarEventsRuleSchema.safeParse(data["rule"]);
|
|
32974
|
+
return parsed.success ? parsed.data : null;
|
|
32975
|
+
},
|
|
32976
|
+
loadLimit: LOAD_LIMIT$1
|
|
32977
|
+
};
|
|
32978
|
+
function applyTemplatePatch$1(merged, template) {
|
|
32979
|
+
if (template === void 0) return merged;
|
|
32980
|
+
if (template !== null) return {
|
|
32981
|
+
...merged,
|
|
32982
|
+
template
|
|
32983
|
+
};
|
|
32984
|
+
const { template: _cleared, ...rest } = merged;
|
|
32985
|
+
return rest;
|
|
32986
|
+
}
|
|
32987
|
+
function applyOptionalStringList(merged, key, value) {
|
|
32988
|
+
if (value === void 0) return merged;
|
|
32989
|
+
if (value !== null) return {
|
|
32990
|
+
...merged,
|
|
32991
|
+
[key]: [...value]
|
|
32992
|
+
};
|
|
32993
|
+
const { [key]: _cleared, ...rest } = merged;
|
|
32994
|
+
return rest;
|
|
32995
|
+
}
|
|
32996
|
+
var NcCalendarEventsStore = class {
|
|
32997
|
+
ledger;
|
|
32998
|
+
store;
|
|
32999
|
+
logger;
|
|
33000
|
+
now;
|
|
33001
|
+
newId;
|
|
33002
|
+
constructor(deps) {
|
|
33003
|
+
this.store = deps.store;
|
|
33004
|
+
this.ledger = new DurableLedger({
|
|
33005
|
+
spec: NC_CALENDAR_EVENTS_RULES_SPEC,
|
|
33006
|
+
store: deps.store,
|
|
33007
|
+
logger: deps.logger
|
|
33008
|
+
});
|
|
33009
|
+
this.logger = deps.logger;
|
|
33010
|
+
this.now = deps.now ?? (() => Date.now());
|
|
33011
|
+
this.newId = deps.newId ?? (() => randomUUID());
|
|
33012
|
+
}
|
|
33013
|
+
static async declare(store) {
|
|
33014
|
+
await DurableLedger.declare(store, NC_CALENDAR_EVENTS_RULES_SPEC);
|
|
33015
|
+
}
|
|
33016
|
+
async load() {
|
|
33017
|
+
await this.ledger.load();
|
|
33018
|
+
this.logger.debug("calendar-events rules loaded", { meta: { rules: this.ledger.size } });
|
|
33019
|
+
}
|
|
33020
|
+
list() {
|
|
33021
|
+
return this.ledger.snapshot().toSorted((a, b) => b.updatedAt - a.updatedAt);
|
|
33022
|
+
}
|
|
33023
|
+
listEnabled() {
|
|
33024
|
+
return this.list().filter((r) => r.enabled);
|
|
33025
|
+
}
|
|
33026
|
+
get(ruleId) {
|
|
33027
|
+
return this.ledger.get(ruleId) ?? null;
|
|
33028
|
+
}
|
|
33029
|
+
listForOwner(userId) {
|
|
33030
|
+
return this.list().filter((r) => r.ownerUserId === void 0 || r.ownerUserId === userId);
|
|
33031
|
+
}
|
|
33032
|
+
async create(input, createdBy, ownerUserId) {
|
|
33033
|
+
const now = this.now();
|
|
33034
|
+
const rule = NcCalendarEventsRuleSchema.parse({
|
|
33035
|
+
...NcCalendarEventsRuleInputSchema.parse(input),
|
|
33036
|
+
id: this.newId(),
|
|
33037
|
+
...ownerUserId !== void 0 ? { ownerUserId } : {},
|
|
33038
|
+
createdBy,
|
|
33039
|
+
createdAt: now,
|
|
33040
|
+
updatedAt: now
|
|
33041
|
+
});
|
|
33042
|
+
await this.ledger.put(rule);
|
|
33043
|
+
return rule;
|
|
33044
|
+
}
|
|
33045
|
+
async update(ruleId, patch) {
|
|
33046
|
+
const existing = this.ledger.get(ruleId);
|
|
33047
|
+
if (!existing) throw new Error(`calendar-events rule not found: ${ruleId}`);
|
|
33048
|
+
const { template, calendars, calendarsExclude, ...rest } = patch;
|
|
33049
|
+
const merged = {
|
|
33050
|
+
...existing,
|
|
33051
|
+
...rest,
|
|
33052
|
+
id: existing.id,
|
|
33053
|
+
...existing.ownerUserId !== void 0 ? { ownerUserId: existing.ownerUserId } : {},
|
|
33054
|
+
...existing.generatedAt !== void 0 ? { generatedAt: existing.generatedAt } : {},
|
|
33055
|
+
createdBy: existing.createdBy,
|
|
33056
|
+
createdAt: existing.createdAt,
|
|
33057
|
+
updatedAt: this.now()
|
|
33058
|
+
};
|
|
33059
|
+
return this.write(applyOptionalStringList(applyOptionalStringList(applyTemplatePatch$1(merged, template), "calendars", calendars), "calendarsExclude", calendarsExclude));
|
|
33060
|
+
}
|
|
33061
|
+
async setEnabled(ruleId, enabled) {
|
|
33062
|
+
return this.update(ruleId, { enabled });
|
|
33063
|
+
}
|
|
33064
|
+
async markGenerated(ruleId, windowEndMs) {
|
|
33065
|
+
const existing = this.ledger.get(ruleId);
|
|
33066
|
+
if (!existing) throw new Error(`calendar-events rule not found: ${ruleId}`);
|
|
33067
|
+
return this.write({
|
|
33068
|
+
...existing,
|
|
33069
|
+
generatedAt: Math.max(existing.generatedAt ?? 0, windowEndMs)
|
|
33070
|
+
});
|
|
33071
|
+
}
|
|
33072
|
+
/**
|
|
33073
|
+
* Idempotent delete — unknown ids are a no-op in RAM, still attempted on disk.
|
|
33074
|
+
*
|
|
33075
|
+
* Deliberately NOT {@link DurableLedger.forget}: that swallows a failed
|
|
33076
|
+
* durable delete. Mirror eviction first; durable half re-throws on failure
|
|
33077
|
+
* (same shape as Summaries).
|
|
33078
|
+
*/
|
|
33079
|
+
async delete(ruleId) {
|
|
33080
|
+
this.ledger.evict(ruleId);
|
|
33081
|
+
try {
|
|
33082
|
+
await this.store.delete.mutate({
|
|
33083
|
+
collection: NC_CALENDAR_EVENTS_RULES_COLLECTION,
|
|
33084
|
+
key: ruleId
|
|
33085
|
+
});
|
|
33086
|
+
} catch (err) {
|
|
33087
|
+
this.logger.warn("calendar-events rule durable delete failed", { meta: {
|
|
33088
|
+
ruleId,
|
|
33089
|
+
error: err instanceof Error ? err.message : String(err)
|
|
33090
|
+
} });
|
|
33091
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
33092
|
+
}
|
|
33093
|
+
}
|
|
33094
|
+
async write(rule) {
|
|
33095
|
+
const validated = NcCalendarEventsRuleSchema.parse(rule);
|
|
33096
|
+
await this.ledger.put(validated);
|
|
33097
|
+
return validated;
|
|
33098
|
+
}
|
|
33099
|
+
};
|
|
33100
|
+
//#endregion
|
|
33101
|
+
//#region src/notification-center/calendar-events/calendar-events-delivery.ts
|
|
33102
|
+
function calendarEventsRecordId(ruleId, windowEndMs) {
|
|
33103
|
+
return `calendar-events:${ruleId}:${windowEndMs}`;
|
|
33104
|
+
}
|
|
33105
|
+
/** `YYYY-MM-DD HH:MM` in the host timezone. */
|
|
33106
|
+
function clockOf$1(atMs) {
|
|
33107
|
+
const d = new Date(atMs);
|
|
33108
|
+
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")}`}`;
|
|
33109
|
+
}
|
|
33110
|
+
function formatEventLine(event) {
|
|
33111
|
+
const when = event.allDay ? clockOf$1(event.startMs).slice(0, 10) : `${clockOf$1(event.startMs)}–${clockOf$1(event.endMs)}`;
|
|
33112
|
+
const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
|
|
33113
|
+
return `• ${when} ${event.title}${where}`;
|
|
33114
|
+
}
|
|
33115
|
+
function derivedBody$1(input) {
|
|
33116
|
+
if (input.events.length === 0) return `No events in the next ${String(input.horizonDays)} day(s).`;
|
|
33117
|
+
const lines = input.events.slice(0, 40).map(formatEventLine);
|
|
33118
|
+
const extra = input.events.length > 40 ? `\n…and ${String(input.events.length - 40)} more` : "";
|
|
33119
|
+
return lines.join("\n") + extra;
|
|
33120
|
+
}
|
|
33121
|
+
function templateVars(input) {
|
|
33122
|
+
return {
|
|
33123
|
+
rule: input.ruleName,
|
|
33124
|
+
count: String(input.events.length),
|
|
33125
|
+
horizonDays: String(input.horizonDays),
|
|
33126
|
+
from: clockOf$1(input.eventWindow.startMs),
|
|
33127
|
+
to: clockOf$1(input.eventWindow.endMs),
|
|
33128
|
+
events: derivedBody$1(input),
|
|
33129
|
+
calendars: String(input.calendarIds.length)
|
|
33130
|
+
};
|
|
33131
|
+
}
|
|
33132
|
+
function paramsFor$1(target) {
|
|
33133
|
+
const params = target.params;
|
|
33134
|
+
if (params === void 0) return void 0;
|
|
33135
|
+
const out = {};
|
|
33136
|
+
for (const [k, v] of Object.entries(params)) if (typeof v === "string") out[k] = v;
|
|
33137
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
33138
|
+
}
|
|
33139
|
+
/**
|
|
33140
|
+
* One outbox row per enabled target. Ready for `NcOutbox.enqueue`.
|
|
33141
|
+
*/
|
|
33142
|
+
function buildCalendarEventsOutboxInputs(input) {
|
|
33143
|
+
const vars = templateVars(input);
|
|
33144
|
+
const optedOut = new Set(input.disabledTargetIds ?? []);
|
|
33145
|
+
const title = renderTemplate(input.template?.title, vars) ?? `${input.ruleName}: ${String(input.events.length)} upcoming`;
|
|
33146
|
+
const body = renderTemplate(input.template?.body, vars) ?? derivedBody$1(input);
|
|
33147
|
+
const recordId = calendarEventsRecordId(input.ruleId, input.scheduleWindow.endMs);
|
|
33148
|
+
return input.targets.filter((t) => !optedOut.has(t.targetId)).map((target) => {
|
|
33149
|
+
const payload = {
|
|
33150
|
+
ruleName: input.ruleName,
|
|
33151
|
+
delivery: "calendar-events",
|
|
33152
|
+
priority: input.priority,
|
|
33153
|
+
template: {
|
|
33154
|
+
title,
|
|
33155
|
+
body
|
|
33156
|
+
},
|
|
33157
|
+
media: "none",
|
|
33158
|
+
...paramsFor$1(target) !== void 0 ? { params: paramsFor$1(target) } : {},
|
|
33159
|
+
subject: {
|
|
33160
|
+
deviceId: NC_SYSTEM_DEVICE_ID$1,
|
|
33161
|
+
className: "calendar-events",
|
|
33162
|
+
zones: [],
|
|
33163
|
+
timestamp: input.scheduleWindow.endMs
|
|
33164
|
+
}
|
|
33165
|
+
};
|
|
33166
|
+
return {
|
|
33167
|
+
ruleId: input.ruleId,
|
|
33168
|
+
targetId: target.targetId,
|
|
33169
|
+
deviceId: NC_SYSTEM_DEVICE_ID$1,
|
|
33170
|
+
recordKind: "calendar-events",
|
|
33171
|
+
recordId,
|
|
33172
|
+
payload
|
|
33173
|
+
};
|
|
33174
|
+
});
|
|
33175
|
+
}
|
|
33176
|
+
//#endregion
|
|
33177
|
+
//#region src/notification-center/calendar-events/calendar-events-selection.ts
|
|
33178
|
+
/**
|
|
33179
|
+
* Expand a rule's calendar scope against the known calendar ids.
|
|
33180
|
+
*
|
|
33181
|
+
* - All (both lists empty/absent) → every known id
|
|
33182
|
+
* - Only → the include list, intersected with known (unknown ids dropped)
|
|
33183
|
+
* - Except → known minus the exclude list
|
|
33184
|
+
*
|
|
33185
|
+
* An empty known set always yields empty — there is nothing to notify about.
|
|
33186
|
+
*/
|
|
33187
|
+
function expandCalendarScope(scope, knownIds) {
|
|
33188
|
+
const known = [...knownIds];
|
|
33189
|
+
if (known.length === 0) return [];
|
|
33190
|
+
const include = scope?.calendars ?? [];
|
|
33191
|
+
const exclude = scope?.calendarsExclude ?? [];
|
|
33192
|
+
if (include.length > 0) {
|
|
33193
|
+
const wanted = new Set(include);
|
|
33194
|
+
return known.filter((id) => wanted.has(id));
|
|
33195
|
+
}
|
|
33196
|
+
if (exclude.length > 0) {
|
|
33197
|
+
const banned = new Set(exclude);
|
|
33198
|
+
return known.filter((id) => !banned.has(id));
|
|
33199
|
+
}
|
|
33200
|
+
return known;
|
|
33201
|
+
}
|
|
33202
|
+
//#endregion
|
|
33203
|
+
//#region src/notification-center/calendar-events/calendar-events-producer.ts
|
|
33204
|
+
/** How far back a closed schedule window may still be owed. */
|
|
33205
|
+
var CALENDAR_EVENTS_LOOKBACK_MS = 2160 * 60 * 1e3;
|
|
33206
|
+
var NcCalendarEventsProducer = class {
|
|
33207
|
+
deps;
|
|
33208
|
+
inFlight = false;
|
|
33209
|
+
now;
|
|
33210
|
+
constructor(deps) {
|
|
33211
|
+
this.deps = deps;
|
|
33212
|
+
this.now = deps.now ?? (() => Date.now());
|
|
33213
|
+
}
|
|
33214
|
+
async tick() {
|
|
33215
|
+
if (this.inFlight) return {
|
|
33216
|
+
examined: 0,
|
|
33217
|
+
sent: 0,
|
|
33218
|
+
empty: 0,
|
|
33219
|
+
skipped: 0,
|
|
33220
|
+
failed: 0
|
|
33221
|
+
};
|
|
33222
|
+
this.inFlight = true;
|
|
33223
|
+
const report = {
|
|
33224
|
+
examined: 0,
|
|
33225
|
+
sent: 0,
|
|
33226
|
+
empty: 0,
|
|
33227
|
+
skipped: 0,
|
|
33228
|
+
failed: 0
|
|
33229
|
+
};
|
|
33230
|
+
try {
|
|
33231
|
+
const rules = this.deps.store.listEnabled();
|
|
33232
|
+
for (const rule of rules) {
|
|
33233
|
+
report.examined += 1;
|
|
33234
|
+
try {
|
|
33235
|
+
const outcome = await this.runRule(rule);
|
|
33236
|
+
if (outcome === "sent") report.sent += 1;
|
|
33237
|
+
else if (outcome === "empty") report.empty += 1;
|
|
33238
|
+
else report.skipped += 1;
|
|
33239
|
+
} catch (err) {
|
|
33240
|
+
report.failed += 1;
|
|
33241
|
+
this.deps.logger.warn("calendar-events rule failed", { meta: {
|
|
33242
|
+
ruleId: rule.id,
|
|
33243
|
+
error: err instanceof Error ? err.message : String(err)
|
|
33244
|
+
} });
|
|
33245
|
+
}
|
|
33246
|
+
}
|
|
33247
|
+
return report;
|
|
33248
|
+
} finally {
|
|
33249
|
+
this.inFlight = false;
|
|
33250
|
+
}
|
|
33251
|
+
}
|
|
33252
|
+
async runRule(rule) {
|
|
33253
|
+
const nowMs = this.now();
|
|
33254
|
+
const scheduleWindow = findLastClosedWindow({
|
|
33255
|
+
schedule: rule.schedule,
|
|
33256
|
+
nowMs,
|
|
33257
|
+
lookbackMs: CALENDAR_EVENTS_LOOKBACK_MS
|
|
33258
|
+
});
|
|
33259
|
+
if (scheduleWindow === null) return "skipped";
|
|
33260
|
+
if ((rule.generatedAt ?? 0) >= scheduleWindow.endMs) return "skipped";
|
|
33261
|
+
const known = await this.deps.calendars.listCalendars();
|
|
33262
|
+
const calendarIds = expandCalendarScope({
|
|
33263
|
+
calendars: rule.calendars,
|
|
33264
|
+
calendarsExclude: rule.calendarsExclude
|
|
33265
|
+
}, known.map((c) => c.id));
|
|
33266
|
+
if (calendarIds.length === 0) {
|
|
33267
|
+
this.deps.logger.info("calendar-events: no calendars in scope", { meta: { ruleId: rule.id } });
|
|
33268
|
+
if (!rule.deliverEmpty) {
|
|
33269
|
+
await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
|
|
33270
|
+
return "empty";
|
|
33271
|
+
}
|
|
33272
|
+
}
|
|
33273
|
+
const eventWindow = calendarEventsWindow(nowMs, rule.horizonDays);
|
|
33274
|
+
const events = calendarIds.length === 0 ? [] : await this.deps.calendars.listEvents({
|
|
33275
|
+
calendarIds,
|
|
33276
|
+
startMs: eventWindow.startMs,
|
|
33277
|
+
endMs: eventWindow.endMs
|
|
33278
|
+
});
|
|
33279
|
+
if (events.length === 0 && !rule.deliverEmpty) {
|
|
33280
|
+
this.deps.logger.info("calendar-events: empty horizon, not delivering", { meta: {
|
|
33281
|
+
ruleId: rule.id,
|
|
33282
|
+
horizonDays: eventWindow.horizonDays
|
|
33283
|
+
} });
|
|
33284
|
+
await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
|
|
33285
|
+
return "empty";
|
|
33286
|
+
}
|
|
33287
|
+
const sorted = [...events].toSorted((a, b) => a.startMs - b.startMs);
|
|
33288
|
+
await this.enqueue(rule, scheduleWindow, eventWindow, calendarIds, sorted);
|
|
33289
|
+
await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
|
|
33290
|
+
return "sent";
|
|
33291
|
+
}
|
|
33292
|
+
async enqueue(rule, scheduleWindow, eventWindow, calendarIds, events) {
|
|
33293
|
+
const inputs = buildCalendarEventsOutboxInputs({
|
|
33294
|
+
ruleId: rule.id,
|
|
33295
|
+
ruleName: rule.name,
|
|
33296
|
+
scheduleWindow,
|
|
33297
|
+
eventWindow: {
|
|
33298
|
+
startMs: eventWindow.startMs,
|
|
33299
|
+
endMs: eventWindow.endMs
|
|
33300
|
+
},
|
|
33301
|
+
horizonDays: eventWindow.horizonDays,
|
|
33302
|
+
events,
|
|
33303
|
+
calendarIds,
|
|
33304
|
+
targets: rule.targets,
|
|
33305
|
+
priority: rule.priority,
|
|
33306
|
+
...rule.template !== void 0 ? { template: rule.template } : {}
|
|
33307
|
+
});
|
|
33308
|
+
for (const input of inputs) await this.deps.outbox.enqueue(input);
|
|
33309
|
+
this.deps.logger.info("calendar-events delivered", { meta: {
|
|
33310
|
+
ruleId: rule.id,
|
|
33311
|
+
events: events.length,
|
|
33312
|
+
targets: inputs.length,
|
|
33313
|
+
windowEndMs: scheduleWindow.endMs
|
|
33314
|
+
} });
|
|
33315
|
+
}
|
|
33316
|
+
};
|
|
33317
|
+
//#endregion
|
|
32584
33318
|
//#region src/notification-center/text-catalog-editor.ts
|
|
32585
33319
|
var NcTextCatalogEditor = class {
|
|
32586
33320
|
store;
|
|
@@ -35550,6 +36284,8 @@ var NC_ALARM_SETTINGS_FALLBACK = {
|
|
|
35550
36284
|
...DEFAULT_DELAYS,
|
|
35551
36285
|
announceTargets: [],
|
|
35552
36286
|
nonBlocking: [],
|
|
36287
|
+
blocking: [],
|
|
36288
|
+
secureOnArmOff: [],
|
|
35553
36289
|
modeDelays: []
|
|
35554
36290
|
};
|
|
35555
36291
|
/**
|
|
@@ -35662,7 +36398,7 @@ function packagePhaseOf(ev) {
|
|
|
35662
36398
|
*/
|
|
35663
36399
|
function historyRecordKind(kind) {
|
|
35664
36400
|
if (kind === "audio-event" || kind === "audio-window") return "object-event";
|
|
35665
|
-
if (kind === "timelapse" || kind === "ai-summary") return "system-event";
|
|
36401
|
+
if (kind === "timelapse" || kind === "ai-summary" || kind === "calendar-events") return "system-event";
|
|
35666
36402
|
return kind;
|
|
35667
36403
|
}
|
|
35668
36404
|
/**
|
|
@@ -35677,7 +36413,8 @@ function historyRecordKind(kind) {
|
|
|
35677
36413
|
function cooldownSubjectKind(kind) {
|
|
35678
36414
|
switch (kind) {
|
|
35679
36415
|
case "timelapse":
|
|
35680
|
-
case "ai-summary":
|
|
36416
|
+
case "ai-summary":
|
|
36417
|
+
case "calendar-events": return "device-event";
|
|
35681
36418
|
default: return kind;
|
|
35682
36419
|
}
|
|
35683
36420
|
}
|
|
@@ -35777,7 +36514,7 @@ function outboxEntryToHistory(texts, entry) {
|
|
|
35777
36514
|
id: entry.id,
|
|
35778
36515
|
ruleId: entry.ruleId,
|
|
35779
36516
|
ruleName: entry.payload.ruleName,
|
|
35780
|
-
delivery: entry.payload.delivery === "timelapse" || entry.payload.delivery === "ai-summary" ? "system-event" : entry.payload.delivery,
|
|
36517
|
+
delivery: entry.payload.delivery === "timelapse" || entry.payload.delivery === "ai-summary" || entry.payload.delivery === "calendar-events" ? "system-event" : entry.payload.delivery,
|
|
35781
36518
|
targetId: entry.targetId,
|
|
35782
36519
|
deviceId: entry.deviceId,
|
|
35783
36520
|
recordKind: historyRecordKind(entry.recordKind),
|
|
@@ -35987,6 +36724,9 @@ var NotificationCenter = class {
|
|
|
35987
36724
|
*/
|
|
35988
36725
|
summaryRules;
|
|
35989
36726
|
summaryProducer = null;
|
|
36727
|
+
/** Calendar-events digest rules + producer (hub-only producer). */
|
|
36728
|
+
calendarEventsRules;
|
|
36729
|
+
calendarEventsProducer = null;
|
|
35990
36730
|
/**
|
|
35991
36731
|
* What the deleted-device / deleted-target prune did, and why — its
|
|
35992
36732
|
* disable-once guard and the source of both UIs' banner. Loaded on EVERY
|
|
@@ -36977,6 +37717,11 @@ var NotificationCenter = class {
|
|
|
36977
37717
|
logger: this.logger.child("summary"),
|
|
36978
37718
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
36979
37719
|
});
|
|
37720
|
+
this.calendarEventsRules = new NcCalendarEventsStore({
|
|
37721
|
+
store: deps.store,
|
|
37722
|
+
logger: this.logger.child("calendar-events"),
|
|
37723
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
37724
|
+
});
|
|
36980
37725
|
this.pruneLedger = new NcPruneLedger({
|
|
36981
37726
|
store: deps.store,
|
|
36982
37727
|
logger: this.logger.child("device-prune")
|
|
@@ -37023,6 +37768,14 @@ var NotificationCenter = class {
|
|
|
37023
37768
|
zoneOwner: this.zoneOwners.lookup()
|
|
37024
37769
|
});
|
|
37025
37770
|
}
|
|
37771
|
+
const calendarsPort = deps.calendars;
|
|
37772
|
+
if (calendarsPort !== void 0) this.calendarEventsProducer = new NcCalendarEventsProducer({
|
|
37773
|
+
store: this.calendarEventsRules,
|
|
37774
|
+
outbox: this.outbox,
|
|
37775
|
+
calendars: calendarsPort,
|
|
37776
|
+
logger: this.logger.child("calendar-events"),
|
|
37777
|
+
now: this.now
|
|
37778
|
+
});
|
|
37026
37779
|
}
|
|
37027
37780
|
/**
|
|
37028
37781
|
* The durable timelapse rule set — exposed for the `nc.*Timelapse*` bridge
|
|
@@ -37036,6 +37789,10 @@ var NotificationCenter = class {
|
|
|
37036
37789
|
get summaryStore() {
|
|
37037
37790
|
return this.summaryRules;
|
|
37038
37791
|
}
|
|
37792
|
+
/** The durable calendar-events rule set — exposed for the `nc.*CalendarEvents*` bridge. */
|
|
37793
|
+
get calendarEventsStore() {
|
|
37794
|
+
return this.calendarEventsRules;
|
|
37795
|
+
}
|
|
37039
37796
|
/**
|
|
37040
37797
|
* Produce one digest window for one rule, right now.
|
|
37041
37798
|
*
|
|
@@ -37109,6 +37866,7 @@ var NotificationCenter = class {
|
|
|
37109
37866
|
await NcDeviceMuteStore.declare(store);
|
|
37110
37867
|
await TimelapseStore.declare(store);
|
|
37111
37868
|
await NcSummaryStore.declare(store);
|
|
37869
|
+
await NcCalendarEventsStore.declare(store);
|
|
37112
37870
|
await NcRuleAiStore.declare(store);
|
|
37113
37871
|
await NcTextCatalogStore.declare(store);
|
|
37114
37872
|
await NcArtifactIndex.declare(store);
|
|
@@ -38554,6 +39312,9 @@ var NotificationCenter = class {
|
|
|
38554
39312
|
alarmArmGuard() {
|
|
38555
39313
|
return {
|
|
38556
39314
|
openings: (mode) => this.alarmOpenings(mode),
|
|
39315
|
+
secureOnArm: (devices) => {
|
|
39316
|
+
this.secureDevicesOnArm(devices);
|
|
39317
|
+
},
|
|
38557
39318
|
announceRefusal: (input) => this.announceArmRefused(input),
|
|
38558
39319
|
announceCheckFailed: (input) => this.announceArmCheckFailed(input)
|
|
38559
39320
|
};
|
|
@@ -38620,7 +39381,9 @@ var NotificationCenter = class {
|
|
|
38620
39381
|
if (verdict.kind === "open") open.push({
|
|
38621
39382
|
deviceId,
|
|
38622
39383
|
name: name(deviceId),
|
|
38623
|
-
state: verdict.state
|
|
39384
|
+
state: verdict.state,
|
|
39385
|
+
controllable: verdict.controllable,
|
|
39386
|
+
cap: verdict.cap
|
|
38624
39387
|
});
|
|
38625
39388
|
}
|
|
38626
39389
|
this.logger.info("alarm arm check", {
|
|
@@ -38639,6 +39402,47 @@ var NotificationCenter = class {
|
|
|
38639
39402
|
};
|
|
38640
39403
|
}
|
|
38641
39404
|
/**
|
|
39405
|
+
* Fire-and-forget secure actuation for controllable openings on arm.
|
|
39406
|
+
* Failures are WARN per device — they never refuse the arm.
|
|
39407
|
+
*/
|
|
39408
|
+
async secureDevicesOnArm(devices) {
|
|
39409
|
+
for (const device of devices) {
|
|
39410
|
+
const actuation = device.cap !== void 0 ? secureActuationFor(device.cap) : null;
|
|
39411
|
+
if (actuation === null) continue;
|
|
39412
|
+
try {
|
|
39413
|
+
await this.actuateWithAttribution({
|
|
39414
|
+
deviceId: device.deviceId,
|
|
39415
|
+
cap: actuation.cap,
|
|
39416
|
+
method: actuation.method,
|
|
39417
|
+
args: {
|
|
39418
|
+
deviceId: device.deviceId,
|
|
39419
|
+
...actuation.args
|
|
39420
|
+
}
|
|
39421
|
+
});
|
|
39422
|
+
this.logger.info("alarm secure-on-arm actuated", {
|
|
39423
|
+
tags: { deviceId: device.deviceId },
|
|
39424
|
+
meta: {
|
|
39425
|
+
name: device.name,
|
|
39426
|
+
state: device.state,
|
|
39427
|
+
cap: actuation.cap,
|
|
39428
|
+
method: actuation.method
|
|
39429
|
+
}
|
|
39430
|
+
});
|
|
39431
|
+
} catch (err) {
|
|
39432
|
+
this.logger.warn("alarm secure-on-arm actuation failed — arm continues", {
|
|
39433
|
+
tags: { deviceId: device.deviceId },
|
|
39434
|
+
meta: {
|
|
39435
|
+
name: device.name,
|
|
39436
|
+
state: device.state,
|
|
39437
|
+
cap: actuation.cap,
|
|
39438
|
+
method: actuation.method,
|
|
39439
|
+
error: String(err)
|
|
39440
|
+
}
|
|
39441
|
+
});
|
|
39442
|
+
}
|
|
39443
|
+
}
|
|
39444
|
+
}
|
|
39445
|
+
/**
|
|
38642
39446
|
* The refusal, as an ordinary notification.
|
|
38643
39447
|
*
|
|
38644
39448
|
* Same path every alarm announcement takes (D126: the alarm speaks through
|
|
@@ -40685,11 +41489,17 @@ var NotificationCenter = class {
|
|
|
40685
41489
|
await this.rules.load();
|
|
40686
41490
|
this.noteInertGroupedRules();
|
|
40687
41491
|
if (this.evaluationActive) this.deviceAlerts.tick();
|
|
40688
|
-
if (this.evaluationActive)
|
|
40689
|
-
this.
|
|
40690
|
-
|
|
41492
|
+
if (this.evaluationActive) {
|
|
41493
|
+
this.stateSummary.tick().catch((err) => {
|
|
41494
|
+
this.logger.warn("state summary tick failed", { meta: { error: String(err) } });
|
|
41495
|
+
});
|
|
41496
|
+
this.calendarEventsProducer?.tick().catch((err) => {
|
|
41497
|
+
this.logger.warn("calendar-events tick failed", { meta: { error: String(err) } });
|
|
41498
|
+
});
|
|
41499
|
+
}
|
|
40691
41500
|
await this.timelapseRules.load();
|
|
40692
41501
|
await this.summaryRules.load();
|
|
41502
|
+
await this.calendarEventsRules.load();
|
|
40693
41503
|
await this.ruleAi.load();
|
|
40694
41504
|
await this.reloadTexts();
|
|
40695
41505
|
this.refreshOccupancyWatch();
|
|
@@ -104500,6 +105310,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
104500
105310
|
store: center.summaryStore,
|
|
104501
105311
|
runNow: (input) => center.runSummaryNow(input)
|
|
104502
105312
|
},
|
|
105313
|
+
calendarEvents: { store: center.calendarEventsStore },
|
|
104503
105314
|
texts: center.textCatalogEditor,
|
|
104504
105315
|
previewTexts: center.textCatalog,
|
|
104505
105316
|
...this.ncArtifactIndex !== null ? { artifacts: this.ncArtifactIndex } : {},
|
|
@@ -105687,7 +106498,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
105687
106498
|
})).tracks;
|
|
105688
106499
|
},
|
|
105689
106500
|
...this.isPostProcessingNode ? { timelapse: this.buildTimelapsePorts(api, stores) } : {},
|
|
105690
|
-
...this.isPostProcessingNode ? { summary: this.buildSummaryPorts(api, stores) } : {}
|
|
106501
|
+
...this.isPostProcessingNode ? { summary: this.buildSummaryPorts(api, stores) } : {},
|
|
106502
|
+
...this.isPostProcessingNode ? { calendars: {
|
|
106503
|
+
listCalendars: async () => {
|
|
106504
|
+
return (await api.calendars.listCalendars.query()).map((c) => ({ id: c.id }));
|
|
106505
|
+
},
|
|
106506
|
+
listEvents: async (input) => api.calendars.listEvents.query({
|
|
106507
|
+
calendarIds: [...input.calendarIds],
|
|
106508
|
+
startMs: input.startMs,
|
|
106509
|
+
endMs: input.endMs
|
|
106510
|
+
})
|
|
106511
|
+
} } : {}
|
|
105691
106512
|
});
|
|
105692
106513
|
this.wireAlarmPanel(this.notificationCenter);
|
|
105693
106514
|
await this.serveNcActionPlane(this.notificationCenter);
|