@camstack/addon-post-analysis 1.2.320 → 1.2.322
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-C_aKxHpy.js → clip-model-registry-BFbP9U7z.js} +427 -0
- package/dist/{clip-model-registry-C-IEAIQQ.mjs → clip-model-registry-CwK2SLQD.mjs} +374 -1
- 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-D553qWb_.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Dmt5eh93.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-mzYK6V9S.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-CuOcvcK7.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-ByDyL2zi.mjs → hostInit-DtKO_7UT.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +916 -50
- package/dist/pipeline-analytics/index.mjs +916 -50
- 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-7e_3hsiC.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-C1V_UEyk.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-BFbP9U7z.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");
|
|
@@ -1948,6 +1948,21 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
|
|
|
1948
1948
|
exitDelaySec: require_clip_model_registry.number().optional(),
|
|
1949
1949
|
entryDelaySec: require_clip_model_registry.number().optional(),
|
|
1950
1950
|
triggeredDurationSec: require_clip_model_registry.number().optional()
|
|
1951
|
+
})).default([]),
|
|
1952
|
+
/**
|
|
1953
|
+
* Active alarm configuration profile id. Empty / absent on a blob older than
|
|
1954
|
+
* profiles → seeded to Default on first read via {@link ensureAlarmProfiles}.
|
|
1955
|
+
*/
|
|
1956
|
+
activeProfileId: require_clip_model_registry.string().default(require_clip_model_registry.NC_ALARM_DEFAULT_PROFILE_ID),
|
|
1957
|
+
/**
|
|
1958
|
+
* Named settings blobs. Loose on purpose — each profile's settings are
|
|
1959
|
+
* re-validated through {@link settingsBlobFromPersisted} so a corrupt entry
|
|
1960
|
+
* drops rather than taking down the panel.
|
|
1961
|
+
*/
|
|
1962
|
+
profiles: require_clip_model_registry.array(require_clip_model_registry.object({
|
|
1963
|
+
id: require_clip_model_registry.string(),
|
|
1964
|
+
name: require_clip_model_registry.string(),
|
|
1965
|
+
settings: require_clip_model_registry.record(require_clip_model_registry.string(), require_clip_model_registry.unknown())
|
|
1951
1966
|
})).default([])
|
|
1952
1967
|
});
|
|
1953
1968
|
/**
|
|
@@ -1970,6 +1985,106 @@ function modeDeviceListFromPersisted(raw) {
|
|
|
1970
1985
|
return lists;
|
|
1971
1986
|
}
|
|
1972
1987
|
/**
|
|
1988
|
+
* Rebuild a trusted {@link NcAlarmSettings} from a persisted profile blob.
|
|
1989
|
+
* Unknown / corrupt fields fall back to system-safe empties via the flat-key
|
|
1990
|
+
* readers the panel already uses for the live settings.
|
|
1991
|
+
*/
|
|
1992
|
+
function settingsBlobFromPersisted(raw) {
|
|
1993
|
+
const exit = typeof raw["exitDelaySec"] === "number" ? raw["exitDelaySec"] : DEFAULT_DELAYS.exitDelaySec;
|
|
1994
|
+
const entry = typeof raw["entryDelaySec"] === "number" ? raw["entryDelaySec"] : DEFAULT_DELAYS.entryDelaySec;
|
|
1995
|
+
const triggered = typeof raw["triggeredDurationSec"] === "number" ? raw["triggeredDurationSec"] : DEFAULT_DELAYS.triggeredDurationSec;
|
|
1996
|
+
const announceTargets = Array.isArray(raw["announceTargets"]) ? raw["announceTargets"].filter((t) => typeof t === "string") : [];
|
|
1997
|
+
const lists = (key) => {
|
|
1998
|
+
const v = raw[key];
|
|
1999
|
+
if (!Array.isArray(v)) return [];
|
|
2000
|
+
const rows = [];
|
|
2001
|
+
for (const item of v) {
|
|
2002
|
+
if (typeof item !== "object" || item === null) continue;
|
|
2003
|
+
const rec = item;
|
|
2004
|
+
if (typeof rec["mode"] !== "string" || !Array.isArray(rec["deviceIds"])) continue;
|
|
2005
|
+
rows.push({
|
|
2006
|
+
mode: rec["mode"],
|
|
2007
|
+
deviceIds: rec["deviceIds"].filter((id) => typeof id === "number")
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
return modeDeviceListFromPersisted(rows);
|
|
2011
|
+
};
|
|
2012
|
+
const modeDelaysRaw = Array.isArray(raw["modeDelays"]) ? raw["modeDelays"] : [];
|
|
2013
|
+
const modeDelaysLoose = [];
|
|
2014
|
+
for (const item of modeDelaysRaw) {
|
|
2015
|
+
if (typeof item !== "object" || item === null) continue;
|
|
2016
|
+
const rec = item;
|
|
2017
|
+
if (typeof rec["mode"] !== "string") continue;
|
|
2018
|
+
const entry = { mode: rec["mode"] };
|
|
2019
|
+
if (typeof rec["exitDelaySec"] === "number") entry.exitDelaySec = rec["exitDelaySec"];
|
|
2020
|
+
if (typeof rec["entryDelaySec"] === "number") entry.entryDelaySec = rec["entryDelaySec"];
|
|
2021
|
+
if (typeof rec["triggeredDurationSec"] === "number") entry.triggeredDurationSec = rec["triggeredDurationSec"];
|
|
2022
|
+
modeDelaysLoose.push(entry);
|
|
2023
|
+
}
|
|
2024
|
+
return {
|
|
2025
|
+
exitDelaySec: Math.max(0, Math.min(600, Math.trunc(exit))),
|
|
2026
|
+
entryDelaySec: Math.max(0, Math.min(600, Math.trunc(entry))),
|
|
2027
|
+
triggeredDurationSec: Math.max(0, Math.min(3600, Math.trunc(triggered))),
|
|
2028
|
+
announceTargets,
|
|
2029
|
+
nonBlocking: lists("nonBlocking"),
|
|
2030
|
+
blocking: lists("blocking"),
|
|
2031
|
+
secureOnArmOff: lists("secureOnArmOff"),
|
|
2032
|
+
modeDelays: modeDelaysFromPersisted(modeDelaysLoose).entries
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
function settingsToPersisted(settings) {
|
|
2036
|
+
return {
|
|
2037
|
+
exitDelaySec: settings.exitDelaySec,
|
|
2038
|
+
entryDelaySec: settings.entryDelaySec,
|
|
2039
|
+
triggeredDurationSec: settings.triggeredDurationSec,
|
|
2040
|
+
announceTargets: [...settings.announceTargets],
|
|
2041
|
+
nonBlocking: settings.nonBlocking.map((l) => ({
|
|
2042
|
+
mode: l.mode,
|
|
2043
|
+
deviceIds: [...l.deviceIds]
|
|
2044
|
+
})),
|
|
2045
|
+
blocking: settings.blocking.map((l) => ({
|
|
2046
|
+
mode: l.mode,
|
|
2047
|
+
deviceIds: [...l.deviceIds]
|
|
2048
|
+
})),
|
|
2049
|
+
secureOnArmOff: settings.secureOnArmOff.map((l) => ({
|
|
2050
|
+
mode: l.mode,
|
|
2051
|
+
deviceIds: [...l.deviceIds]
|
|
2052
|
+
})),
|
|
2053
|
+
modeDelays: settings.modeDelays.map((e) => ({ ...e }))
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
function profilesStateFromPersisted(activeId, rows, current) {
|
|
2057
|
+
const profiles = rows.filter((r) => r.id.length > 0 && r.name.trim().length > 0).map((r) => ({
|
|
2058
|
+
id: r.id,
|
|
2059
|
+
name: r.name,
|
|
2060
|
+
settings: settingsBlobFromPersisted(r.settings)
|
|
2061
|
+
}));
|
|
2062
|
+
return require_clip_model_registry.ensureAlarmProfiles(profiles.length > 0 ? {
|
|
2063
|
+
activeId,
|
|
2064
|
+
profiles
|
|
2065
|
+
} : null, current);
|
|
2066
|
+
}
|
|
2067
|
+
function profilesToPersisted(state) {
|
|
2068
|
+
return {
|
|
2069
|
+
activeProfileId: state.activeId,
|
|
2070
|
+
profiles: state.profiles.map((p) => ({
|
|
2071
|
+
id: p.id,
|
|
2072
|
+
name: p.name,
|
|
2073
|
+
settings: settingsToPersisted(p.settings)
|
|
2074
|
+
}))
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
function profilesViewOf(state) {
|
|
2078
|
+
return {
|
|
2079
|
+
activeId: state.activeId,
|
|
2080
|
+
items: state.profiles.map((p) => ({
|
|
2081
|
+
id: p.id,
|
|
2082
|
+
name: p.name,
|
|
2083
|
+
isDefault: require_clip_model_registry.isDefaultAlarmProfileId(p.id)
|
|
2084
|
+
}))
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
1973
2088
|
* The log line for a trigger the state machine ignored, per state. `trigger`
|
|
1974
2089
|
* ignores four: disarmed, arming (the exit delay), and pending / triggered
|
|
1975
2090
|
* (already counting down or going off — a second sensor must not restart it).
|
|
@@ -2217,7 +2332,134 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2217
2332
|
changed: membershipChanged
|
|
2218
2333
|
}
|
|
2219
2334
|
});
|
|
2220
|
-
|
|
2335
|
+
const settled = this.settings();
|
|
2336
|
+
await this.persistProfilesState(require_clip_model_registry.syncActiveAlarmProfileSettings(this.readProfilesState(), settled));
|
|
2337
|
+
return settled;
|
|
2338
|
+
}
|
|
2339
|
+
/** Named profiles + which one is active — for {@link NcAlarmConfig.profiles}. */
|
|
2340
|
+
profilesView() {
|
|
2341
|
+
return profilesViewOf(this.readProfilesState());
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Create a named profile (system defaults, or clone). Does not activate.
|
|
2345
|
+
* `newId` is assigned by the caller (usually `randomUUID`).
|
|
2346
|
+
*/
|
|
2347
|
+
async createProfile(input) {
|
|
2348
|
+
const result = require_clip_model_registry.createAlarmProfile(this.readProfilesState(), {
|
|
2349
|
+
name: input.name,
|
|
2350
|
+
id: input.newId,
|
|
2351
|
+
source: input.source
|
|
2352
|
+
});
|
|
2353
|
+
if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
|
|
2354
|
+
await this.persistProfilesState(result.state);
|
|
2355
|
+
this.ctx.logger.info("alarm profile created", {
|
|
2356
|
+
tags: { deviceId: this.id },
|
|
2357
|
+
meta: {
|
|
2358
|
+
profileId: input.newId,
|
|
2359
|
+
name: input.name.trim(),
|
|
2360
|
+
source: input.source.kind
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
return profilesViewOf(result.state);
|
|
2364
|
+
}
|
|
2365
|
+
async renameProfile(id, name) {
|
|
2366
|
+
const result = require_clip_model_registry.renameAlarmProfile(this.readProfilesState(), {
|
|
2367
|
+
id,
|
|
2368
|
+
name
|
|
2369
|
+
});
|
|
2370
|
+
if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
|
|
2371
|
+
await this.persistProfilesState(result.state);
|
|
2372
|
+
this.ctx.logger.info("alarm profile renamed", {
|
|
2373
|
+
tags: { deviceId: this.id },
|
|
2374
|
+
meta: {
|
|
2375
|
+
profileId: id,
|
|
2376
|
+
name: name.trim()
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
return profilesViewOf(result.state);
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Delete a non-default profile. If it was active, Default becomes active and
|
|
2383
|
+
* its settings are applied to the flat keys (arm path).
|
|
2384
|
+
*/
|
|
2385
|
+
async deleteProfile(id) {
|
|
2386
|
+
const before = this.readProfilesState();
|
|
2387
|
+
const result = require_clip_model_registry.deleteAlarmProfile(before, id);
|
|
2388
|
+
if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
|
|
2389
|
+
const switched = before.activeId === id;
|
|
2390
|
+
await this.persistProfilesState(result.state);
|
|
2391
|
+
if (switched) {
|
|
2392
|
+
const def = result.state.profiles.find((p) => p.id === require_clip_model_registry.NC_ALARM_DEFAULT_PROFILE_ID);
|
|
2393
|
+
if (def !== void 0) await this.replaceFlatSettings(def.settings);
|
|
2394
|
+
}
|
|
2395
|
+
this.ctx.logger.info("alarm profile deleted", {
|
|
2396
|
+
tags: { deviceId: this.id },
|
|
2397
|
+
meta: {
|
|
2398
|
+
profileId: id,
|
|
2399
|
+
activatedDefault: switched
|
|
2400
|
+
}
|
|
2401
|
+
});
|
|
2402
|
+
return profilesViewOf(result.state);
|
|
2403
|
+
}
|
|
2404
|
+
/**
|
|
2405
|
+
* Make a profile active and apply its blob onto the flat settings the arm
|
|
2406
|
+
* path reads.
|
|
2407
|
+
*/
|
|
2408
|
+
async activateProfile(id) {
|
|
2409
|
+
const result = require_clip_model_registry.activateAlarmProfile(require_clip_model_registry.syncActiveAlarmProfileSettings(this.readProfilesState(), this.settings()), id);
|
|
2410
|
+
if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
|
|
2411
|
+
const next = result.state.profiles.find((p) => p.id === id);
|
|
2412
|
+
if (next === void 0) throw this.profilesRefuseError("not-found", id);
|
|
2413
|
+
await this.persistProfilesState(result.state);
|
|
2414
|
+
await this.replaceFlatSettings(next.settings);
|
|
2415
|
+
this.ctx.logger.info("alarm profile activated", {
|
|
2416
|
+
tags: { deviceId: this.id },
|
|
2417
|
+
meta: {
|
|
2418
|
+
profileId: id,
|
|
2419
|
+
name: next.name
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
return profilesViewOf(result.state);
|
|
2423
|
+
}
|
|
2424
|
+
readProfilesState() {
|
|
2425
|
+
return profilesStateFromPersisted(this.config.get("activeProfileId"), this.config.get("profiles"), this.settings());
|
|
2426
|
+
}
|
|
2427
|
+
async persistProfilesState(state) {
|
|
2428
|
+
const persisted = profilesToPersisted(state);
|
|
2429
|
+
await Promise.all([this.config.set("activeProfileId", persisted.activeProfileId), this.config.set("profiles", persisted.profiles)]);
|
|
2430
|
+
}
|
|
2431
|
+
/** Write a full settings blob onto the flat keys the arm path reads. */
|
|
2432
|
+
async replaceFlatSettings(settings) {
|
|
2433
|
+
const blob = require_clip_model_registry.cloneAlarmSettings(settings);
|
|
2434
|
+
await Promise.all([
|
|
2435
|
+
this.config.set("exitDelaySec", blob.exitDelaySec),
|
|
2436
|
+
this.config.set("entryDelaySec", blob.entryDelaySec),
|
|
2437
|
+
this.config.set("triggeredDurationSec", blob.triggeredDurationSec),
|
|
2438
|
+
this.config.set("announceTargets", [...blob.announceTargets]),
|
|
2439
|
+
this.config.set("nonBlocking", blob.nonBlocking.map((l) => ({
|
|
2440
|
+
mode: l.mode,
|
|
2441
|
+
deviceIds: [...l.deviceIds]
|
|
2442
|
+
}))),
|
|
2443
|
+
this.config.set("blocking", blob.blocking.map((l) => ({
|
|
2444
|
+
mode: l.mode,
|
|
2445
|
+
deviceIds: [...l.deviceIds]
|
|
2446
|
+
}))),
|
|
2447
|
+
this.config.set("secureOnArmOff", blob.secureOnArmOff.map((l) => ({
|
|
2448
|
+
mode: l.mode,
|
|
2449
|
+
deviceIds: [...l.deviceIds]
|
|
2450
|
+
}))),
|
|
2451
|
+
this.config.set("modeDelays", blob.modeDelays.map((e) => ({ ...e })))
|
|
2452
|
+
]);
|
|
2453
|
+
}
|
|
2454
|
+
profilesRefuseError(reason, detail) {
|
|
2455
|
+
switch (reason) {
|
|
2456
|
+
case "name-empty": return /* @__PURE__ */ new Error("alarm profile name must not be empty");
|
|
2457
|
+
case "name-taken": return /* @__PURE__ */ new Error(`an alarm profile named '${detail ?? ""}' already exists`);
|
|
2458
|
+
case "cannot-delete-default": return /* @__PURE__ */ new Error("the Default alarm profile cannot be deleted");
|
|
2459
|
+
case "clone-source-missing": return /* @__PURE__ */ new Error(`alarm profile '${detail ?? ""}' was not found to clone`);
|
|
2460
|
+
case "not-found": return /* @__PURE__ */ new Error(`alarm profile '${detail ?? ""}' was not found`);
|
|
2461
|
+
default: return /* @__PURE__ */ new Error(`alarm profile operation refused (${reason})`);
|
|
2462
|
+
}
|
|
2221
2463
|
}
|
|
2222
2464
|
/**
|
|
2223
2465
|
* Refuse a mode this panel does not offer — logged by reason, then thrown. A membership edit
|
|
@@ -2950,6 +3192,11 @@ function ncAlarmPanelPort(panel) {
|
|
|
2950
3192
|
deviceId: panel.id,
|
|
2951
3193
|
settings: () => panel.settings(),
|
|
2952
3194
|
applySettings: (patch) => panel.applySettings(patch),
|
|
3195
|
+
profilesView: () => panel.profilesView(),
|
|
3196
|
+
createProfile: (input) => panel.createProfile(input),
|
|
3197
|
+
renameProfile: (id, name) => panel.renameProfile(id, name),
|
|
3198
|
+
deleteProfile: (id) => panel.deleteProfile(id),
|
|
3199
|
+
activateProfile: (id) => panel.activateProfile(id),
|
|
2953
3200
|
availableModes: () => NC_ALARM_MODES,
|
|
2954
3201
|
noteRuleTrigger: (source) => panel.noteRuleTrigger(source),
|
|
2955
3202
|
excludedDevices: () => panel.excludedDevices(),
|
|
@@ -5448,7 +5695,7 @@ function isAdoptable(record, nowMs) {
|
|
|
5448
5695
|
* what `localDayMinute` does for the same input — one behaviour for a bad
|
|
5449
5696
|
* zone, not two.
|
|
5450
5697
|
*/
|
|
5451
|
-
function clockOf$
|
|
5698
|
+
function clockOf$6(atMs, timeZone) {
|
|
5452
5699
|
if (timeZone !== void 0) try {
|
|
5453
5700
|
return new Intl.DateTimeFormat("en-GB", {
|
|
5454
5701
|
timeZone,
|
|
@@ -5584,7 +5831,7 @@ var TimelapseRenderer = class {
|
|
|
5584
5831
|
includeAudio: false,
|
|
5585
5832
|
maxLifeMs: await this.ports.exportLifeMs(),
|
|
5586
5833
|
deleteAfterDownload: false,
|
|
5587
|
-
title: `${request.ruleName} ${clockOf$
|
|
5834
|
+
title: `${request.ruleName} ${clockOf$6(window.startMs, request.timezone)}→${clockOf$6(window.endMs, request.timezone)}`,
|
|
5588
5835
|
timelapse: {
|
|
5589
5836
|
everyMs,
|
|
5590
5837
|
outputFps: request.framerate,
|
|
@@ -5915,7 +6162,7 @@ var TimelapseRenderer = class {
|
|
|
5915
6162
|
...meta,
|
|
5916
6163
|
unservedWindowCount: verdict.unservedWindows.length,
|
|
5917
6164
|
unservedWindows: verdict.unservedWindows.slice(0, 12).map((w) => ({
|
|
5918
|
-
at: clockOf$
|
|
6165
|
+
at: clockOf$6(request.window.startMs + w.fromSec * 1e3, request.timezone),
|
|
5919
6166
|
fromSec: Math.round(w.fromSec),
|
|
5920
6167
|
toSec: Math.round(w.toSec),
|
|
5921
6168
|
expectedDenseFrames: w.expectedDenseFrames,
|
|
@@ -13900,7 +14147,7 @@ var NcPruneRecordSchema = require_clip_model_registry.object({
|
|
|
13900
14147
|
source: require_clip_model_registry._enum(["event", "reconcile"])
|
|
13901
14148
|
});
|
|
13902
14149
|
/** Rules × deleted subjects — small; a generous ceiling. */
|
|
13903
|
-
var LOAD_LIMIT$
|
|
14150
|
+
var LOAD_LIMIT$5 = 1e4;
|
|
13904
14151
|
function keyOf$1(row) {
|
|
13905
14152
|
return `${row.kind}:${row.ruleId}:${row.field}:${row.subject}`;
|
|
13906
14153
|
}
|
|
@@ -13919,7 +14166,7 @@ var NC_DEVICE_PRUNES_SPEC = {
|
|
|
13919
14166
|
return parsed.success ? parsed.data : null;
|
|
13920
14167
|
},
|
|
13921
14168
|
deviceIdOf: (row) => row.subjectKind === "device" ? Number(row.subject) : void 0,
|
|
13922
|
-
loadLimit: LOAD_LIMIT$
|
|
14169
|
+
loadLimit: LOAD_LIMIT$5
|
|
13923
14170
|
};
|
|
13924
14171
|
/** The outcomes that mean "this last reference was already dealt with". */
|
|
13925
14172
|
var KEPT_OUTCOMES = new Set(["kept-disabled", "kept"]);
|
|
@@ -13954,7 +14201,7 @@ var NcPruneLedger = class {
|
|
|
13954
14201
|
try {
|
|
13955
14202
|
const records = await this.store.query.query({
|
|
13956
14203
|
collection: NC_DEVICE_PRUNES_COLLECTION,
|
|
13957
|
-
filter: { limit: LOAD_LIMIT$
|
|
14204
|
+
filter: { limit: LOAD_LIMIT$5 }
|
|
13958
14205
|
});
|
|
13959
14206
|
const next = /* @__PURE__ */ new Map();
|
|
13960
14207
|
for (const record of records) {
|
|
@@ -17279,12 +17526,12 @@ var NcSummaryTemplateSchema = require_clip_model_registry.object({
|
|
|
17279
17526
|
title: require_clip_model_registry.string().max(500).optional(),
|
|
17280
17527
|
body: require_clip_model_registry.string().max(2e3).optional()
|
|
17281
17528
|
});
|
|
17282
|
-
var NameField = require_clip_model_registry.string().min(1).max(200);
|
|
17529
|
+
var NameField$1 = require_clip_model_registry.string().min(1).max(200);
|
|
17283
17530
|
/** MULTI-CAMERA is the point of the feature — a one-camera digest is a
|
|
17284
17531
|
* timelapse with extra steps, but it is allowed and it is cheap. */
|
|
17285
17532
|
var DeviceIdsField = require_clip_model_registry.array(require_clip_model_registry.number()).min(1).max(64);
|
|
17286
|
-
var TargetsField = require_clip_model_registry.array(require_clip_model_registry.NcRuleTargetSchema).min(1);
|
|
17287
|
-
var PriorityField = require_clip_model_registry.number().int().min(1).max(5);
|
|
17533
|
+
var TargetsField$1 = require_clip_model_registry.array(require_clip_model_registry.NcRuleTargetSchema).min(1);
|
|
17534
|
+
var PriorityField$1 = require_clip_model_registry.number().int().min(1).max(5);
|
|
17288
17535
|
/**
|
|
17289
17536
|
* The mosaic's tile ceiling. 9 (a 3×3 grid) is the shipped default: it is the
|
|
17290
17537
|
* most a phone lock-screen thumbnail can carry and still be read, and it is the
|
|
@@ -17449,7 +17696,7 @@ var NcSummaryAiSchema = require_clip_model_registry.object({
|
|
|
17449
17696
|
onFailure: require_clip_model_registry._enum(["deliver-without-text", "skip"]).optional()
|
|
17450
17697
|
});
|
|
17451
17698
|
var NcSummaryRuleInputSchema = require_clip_model_registry.object({
|
|
17452
|
-
name: NameField,
|
|
17699
|
+
name: NameField$1,
|
|
17453
17700
|
enabled: require_clip_model_registry.boolean().default(true),
|
|
17454
17701
|
deviceIds: DeviceIdsField,
|
|
17455
17702
|
window: NcSummaryWindowSchema,
|
|
@@ -17458,7 +17705,7 @@ var NcSummaryRuleInputSchema = require_clip_model_registry.object({
|
|
|
17458
17705
|
maxAiImages: MaxAiImagesField.default(NC_SUMMARY_DEFAULT_AI_IMAGES),
|
|
17459
17706
|
rank: NcSummaryRankSchema.default("importance"),
|
|
17460
17707
|
barrierSeconds: BarrierSecondsField.default(120),
|
|
17461
|
-
targets: TargetsField,
|
|
17708
|
+
targets: TargetsField$1,
|
|
17462
17709
|
template: NcSummaryTemplateSchema.optional(),
|
|
17463
17710
|
/** Burned onto the mosaic's title band — and ONLY when `captions` is `all`. */
|
|
17464
17711
|
captions: NcSummaryCaptionsSchema.default(NC_SUMMARY_DEFAULT_CAPTIONS),
|
|
@@ -17468,7 +17715,7 @@ var NcSummaryRuleInputSchema = require_clip_model_registry.object({
|
|
|
17468
17715
|
/** Deliver a window that matched NOTHING? Default no: a nightly "0 events"
|
|
17469
17716
|
* is a line nobody reads, and it costs the lines beside it their reader. */
|
|
17470
17717
|
deliverEmpty: require_clip_model_registry.boolean().default(false),
|
|
17471
|
-
priority: PriorityField.default(3),
|
|
17718
|
+
priority: PriorityField$1.default(3),
|
|
17472
17719
|
ai: NcSummaryAiSchema.optional()
|
|
17473
17720
|
});
|
|
17474
17721
|
/**
|
|
@@ -17480,7 +17727,7 @@ var NcSummaryRuleInputSchema = require_clip_model_registry.object({
|
|
|
17480
17727
|
* plain `.optional()`.
|
|
17481
17728
|
*/
|
|
17482
17729
|
var NcSummaryRulePatchSchema = require_clip_model_registry.object({
|
|
17483
|
-
name: NameField.optional(),
|
|
17730
|
+
name: NameField$1.optional(),
|
|
17484
17731
|
enabled: require_clip_model_registry.boolean().optional(),
|
|
17485
17732
|
deviceIds: DeviceIdsField.optional(),
|
|
17486
17733
|
window: NcSummaryWindowSchema.optional(),
|
|
@@ -17489,12 +17736,12 @@ var NcSummaryRulePatchSchema = require_clip_model_registry.object({
|
|
|
17489
17736
|
maxAiImages: MaxAiImagesField.optional(),
|
|
17490
17737
|
rank: NcSummaryRankSchema.optional(),
|
|
17491
17738
|
barrierSeconds: BarrierSecondsField.optional(),
|
|
17492
|
-
targets: TargetsField.optional(),
|
|
17739
|
+
targets: TargetsField$1.optional(),
|
|
17493
17740
|
template: NcSummaryTemplateSchema.nullable().optional(),
|
|
17494
17741
|
captions: NcSummaryCaptionsSchema.optional(),
|
|
17495
17742
|
captionText: require_clip_model_registry.string().max(200).optional(),
|
|
17496
17743
|
deliverEmpty: require_clip_model_registry.boolean().optional(),
|
|
17497
|
-
priority: PriorityField.optional(),
|
|
17744
|
+
priority: PriorityField$1.optional(),
|
|
17498
17745
|
ai: NcSummaryAiSchema.nullable().optional()
|
|
17499
17746
|
});
|
|
17500
17747
|
/**
|
|
@@ -17527,6 +17774,89 @@ function assertSummaryBudget(rule) {
|
|
|
17527
17774
|
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`);
|
|
17528
17775
|
}
|
|
17529
17776
|
//#endregion
|
|
17777
|
+
//#region src/notification-center/calendar-events/calendar-events-window.ts
|
|
17778
|
+
/** One day in milliseconds. */
|
|
17779
|
+
var CALENDAR_EVENTS_DAY_MS = 1440 * 60 * 1e3;
|
|
17780
|
+
/**
|
|
17781
|
+
* Floor / ceiling on the horizon days a rule may declare.
|
|
17782
|
+
* 1 day is the minimum useful digest; 30 keeps a single notification bounded.
|
|
17783
|
+
*/
|
|
17784
|
+
var CALENDAR_EVENTS_MIN_HORIZON_DAYS = 1;
|
|
17785
|
+
var CALENDAR_EVENTS_MAX_HORIZON_DAYS = 30;
|
|
17786
|
+
var CALENDAR_EVENTS_DEFAULT_HORIZON_DAYS = 7;
|
|
17787
|
+
/**
|
|
17788
|
+
* Build the producer window for one fire.
|
|
17789
|
+
*
|
|
17790
|
+
* `horizonDays` is clamped into the legal range rather than refused — a
|
|
17791
|
+
* stored rule that predates a tighter bound must still fire.
|
|
17792
|
+
*/
|
|
17793
|
+
function calendarEventsWindow(nowMs, horizonDays) {
|
|
17794
|
+
const days = Math.min(CALENDAR_EVENTS_MAX_HORIZON_DAYS, Math.max(CALENDAR_EVENTS_MIN_HORIZON_DAYS, Math.floor(horizonDays)));
|
|
17795
|
+
return {
|
|
17796
|
+
startMs: nowMs,
|
|
17797
|
+
endMs: nowMs + days * CALENDAR_EVENTS_DAY_MS,
|
|
17798
|
+
horizonDays: days
|
|
17799
|
+
};
|
|
17800
|
+
}
|
|
17801
|
+
//#endregion
|
|
17802
|
+
//#region src/notification-center/calendar-events/calendar-events-rule.ts
|
|
17803
|
+
/**
|
|
17804
|
+
* NcCalendarEventsRule — scheduled "upcoming calendar events" digest.
|
|
17805
|
+
*
|
|
17806
|
+
* Same ownership model as Summaries/Timelapses: lives in the addon (not
|
|
17807
|
+
* `@camstack/types`) so a field is live on the next post-analysis deploy.
|
|
17808
|
+
* Rides no `NcDelivery` member; reaches the outside world through
|
|
17809
|
+
* `nc.*CalendarEvents*` bridge actions.
|
|
17810
|
+
*
|
|
17811
|
+
* Calendar selection mirrors detection classes: `calendars` = Only,
|
|
17812
|
+
* `calendarsExclude` = Except, both empty = All.
|
|
17813
|
+
*/
|
|
17814
|
+
var NcCalendarEventsTemplateSchema = require_clip_model_registry.object({
|
|
17815
|
+
title: require_clip_model_registry.string().max(500).optional(),
|
|
17816
|
+
body: require_clip_model_registry.string().max(4e3).optional()
|
|
17817
|
+
});
|
|
17818
|
+
var NameField = require_clip_model_registry.string().min(1).max(200);
|
|
17819
|
+
var TargetsField = require_clip_model_registry.array(require_clip_model_registry.NcRuleTargetSchema).min(1);
|
|
17820
|
+
var PriorityField = require_clip_model_registry.number().int().min(1).max(5);
|
|
17821
|
+
var HorizonDaysField = require_clip_model_registry.number().int().min(CALENDAR_EVENTS_MIN_HORIZON_DAYS).max(CALENDAR_EVENTS_MAX_HORIZON_DAYS);
|
|
17822
|
+
var CalendarIdsField = require_clip_model_registry.array(require_clip_model_registry.string().min(1).max(200)).max(64);
|
|
17823
|
+
var NcCalendarEventsRuleInputSchema = require_clip_model_registry.object({
|
|
17824
|
+
name: NameField,
|
|
17825
|
+
enabled: require_clip_model_registry.boolean().default(true),
|
|
17826
|
+
schedule: require_clip_model_registry.NcScheduleSchema,
|
|
17827
|
+
/** Only these calendars. Empty/absent with no exclude = All. */
|
|
17828
|
+
calendars: CalendarIdsField.optional(),
|
|
17829
|
+
/** All except these. Ignored when `calendars` is non-empty. */
|
|
17830
|
+
calendarsExclude: CalendarIdsField.optional(),
|
|
17831
|
+
horizonDays: HorizonDaysField.default(CALENDAR_EVENTS_DEFAULT_HORIZON_DAYS),
|
|
17832
|
+
/** Deliver when the horizon holds no events? Default no. */
|
|
17833
|
+
deliverEmpty: require_clip_model_registry.boolean().default(false),
|
|
17834
|
+
targets: TargetsField,
|
|
17835
|
+
template: NcCalendarEventsTemplateSchema.optional(),
|
|
17836
|
+
priority: PriorityField.default(3)
|
|
17837
|
+
});
|
|
17838
|
+
var NcCalendarEventsRulePatchSchema = require_clip_model_registry.object({
|
|
17839
|
+
name: NameField.optional(),
|
|
17840
|
+
enabled: require_clip_model_registry.boolean().optional(),
|
|
17841
|
+
schedule: require_clip_model_registry.NcScheduleSchema.optional(),
|
|
17842
|
+
calendars: CalendarIdsField.nullable().optional(),
|
|
17843
|
+
calendarsExclude: CalendarIdsField.nullable().optional(),
|
|
17844
|
+
horizonDays: HorizonDaysField.optional(),
|
|
17845
|
+
deliverEmpty: require_clip_model_registry.boolean().optional(),
|
|
17846
|
+
targets: TargetsField.optional(),
|
|
17847
|
+
template: NcCalendarEventsTemplateSchema.nullable().optional(),
|
|
17848
|
+
priority: PriorityField.optional()
|
|
17849
|
+
});
|
|
17850
|
+
var NcCalendarEventsRuleSchema = NcCalendarEventsRuleInputSchema.extend({
|
|
17851
|
+
id: require_clip_model_registry.string(),
|
|
17852
|
+
ownerUserId: require_clip_model_registry.string().optional(),
|
|
17853
|
+
/** Epoch-ms of the schedule window end that was last delivered. */
|
|
17854
|
+
generatedAt: require_clip_model_registry.number().optional(),
|
|
17855
|
+
createdBy: require_clip_model_registry.string(),
|
|
17856
|
+
createdAt: require_clip_model_registry.number(),
|
|
17857
|
+
updatedAt: require_clip_model_registry.number()
|
|
17858
|
+
});
|
|
17859
|
+
//#endregion
|
|
17530
17860
|
//#region src/notification-center/unstored-media-id.ts
|
|
17531
17861
|
/**
|
|
17532
17862
|
* An id for media bytes that were NEVER STORED.
|
|
@@ -18687,6 +19017,22 @@ var ncActions = require_clip_model_registry.defineCustomActions({
|
|
|
18687
19017
|
kind: "mutation",
|
|
18688
19018
|
caller: "required"
|
|
18689
19019
|
}),
|
|
19020
|
+
"nc.listCalendarEventsRules": require_clip_model_registry.customAction(require_clip_model_registry.object({}), require_clip_model_registry.object({
|
|
19021
|
+
rules: require_clip_model_registry.array(NcCalendarEventsRuleSchema.extend({ readOnly: require_clip_model_registry.boolean() })),
|
|
19022
|
+
canAuthor: require_clip_model_registry.boolean()
|
|
19023
|
+
}), { caller: "required" }),
|
|
19024
|
+
"nc.upsertCalendarEventsRule": require_clip_model_registry.customAction(require_clip_model_registry.object({
|
|
19025
|
+
ruleId: require_clip_model_registry.string().optional(),
|
|
19026
|
+
rule: NcCalendarEventsRuleInputSchema.optional(),
|
|
19027
|
+
patch: NcCalendarEventsRulePatchSchema.optional()
|
|
19028
|
+
}), require_clip_model_registry.object({ rule: NcCalendarEventsRuleSchema }), {
|
|
19029
|
+
kind: "mutation",
|
|
19030
|
+
caller: "required"
|
|
19031
|
+
}),
|
|
19032
|
+
"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) }), {
|
|
19033
|
+
kind: "mutation",
|
|
19034
|
+
caller: "required"
|
|
19035
|
+
}),
|
|
18690
19036
|
/**
|
|
18691
19037
|
* Produce one digest window RIGHT NOW — the operator's Test button.
|
|
18692
19038
|
*
|
|
@@ -18973,6 +19319,11 @@ function makeNcActionHandlers(deps) {
|
|
|
18973
19319
|
if (sum === void 0) throw new Error("this node does not host the summary producer — use the hub");
|
|
18974
19320
|
return sum;
|
|
18975
19321
|
};
|
|
19322
|
+
const requireCalendarEvents = () => {
|
|
19323
|
+
const ce = deps.calendarEvents;
|
|
19324
|
+
if (ce === void 0) throw new Error("this node does not host calendar-events rules — use the hub");
|
|
19325
|
+
return ce;
|
|
19326
|
+
};
|
|
18976
19327
|
/** The artefact index, or an honest refusal. */
|
|
18977
19328
|
const requireArtifacts = () => {
|
|
18978
19329
|
const index = deps.artifacts;
|
|
@@ -19001,6 +19352,13 @@ function makeNcActionHandlers(deps) {
|
|
|
19001
19352
|
if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: summary rule not owned: ${ruleId}`);
|
|
19002
19353
|
return rule;
|
|
19003
19354
|
};
|
|
19355
|
+
const assertCalendarEventsEditable = (ruleId, caller, ce) => {
|
|
19356
|
+
const rule = ce.store.get(ruleId);
|
|
19357
|
+
if (rule === null) throw new Error(`forbidden: calendar-events rule not found: ${ruleId}`);
|
|
19358
|
+
if (caller.isAdmin) return rule;
|
|
19359
|
+
if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: calendar-events rule not owned: ${ruleId}`);
|
|
19360
|
+
return rule;
|
|
19361
|
+
};
|
|
19004
19362
|
/** A timelapse rule the caller may EDIT. Same admin bypass as `NcRule`. */
|
|
19005
19363
|
const assertTimelapseEditable = (ruleId, caller, tl) => {
|
|
19006
19364
|
const rule = tl.store.get(ruleId);
|
|
@@ -19542,6 +19900,53 @@ function makeNcActionHandlers(deps) {
|
|
|
19542
19900
|
...out.skippedByAi === true ? { skippedByAi: true } : {}
|
|
19543
19901
|
};
|
|
19544
19902
|
},
|
|
19903
|
+
"nc.listCalendarEventsRules": async (_input, caller) => {
|
|
19904
|
+
const c = requireCaller$1(caller);
|
|
19905
|
+
const ce = requireCalendarEvents();
|
|
19906
|
+
return {
|
|
19907
|
+
rules: (c.isAdmin ? ce.store.list() : ce.store.listForOwner(c.userId)).map((r) => ({
|
|
19908
|
+
...r,
|
|
19909
|
+
readOnly: !c.isAdmin && r.ownerUserId !== c.userId
|
|
19910
|
+
})),
|
|
19911
|
+
canAuthor: true
|
|
19912
|
+
};
|
|
19913
|
+
},
|
|
19914
|
+
"nc.upsertCalendarEventsRule": async (input, caller) => {
|
|
19915
|
+
const c = requireCaller$1(caller);
|
|
19916
|
+
const ce = requireCalendarEvents();
|
|
19917
|
+
if (input.ruleId === void 0) {
|
|
19918
|
+
if (input.rule === void 0) throw new Error("upsertCalendarEventsRule: rule or ruleId required");
|
|
19919
|
+
const parsed = NcCalendarEventsRuleInputSchema.parse(input.rule);
|
|
19920
|
+
await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
|
|
19921
|
+
const rule = await ce.store.create(parsed, c.userId, c.isAdmin ? void 0 : c.userId);
|
|
19922
|
+
deps.logger.info("calendar-events rule created", { meta: {
|
|
19923
|
+
ruleId: rule.id,
|
|
19924
|
+
owner: c.userId
|
|
19925
|
+
} });
|
|
19926
|
+
return { rule };
|
|
19927
|
+
}
|
|
19928
|
+
assertCalendarEventsEditable(input.ruleId, c, ce);
|
|
19929
|
+
if (input.patch === void 0) throw new Error("upsertCalendarEventsRule: patch required");
|
|
19930
|
+
const patch = NcCalendarEventsRulePatchSchema.parse(input.patch);
|
|
19931
|
+
if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
|
|
19932
|
+
const rule = await ce.store.update(input.ruleId, patch);
|
|
19933
|
+
deps.logger.info("calendar-events rule updated", { meta: {
|
|
19934
|
+
ruleId: rule.id,
|
|
19935
|
+
owner: c.userId
|
|
19936
|
+
} });
|
|
19937
|
+
return { rule };
|
|
19938
|
+
},
|
|
19939
|
+
"nc.deleteCalendarEventsRule": async (input, caller) => {
|
|
19940
|
+
const c = requireCaller$1(caller);
|
|
19941
|
+
const ce = requireCalendarEvents();
|
|
19942
|
+
assertCalendarEventsEditable(input.ruleId, c, ce);
|
|
19943
|
+
await ce.store.delete(input.ruleId);
|
|
19944
|
+
deps.logger.info("calendar-events rule deleted", { meta: {
|
|
19945
|
+
ruleId: input.ruleId,
|
|
19946
|
+
owner: c.userId
|
|
19947
|
+
} });
|
|
19948
|
+
return { success: true };
|
|
19949
|
+
},
|
|
19545
19950
|
"nc.getTexts": async (input) => {
|
|
19546
19951
|
const texts = requireTexts();
|
|
19547
19952
|
return input.language === void 0 ? texts.view() : texts.view(input.language);
|
|
@@ -28405,7 +28810,8 @@ var OUTBOX_RECORD_KINDS = new Set([
|
|
|
28405
28810
|
"audio-window",
|
|
28406
28811
|
"system-event",
|
|
28407
28812
|
"timelapse",
|
|
28408
|
-
"ai-summary"
|
|
28813
|
+
"ai-summary",
|
|
28814
|
+
"calendar-events"
|
|
28409
28815
|
]);
|
|
28410
28816
|
function isOutboxRecordKind(x) {
|
|
28411
28817
|
return typeof x === "string" && OUTBOX_RECORD_KINDS.has(x);
|
|
@@ -28758,7 +29164,7 @@ function buildRuleAiSystemPrompt(shape, language, maxWords, grouped = false) {
|
|
|
28758
29164
|
].join("");
|
|
28759
29165
|
}
|
|
28760
29166
|
/** `HH:MM` in the host timezone. */
|
|
28761
|
-
function clockOf$
|
|
29167
|
+
function clockOf$5(atMs) {
|
|
28762
29168
|
const d = new Date(atMs);
|
|
28763
29169
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
28764
29170
|
}
|
|
@@ -28796,7 +29202,7 @@ function buildRuleAiPrompt(input) {
|
|
|
28796
29202
|
if (shape === "detection-track") {
|
|
28797
29203
|
lines.push(`Camera: ${camera}.${located} Subject: ${subject}. The stills, in order:`);
|
|
28798
29204
|
for (const [i, frame] of input.frames.entries()) {
|
|
28799
|
-
const when = frame.atMs !== void 0 ? ` · ${clockOf$
|
|
29205
|
+
const when = frame.atMs !== void 0 ? ` · ${clockOf$5(frame.atMs)}` : "";
|
|
28800
29206
|
lines.push(`${String(i + 1)}.${when}`);
|
|
28801
29207
|
}
|
|
28802
29208
|
return lines.join("\n");
|
|
@@ -29093,7 +29499,7 @@ var NcRuleAiRowSchema = require_clip_model_registry.object({
|
|
|
29093
29499
|
updatedAt: require_clip_model_registry.number()
|
|
29094
29500
|
});
|
|
29095
29501
|
/** Operator-authored and tiny; a generous ceiling. */
|
|
29096
|
-
var LOAD_LIMIT$
|
|
29502
|
+
var LOAD_LIMIT$4 = 1e4;
|
|
29097
29503
|
var NC_RULE_AI_SPEC = {
|
|
29098
29504
|
collection: NC_RULE_AI_COLLECTION,
|
|
29099
29505
|
columns: [...NC_RULE_AI_COLUMNS],
|
|
@@ -29109,7 +29515,7 @@ var NC_RULE_AI_SPEC = {
|
|
|
29109
29515
|
const parsed = NcRuleAiRowSchema.safeParse(data["row"]);
|
|
29110
29516
|
return parsed.success ? parsed.data : null;
|
|
29111
29517
|
},
|
|
29112
|
-
loadLimit: LOAD_LIMIT$
|
|
29518
|
+
loadLimit: LOAD_LIMIT$4
|
|
29113
29519
|
};
|
|
29114
29520
|
var NcRuleAiStore = class {
|
|
29115
29521
|
ledger;
|
|
@@ -29446,7 +29852,7 @@ var NC_RULES_INDEXES = [{
|
|
|
29446
29852
|
columns: ["enabled"]
|
|
29447
29853
|
}];
|
|
29448
29854
|
/** Query cap — the rule set is operator-authored and tiny; a generous ceiling. */
|
|
29449
|
-
var LOAD_LIMIT$
|
|
29855
|
+
var LOAD_LIMIT$3 = 1e4;
|
|
29450
29856
|
function isRecord$3(value) {
|
|
29451
29857
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29452
29858
|
}
|
|
@@ -29495,7 +29901,7 @@ function makeRulesSpec(onProjection) {
|
|
|
29495
29901
|
if (projection.kind === "projected") onProjection?.(key, projection);
|
|
29496
29902
|
return parsed.data;
|
|
29497
29903
|
},
|
|
29498
|
-
loadLimit: LOAD_LIMIT$
|
|
29904
|
+
loadLimit: LOAD_LIMIT$3
|
|
29499
29905
|
};
|
|
29500
29906
|
}
|
|
29501
29907
|
/** The spec `declare` needs — no sink: declaring a collection projects nothing. */
|
|
@@ -30567,7 +30973,7 @@ var DEFAULT_PROMPT_EN = "What happened during this period? Summarise it for a ho
|
|
|
30567
30973
|
*/
|
|
30568
30974
|
var SummaryAnswerSchema = require_clip_model_registry.object({ summary: require_clip_model_registry.string() });
|
|
30569
30975
|
/** `HH:MM` in the host timezone — the two numbers a frame label needs. */
|
|
30570
|
-
function clockOf$
|
|
30976
|
+
function clockOf$4(atMs) {
|
|
30571
30977
|
const d = new Date(atMs);
|
|
30572
30978
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
30573
30979
|
}
|
|
@@ -30590,7 +30996,7 @@ function buildSummaryAiPrompt(input, frames) {
|
|
|
30590
30996
|
const roster = frames.map((f, i) => {
|
|
30591
30997
|
const location = plainLabel(f.location ?? "");
|
|
30592
30998
|
const where = location.length > 0 ? ` (${location})` : "";
|
|
30593
|
-
return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$
|
|
30999
|
+
return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$4(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
|
|
30594
31000
|
}).join("\n");
|
|
30595
31001
|
const described = /* @__PURE__ */ new Map();
|
|
30596
31002
|
for (const f of frames) {
|
|
@@ -31193,7 +31599,7 @@ var NO_DETECTIONS$1 = {
|
|
|
31193
31599
|
* has to check is a key somebody can get wrong. */
|
|
31194
31600
|
var BODY_SEPARATOR$1 = " · ";
|
|
31195
31601
|
/** `HH:MM` in the host timezone. */
|
|
31196
|
-
function clockOf$
|
|
31602
|
+
function clockOf$3(atMs) {
|
|
31197
31603
|
const d = new Date(atMs);
|
|
31198
31604
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
31199
31605
|
}
|
|
@@ -31207,8 +31613,8 @@ function clockOf$2(atMs) {
|
|
|
31207
31613
|
function summaryTemplateVars(input) {
|
|
31208
31614
|
return {
|
|
31209
31615
|
rule: input.ruleName,
|
|
31210
|
-
from: clockOf$
|
|
31211
|
-
to: clockOf$
|
|
31616
|
+
from: clockOf$3(input.window.startMs),
|
|
31617
|
+
to: clockOf$3(input.window.endMs),
|
|
31212
31618
|
time: ncClockText(input.texts.language, input.generatedAt),
|
|
31213
31619
|
events: String(input.tileCount),
|
|
31214
31620
|
cameras: String(input.cameraCount),
|
|
@@ -31252,7 +31658,7 @@ function composeSummaryBody(input) {
|
|
|
31252
31658
|
* string, for the reason the timelapse learned: a hand-rolled `n === 1` shipped
|
|
31253
31659
|
* "1 intervalli ripresi" to a live phone.
|
|
31254
31660
|
*/
|
|
31255
|
-
function derivedBody$
|
|
31661
|
+
function derivedBody$2(input, vars) {
|
|
31256
31662
|
const texts = input.texts;
|
|
31257
31663
|
const parts = [texts.text({
|
|
31258
31664
|
key: "summary.body.window",
|
|
@@ -31287,7 +31693,7 @@ function derivedBody$1(input, vars) {
|
|
|
31287
31693
|
}
|
|
31288
31694
|
/** Per-target params with the derived `clickUrl` filled in where absent. A
|
|
31289
31695
|
* value the operator set on the target WINS — he chose it. */
|
|
31290
|
-
function paramsFor$
|
|
31696
|
+
function paramsFor$2(target, clickUrl) {
|
|
31291
31697
|
if (clickUrl === void 0) return target.params;
|
|
31292
31698
|
if (target.params === void 0) return { clickUrl };
|
|
31293
31699
|
if (typeof target.params["clickUrl"] === "string") return target.params;
|
|
@@ -31303,11 +31709,11 @@ function summaryRecordId(ruleId, windowEndMs) {
|
|
|
31303
31709
|
/** The caption burned onto the mosaic's title band, with the same `{{var}}`
|
|
31304
31710
|
* vocabulary the title and body use. `''` is a real answer ("no caption"). */
|
|
31305
31711
|
function summaryCaptionText(input) {
|
|
31306
|
-
if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$
|
|
31712
|
+
if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$3(input.window.startMs)}→${clockOf$3(input.window.endMs)}`;
|
|
31307
31713
|
return renderTemplate(input.captionText, {
|
|
31308
31714
|
rule: input.ruleName,
|
|
31309
|
-
from: clockOf$
|
|
31310
|
-
to: clockOf$
|
|
31715
|
+
from: clockOf$3(input.window.startMs),
|
|
31716
|
+
to: clockOf$3(input.window.endMs)
|
|
31311
31717
|
}) ?? "";
|
|
31312
31718
|
}
|
|
31313
31719
|
/**
|
|
@@ -31325,7 +31731,7 @@ function buildSummaryOutboxInputs(input) {
|
|
|
31325
31731
|
const body = composeSummaryBody({
|
|
31326
31732
|
aiText: aiSentence(input),
|
|
31327
31733
|
renderedTemplateBody: renderTemplate(input.template?.body, vars),
|
|
31328
|
-
derivedBody: derivedBody$
|
|
31734
|
+
derivedBody: derivedBody$2(input, vars),
|
|
31329
31735
|
templateNamesAi: templatePlaceholders(input.template?.body ?? "").includes("ai")
|
|
31330
31736
|
});
|
|
31331
31737
|
const recordId = summaryRecordId(input.ruleId, input.window.endMs);
|
|
@@ -31344,7 +31750,7 @@ function buildSummaryOutboxInputs(input) {
|
|
|
31344
31750
|
},
|
|
31345
31751
|
media: "none",
|
|
31346
31752
|
...artifacts.length > 0 ? { artifacts } : {},
|
|
31347
|
-
...paramsFor$
|
|
31753
|
+
...paramsFor$2(target, input.clickUrl) !== void 0 ? { params: paramsFor$2(target, input.clickUrl) } : {},
|
|
31348
31754
|
subject: {
|
|
31349
31755
|
deviceId: input.primaryDeviceId,
|
|
31350
31756
|
className: "summary",
|
|
@@ -32239,7 +32645,7 @@ var NcSummaryProducer = class {
|
|
|
32239
32645
|
purpose: "summary",
|
|
32240
32646
|
tiles: tiles.map(({ candidate, jpeg }) => ({
|
|
32241
32647
|
jpeg,
|
|
32242
|
-
...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$
|
|
32648
|
+
...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$2(candidate.firstSeen)} · ${candidate.className}` } : {},
|
|
32243
32649
|
deviceId: candidate.deviceId,
|
|
32244
32650
|
sourceId: candidate.trackId
|
|
32245
32651
|
})),
|
|
@@ -32462,7 +32868,7 @@ var NcSummaryProducer = class {
|
|
|
32462
32868
|
}
|
|
32463
32869
|
};
|
|
32464
32870
|
/** `HH:MM` in the host timezone — the two numbers a tile label needs. */
|
|
32465
|
-
function clockOf$
|
|
32871
|
+
function clockOf$2(atMs) {
|
|
32466
32872
|
const d = new Date(atMs);
|
|
32467
32873
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
32468
32874
|
}
|
|
@@ -32538,7 +32944,7 @@ var NC_SUMMARY_RULES_INDEXES = [{
|
|
|
32538
32944
|
columns: ["enabled"]
|
|
32539
32945
|
}];
|
|
32540
32946
|
/** The rule set is operator-authored and tiny; a generous ceiling. */
|
|
32541
|
-
var LOAD_LIMIT$
|
|
32947
|
+
var LOAD_LIMIT$2 = 1e4;
|
|
32542
32948
|
/**
|
|
32543
32949
|
* The ledger contract for the digest rule set.
|
|
32544
32950
|
*
|
|
@@ -32568,7 +32974,7 @@ var NC_SUMMARY_RULES_SPEC = {
|
|
|
32568
32974
|
const parsed = NcSummaryRuleSchema.safeParse(data["rule"]);
|
|
32569
32975
|
return parsed.success ? parsed.data : null;
|
|
32570
32976
|
},
|
|
32571
|
-
loadLimit: LOAD_LIMIT$
|
|
32977
|
+
loadLimit: LOAD_LIMIT$2
|
|
32572
32978
|
};
|
|
32573
32979
|
/**
|
|
32574
32980
|
* The three-way CLEARABLE patch signal, one function per field: `undefined`
|
|
@@ -32579,7 +32985,7 @@ var NC_SUMMARY_RULES_SPEC = {
|
|
|
32579
32985
|
* cast here would be a cast on the exact path that decides whether a persisted
|
|
32580
32986
|
* rule keeps a field. Three literal destructurings are three provable ones.
|
|
32581
32987
|
*/
|
|
32582
|
-
function applyTemplatePatch$
|
|
32988
|
+
function applyTemplatePatch$2(merged, template) {
|
|
32583
32989
|
if (template === void 0) return merged;
|
|
32584
32990
|
if (template !== null) return {
|
|
32585
32991
|
...merged,
|
|
@@ -32725,7 +33131,7 @@ var NcSummaryStore = class {
|
|
|
32725
33131
|
createdAt: existing.createdAt,
|
|
32726
33132
|
updatedAt: this.now()
|
|
32727
33133
|
};
|
|
32728
|
-
return this.write(applyAiPatch(applyFiltersPatch(applyTemplatePatch$
|
|
33134
|
+
return this.write(applyAiPatch(applyFiltersPatch(applyTemplatePatch$2(merged, template), filters), ai));
|
|
32729
33135
|
}
|
|
32730
33136
|
async setEnabled(ruleId, enabled) {
|
|
32731
33137
|
return this.update(ruleId, { enabled });
|
|
@@ -32777,6 +33183,408 @@ var NcSummaryStore = class {
|
|
|
32777
33183
|
}
|
|
32778
33184
|
};
|
|
32779
33185
|
//#endregion
|
|
33186
|
+
//#region src/notification-center/calendar-events/calendar-events-store.ts
|
|
33187
|
+
/**
|
|
33188
|
+
* Durable calendar-events rule set — same DurableLedger mechanics as
|
|
33189
|
+
* Summaries, with a per-rule `generatedAt` stamp (one fire per schedule window).
|
|
33190
|
+
*/
|
|
33191
|
+
/**
|
|
33192
|
+
* @durable class=config owner=notification-center
|
|
33193
|
+
* write="an operator creates or edits a calendar-events rule, or the producer stamps a delivered window via markGenerated"
|
|
33194
|
+
* retention="none — a row goes only when the operator deletes the rule."
|
|
33195
|
+
*/
|
|
33196
|
+
var NC_CALENDAR_EVENTS_RULES_COLLECTION = "notification-center:calendar-events-rules";
|
|
33197
|
+
var NC_CALENDAR_EVENTS_RULES_COLUMNS = [
|
|
33198
|
+
{
|
|
33199
|
+
name: "id",
|
|
33200
|
+
type: "TEXT",
|
|
33201
|
+
primaryKey: true,
|
|
33202
|
+
notNull: true
|
|
33203
|
+
},
|
|
33204
|
+
{
|
|
33205
|
+
name: "name",
|
|
33206
|
+
type: "TEXT",
|
|
33207
|
+
notNull: true
|
|
33208
|
+
},
|
|
33209
|
+
{
|
|
33210
|
+
name: "enabled",
|
|
33211
|
+
type: "BOOLEAN",
|
|
33212
|
+
notNull: true
|
|
33213
|
+
},
|
|
33214
|
+
{
|
|
33215
|
+
name: "updatedAt",
|
|
33216
|
+
type: "INTEGER",
|
|
33217
|
+
notNull: true
|
|
33218
|
+
},
|
|
33219
|
+
{
|
|
33220
|
+
name: "rule",
|
|
33221
|
+
type: "JSON",
|
|
33222
|
+
notNull: true
|
|
33223
|
+
}
|
|
33224
|
+
];
|
|
33225
|
+
var NC_CALENDAR_EVENTS_RULES_INDEXES = [{
|
|
33226
|
+
name: "idx_nc_calendar_events_rules_enabled",
|
|
33227
|
+
columns: ["enabled"]
|
|
33228
|
+
}];
|
|
33229
|
+
var LOAD_LIMIT$1 = 1e4;
|
|
33230
|
+
var NC_CALENDAR_EVENTS_RULES_SPEC = {
|
|
33231
|
+
collection: NC_CALENDAR_EVENTS_RULES_COLLECTION,
|
|
33232
|
+
columns: [...NC_CALENDAR_EVENTS_RULES_COLUMNS],
|
|
33233
|
+
indexes: [...NC_CALENDAR_EVENTS_RULES_INDEXES],
|
|
33234
|
+
writeMode: "write-through",
|
|
33235
|
+
keyOf: (rule) => rule.id,
|
|
33236
|
+
toValue: (rule) => ({
|
|
33237
|
+
name: rule.name,
|
|
33238
|
+
enabled: rule.enabled,
|
|
33239
|
+
updatedAt: rule.updatedAt,
|
|
33240
|
+
rule
|
|
33241
|
+
}),
|
|
33242
|
+
fromRecord: (_key, data) => {
|
|
33243
|
+
const parsed = NcCalendarEventsRuleSchema.safeParse(data["rule"]);
|
|
33244
|
+
return parsed.success ? parsed.data : null;
|
|
33245
|
+
},
|
|
33246
|
+
loadLimit: LOAD_LIMIT$1
|
|
33247
|
+
};
|
|
33248
|
+
function applyTemplatePatch$1(merged, template) {
|
|
33249
|
+
if (template === void 0) return merged;
|
|
33250
|
+
if (template !== null) return {
|
|
33251
|
+
...merged,
|
|
33252
|
+
template
|
|
33253
|
+
};
|
|
33254
|
+
const { template: _cleared, ...rest } = merged;
|
|
33255
|
+
return rest;
|
|
33256
|
+
}
|
|
33257
|
+
function applyOptionalStringList(merged, key, value) {
|
|
33258
|
+
if (value === void 0) return merged;
|
|
33259
|
+
if (value !== null) return {
|
|
33260
|
+
...merged,
|
|
33261
|
+
[key]: [...value]
|
|
33262
|
+
};
|
|
33263
|
+
const { [key]: _cleared, ...rest } = merged;
|
|
33264
|
+
return rest;
|
|
33265
|
+
}
|
|
33266
|
+
var NcCalendarEventsStore = class {
|
|
33267
|
+
ledger;
|
|
33268
|
+
store;
|
|
33269
|
+
logger;
|
|
33270
|
+
now;
|
|
33271
|
+
newId;
|
|
33272
|
+
constructor(deps) {
|
|
33273
|
+
this.store = deps.store;
|
|
33274
|
+
this.ledger = new DurableLedger({
|
|
33275
|
+
spec: NC_CALENDAR_EVENTS_RULES_SPEC,
|
|
33276
|
+
store: deps.store,
|
|
33277
|
+
logger: deps.logger
|
|
33278
|
+
});
|
|
33279
|
+
this.logger = deps.logger;
|
|
33280
|
+
this.now = deps.now ?? (() => Date.now());
|
|
33281
|
+
this.newId = deps.newId ?? (() => (0, node_crypto.randomUUID)());
|
|
33282
|
+
}
|
|
33283
|
+
static async declare(store) {
|
|
33284
|
+
await DurableLedger.declare(store, NC_CALENDAR_EVENTS_RULES_SPEC);
|
|
33285
|
+
}
|
|
33286
|
+
async load() {
|
|
33287
|
+
await this.ledger.load();
|
|
33288
|
+
this.logger.debug("calendar-events rules loaded", { meta: { rules: this.ledger.size } });
|
|
33289
|
+
}
|
|
33290
|
+
list() {
|
|
33291
|
+
return this.ledger.snapshot().toSorted((a, b) => b.updatedAt - a.updatedAt);
|
|
33292
|
+
}
|
|
33293
|
+
listEnabled() {
|
|
33294
|
+
return this.list().filter((r) => r.enabled);
|
|
33295
|
+
}
|
|
33296
|
+
get(ruleId) {
|
|
33297
|
+
return this.ledger.get(ruleId) ?? null;
|
|
33298
|
+
}
|
|
33299
|
+
listForOwner(userId) {
|
|
33300
|
+
return this.list().filter((r) => r.ownerUserId === void 0 || r.ownerUserId === userId);
|
|
33301
|
+
}
|
|
33302
|
+
async create(input, createdBy, ownerUserId) {
|
|
33303
|
+
const now = this.now();
|
|
33304
|
+
const rule = NcCalendarEventsRuleSchema.parse({
|
|
33305
|
+
...NcCalendarEventsRuleInputSchema.parse(input),
|
|
33306
|
+
id: this.newId(),
|
|
33307
|
+
...ownerUserId !== void 0 ? { ownerUserId } : {},
|
|
33308
|
+
createdBy,
|
|
33309
|
+
createdAt: now,
|
|
33310
|
+
updatedAt: now
|
|
33311
|
+
});
|
|
33312
|
+
await this.ledger.put(rule);
|
|
33313
|
+
return rule;
|
|
33314
|
+
}
|
|
33315
|
+
async update(ruleId, patch) {
|
|
33316
|
+
const existing = this.ledger.get(ruleId);
|
|
33317
|
+
if (!existing) throw new Error(`calendar-events rule not found: ${ruleId}`);
|
|
33318
|
+
const { template, calendars, calendarsExclude, ...rest } = patch;
|
|
33319
|
+
const merged = {
|
|
33320
|
+
...existing,
|
|
33321
|
+
...rest,
|
|
33322
|
+
id: existing.id,
|
|
33323
|
+
...existing.ownerUserId !== void 0 ? { ownerUserId: existing.ownerUserId } : {},
|
|
33324
|
+
...existing.generatedAt !== void 0 ? { generatedAt: existing.generatedAt } : {},
|
|
33325
|
+
createdBy: existing.createdBy,
|
|
33326
|
+
createdAt: existing.createdAt,
|
|
33327
|
+
updatedAt: this.now()
|
|
33328
|
+
};
|
|
33329
|
+
return this.write(applyOptionalStringList(applyOptionalStringList(applyTemplatePatch$1(merged, template), "calendars", calendars), "calendarsExclude", calendarsExclude));
|
|
33330
|
+
}
|
|
33331
|
+
async setEnabled(ruleId, enabled) {
|
|
33332
|
+
return this.update(ruleId, { enabled });
|
|
33333
|
+
}
|
|
33334
|
+
async markGenerated(ruleId, windowEndMs) {
|
|
33335
|
+
const existing = this.ledger.get(ruleId);
|
|
33336
|
+
if (!existing) throw new Error(`calendar-events rule not found: ${ruleId}`);
|
|
33337
|
+
return this.write({
|
|
33338
|
+
...existing,
|
|
33339
|
+
generatedAt: Math.max(existing.generatedAt ?? 0, windowEndMs)
|
|
33340
|
+
});
|
|
33341
|
+
}
|
|
33342
|
+
/**
|
|
33343
|
+
* Idempotent delete — unknown ids are a no-op in RAM, still attempted on disk.
|
|
33344
|
+
*
|
|
33345
|
+
* Deliberately NOT {@link DurableLedger.forget}: that swallows a failed
|
|
33346
|
+
* durable delete. Mirror eviction first; durable half re-throws on failure
|
|
33347
|
+
* (same shape as Summaries).
|
|
33348
|
+
*/
|
|
33349
|
+
async delete(ruleId) {
|
|
33350
|
+
this.ledger.evict(ruleId);
|
|
33351
|
+
try {
|
|
33352
|
+
await this.store.delete.mutate({
|
|
33353
|
+
collection: NC_CALENDAR_EVENTS_RULES_COLLECTION,
|
|
33354
|
+
key: ruleId
|
|
33355
|
+
});
|
|
33356
|
+
} catch (err) {
|
|
33357
|
+
this.logger.warn("calendar-events rule durable delete failed", { meta: {
|
|
33358
|
+
ruleId,
|
|
33359
|
+
error: err instanceof Error ? err.message : String(err)
|
|
33360
|
+
} });
|
|
33361
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
33362
|
+
}
|
|
33363
|
+
}
|
|
33364
|
+
async write(rule) {
|
|
33365
|
+
const validated = NcCalendarEventsRuleSchema.parse(rule);
|
|
33366
|
+
await this.ledger.put(validated);
|
|
33367
|
+
return validated;
|
|
33368
|
+
}
|
|
33369
|
+
};
|
|
33370
|
+
//#endregion
|
|
33371
|
+
//#region src/notification-center/calendar-events/calendar-events-delivery.ts
|
|
33372
|
+
function calendarEventsRecordId(ruleId, windowEndMs) {
|
|
33373
|
+
return `calendar-events:${ruleId}:${windowEndMs}`;
|
|
33374
|
+
}
|
|
33375
|
+
/** `YYYY-MM-DD HH:MM` in the host timezone. */
|
|
33376
|
+
function clockOf$1(atMs) {
|
|
33377
|
+
const d = new Date(atMs);
|
|
33378
|
+
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")}`}`;
|
|
33379
|
+
}
|
|
33380
|
+
function formatEventLine(event) {
|
|
33381
|
+
const when = event.allDay ? clockOf$1(event.startMs).slice(0, 10) : `${clockOf$1(event.startMs)}–${clockOf$1(event.endMs)}`;
|
|
33382
|
+
const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
|
|
33383
|
+
return `• ${when} ${event.title}${where}`;
|
|
33384
|
+
}
|
|
33385
|
+
function derivedBody$1(input) {
|
|
33386
|
+
if (input.events.length === 0) return `No events in the next ${String(input.horizonDays)} day(s).`;
|
|
33387
|
+
const lines = input.events.slice(0, 40).map(formatEventLine);
|
|
33388
|
+
const extra = input.events.length > 40 ? `\n…and ${String(input.events.length - 40)} more` : "";
|
|
33389
|
+
return lines.join("\n") + extra;
|
|
33390
|
+
}
|
|
33391
|
+
function templateVars(input) {
|
|
33392
|
+
return {
|
|
33393
|
+
rule: input.ruleName,
|
|
33394
|
+
count: String(input.events.length),
|
|
33395
|
+
horizonDays: String(input.horizonDays),
|
|
33396
|
+
from: clockOf$1(input.eventWindow.startMs),
|
|
33397
|
+
to: clockOf$1(input.eventWindow.endMs),
|
|
33398
|
+
events: derivedBody$1(input),
|
|
33399
|
+
calendars: String(input.calendarIds.length)
|
|
33400
|
+
};
|
|
33401
|
+
}
|
|
33402
|
+
function paramsFor$1(target) {
|
|
33403
|
+
const params = target.params;
|
|
33404
|
+
if (params === void 0) return void 0;
|
|
33405
|
+
const out = {};
|
|
33406
|
+
for (const [k, v] of Object.entries(params)) if (typeof v === "string") out[k] = v;
|
|
33407
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
33408
|
+
}
|
|
33409
|
+
/**
|
|
33410
|
+
* One outbox row per enabled target. Ready for `NcOutbox.enqueue`.
|
|
33411
|
+
*/
|
|
33412
|
+
function buildCalendarEventsOutboxInputs(input) {
|
|
33413
|
+
const vars = templateVars(input);
|
|
33414
|
+
const optedOut = new Set(input.disabledTargetIds ?? []);
|
|
33415
|
+
const title = renderTemplate(input.template?.title, vars) ?? `${input.ruleName}: ${String(input.events.length)} upcoming`;
|
|
33416
|
+
const body = renderTemplate(input.template?.body, vars) ?? derivedBody$1(input);
|
|
33417
|
+
const recordId = calendarEventsRecordId(input.ruleId, input.scheduleWindow.endMs);
|
|
33418
|
+
return input.targets.filter((t) => !optedOut.has(t.targetId)).map((target) => {
|
|
33419
|
+
const payload = {
|
|
33420
|
+
ruleName: input.ruleName,
|
|
33421
|
+
delivery: "calendar-events",
|
|
33422
|
+
priority: input.priority,
|
|
33423
|
+
template: {
|
|
33424
|
+
title,
|
|
33425
|
+
body
|
|
33426
|
+
},
|
|
33427
|
+
media: "none",
|
|
33428
|
+
...paramsFor$1(target) !== void 0 ? { params: paramsFor$1(target) } : {},
|
|
33429
|
+
subject: {
|
|
33430
|
+
deviceId: NC_SYSTEM_DEVICE_ID$1,
|
|
33431
|
+
className: "calendar-events",
|
|
33432
|
+
zones: [],
|
|
33433
|
+
timestamp: input.scheduleWindow.endMs
|
|
33434
|
+
}
|
|
33435
|
+
};
|
|
33436
|
+
return {
|
|
33437
|
+
ruleId: input.ruleId,
|
|
33438
|
+
targetId: target.targetId,
|
|
33439
|
+
deviceId: NC_SYSTEM_DEVICE_ID$1,
|
|
33440
|
+
recordKind: "calendar-events",
|
|
33441
|
+
recordId,
|
|
33442
|
+
payload
|
|
33443
|
+
};
|
|
33444
|
+
});
|
|
33445
|
+
}
|
|
33446
|
+
//#endregion
|
|
33447
|
+
//#region src/notification-center/calendar-events/calendar-events-selection.ts
|
|
33448
|
+
/**
|
|
33449
|
+
* Expand a rule's calendar scope against the known calendar ids.
|
|
33450
|
+
*
|
|
33451
|
+
* - All (both lists empty/absent) → every known id
|
|
33452
|
+
* - Only → the include list, intersected with known (unknown ids dropped)
|
|
33453
|
+
* - Except → known minus the exclude list
|
|
33454
|
+
*
|
|
33455
|
+
* An empty known set always yields empty — there is nothing to notify about.
|
|
33456
|
+
*/
|
|
33457
|
+
function expandCalendarScope(scope, knownIds) {
|
|
33458
|
+
const known = [...knownIds];
|
|
33459
|
+
if (known.length === 0) return [];
|
|
33460
|
+
const include = scope?.calendars ?? [];
|
|
33461
|
+
const exclude = scope?.calendarsExclude ?? [];
|
|
33462
|
+
if (include.length > 0) {
|
|
33463
|
+
const wanted = new Set(include);
|
|
33464
|
+
return known.filter((id) => wanted.has(id));
|
|
33465
|
+
}
|
|
33466
|
+
if (exclude.length > 0) {
|
|
33467
|
+
const banned = new Set(exclude);
|
|
33468
|
+
return known.filter((id) => !banned.has(id));
|
|
33469
|
+
}
|
|
33470
|
+
return known;
|
|
33471
|
+
}
|
|
33472
|
+
//#endregion
|
|
33473
|
+
//#region src/notification-center/calendar-events/calendar-events-producer.ts
|
|
33474
|
+
/** How far back a closed schedule window may still be owed. */
|
|
33475
|
+
var CALENDAR_EVENTS_LOOKBACK_MS = 2160 * 60 * 1e3;
|
|
33476
|
+
var NcCalendarEventsProducer = class {
|
|
33477
|
+
deps;
|
|
33478
|
+
inFlight = false;
|
|
33479
|
+
now;
|
|
33480
|
+
constructor(deps) {
|
|
33481
|
+
this.deps = deps;
|
|
33482
|
+
this.now = deps.now ?? (() => Date.now());
|
|
33483
|
+
}
|
|
33484
|
+
async tick() {
|
|
33485
|
+
if (this.inFlight) return {
|
|
33486
|
+
examined: 0,
|
|
33487
|
+
sent: 0,
|
|
33488
|
+
empty: 0,
|
|
33489
|
+
skipped: 0,
|
|
33490
|
+
failed: 0
|
|
33491
|
+
};
|
|
33492
|
+
this.inFlight = true;
|
|
33493
|
+
const report = {
|
|
33494
|
+
examined: 0,
|
|
33495
|
+
sent: 0,
|
|
33496
|
+
empty: 0,
|
|
33497
|
+
skipped: 0,
|
|
33498
|
+
failed: 0
|
|
33499
|
+
};
|
|
33500
|
+
try {
|
|
33501
|
+
const rules = this.deps.store.listEnabled();
|
|
33502
|
+
for (const rule of rules) {
|
|
33503
|
+
report.examined += 1;
|
|
33504
|
+
try {
|
|
33505
|
+
const outcome = await this.runRule(rule);
|
|
33506
|
+
if (outcome === "sent") report.sent += 1;
|
|
33507
|
+
else if (outcome === "empty") report.empty += 1;
|
|
33508
|
+
else report.skipped += 1;
|
|
33509
|
+
} catch (err) {
|
|
33510
|
+
report.failed += 1;
|
|
33511
|
+
this.deps.logger.warn("calendar-events rule failed", { meta: {
|
|
33512
|
+
ruleId: rule.id,
|
|
33513
|
+
error: err instanceof Error ? err.message : String(err)
|
|
33514
|
+
} });
|
|
33515
|
+
}
|
|
33516
|
+
}
|
|
33517
|
+
return report;
|
|
33518
|
+
} finally {
|
|
33519
|
+
this.inFlight = false;
|
|
33520
|
+
}
|
|
33521
|
+
}
|
|
33522
|
+
async runRule(rule) {
|
|
33523
|
+
const nowMs = this.now();
|
|
33524
|
+
const scheduleWindow = findLastClosedWindow({
|
|
33525
|
+
schedule: rule.schedule,
|
|
33526
|
+
nowMs,
|
|
33527
|
+
lookbackMs: CALENDAR_EVENTS_LOOKBACK_MS
|
|
33528
|
+
});
|
|
33529
|
+
if (scheduleWindow === null) return "skipped";
|
|
33530
|
+
if ((rule.generatedAt ?? 0) >= scheduleWindow.endMs) return "skipped";
|
|
33531
|
+
const known = await this.deps.calendars.listCalendars();
|
|
33532
|
+
const calendarIds = expandCalendarScope({
|
|
33533
|
+
calendars: rule.calendars,
|
|
33534
|
+
calendarsExclude: rule.calendarsExclude
|
|
33535
|
+
}, known.map((c) => c.id));
|
|
33536
|
+
if (calendarIds.length === 0) {
|
|
33537
|
+
this.deps.logger.info("calendar-events: no calendars in scope", { meta: { ruleId: rule.id } });
|
|
33538
|
+
if (!rule.deliverEmpty) {
|
|
33539
|
+
await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
|
|
33540
|
+
return "empty";
|
|
33541
|
+
}
|
|
33542
|
+
}
|
|
33543
|
+
const eventWindow = calendarEventsWindow(nowMs, rule.horizonDays);
|
|
33544
|
+
const events = calendarIds.length === 0 ? [] : await this.deps.calendars.listEvents({
|
|
33545
|
+
calendarIds,
|
|
33546
|
+
startMs: eventWindow.startMs,
|
|
33547
|
+
endMs: eventWindow.endMs
|
|
33548
|
+
});
|
|
33549
|
+
if (events.length === 0 && !rule.deliverEmpty) {
|
|
33550
|
+
this.deps.logger.info("calendar-events: empty horizon, not delivering", { meta: {
|
|
33551
|
+
ruleId: rule.id,
|
|
33552
|
+
horizonDays: eventWindow.horizonDays
|
|
33553
|
+
} });
|
|
33554
|
+
await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
|
|
33555
|
+
return "empty";
|
|
33556
|
+
}
|
|
33557
|
+
const sorted = [...events].toSorted((a, b) => a.startMs - b.startMs);
|
|
33558
|
+
await this.enqueue(rule, scheduleWindow, eventWindow, calendarIds, sorted);
|
|
33559
|
+
await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
|
|
33560
|
+
return "sent";
|
|
33561
|
+
}
|
|
33562
|
+
async enqueue(rule, scheduleWindow, eventWindow, calendarIds, events) {
|
|
33563
|
+
const inputs = buildCalendarEventsOutboxInputs({
|
|
33564
|
+
ruleId: rule.id,
|
|
33565
|
+
ruleName: rule.name,
|
|
33566
|
+
scheduleWindow,
|
|
33567
|
+
eventWindow: {
|
|
33568
|
+
startMs: eventWindow.startMs,
|
|
33569
|
+
endMs: eventWindow.endMs
|
|
33570
|
+
},
|
|
33571
|
+
horizonDays: eventWindow.horizonDays,
|
|
33572
|
+
events,
|
|
33573
|
+
calendarIds,
|
|
33574
|
+
targets: rule.targets,
|
|
33575
|
+
priority: rule.priority,
|
|
33576
|
+
...rule.template !== void 0 ? { template: rule.template } : {}
|
|
33577
|
+
});
|
|
33578
|
+
for (const input of inputs) await this.deps.outbox.enqueue(input);
|
|
33579
|
+
this.deps.logger.info("calendar-events delivered", { meta: {
|
|
33580
|
+
ruleId: rule.id,
|
|
33581
|
+
events: events.length,
|
|
33582
|
+
targets: inputs.length,
|
|
33583
|
+
windowEndMs: scheduleWindow.endMs
|
|
33584
|
+
} });
|
|
33585
|
+
}
|
|
33586
|
+
};
|
|
33587
|
+
//#endregion
|
|
32780
33588
|
//#region src/notification-center/text-catalog-editor.ts
|
|
32781
33589
|
var NcTextCatalogEditor = class {
|
|
32782
33590
|
store;
|
|
@@ -35860,7 +36668,7 @@ function packagePhaseOf(ev) {
|
|
|
35860
36668
|
*/
|
|
35861
36669
|
function historyRecordKind(kind) {
|
|
35862
36670
|
if (kind === "audio-event" || kind === "audio-window") return "object-event";
|
|
35863
|
-
if (kind === "timelapse" || kind === "ai-summary") return "system-event";
|
|
36671
|
+
if (kind === "timelapse" || kind === "ai-summary" || kind === "calendar-events") return "system-event";
|
|
35864
36672
|
return kind;
|
|
35865
36673
|
}
|
|
35866
36674
|
/**
|
|
@@ -35875,7 +36683,8 @@ function historyRecordKind(kind) {
|
|
|
35875
36683
|
function cooldownSubjectKind(kind) {
|
|
35876
36684
|
switch (kind) {
|
|
35877
36685
|
case "timelapse":
|
|
35878
|
-
case "ai-summary":
|
|
36686
|
+
case "ai-summary":
|
|
36687
|
+
case "calendar-events": return "device-event";
|
|
35879
36688
|
default: return kind;
|
|
35880
36689
|
}
|
|
35881
36690
|
}
|
|
@@ -35975,7 +36784,7 @@ function outboxEntryToHistory(texts, entry) {
|
|
|
35975
36784
|
id: entry.id,
|
|
35976
36785
|
ruleId: entry.ruleId,
|
|
35977
36786
|
ruleName: entry.payload.ruleName,
|
|
35978
|
-
delivery: entry.payload.delivery === "timelapse" || entry.payload.delivery === "ai-summary" ? "system-event" : entry.payload.delivery,
|
|
36787
|
+
delivery: entry.payload.delivery === "timelapse" || entry.payload.delivery === "ai-summary" || entry.payload.delivery === "calendar-events" ? "system-event" : entry.payload.delivery,
|
|
35979
36788
|
targetId: entry.targetId,
|
|
35980
36789
|
deviceId: entry.deviceId,
|
|
35981
36790
|
recordKind: historyRecordKind(entry.recordKind),
|
|
@@ -36185,6 +36994,9 @@ var NotificationCenter = class {
|
|
|
36185
36994
|
*/
|
|
36186
36995
|
summaryRules;
|
|
36187
36996
|
summaryProducer = null;
|
|
36997
|
+
/** Calendar-events digest rules + producer (hub-only producer). */
|
|
36998
|
+
calendarEventsRules;
|
|
36999
|
+
calendarEventsProducer = null;
|
|
36188
37000
|
/**
|
|
36189
37001
|
* What the deleted-device / deleted-target prune did, and why — its
|
|
36190
37002
|
* disable-once guard and the source of both UIs' banner. Loaded on EVERY
|
|
@@ -37175,6 +37987,11 @@ var NotificationCenter = class {
|
|
|
37175
37987
|
logger: this.logger.child("summary"),
|
|
37176
37988
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
37177
37989
|
});
|
|
37990
|
+
this.calendarEventsRules = new NcCalendarEventsStore({
|
|
37991
|
+
store: deps.store,
|
|
37992
|
+
logger: this.logger.child("calendar-events"),
|
|
37993
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
37994
|
+
});
|
|
37178
37995
|
this.pruneLedger = new NcPruneLedger({
|
|
37179
37996
|
store: deps.store,
|
|
37180
37997
|
logger: this.logger.child("device-prune")
|
|
@@ -37221,6 +38038,14 @@ var NotificationCenter = class {
|
|
|
37221
38038
|
zoneOwner: this.zoneOwners.lookup()
|
|
37222
38039
|
});
|
|
37223
38040
|
}
|
|
38041
|
+
const calendarsPort = deps.calendars;
|
|
38042
|
+
if (calendarsPort !== void 0) this.calendarEventsProducer = new NcCalendarEventsProducer({
|
|
38043
|
+
store: this.calendarEventsRules,
|
|
38044
|
+
outbox: this.outbox,
|
|
38045
|
+
calendars: calendarsPort,
|
|
38046
|
+
logger: this.logger.child("calendar-events"),
|
|
38047
|
+
now: this.now
|
|
38048
|
+
});
|
|
37224
38049
|
}
|
|
37225
38050
|
/**
|
|
37226
38051
|
* The durable timelapse rule set — exposed for the `nc.*Timelapse*` bridge
|
|
@@ -37234,6 +38059,10 @@ var NotificationCenter = class {
|
|
|
37234
38059
|
get summaryStore() {
|
|
37235
38060
|
return this.summaryRules;
|
|
37236
38061
|
}
|
|
38062
|
+
/** The durable calendar-events rule set — exposed for the `nc.*CalendarEvents*` bridge. */
|
|
38063
|
+
get calendarEventsStore() {
|
|
38064
|
+
return this.calendarEventsRules;
|
|
38065
|
+
}
|
|
37237
38066
|
/**
|
|
37238
38067
|
* Produce one digest window for one rule, right now.
|
|
37239
38068
|
*
|
|
@@ -37307,6 +38136,7 @@ var NotificationCenter = class {
|
|
|
37307
38136
|
await NcDeviceMuteStore.declare(store);
|
|
37308
38137
|
await TimelapseStore.declare(store);
|
|
37309
38138
|
await NcSummaryStore.declare(store);
|
|
38139
|
+
await NcCalendarEventsStore.declare(store);
|
|
37310
38140
|
await NcRuleAiStore.declare(store);
|
|
37311
38141
|
await NcTextCatalogStore.declare(store);
|
|
37312
38142
|
await NcArtifactIndex.declare(store);
|
|
@@ -38721,7 +39551,18 @@ var NotificationCenter = class {
|
|
|
38721
39551
|
},
|
|
38722
39552
|
getAlarmConfig: async () => this.alarmConfig(),
|
|
38723
39553
|
getAlarmSensors: async (input) => this.alarmSensors(input),
|
|
38724
|
-
setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch)
|
|
39554
|
+
setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch),
|
|
39555
|
+
createAlarmProfile: async ({ name, cloneFromId }) => this.mutateAlarmProfile((panel) => panel.createProfile({
|
|
39556
|
+
name,
|
|
39557
|
+
newId: (0, node_crypto.randomUUID)(),
|
|
39558
|
+
source: cloneFromId === void 0 ? { kind: "defaults" } : {
|
|
39559
|
+
kind: "clone",
|
|
39560
|
+
fromId: cloneFromId
|
|
39561
|
+
}
|
|
39562
|
+
})),
|
|
39563
|
+
renameAlarmProfile: async ({ id, name }) => this.mutateAlarmProfile((panel) => panel.renameProfile(id, name)),
|
|
39564
|
+
deleteAlarmProfile: async ({ id }) => this.mutateAlarmProfile((panel) => panel.deleteProfile(id)),
|
|
39565
|
+
activateAlarmProfile: async ({ id }) => this.mutateAlarmProfile((panel) => panel.activateProfile(id))
|
|
38725
39566
|
};
|
|
38726
39567
|
}
|
|
38727
39568
|
/**
|
|
@@ -39014,6 +39855,13 @@ var NotificationCenter = class {
|
|
|
39014
39855
|
});
|
|
39015
39856
|
return this.alarmConfig();
|
|
39016
39857
|
}
|
|
39858
|
+
/** Profile CRUD — each mutation returns the whole config (settings applied). */
|
|
39859
|
+
async mutateAlarmProfile(run) {
|
|
39860
|
+
const panel = this.alarmPanel;
|
|
39861
|
+
if (panel === null) throw new Error("this node has no alarm panel");
|
|
39862
|
+
await run(panel);
|
|
39863
|
+
return this.alarmConfig();
|
|
39864
|
+
}
|
|
39017
39865
|
/**
|
|
39018
39866
|
* The panel as it is NOW — see `alarm-live.ts`.
|
|
39019
39867
|
*
|
|
@@ -39110,6 +39958,7 @@ var NotificationCenter = class {
|
|
|
39110
39958
|
return {
|
|
39111
39959
|
deviceId: panel.deviceId,
|
|
39112
39960
|
settings: panel.settings(),
|
|
39961
|
+
profiles: panel.profilesView(),
|
|
39113
39962
|
live: await this.alarmLive(panel),
|
|
39114
39963
|
coverage: this.coverageFor(panel).map((c) => ({
|
|
39115
39964
|
...c,
|
|
@@ -40929,11 +41778,17 @@ var NotificationCenter = class {
|
|
|
40929
41778
|
await this.rules.load();
|
|
40930
41779
|
this.noteInertGroupedRules();
|
|
40931
41780
|
if (this.evaluationActive) this.deviceAlerts.tick();
|
|
40932
|
-
if (this.evaluationActive)
|
|
40933
|
-
this.
|
|
40934
|
-
|
|
41781
|
+
if (this.evaluationActive) {
|
|
41782
|
+
this.stateSummary.tick().catch((err) => {
|
|
41783
|
+
this.logger.warn("state summary tick failed", { meta: { error: String(err) } });
|
|
41784
|
+
});
|
|
41785
|
+
this.calendarEventsProducer?.tick().catch((err) => {
|
|
41786
|
+
this.logger.warn("calendar-events tick failed", { meta: { error: String(err) } });
|
|
41787
|
+
});
|
|
41788
|
+
}
|
|
40935
41789
|
await this.timelapseRules.load();
|
|
40936
41790
|
await this.summaryRules.load();
|
|
41791
|
+
await this.calendarEventsRules.load();
|
|
40937
41792
|
await this.ruleAi.load();
|
|
40938
41793
|
await this.reloadTexts();
|
|
40939
41794
|
this.refreshOccupancyWatch();
|
|
@@ -104816,6 +105671,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
104816
105671
|
store: center.summaryStore,
|
|
104817
105672
|
runNow: (input) => center.runSummaryNow(input)
|
|
104818
105673
|
},
|
|
105674
|
+
calendarEvents: { store: center.calendarEventsStore },
|
|
104819
105675
|
texts: center.textCatalogEditor,
|
|
104820
105676
|
previewTexts: center.textCatalog,
|
|
104821
105677
|
...this.ncArtifactIndex !== null ? { artifacts: this.ncArtifactIndex } : {},
|
|
@@ -106003,7 +106859,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
106003
106859
|
})).tracks;
|
|
106004
106860
|
},
|
|
106005
106861
|
...this.isPostProcessingNode ? { timelapse: this.buildTimelapsePorts(api, stores) } : {},
|
|
106006
|
-
...this.isPostProcessingNode ? { summary: this.buildSummaryPorts(api, stores) } : {}
|
|
106862
|
+
...this.isPostProcessingNode ? { summary: this.buildSummaryPorts(api, stores) } : {},
|
|
106863
|
+
...this.isPostProcessingNode ? { calendars: {
|
|
106864
|
+
listCalendars: async () => {
|
|
106865
|
+
return (await api.calendars.listCalendars.query()).map((c) => ({ id: c.id }));
|
|
106866
|
+
},
|
|
106867
|
+
listEvents: async (input) => api.calendars.listEvents.query({
|
|
106868
|
+
calendarIds: [...input.calendarIds],
|
|
106869
|
+
startMs: input.startMs,
|
|
106870
|
+
endMs: input.endMs
|
|
106871
|
+
})
|
|
106872
|
+
} } : {}
|
|
106007
106873
|
});
|
|
106008
106874
|
this.wireAlarmPanel(this.notificationCenter);
|
|
106009
106875
|
await this.serveNcActionPlane(this.notificationCenter);
|