@camstack/addon-post-analysis 1.2.321 → 1.2.323
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-BCx3qCL5.js → clip-model-registry-8IMzj7jJ.js} +4542 -3562
- package/dist/{clip-model-registry-Dnm9dfy0.mjs → clip-model-registry-BENGs4rw.mjs} +5926 -5036
- 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-CPcZN3mA.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CGo6RmK3.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CBG_lEGV.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-Bnb-yu_h.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-Dt-jaGuG.mjs → hostInit-D5UUhDKa.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +941 -108
- package/dist/pipeline-analytics/index.mjs +941 -108
- 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-CGUnHrR_.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-CVH5tT7I.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-8IMzj7jJ.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");
|
|
@@ -945,7 +945,7 @@ function grantMeta(grant) {
|
|
|
945
945
|
...common,
|
|
946
946
|
ruleId: grant.ruleId,
|
|
947
947
|
rule: grant.ruleName,
|
|
948
|
-
|
|
948
|
+
sequenceId: grant.sequenceId
|
|
949
949
|
};
|
|
950
950
|
case "snooze": return {
|
|
951
951
|
...common,
|
|
@@ -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(),
|
|
@@ -4821,6 +5068,27 @@ function planDenseRanges(input) {
|
|
|
4821
5068
|
}
|
|
4822
5069
|
//#endregion
|
|
4823
5070
|
//#region src/notification-center/text-compose.ts
|
|
5071
|
+
/**
|
|
5072
|
+
* The COMPOSITE variables — phrases assembled from catalog fragments before
|
|
5073
|
+
* the one and only render pass.
|
|
5074
|
+
*
|
|
5075
|
+
* A notification body is not a sentence with slots. "person (John) on Front
|
|
5076
|
+
* door in gate" contains three sub-phrases whose SHAPE differs per language
|
|
5077
|
+
* and whose presence is conditional: the label parentheses, the zone clause,
|
|
5078
|
+
* the plural noun. Expressing that with `{{class}}{{label}}{{zones}}` and
|
|
5079
|
+
* three scalars pushes the grammar into the template, where a translator
|
|
5080
|
+
* cannot fix it. So each becomes ONE variable — `{{subject}}`, `{{inZones}}`,
|
|
5081
|
+
* `{{op}}`, `{{detectionSummary}}` — composed here from its own keys and handed
|
|
5082
|
+
* to {@link renderTemplate} as a finished string.
|
|
5083
|
+
*
|
|
5084
|
+
* **Fragments see the SCALARS only.** Never the composites. That is what makes
|
|
5085
|
+
* the depth exactly one and not "one so far": a fragment key edited to say
|
|
5086
|
+
* `{{inZones}}` renders empty rather than starting a second round, and
|
|
5087
|
+
* `renderTemplate` never re-scans what it inserted, so a composite's own value
|
|
5088
|
+
* is printed verbatim whatever it contains.
|
|
5089
|
+
*
|
|
5090
|
+
* Pure: a catalog in, a string out. No clock, no store, no I/O.
|
|
5091
|
+
*/
|
|
4824
5092
|
/** One normalisation, used on both sides of every class lookup — a pipeline
|
|
4825
5093
|
* that emitted `Person` must find the key written `class.person`. */
|
|
4826
5094
|
function normaliseClass(className) {
|
|
@@ -4829,24 +5097,35 @@ function normaliseClass(className) {
|
|
|
4829
5097
|
/**
|
|
4830
5098
|
* The noun for a class, in the count's plural category.
|
|
4831
5099
|
*
|
|
4832
|
-
*
|
|
4833
|
-
*
|
|
4834
|
-
*
|
|
4835
|
-
* detection.
|
|
5100
|
+
* Lookup order: the NC text catalog (`class.*`, plural-aware) → the shared
|
|
5101
|
+
* {@link eventKindLabel} map (viewer filters / EVENT_TAXONOMY) → `class.other`
|
|
5102
|
+
* with the RAW wire id. A custom model class must still appear rather than
|
|
5103
|
+
* vanish; a missing translation is a smaller defect than a missing detection.
|
|
4836
5104
|
*/
|
|
4837
5105
|
function ncClassNoun(texts, className, count) {
|
|
4838
|
-
const
|
|
5106
|
+
const normalised = normaliseClass(className);
|
|
5107
|
+
const key = `class.${normalised}`;
|
|
4839
5108
|
if (texts.has(key)) return texts.text({
|
|
4840
5109
|
key,
|
|
4841
5110
|
count,
|
|
4842
5111
|
vars: { name: className }
|
|
4843
5112
|
});
|
|
5113
|
+
if (require_clip_model_registry.isEventKindLabelKey(normalised)) return require_clip_model_registry.eventKindLabel(normalised, texts.language);
|
|
4844
5114
|
return texts.text({
|
|
4845
5115
|
key: "class.other",
|
|
4846
5116
|
count,
|
|
4847
5117
|
vars: { name: className }
|
|
4848
5118
|
});
|
|
4849
5119
|
}
|
|
5120
|
+
/**
|
|
5121
|
+
* The parenthetical half of a labelled subject: a recognised person or plate
|
|
5122
|
+
* is printed verbatim; a classification (species, COCO sub, …) goes through
|
|
5123
|
+
* the shared event-kind map so "animal (cat)" becomes "animale (Gatto)".
|
|
5124
|
+
*/
|
|
5125
|
+
function ncSubjectLabelText(texts, label, labelKind) {
|
|
5126
|
+
if (labelKind === "identity" || labelKind === "plate") return label;
|
|
5127
|
+
return require_clip_model_registry.eventKindLabel(label, texts.language);
|
|
5128
|
+
}
|
|
4850
5129
|
/** `person (John)` / `persona (John)` — the thing the notification is about. */
|
|
4851
5130
|
function ncSubjectText(texts, input) {
|
|
4852
5131
|
const classNoun = ncClassNoun(texts, input.className, 1);
|
|
@@ -4858,7 +5137,7 @@ function ncSubjectText(texts, input) {
|
|
|
4858
5137
|
key: subjectLabelKey(input.labelKind),
|
|
4859
5138
|
vars: {
|
|
4860
5139
|
classNoun,
|
|
4861
|
-
label: input.label
|
|
5140
|
+
label: ncSubjectLabelText(texts, input.label, input.labelKind)
|
|
4862
5141
|
}
|
|
4863
5142
|
});
|
|
4864
5143
|
}
|
|
@@ -8428,31 +8707,48 @@ function replaceConditionReferences(monitor, condition, reference) {
|
|
|
8428
8707
|
/** The cap whose presence in a sequence makes the rule the alarm's trigger. */
|
|
8429
8708
|
var ALARM_PANEL_CAP$1 = "alarm-panel";
|
|
8430
8709
|
var ALARM_KINDS = new Set(require_clip_model_registry.NC_ALARM_SYSTEM_EVENT_KINDS);
|
|
8431
|
-
function
|
|
8432
|
-
|
|
8710
|
+
function stepActuatesAlarm(step) {
|
|
8711
|
+
return step.kind === "cap" && step.cap === ALARM_PANEL_CAP$1;
|
|
8712
|
+
}
|
|
8713
|
+
function bindingActuatesAlarm(binding, sequenceActionsOf) {
|
|
8714
|
+
if (binding.kind === "do") return stepActuatesAlarm(binding.action);
|
|
8715
|
+
const steps = sequenceActionsOf?.(binding.sequenceId);
|
|
8716
|
+
if (steps === void 0) return false;
|
|
8717
|
+
return steps.some(stepActuatesAlarm);
|
|
8718
|
+
}
|
|
8719
|
+
function actionsActuateAlarm(rule, sequenceActionsOf) {
|
|
8720
|
+
const actions = rule.actions;
|
|
8721
|
+
if (actions === void 0) return false;
|
|
8722
|
+
const hooks = [
|
|
8723
|
+
actions.onActivation,
|
|
8724
|
+
actions.onDeactivation,
|
|
8725
|
+
actions.onTrigger,
|
|
8726
|
+
actions.onTriggerOff
|
|
8727
|
+
];
|
|
8728
|
+
for (const bindings of hooks) for (const binding of bindings ?? []) if (bindingActuatesAlarm(binding, sequenceActionsOf)) return true;
|
|
8729
|
+
for (const button of actions.buttons ?? []) {
|
|
8730
|
+
const steps = sequenceActionsOf?.(button.sequenceId);
|
|
8731
|
+
if (steps !== void 0 && steps.some(stepActuatesAlarm)) return true;
|
|
8732
|
+
}
|
|
8433
8733
|
return false;
|
|
8434
8734
|
}
|
|
8435
8735
|
/**
|
|
8436
8736
|
* May a snooze window silence this rule?
|
|
8437
8737
|
*
|
|
8438
|
-
* `true` = never. Deliberately answered from the rule alone
|
|
8439
|
-
* on the delivery path, where a fallible
|
|
8440
|
-
* DESTROY the exemption (D49) — and the
|
|
8441
|
-
* silencing an alarm.
|
|
8738
|
+
* `true` = never. Deliberately answered from the rule alone (plus an optional
|
|
8739
|
+
* sequence lookup): the gate reads it on the delivery path, where a fallible
|
|
8740
|
+
* lookup would be a read that can DESTROY the exemption (D49) — and the
|
|
8741
|
+
* failure direction of this one is silencing an alarm. A missing sequence
|
|
8742
|
+
* lookup means sequence-ref bindings cannot prove exemption; Do bindings and
|
|
8743
|
+
* system-event kinds still can.
|
|
8442
8744
|
*/
|
|
8443
|
-
function isSnoozeExempt(rule) {
|
|
8745
|
+
function isSnoozeExempt(rule, sequenceActionsOf) {
|
|
8444
8746
|
const kinds = rule.conditions.systemEvent?.kinds ?? [];
|
|
8445
8747
|
for (const kind of kinds) if (ALARM_KINDS.has(kind)) return true;
|
|
8446
|
-
return
|
|
8748
|
+
return actionsActuateAlarm(rule, sequenceActionsOf);
|
|
8447
8749
|
}
|
|
8448
8750
|
//#endregion
|
|
8449
8751
|
//#region src/notification-center/action-buttons.ts
|
|
8450
|
-
/** Sequence names the rule actually declares. A button may only name one. */
|
|
8451
|
-
function declaredSequences(actions) {
|
|
8452
|
-
const names = /* @__PURE__ */ new Set();
|
|
8453
|
-
for (const sequence of actions.onTrigger ?? []) names.add(sequence.name);
|
|
8454
|
-
return names;
|
|
8455
|
-
}
|
|
8456
8752
|
/**
|
|
8457
8753
|
* Build the notification's buttons.
|
|
8458
8754
|
*
|
|
@@ -8465,18 +8761,18 @@ function buildActionButtons(input) {
|
|
|
8465
8761
|
if (actions === void 0) return [];
|
|
8466
8762
|
const buttons = actions.buttons ?? [];
|
|
8467
8763
|
if (buttons.length === 0) return [];
|
|
8468
|
-
const known =
|
|
8764
|
+
const known = input.knownSequenceIds;
|
|
8469
8765
|
const seen = /* @__PURE__ */ new Set();
|
|
8470
8766
|
const out = [];
|
|
8471
8767
|
for (const button of buttons) {
|
|
8472
|
-
if (!known.has(button.
|
|
8768
|
+
if (!known.has(button.sequenceId)) continue;
|
|
8473
8769
|
if (seen.has(button.id)) continue;
|
|
8474
8770
|
seen.add(button.id);
|
|
8475
8771
|
out.push({
|
|
8476
8772
|
id: button.id,
|
|
8477
8773
|
label: button.label,
|
|
8478
8774
|
url: input.mintUrl({
|
|
8479
|
-
|
|
8775
|
+
sequenceId: button.sequenceId,
|
|
8480
8776
|
actionId: button.id
|
|
8481
8777
|
}),
|
|
8482
8778
|
...button.icon !== void 0 ? { icon: button.icon } : {},
|
|
@@ -8486,18 +8782,17 @@ function buildActionButtons(input) {
|
|
|
8486
8782
|
return out;
|
|
8487
8783
|
}
|
|
8488
8784
|
/**
|
|
8489
|
-
* Which of a rule's buttons name a sequence
|
|
8785
|
+
* Which of a rule's buttons name a sequence that is not known.
|
|
8490
8786
|
*
|
|
8491
8787
|
* Exported so the caller can LOG the drop. A button silently missing from a
|
|
8492
8788
|
* notification is the exact shape of failure this repo keeps paying for — the
|
|
8493
8789
|
* operator authored it, it never appeared, and nothing said why.
|
|
8494
8790
|
*/
|
|
8495
|
-
function unresolvableButtons(actions) {
|
|
8791
|
+
function unresolvableButtons(actions, knownSequenceIds) {
|
|
8496
8792
|
if (actions === void 0) return [];
|
|
8497
8793
|
const buttons = actions.buttons ?? [];
|
|
8498
8794
|
if (buttons.length === 0) return [];
|
|
8499
|
-
|
|
8500
|
-
return buttons.filter((b) => !known.has(b.sequence)).map((b) => `${b.id}→${b.sequence}`);
|
|
8795
|
+
return buttons.filter((b) => !knownSequenceIds.has(b.sequenceId)).map((b) => `${b.id}→${b.sequenceId}`);
|
|
8501
8796
|
}
|
|
8502
8797
|
/** How many snooze choices one notification may carry. The schema caps the
|
|
8503
8798
|
* authored list at 4; this is the same bound applied to a stored rule that
|
|
@@ -8548,7 +8843,7 @@ function snoozeChoices(options) {
|
|
|
8548
8843
|
* attention than the notification did.
|
|
8549
8844
|
*/
|
|
8550
8845
|
function buildSnoozeButtons(input) {
|
|
8551
|
-
if (isSnoozeExempt(input.rule)) return [];
|
|
8846
|
+
if (isSnoozeExempt(input.rule, input.sequenceActionsOf)) return [];
|
|
8552
8847
|
return snoozeChoices(input.rule.snoozeOptions).map((minutes) => {
|
|
8553
8848
|
const actionId = snoozeButtonId(minutes);
|
|
8554
8849
|
return {
|
|
@@ -10837,10 +11132,11 @@ function matchesIdentityScope(subject, scope) {
|
|
|
10837
11132
|
* Derived, never a stored flag: both rule editors prune condition keys to the
|
|
10838
11133
|
* catalog on save, so a new key would be dropped by the first edit.
|
|
10839
11134
|
*/
|
|
10840
|
-
function ruleBelongsToAlarm(rule) {
|
|
11135
|
+
function ruleBelongsToAlarm(rule, sequenceActionsOf) {
|
|
10841
11136
|
return require_clip_model_registry.ruleBelongsToAlarm({
|
|
10842
11137
|
conditions: rule.conditions,
|
|
10843
|
-
...rule.actions
|
|
11138
|
+
...rule.actions !== void 0 ? { actions: rule.actions } : {},
|
|
11139
|
+
...sequenceActionsOf !== void 0 ? { sequenceActionsOf } : {}
|
|
10844
11140
|
});
|
|
10845
11141
|
}
|
|
10846
11142
|
/**
|
|
@@ -12728,7 +13024,7 @@ function deriveTankBand(reading) {
|
|
|
12728
13024
|
case null: return null;
|
|
12729
13025
|
}
|
|
12730
13026
|
}
|
|
12731
|
-
function isRecord$
|
|
13027
|
+
function isRecord$9(value) {
|
|
12732
13028
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12733
13029
|
}
|
|
12734
13030
|
function tankStatusOf(value) {
|
|
@@ -12744,11 +13040,11 @@ function vacuumTanksOf(data) {
|
|
|
12744
13040
|
if (data["capName"] !== "vacuum-control") return null;
|
|
12745
13041
|
const deviceId = data["deviceId"];
|
|
12746
13042
|
const slice = data["slice"];
|
|
12747
|
-
if (typeof deviceId !== "number" || !isRecord$
|
|
13043
|
+
if (typeof deviceId !== "number" || !isRecord$9(slice)) return null;
|
|
12748
13044
|
const tanks = [];
|
|
12749
13045
|
for (const key of NC_TANK_KEYS) {
|
|
12750
13046
|
const raw = slice[key];
|
|
12751
|
-
if (!isRecord$
|
|
13047
|
+
if (!isRecord$9(raw)) continue;
|
|
12752
13048
|
const level = raw["level"];
|
|
12753
13049
|
tanks.push({
|
|
12754
13050
|
key,
|
|
@@ -20086,6 +20382,350 @@ var NcDeviceMuteStore = class {
|
|
|
20086
20382
|
}
|
|
20087
20383
|
};
|
|
20088
20384
|
//#endregion
|
|
20385
|
+
//#region src/notification-center/migrate-legacy-actions.ts
|
|
20386
|
+
function isRecord$8(value) {
|
|
20387
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20388
|
+
}
|
|
20389
|
+
/** A legacy inline sequence row — detected by `name` + `actions` + `enabled`. */
|
|
20390
|
+
function asLegacySequence(value) {
|
|
20391
|
+
const parsed = require_clip_model_registry.NcLegacyRuleActionSequenceSchema.safeParse(value);
|
|
20392
|
+
return parsed.success ? parsed.data : null;
|
|
20393
|
+
}
|
|
20394
|
+
/**
|
|
20395
|
+
* True when `onTrigger` still looks like the pre-shared-sequence array of
|
|
20396
|
+
* named sequences (not an array of `{ kind: 'sequence' | 'do', … }` bindings).
|
|
20397
|
+
*/
|
|
20398
|
+
function onTriggerLooksLegacy(onTrigger) {
|
|
20399
|
+
if (!Array.isArray(onTrigger) || onTrigger.length === 0) return false;
|
|
20400
|
+
const first = onTrigger[0];
|
|
20401
|
+
if (!isRecord$8(first)) return false;
|
|
20402
|
+
if (first["kind"] === "sequence" || first["kind"] === "do") return false;
|
|
20403
|
+
return asLegacySequence(first) !== null;
|
|
20404
|
+
}
|
|
20405
|
+
function legacyButtonSequenceName(button) {
|
|
20406
|
+
if (!isRecord$8(button)) return null;
|
|
20407
|
+
const name = button["sequence"];
|
|
20408
|
+
return typeof name === "string" && name.length > 0 ? name : null;
|
|
20409
|
+
}
|
|
20410
|
+
/**
|
|
20411
|
+
* Rewrite a raw `actions` block. Returns the (possibly unchanged) new-shape
|
|
20412
|
+
* actions and whether anything was materialised into the sequence store.
|
|
20413
|
+
*/
|
|
20414
|
+
async function migrateLegacyRuleActions(actions, createSequence) {
|
|
20415
|
+
if (actions === void 0 || actions === null) return {
|
|
20416
|
+
actions: void 0,
|
|
20417
|
+
migrated: false
|
|
20418
|
+
};
|
|
20419
|
+
if (!isRecord$8(actions)) return {
|
|
20420
|
+
actions: void 0,
|
|
20421
|
+
migrated: false
|
|
20422
|
+
};
|
|
20423
|
+
if (!onTriggerLooksLegacy(actions["onTrigger"]) && actions["sequence"] === void 0) {
|
|
20424
|
+
const buttons = actions["buttons"];
|
|
20425
|
+
if (!(Array.isArray(buttons) && buttons.some((b) => legacyButtonSequenceName(b) !== null))) {
|
|
20426
|
+
const parsed = require_clip_model_registry.NcRuleActionsSchema.safeParse(actions);
|
|
20427
|
+
if (parsed.success) return {
|
|
20428
|
+
actions: parsed.data,
|
|
20429
|
+
migrated: false
|
|
20430
|
+
};
|
|
20431
|
+
return {
|
|
20432
|
+
actions: void 0,
|
|
20433
|
+
migrated: false
|
|
20434
|
+
};
|
|
20435
|
+
}
|
|
20436
|
+
}
|
|
20437
|
+
const legacySequences = [];
|
|
20438
|
+
const onTriggerRaw = actions["onTrigger"];
|
|
20439
|
+
if (Array.isArray(onTriggerRaw)) for (const entry of onTriggerRaw) {
|
|
20440
|
+
const seq = asLegacySequence(entry);
|
|
20441
|
+
if (seq !== null) legacySequences.push(seq);
|
|
20442
|
+
}
|
|
20443
|
+
const nameToId = /* @__PURE__ */ new Map();
|
|
20444
|
+
for (const sequence of legacySequences) {
|
|
20445
|
+
const key = sequence.name.trim();
|
|
20446
|
+
if (key === "" || nameToId.has(key)) continue;
|
|
20447
|
+
const created = await createSequence({
|
|
20448
|
+
name: sequence.name,
|
|
20449
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20450
|
+
actions: [...sequence.actions]
|
|
20451
|
+
});
|
|
20452
|
+
nameToId.set(key, created.id);
|
|
20453
|
+
}
|
|
20454
|
+
const onTriggerBindings = [];
|
|
20455
|
+
for (const sequence of legacySequences) {
|
|
20456
|
+
if (!sequence.enabled) continue;
|
|
20457
|
+
const id = nameToId.get(sequence.name.trim());
|
|
20458
|
+
if (id === void 0) continue;
|
|
20459
|
+
onTriggerBindings.push({
|
|
20460
|
+
kind: "sequence",
|
|
20461
|
+
sequenceId: id
|
|
20462
|
+
});
|
|
20463
|
+
}
|
|
20464
|
+
const buttonsRaw = actions["buttons"];
|
|
20465
|
+
const buttons = [];
|
|
20466
|
+
if (Array.isArray(buttonsRaw)) for (const button of buttonsRaw) {
|
|
20467
|
+
if (!isRecord$8(button)) continue;
|
|
20468
|
+
const id = typeof button["id"] === "string" ? button["id"] : "";
|
|
20469
|
+
const label = typeof button["label"] === "string" ? button["label"] : "";
|
|
20470
|
+
if (id === "" || label === "") continue;
|
|
20471
|
+
const existingId = typeof button["sequenceId"] === "string" && button["sequenceId"].length > 0 ? button["sequenceId"] : void 0;
|
|
20472
|
+
const legacyName = legacyButtonSequenceName(button);
|
|
20473
|
+
const sequenceId = existingId ?? (legacyName !== null ? nameToId.get(legacyName.trim()) : void 0);
|
|
20474
|
+
if (sequenceId === void 0) continue;
|
|
20475
|
+
buttons.push({
|
|
20476
|
+
id,
|
|
20477
|
+
label,
|
|
20478
|
+
sequenceId,
|
|
20479
|
+
...typeof button["icon"] === "string" ? { icon: button["icon"] } : {},
|
|
20480
|
+
...typeof button["destructive"] === "boolean" ? { destructive: button["destructive"] } : {}
|
|
20481
|
+
});
|
|
20482
|
+
}
|
|
20483
|
+
const next = {
|
|
20484
|
+
...Array.isArray(actions["onActivation"]) ? { onActivation: actions["onActivation"] } : {},
|
|
20485
|
+
...Array.isArray(actions["onDeactivation"]) ? { onDeactivation: actions["onDeactivation"] } : {},
|
|
20486
|
+
...onTriggerBindings.length > 0 ? { onTrigger: onTriggerBindings } : {},
|
|
20487
|
+
...Array.isArray(actions["onTriggerOff"]) ? { onTriggerOff: actions["onTriggerOff"] } : {},
|
|
20488
|
+
...buttons.length > 0 ? { buttons } : {}
|
|
20489
|
+
};
|
|
20490
|
+
const parsed = require_clip_model_registry.NcRuleActionsSchema.safeParse(next);
|
|
20491
|
+
if (!parsed.success) return {
|
|
20492
|
+
actions: void 0,
|
|
20493
|
+
migrated: true
|
|
20494
|
+
};
|
|
20495
|
+
return {
|
|
20496
|
+
actions: (parsed.data.onActivation?.length ?? 0) === 0 && (parsed.data.onDeactivation?.length ?? 0) === 0 && (parsed.data.onTrigger?.length ?? 0) === 0 && (parsed.data.onTriggerOff?.length ?? 0) === 0 && (parsed.data.buttons?.length ?? 0) === 0 ? void 0 : parsed.data,
|
|
20497
|
+
migrated: true
|
|
20498
|
+
};
|
|
20499
|
+
}
|
|
20500
|
+
//#endregion
|
|
20501
|
+
//#region src/notification-center/sequence-store.ts
|
|
20502
|
+
/**
|
|
20503
|
+
* Shared action sequences for the notification centre.
|
|
20504
|
+
*
|
|
20505
|
+
* A sequence is a named, ordered run of steps (`wait` | `cap`) with its own
|
|
20506
|
+
* throttle. Rules and notification buttons bind to it by id — never by copying
|
|
20507
|
+
* the steps inline — so two rules that open the same gate share one budget and
|
|
20508
|
+
* one authoring surface.
|
|
20509
|
+
*
|
|
20510
|
+
* Mirrors {@link NcDeviceMuteStore}: declare the collection, mirror into RAM,
|
|
20511
|
+
* write-through so a failed persist never leaves a phantom sequence the next
|
|
20512
|
+
* boot will not know.
|
|
20513
|
+
*/
|
|
20514
|
+
/**
|
|
20515
|
+
* @durable class=config owner=notification-center
|
|
20516
|
+
* write="an admin creates/edits/deletes a shared sequence via
|
|
20517
|
+
* `notificationRules.createSequence` / `updateSequence` / `deleteSequence`,
|
|
20518
|
+
* or the one-shot boot migration that materialises legacy inline sequences"
|
|
20519
|
+
* retention="none — a row goes only when an admin deletes it. Bounded by the
|
|
20520
|
+
* sequence set a human is willing to author."
|
|
20521
|
+
*/
|
|
20522
|
+
var NC_SEQUENCES_COLLECTION = "notification-center:sequences";
|
|
20523
|
+
var NC_SEQUENCES_COLUMNS = [
|
|
20524
|
+
{
|
|
20525
|
+
name: "id",
|
|
20526
|
+
type: "TEXT",
|
|
20527
|
+
primaryKey: true,
|
|
20528
|
+
notNull: true
|
|
20529
|
+
},
|
|
20530
|
+
{
|
|
20531
|
+
name: "name",
|
|
20532
|
+
type: "TEXT",
|
|
20533
|
+
notNull: true
|
|
20534
|
+
},
|
|
20535
|
+
{
|
|
20536
|
+
name: "minDelaySec",
|
|
20537
|
+
type: "INTEGER"
|
|
20538
|
+
},
|
|
20539
|
+
{
|
|
20540
|
+
name: "actions",
|
|
20541
|
+
type: "JSON",
|
|
20542
|
+
notNull: true
|
|
20543
|
+
},
|
|
20544
|
+
{
|
|
20545
|
+
name: "createdAt",
|
|
20546
|
+
type: "INTEGER",
|
|
20547
|
+
notNull: true
|
|
20548
|
+
},
|
|
20549
|
+
{
|
|
20550
|
+
name: "updatedAt",
|
|
20551
|
+
type: "INTEGER",
|
|
20552
|
+
notNull: true
|
|
20553
|
+
}
|
|
20554
|
+
];
|
|
20555
|
+
function isRecord$7(value) {
|
|
20556
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20557
|
+
}
|
|
20558
|
+
function parseActions(raw) {
|
|
20559
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
20560
|
+
const out = [];
|
|
20561
|
+
for (const entry of raw) {
|
|
20562
|
+
const parsed = require_clip_model_registry.NcRuleActionSchema.safeParse(entry);
|
|
20563
|
+
if (!parsed.success) return null;
|
|
20564
|
+
out.push(parsed.data);
|
|
20565
|
+
}
|
|
20566
|
+
return out;
|
|
20567
|
+
}
|
|
20568
|
+
function rowToSequence(data) {
|
|
20569
|
+
const id = data["id"];
|
|
20570
|
+
const name = data["name"];
|
|
20571
|
+
if (typeof id !== "string" || id.length === 0) return null;
|
|
20572
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
20573
|
+
const actions = parseActions(data["actions"]);
|
|
20574
|
+
if (actions === null) return null;
|
|
20575
|
+
const minDelayRaw = data["minDelaySec"];
|
|
20576
|
+
const minDelaySec = typeof minDelayRaw === "number" && Number.isInteger(minDelayRaw) ? minDelayRaw : void 0;
|
|
20577
|
+
const candidate = {
|
|
20578
|
+
id,
|
|
20579
|
+
name,
|
|
20580
|
+
...minDelaySec !== void 0 ? { minDelaySec } : {},
|
|
20581
|
+
actions: [...actions]
|
|
20582
|
+
};
|
|
20583
|
+
const parsed = require_clip_model_registry.NcSharedSequenceSchema.safeParse(candidate);
|
|
20584
|
+
return parsed.success ? parsed.data : null;
|
|
20585
|
+
}
|
|
20586
|
+
var NcSequenceStore = class {
|
|
20587
|
+
byId = /* @__PURE__ */ new Map();
|
|
20588
|
+
store;
|
|
20589
|
+
logger;
|
|
20590
|
+
now;
|
|
20591
|
+
constructor(deps) {
|
|
20592
|
+
this.store = deps.store;
|
|
20593
|
+
this.logger = deps.logger;
|
|
20594
|
+
this.now = deps.now ?? (() => Date.now());
|
|
20595
|
+
}
|
|
20596
|
+
static async declare(store) {
|
|
20597
|
+
await store.declareCollection.mutate({
|
|
20598
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20599
|
+
columns: [...NC_SEQUENCES_COLUMNS]
|
|
20600
|
+
});
|
|
20601
|
+
}
|
|
20602
|
+
/**
|
|
20603
|
+
* (Re)hydrate. A failed load KEEPS what is already in memory — dropping every
|
|
20604
|
+
* sequence on a transient store error would leave every button and binding
|
|
20605
|
+
* pointing at nothing until the next successful load.
|
|
20606
|
+
*/
|
|
20607
|
+
async load() {
|
|
20608
|
+
try {
|
|
20609
|
+
const rows = await this.store.query.query({
|
|
20610
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20611
|
+
filter: { limit: 1e4 }
|
|
20612
|
+
});
|
|
20613
|
+
const next = /* @__PURE__ */ new Map();
|
|
20614
|
+
let skipped = 0;
|
|
20615
|
+
for (const row of rows) {
|
|
20616
|
+
if (!isRecord$7(row.data)) {
|
|
20617
|
+
skipped += 1;
|
|
20618
|
+
continue;
|
|
20619
|
+
}
|
|
20620
|
+
const sequence = rowToSequence(row.data);
|
|
20621
|
+
if (sequence === null) {
|
|
20622
|
+
skipped += 1;
|
|
20623
|
+
continue;
|
|
20624
|
+
}
|
|
20625
|
+
next.set(sequence.id, sequence);
|
|
20626
|
+
}
|
|
20627
|
+
this.byId.clear();
|
|
20628
|
+
for (const [id, sequence] of next) this.byId.set(id, sequence);
|
|
20629
|
+
if (skipped > 0) this.logger.warn("sequence rows skipped as malformed — bindings to them will miss", { meta: { skipped } });
|
|
20630
|
+
} catch (err) {
|
|
20631
|
+
this.logger.warn("sequence load failed — keeping the sequences already in memory", { meta: {
|
|
20632
|
+
error: String(err),
|
|
20633
|
+
sequences: this.byId.size
|
|
20634
|
+
} });
|
|
20635
|
+
}
|
|
20636
|
+
}
|
|
20637
|
+
list() {
|
|
20638
|
+
return [...this.byId.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
20639
|
+
}
|
|
20640
|
+
get(sequenceId) {
|
|
20641
|
+
return this.byId.get(sequenceId) ?? null;
|
|
20642
|
+
}
|
|
20643
|
+
/** Hot-path existence check for button minting. */
|
|
20644
|
+
has(sequenceId) {
|
|
20645
|
+
return this.byId.has(sequenceId);
|
|
20646
|
+
}
|
|
20647
|
+
knownIds() {
|
|
20648
|
+
return new Set(this.byId.keys());
|
|
20649
|
+
}
|
|
20650
|
+
async create(input) {
|
|
20651
|
+
const now = this.now();
|
|
20652
|
+
const sequence = {
|
|
20653
|
+
id: (0, node_crypto.randomUUID)(),
|
|
20654
|
+
name: input.name,
|
|
20655
|
+
...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
|
|
20656
|
+
actions: [...input.actions]
|
|
20657
|
+
};
|
|
20658
|
+
await this.persist(sequence, now, now);
|
|
20659
|
+
this.byId.set(sequence.id, sequence);
|
|
20660
|
+
return sequence;
|
|
20661
|
+
}
|
|
20662
|
+
/**
|
|
20663
|
+
* Create with a CALLER-chosen id — used by the legacy migration so a rewrite
|
|
20664
|
+
* can name the sequence before rewriting the rule that points at it.
|
|
20665
|
+
*/
|
|
20666
|
+
async createWithId(sequence) {
|
|
20667
|
+
const now = this.now();
|
|
20668
|
+
const stored = {
|
|
20669
|
+
id: sequence.id,
|
|
20670
|
+
name: sequence.name,
|
|
20671
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20672
|
+
actions: [...sequence.actions]
|
|
20673
|
+
};
|
|
20674
|
+
await this.persist(stored, now, now);
|
|
20675
|
+
this.byId.set(stored.id, stored);
|
|
20676
|
+
return stored;
|
|
20677
|
+
}
|
|
20678
|
+
async update(sequenceId, input) {
|
|
20679
|
+
if (this.byId.get(sequenceId) === void 0) throw new Error(`sequence ${sequenceId} does not exist`);
|
|
20680
|
+
const now = this.now();
|
|
20681
|
+
const sequence = {
|
|
20682
|
+
id: sequenceId,
|
|
20683
|
+
name: input.name,
|
|
20684
|
+
...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
|
|
20685
|
+
actions: [...input.actions]
|
|
20686
|
+
};
|
|
20687
|
+
const createdAt = await this.readCreatedAt(sequenceId, now);
|
|
20688
|
+
await this.persist(sequence, createdAt, now);
|
|
20689
|
+
this.byId.set(sequenceId, sequence);
|
|
20690
|
+
return sequence;
|
|
20691
|
+
}
|
|
20692
|
+
async delete(sequenceId) {
|
|
20693
|
+
await this.store.delete.mutate({
|
|
20694
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20695
|
+
key: sequenceId
|
|
20696
|
+
});
|
|
20697
|
+
this.byId.delete(sequenceId);
|
|
20698
|
+
}
|
|
20699
|
+
async readCreatedAt(sequenceId, fallback) {
|
|
20700
|
+
try {
|
|
20701
|
+
const raw = (await this.store.query.query({
|
|
20702
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20703
|
+
filter: {
|
|
20704
|
+
where: { id: sequenceId },
|
|
20705
|
+
limit: 1
|
|
20706
|
+
}
|
|
20707
|
+
}))[0]?.data["createdAt"];
|
|
20708
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : fallback;
|
|
20709
|
+
} catch {
|
|
20710
|
+
return fallback;
|
|
20711
|
+
}
|
|
20712
|
+
}
|
|
20713
|
+
async persist(sequence, createdAt, updatedAt) {
|
|
20714
|
+
await this.store.set.mutate({
|
|
20715
|
+
collection: NC_SEQUENCES_COLLECTION,
|
|
20716
|
+
key: sequence.id,
|
|
20717
|
+
value: {
|
|
20718
|
+
id: sequence.id,
|
|
20719
|
+
name: sequence.name,
|
|
20720
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
20721
|
+
actions: sequence.actions,
|
|
20722
|
+
createdAt,
|
|
20723
|
+
updatedAt
|
|
20724
|
+
}
|
|
20725
|
+
});
|
|
20726
|
+
}
|
|
20727
|
+
};
|
|
20728
|
+
//#endregion
|
|
20089
20729
|
//#region src/notification-center/device-prune.ts
|
|
20090
20730
|
function judgeList(list, isDead) {
|
|
20091
20731
|
const dead = list.filter(isDead);
|
|
@@ -20199,58 +20839,49 @@ function pruneRuleConditions(conditions, isDead, notes) {
|
|
|
20199
20839
|
if (sceneDevice !== void 0 && isDead(sceneDevice)) notes.kept("conditions.scene", [String(sceneDevice)]);
|
|
20200
20840
|
return changed ? next : null;
|
|
20201
20841
|
}
|
|
20202
|
-
/** A
|
|
20203
|
-
|
|
20204
|
-
|
|
20205
|
-
|
|
20206
|
-
|
|
20207
|
-
|
|
20208
|
-
return false;
|
|
20209
|
-
});
|
|
20210
|
-
if (dead.length === 0) return null;
|
|
20211
|
-
const anyDeviceStep = kept.some((step) => step.kind === "cap");
|
|
20212
|
-
const [first, ...rest] = kept;
|
|
20213
|
-
if (!anyDeviceStep || first === void 0) return {
|
|
20214
|
-
next: "removed",
|
|
20215
|
-
dead
|
|
20216
|
-
};
|
|
20217
|
-
return {
|
|
20218
|
-
next: {
|
|
20219
|
-
...sequence,
|
|
20220
|
-
actions: [first, ...rest]
|
|
20221
|
-
},
|
|
20222
|
-
dead
|
|
20842
|
+
/** A Do binding whose device is dead is dropped; sequence refs are left alone
|
|
20843
|
+
* (shared sequences are pruned on their own store, not per-rule). */
|
|
20844
|
+
function pruneHookBindings(bindings, field, isDead, notes) {
|
|
20845
|
+
if (bindings === void 0) return {
|
|
20846
|
+
next: void 0,
|
|
20847
|
+
changed: false
|
|
20223
20848
|
};
|
|
20224
|
-
|
|
20225
|
-
/** The actions after a device prune — `null` when no step moved. */
|
|
20226
|
-
function pruneRuleActions(actions, isDead, notes) {
|
|
20227
|
-
const sequences = actions.onTrigger;
|
|
20228
|
-
if (sequences === void 0) return null;
|
|
20849
|
+
const kept = [];
|
|
20229
20850
|
let changed = false;
|
|
20230
|
-
const
|
|
20231
|
-
|
|
20232
|
-
|
|
20233
|
-
const pruned = pruneSequence(sequence, isDead);
|
|
20234
|
-
if (pruned === null) {
|
|
20235
|
-
nextSequences.push(sequence);
|
|
20851
|
+
for (const binding of bindings) {
|
|
20852
|
+
if (binding.kind === "sequence") {
|
|
20853
|
+
kept.push(binding);
|
|
20236
20854
|
continue;
|
|
20237
20855
|
}
|
|
20238
|
-
|
|
20239
|
-
|
|
20240
|
-
|
|
20241
|
-
|
|
20242
|
-
notes.removed(field, deviceSubjects(pruned.dead), "sequence-removed");
|
|
20243
|
-
} else {
|
|
20244
|
-
nextSequences.push(pruned.next);
|
|
20245
|
-
notes.removed(field, deviceSubjects(pruned.dead));
|
|
20856
|
+
if (binding.action.kind === "cap" && isDead(binding.action.deviceId)) {
|
|
20857
|
+
changed = true;
|
|
20858
|
+
notes.removed(field, deviceSubjects([binding.action.deviceId]), "binding-removed");
|
|
20859
|
+
continue;
|
|
20246
20860
|
}
|
|
20861
|
+
kept.push(binding);
|
|
20247
20862
|
}
|
|
20248
|
-
if (!changed) return
|
|
20249
|
-
|
|
20863
|
+
if (!changed) return {
|
|
20864
|
+
next: bindings,
|
|
20865
|
+
changed: false
|
|
20866
|
+
};
|
|
20250
20867
|
return {
|
|
20251
|
-
|
|
20252
|
-
|
|
20253
|
-
|
|
20868
|
+
next: kept.length > 0 ? kept : void 0,
|
|
20869
|
+
changed: true
|
|
20870
|
+
};
|
|
20871
|
+
}
|
|
20872
|
+
/** The actions after a device prune — `null` when no binding moved. */
|
|
20873
|
+
function pruneRuleActions(actions, isDead, notes) {
|
|
20874
|
+
const onActivation = pruneHookBindings(actions.onActivation, "actions.onActivation", isDead, notes);
|
|
20875
|
+
const onDeactivation = pruneHookBindings(actions.onDeactivation, "actions.onDeactivation", isDead, notes);
|
|
20876
|
+
const onTrigger = pruneHookBindings(actions.onTrigger, "actions.onTrigger", isDead, notes);
|
|
20877
|
+
const onTriggerOff = pruneHookBindings(actions.onTriggerOff, "actions.onTriggerOff", isDead, notes);
|
|
20878
|
+
if (!onActivation.changed && !onDeactivation.changed && !onTrigger.changed && !onTriggerOff.changed) return null;
|
|
20879
|
+
return {
|
|
20880
|
+
...onActivation.next !== void 0 ? { onActivation: [...onActivation.next] } : {},
|
|
20881
|
+
...onDeactivation.next !== void 0 ? { onDeactivation: [...onDeactivation.next] } : {},
|
|
20882
|
+
...onTrigger.next !== void 0 ? { onTrigger: [...onTrigger.next] } : {},
|
|
20883
|
+
...onTriggerOff.next !== void 0 ? { onTriggerOff: [...onTriggerOff.next] } : {},
|
|
20884
|
+
...actions.buttons !== void 0 ? { buttons: actions.buttons } : {}
|
|
20254
20885
|
};
|
|
20255
20886
|
}
|
|
20256
20887
|
function planRuleDevices(rule, isDead, guard) {
|
|
@@ -24485,6 +25116,10 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
|
|
|
24485
25116
|
return {
|
|
24486
25117
|
camera: deviceName,
|
|
24487
25118
|
class: detection?.className ?? (systemEvent !== void 0 ? "" : subject.className),
|
|
25119
|
+
classLabel: (() => {
|
|
25120
|
+
const wire = detection?.className ?? (systemEvent !== void 0 ? "" : subject.className);
|
|
25121
|
+
return wire.length === 0 ? "" : require_clip_model_registry.eventKindLabel(wire, texts.language);
|
|
25122
|
+
})(),
|
|
24488
25123
|
label: detection?.label ?? subject.label ?? "",
|
|
24489
25124
|
zones: zoneLabels.join(", "),
|
|
24490
25125
|
subject: detection?.className !== void 0 ? ncSubjectText(texts, {
|
|
@@ -28626,9 +29261,32 @@ function lastRunRow(data) {
|
|
|
28626
29261
|
}
|
|
28627
29262
|
//#endregion
|
|
28628
29263
|
//#region src/notification-center/rule-actions.ts
|
|
29264
|
+
function wrapDo(action) {
|
|
29265
|
+
const name = action.kind === "wait" ? `wait ${String(action.seconds)}s` : `${action.cap}.${action.method}`;
|
|
29266
|
+
return {
|
|
29267
|
+
id: `do:${name}`,
|
|
29268
|
+
name,
|
|
29269
|
+
enabled: true,
|
|
29270
|
+
actions: [action],
|
|
29271
|
+
throttle: false
|
|
29272
|
+
};
|
|
29273
|
+
}
|
|
29274
|
+
function resolveBinding(binding, lookup) {
|
|
29275
|
+
if (binding.kind === "do") return wrapDo(binding.action);
|
|
29276
|
+
const sequence = lookup(binding.sequenceId);
|
|
29277
|
+
if (sequence === null) return { missing: binding.sequenceId };
|
|
29278
|
+
return {
|
|
29279
|
+
id: sequence.id,
|
|
29280
|
+
name: sequence.name,
|
|
29281
|
+
enabled: true,
|
|
29282
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
29283
|
+
actions: sequence.actions,
|
|
29284
|
+
throttle: true
|
|
29285
|
+
};
|
|
29286
|
+
}
|
|
28629
29287
|
var NcRuleActionRunner = class {
|
|
28630
29288
|
deps;
|
|
28631
|
-
/** Last run per
|
|
29289
|
+
/** Last run per `sequenceId` — the throttle's memory (shared across rules). */
|
|
28632
29290
|
lastRunAt = /* @__PURE__ */ new Map();
|
|
28633
29291
|
now;
|
|
28634
29292
|
sleep;
|
|
@@ -28638,11 +29296,49 @@ var NcRuleActionRunner = class {
|
|
|
28638
29296
|
this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
28639
29297
|
}
|
|
28640
29298
|
/**
|
|
28641
|
-
*
|
|
29299
|
+
* Resolve bindings via the sequence store and run each, in order.
|
|
28642
29300
|
*
|
|
28643
29301
|
* Never throws: an actuation that fails must not cost the NOTIFICATION about
|
|
28644
|
-
* the very same event.
|
|
28645
|
-
|
|
29302
|
+
* the very same event.
|
|
29303
|
+
*/
|
|
29304
|
+
async runBindings(input) {
|
|
29305
|
+
const sequences = [];
|
|
29306
|
+
const missing = [];
|
|
29307
|
+
for (const binding of input.bindings) {
|
|
29308
|
+
const resolved = resolveBinding(binding, input.lookup);
|
|
29309
|
+
if ("missing" in resolved) {
|
|
29310
|
+
this.deps.logger.warn("rule action binding skipped — sequence missing", {
|
|
29311
|
+
tags: { deviceId: input.deviceId },
|
|
29312
|
+
meta: {
|
|
29313
|
+
ruleId: input.ruleId,
|
|
29314
|
+
sequenceId: resolved.missing
|
|
29315
|
+
}
|
|
29316
|
+
});
|
|
29317
|
+
missing.push({
|
|
29318
|
+
id: resolved.missing,
|
|
29319
|
+
name: resolved.missing,
|
|
29320
|
+
ran: false,
|
|
29321
|
+
stepsRun: 0,
|
|
29322
|
+
skippedReason: "missing"
|
|
29323
|
+
});
|
|
29324
|
+
continue;
|
|
29325
|
+
}
|
|
29326
|
+
sequences.push(resolved);
|
|
29327
|
+
}
|
|
29328
|
+
const ran = await this.run({
|
|
29329
|
+
ruleId: input.ruleId,
|
|
29330
|
+
ruleName: input.ruleName,
|
|
29331
|
+
deviceId: input.deviceId,
|
|
29332
|
+
sequences,
|
|
29333
|
+
...input.origin !== void 0 ? { origin: input.origin } : {}
|
|
29334
|
+
});
|
|
29335
|
+
return [...missing, ...ran];
|
|
29336
|
+
}
|
|
29337
|
+
/**
|
|
29338
|
+
* Run every enabled sequence, in order.
|
|
29339
|
+
*
|
|
29340
|
+
* Prefer {@link runBindings} at call sites that hold bindings. This remains
|
|
29341
|
+
* for button grants (force-enabled shared sequence) and tests.
|
|
28646
29342
|
*/
|
|
28647
29343
|
async run(input) {
|
|
28648
29344
|
const out = [];
|
|
@@ -28651,38 +29347,41 @@ var NcRuleActionRunner = class {
|
|
|
28651
29347
|
}
|
|
28652
29348
|
async runOne(input, sequence) {
|
|
28653
29349
|
if (!sequence.enabled) return {
|
|
29350
|
+
id: sequence.id,
|
|
28654
29351
|
name: sequence.name,
|
|
28655
29352
|
ran: false,
|
|
28656
29353
|
stepsRun: 0,
|
|
28657
29354
|
skippedReason: "disabled"
|
|
28658
29355
|
};
|
|
28659
29356
|
if (sequence.actions.length === 0) return {
|
|
29357
|
+
id: sequence.id,
|
|
28660
29358
|
name: sequence.name,
|
|
28661
29359
|
ran: false,
|
|
28662
29360
|
stepsRun: 0,
|
|
28663
29361
|
skippedReason: "empty"
|
|
28664
29362
|
};
|
|
28665
|
-
const key = `${input.ruleId}:${sequence.name}`;
|
|
28666
29363
|
const now = this.now();
|
|
28667
|
-
const minDelayMs = (sequence.minDelaySec ?? 0) * 1e3;
|
|
28668
|
-
const last = this.lastRunAt.get(
|
|
29364
|
+
const minDelayMs = sequence.throttle ? (sequence.minDelaySec ?? 0) * 1e3 : 0;
|
|
29365
|
+
const last = this.lastRunAt.get(sequence.id);
|
|
28669
29366
|
if (minDelayMs > 0 && last !== void 0 && now - last < minDelayMs) {
|
|
28670
29367
|
this.deps.logger.debug("rule action sequence throttled", {
|
|
28671
29368
|
tags: { deviceId: input.deviceId },
|
|
28672
29369
|
meta: {
|
|
28673
29370
|
ruleId: input.ruleId,
|
|
29371
|
+
sequenceId: sequence.id,
|
|
28674
29372
|
sequence: sequence.name,
|
|
28675
29373
|
sinceLastMs: now - last
|
|
28676
29374
|
}
|
|
28677
29375
|
});
|
|
28678
29376
|
return {
|
|
29377
|
+
id: sequence.id,
|
|
28679
29378
|
name: sequence.name,
|
|
28680
29379
|
ran: false,
|
|
28681
29380
|
stepsRun: 0,
|
|
28682
29381
|
skippedReason: "throttled"
|
|
28683
29382
|
};
|
|
28684
29383
|
}
|
|
28685
|
-
this.lastRunAt.set(
|
|
29384
|
+
if (sequence.throttle) this.lastRunAt.set(sequence.id, now);
|
|
28686
29385
|
let stepsRun = 0;
|
|
28687
29386
|
for (const [index, action] of sequence.actions.entries()) try {
|
|
28688
29387
|
await this.step(action, input.origin);
|
|
@@ -28693,6 +29392,7 @@ var NcRuleActionRunner = class {
|
|
|
28693
29392
|
meta: {
|
|
28694
29393
|
ruleId: input.ruleId,
|
|
28695
29394
|
rule: input.ruleName,
|
|
29395
|
+
sequenceId: sequence.id,
|
|
28696
29396
|
sequence: sequence.name,
|
|
28697
29397
|
step: index,
|
|
28698
29398
|
action: action.kind === "cap" ? `${action.cap}.${action.method}` : "wait",
|
|
@@ -28700,6 +29400,7 @@ var NcRuleActionRunner = class {
|
|
|
28700
29400
|
}
|
|
28701
29401
|
});
|
|
28702
29402
|
return {
|
|
29403
|
+
id: sequence.id,
|
|
28703
29404
|
name: sequence.name,
|
|
28704
29405
|
ran: true,
|
|
28705
29406
|
stepsRun,
|
|
@@ -28711,11 +29412,13 @@ var NcRuleActionRunner = class {
|
|
|
28711
29412
|
meta: {
|
|
28712
29413
|
ruleId: input.ruleId,
|
|
28713
29414
|
rule: input.ruleName,
|
|
29415
|
+
sequenceId: sequence.id,
|
|
28714
29416
|
sequence: sequence.name,
|
|
28715
29417
|
steps: stepsRun
|
|
28716
29418
|
}
|
|
28717
29419
|
});
|
|
28718
29420
|
return {
|
|
29421
|
+
id: sequence.id,
|
|
28719
29422
|
name: sequence.name,
|
|
28720
29423
|
ran: true,
|
|
28721
29424
|
stepsRun
|
|
@@ -28736,6 +29439,17 @@ var NcRuleActionRunner = class {
|
|
|
28736
29439
|
});
|
|
28737
29440
|
}
|
|
28738
29441
|
};
|
|
29442
|
+
/** Force-enable a shared sequence for a button tap (always runs, still throttled). */
|
|
29443
|
+
function asForcedSequence(sequence) {
|
|
29444
|
+
return {
|
|
29445
|
+
id: sequence.id,
|
|
29446
|
+
name: sequence.name,
|
|
29447
|
+
enabled: true,
|
|
29448
|
+
...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
|
|
29449
|
+
actions: sequence.actions,
|
|
29450
|
+
throttle: true
|
|
29451
|
+
};
|
|
29452
|
+
}
|
|
28739
29453
|
//#endregion
|
|
28740
29454
|
//#region src/notification-center/rule-ai-analyst.ts
|
|
28741
29455
|
/**
|
|
@@ -30582,7 +31296,7 @@ function snoozeVerdict(input, deps) {
|
|
|
30582
31296
|
});
|
|
30583
31297
|
if (active === null) return { kind: "clear" };
|
|
30584
31298
|
const rule = deps.ruleFor(input.ruleId);
|
|
30585
|
-
if (rule !== null && isSnoozeExempt(rule)) return {
|
|
31299
|
+
if (rule !== null && isSnoozeExempt(rule, deps.sequenceActionsOf)) return {
|
|
30586
31300
|
kind: "exempt",
|
|
30587
31301
|
snooze: active
|
|
30588
31302
|
};
|
|
@@ -36694,6 +37408,8 @@ var NotificationCenter = class {
|
|
|
36694
37408
|
snoozes;
|
|
36695
37409
|
/** The permanent per-camera notification off switch — see `device-mute-store.ts`. */
|
|
36696
37410
|
deviceMutes;
|
|
37411
|
+
/** Shared action sequences rules and buttons bind to by id. */
|
|
37412
|
+
sequences;
|
|
36697
37413
|
/** The durable episode rows behind the sustained-state alerts. */
|
|
36698
37414
|
alertStore;
|
|
36699
37415
|
/** Battery band → notification, and the reminder sweep. */
|
|
@@ -36932,7 +37648,8 @@ var NotificationCenter = class {
|
|
|
36932
37648
|
const drafts = [];
|
|
36933
37649
|
const sequenceButtons = this.actionRunner === null || stored === null ? [] : buildActionButtons({
|
|
36934
37650
|
actions: stored.actions,
|
|
36935
|
-
|
|
37651
|
+
knownSequenceIds: this.sequences.knownIds(),
|
|
37652
|
+
mintUrl: ({ sequenceId, actionId }) => {
|
|
36936
37653
|
const id = (0, node_crypto.randomUUID)();
|
|
36937
37654
|
drafts.push({
|
|
36938
37655
|
id,
|
|
@@ -36942,7 +37659,7 @@ var NotificationCenter = class {
|
|
|
36942
37659
|
id,
|
|
36943
37660
|
ruleId: input.ruleId,
|
|
36944
37661
|
ruleName: input.ruleName,
|
|
36945
|
-
|
|
37662
|
+
sequenceId,
|
|
36946
37663
|
deviceId: input.deviceId,
|
|
36947
37664
|
actionId,
|
|
36948
37665
|
targetId: input.targetId,
|
|
@@ -36954,6 +37671,7 @@ var NotificationCenter = class {
|
|
|
36954
37671
|
});
|
|
36955
37672
|
const snoozeButtons = input.ownerUserId === void 0 || stored === null ? [] : buildSnoozeButtons({
|
|
36956
37673
|
rule: stored,
|
|
37674
|
+
sequenceActionsOf: this.sequenceActionsOf,
|
|
36957
37675
|
label: (minutes) => this.snoozeButtonLabel(minutes),
|
|
36958
37676
|
mintUrl: ({ actionId, minutes }) => {
|
|
36959
37677
|
const id = (0, node_crypto.randomUUID)();
|
|
@@ -37194,7 +37912,7 @@ var NotificationCenter = class {
|
|
|
37194
37912
|
*/
|
|
37195
37913
|
async applyGrantedSnooze(grant) {
|
|
37196
37914
|
const rule = this.rules.get(grant.ruleId);
|
|
37197
|
-
if (rule !== null && isSnoozeExempt(rule)) throw new Error(`"${grant.ruleName}" is an alarm rule and cannot be snoozed`);
|
|
37915
|
+
if (rule !== null && isSnoozeExempt(rule, this.sequenceActionsOf)) throw new Error(`"${grant.ruleName}" is an alarm rule and cannot be snoozed`);
|
|
37198
37916
|
const snooze = await this.snoozes.create({
|
|
37199
37917
|
userId: grant.userId,
|
|
37200
37918
|
scope: "device",
|
|
@@ -37493,16 +38211,13 @@ var NotificationCenter = class {
|
|
|
37493
38211
|
if (this.actionRunner === null) throw new Error("no actuator is wired on this node");
|
|
37494
38212
|
const rule = this.rules.get(grant.ruleId);
|
|
37495
38213
|
if (rule === null) throw new Error(`rule ${grant.ruleId} no longer exists`);
|
|
37496
|
-
const sequence =
|
|
37497
|
-
if (sequence ===
|
|
38214
|
+
const sequence = this.sequences.get(grant.sequenceId);
|
|
38215
|
+
if (sequence === null) throw new Error(`sequence "${grant.sequenceId}" no longer exists`);
|
|
37498
38216
|
const [outcome] = await this.actionRunner.run({
|
|
37499
38217
|
ruleId: rule.id,
|
|
37500
38218
|
ruleName: rule.name,
|
|
37501
38219
|
deviceId: grant.deviceId,
|
|
37502
|
-
sequences: [
|
|
37503
|
-
...sequence,
|
|
37504
|
-
enabled: true
|
|
37505
|
-
}]
|
|
38220
|
+
sequences: [asForcedSequence(sequence)]
|
|
37506
38221
|
});
|
|
37507
38222
|
if (outcome === void 0) throw new Error("the sequence did not run");
|
|
37508
38223
|
if (outcome.failedAt !== void 0) throw new Error(`step ${String(outcome.failedAt)} of "${sequence.name}" failed`);
|
|
@@ -37621,6 +38336,11 @@ var NotificationCenter = class {
|
|
|
37621
38336
|
logger: this.logger.child("device-mute"),
|
|
37622
38337
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
37623
38338
|
});
|
|
38339
|
+
this.sequences = new NcSequenceStore({
|
|
38340
|
+
store: deps.store,
|
|
38341
|
+
logger: this.logger.child("sequences"),
|
|
38342
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
38343
|
+
});
|
|
37624
38344
|
this.alertStore = new NcAlertStore({
|
|
37625
38345
|
store: deps.store,
|
|
37626
38346
|
logger: this.logger.child("device-alerts")
|
|
@@ -37887,6 +38607,7 @@ var NotificationCenter = class {
|
|
|
37887
38607
|
await NcStateSummaryService.declare(store);
|
|
37888
38608
|
await NcSnoozeStore.declare(store);
|
|
37889
38609
|
await NcDeviceMuteStore.declare(store);
|
|
38610
|
+
await NcSequenceStore.declare(store);
|
|
37890
38611
|
await TimelapseStore.declare(store);
|
|
37891
38612
|
await NcSummaryStore.declare(store);
|
|
37892
38613
|
await NcCalendarEventsStore.declare(store);
|
|
@@ -37948,6 +38669,8 @@ var NotificationCenter = class {
|
|
|
37948
38669
|
sinceStartMs: now - t0
|
|
37949
38670
|
} });
|
|
37950
38671
|
};
|
|
38672
|
+
await timed("sequences", () => this.sequences.load());
|
|
38673
|
+
await timed("migrateLegacyActions", () => this.migrateLegacyRuleActions());
|
|
37951
38674
|
await Promise.all([
|
|
37952
38675
|
timed("texts", () => this.reloadTexts()),
|
|
37953
38676
|
timed("rules", () => this.rules.load()),
|
|
@@ -39247,7 +39970,32 @@ var NotificationCenter = class {
|
|
|
39247
39970
|
return { success: true };
|
|
39248
39971
|
},
|
|
39249
39972
|
setRuleEnabled: async ({ ruleId, enabled }) => {
|
|
39250
|
-
await this.rules.setEnabled(ruleId, enabled);
|
|
39973
|
+
const updated = await this.rules.setEnabled(ruleId, enabled);
|
|
39974
|
+
const bindings = enabled ? updated.actions?.onActivation : updated.actions?.onDeactivation;
|
|
39975
|
+
if (bindings !== void 0 && bindings.length > 0 && this.actionRunner !== null) this.actionRunner.runBindings({
|
|
39976
|
+
ruleId: updated.id,
|
|
39977
|
+
ruleName: updated.name,
|
|
39978
|
+
deviceId: this.deviceIdForRuleActions(updated),
|
|
39979
|
+
bindings,
|
|
39980
|
+
lookup: (id) => this.sequences.get(id)
|
|
39981
|
+
});
|
|
39982
|
+
return { success: true };
|
|
39983
|
+
},
|
|
39984
|
+
listSequences: async () => ({ sequences: [...this.sequences.list()] }),
|
|
39985
|
+
getSequence: async ({ sequenceId }) => ({ sequence: this.sequences.get(sequenceId) }),
|
|
39986
|
+
createSequence: async ({ sequence }) => {
|
|
39987
|
+
const created = await this.sequences.create(sequence);
|
|
39988
|
+
this.logger.info("notification sequence created", { meta: { sequenceId: created.id } });
|
|
39989
|
+
return { sequence: created };
|
|
39990
|
+
},
|
|
39991
|
+
updateSequence: async ({ sequenceId, sequence }) => {
|
|
39992
|
+
const updated = await this.sequences.update(sequenceId, sequence);
|
|
39993
|
+
this.logger.info("notification sequence updated", { meta: { sequenceId } });
|
|
39994
|
+
return { sequence: updated };
|
|
39995
|
+
},
|
|
39996
|
+
deleteSequence: async ({ sequenceId }) => {
|
|
39997
|
+
await this.sequences.delete(sequenceId);
|
|
39998
|
+
this.logger.info("notification sequence deleted", { meta: { sequenceId } });
|
|
39251
39999
|
return { success: true };
|
|
39252
40000
|
},
|
|
39253
40001
|
testRule: async ({ rule, lookbackMinutes }) => ({ results: [...await this.dryRun(rule, lookbackMinutes)] }),
|
|
@@ -39304,7 +40052,18 @@ var NotificationCenter = class {
|
|
|
39304
40052
|
},
|
|
39305
40053
|
getAlarmConfig: async () => this.alarmConfig(),
|
|
39306
40054
|
getAlarmSensors: async (input) => this.alarmSensors(input),
|
|
39307
|
-
setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch)
|
|
40055
|
+
setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch),
|
|
40056
|
+
createAlarmProfile: async ({ name, cloneFromId }) => this.mutateAlarmProfile((panel) => panel.createProfile({
|
|
40057
|
+
name,
|
|
40058
|
+
newId: (0, node_crypto.randomUUID)(),
|
|
40059
|
+
source: cloneFromId === void 0 ? { kind: "defaults" } : {
|
|
40060
|
+
kind: "clone",
|
|
40061
|
+
fromId: cloneFromId
|
|
40062
|
+
}
|
|
40063
|
+
})),
|
|
40064
|
+
renameAlarmProfile: async ({ id, name }) => this.mutateAlarmProfile((panel) => panel.renameProfile(id, name)),
|
|
40065
|
+
deleteAlarmProfile: async ({ id }) => this.mutateAlarmProfile((panel) => panel.deleteProfile(id)),
|
|
40066
|
+
activateAlarmProfile: async ({ id }) => this.mutateAlarmProfile((panel) => panel.activateProfile(id))
|
|
39308
40067
|
};
|
|
39309
40068
|
}
|
|
39310
40069
|
/**
|
|
@@ -39597,6 +40356,13 @@ var NotificationCenter = class {
|
|
|
39597
40356
|
});
|
|
39598
40357
|
return this.alarmConfig();
|
|
39599
40358
|
}
|
|
40359
|
+
/** Profile CRUD — each mutation returns the whole config (settings applied). */
|
|
40360
|
+
async mutateAlarmProfile(run) {
|
|
40361
|
+
const panel = this.alarmPanel;
|
|
40362
|
+
if (panel === null) throw new Error("this node has no alarm panel");
|
|
40363
|
+
await run(panel);
|
|
40364
|
+
return this.alarmConfig();
|
|
40365
|
+
}
|
|
39600
40366
|
/**
|
|
39601
40367
|
* The panel as it is NOW — see `alarm-live.ts`.
|
|
39602
40368
|
*
|
|
@@ -39693,6 +40459,7 @@ var NotificationCenter = class {
|
|
|
39693
40459
|
return {
|
|
39694
40460
|
deviceId: panel.deviceId,
|
|
39695
40461
|
settings: panel.settings(),
|
|
40462
|
+
profiles: panel.profilesView(),
|
|
39696
40463
|
live: await this.alarmLive(panel),
|
|
39697
40464
|
coverage: this.coverageFor(panel).map((c) => ({
|
|
39698
40465
|
...c,
|
|
@@ -40725,7 +41492,7 @@ var NotificationCenter = class {
|
|
|
40725
41492
|
} : {}
|
|
40726
41493
|
}
|
|
40727
41494
|
});
|
|
40728
|
-
const unresolvable = unresolvableButtons(rule.actions);
|
|
41495
|
+
const unresolvable = unresolvableButtons(rule.actions, this.sequences.knownIds());
|
|
40729
41496
|
if (unresolvable.length > 0) this.logger.warn("rule buttons reference sequences that do not exist — not sent", {
|
|
40730
41497
|
tags: { deviceId: subject.deviceId },
|
|
40731
41498
|
meta: {
|
|
@@ -40852,20 +41619,85 @@ var NotificationCenter = class {
|
|
|
40852
41619
|
return this.groupStore.list(query);
|
|
40853
41620
|
}
|
|
40854
41621
|
/**
|
|
40855
|
-
*
|
|
41622
|
+
* Materialise legacy inline `onTrigger` sequences into the shared store and
|
|
41623
|
+
* rewrite each rule's actions to bindings + `buttons.sequenceId`. Runs on RAW
|
|
41624
|
+
* rows before {@link NcRuleStore.load} so Zod never skips them.
|
|
41625
|
+
*/
|
|
41626
|
+
async migrateLegacyRuleActions() {
|
|
41627
|
+
let rows;
|
|
41628
|
+
try {
|
|
41629
|
+
rows = await this.deps.store.query.query({
|
|
41630
|
+
collection: NC_RULES_COLLECTION,
|
|
41631
|
+
filter: { limit: 1e4 }
|
|
41632
|
+
});
|
|
41633
|
+
} catch (err) {
|
|
41634
|
+
this.logger.warn("legacy actions migration skipped — could not read rules", { meta: { error: String(err) } });
|
|
41635
|
+
return;
|
|
41636
|
+
}
|
|
41637
|
+
let migrated = 0;
|
|
41638
|
+
for (const row of rows) {
|
|
41639
|
+
const ruleBlob = row.data["rule"];
|
|
41640
|
+
if (typeof ruleBlob !== "object" || ruleBlob === null || Array.isArray(ruleBlob)) continue;
|
|
41641
|
+
const rule = { ...ruleBlob };
|
|
41642
|
+
const ruleId = typeof rule["id"] === "string" ? rule["id"] : void 0;
|
|
41643
|
+
if (ruleId === void 0) continue;
|
|
41644
|
+
const result = await migrateLegacyRuleActions(rule["actions"], async (input) => {
|
|
41645
|
+
return { id: (await this.sequences.create(input)).id };
|
|
41646
|
+
});
|
|
41647
|
+
if (!result.migrated) continue;
|
|
41648
|
+
if (result.actions !== void 0) rule["actions"] = result.actions;
|
|
41649
|
+
else delete rule["actions"];
|
|
41650
|
+
const { id: _rowId, ...columns } = row.data;
|
|
41651
|
+
await this.deps.store.set.mutate({
|
|
41652
|
+
collection: NC_RULES_COLLECTION,
|
|
41653
|
+
key: ruleId,
|
|
41654
|
+
value: {
|
|
41655
|
+
...columns,
|
|
41656
|
+
rule
|
|
41657
|
+
}
|
|
41658
|
+
});
|
|
41659
|
+
migrated += 1;
|
|
41660
|
+
}
|
|
41661
|
+
if (migrated > 0) this.logger.info("migrated legacy inline rule actions into shared sequences", { meta: {
|
|
41662
|
+
rules: migrated,
|
|
41663
|
+
sequences: this.sequences.list().length
|
|
41664
|
+
} });
|
|
41665
|
+
}
|
|
41666
|
+
/**
|
|
41667
|
+
* True when this match is a clear / leave edge — the subject for
|
|
41668
|
+
* `onTriggerOff`. Raise / enter / ordinary matches use `onTrigger`.
|
|
41669
|
+
*/
|
|
41670
|
+
isTriggerOffSubject(subject) {
|
|
41671
|
+
if (subject.sensorRaised === false) return true;
|
|
41672
|
+
if (subject.occupancy !== void 0 && subject.occupancy.occupied === false) return true;
|
|
41673
|
+
return false;
|
|
41674
|
+
}
|
|
41675
|
+
/** Device id for activation/deactivation log tags — first scoped device, else 0. */
|
|
41676
|
+
deviceIdForRuleActions(rule) {
|
|
41677
|
+
const devices = rule.conditions.devices;
|
|
41678
|
+
if (devices !== void 0 && devices.length > 0) {
|
|
41679
|
+
const first = devices[0];
|
|
41680
|
+
if (first !== void 0) return first;
|
|
41681
|
+
}
|
|
41682
|
+
return 0;
|
|
41683
|
+
}
|
|
41684
|
+
sequenceActionsOf = (sequenceId) => this.sequences.get(sequenceId)?.actions;
|
|
41685
|
+
/**
|
|
41686
|
+
* Run this rule's onTrigger / onTriggerOff bindings for this match.
|
|
40856
41687
|
*
|
|
40857
41688
|
* Called BEFORE the cooldown gate — see the call site for why. Everything
|
|
40858
41689
|
* here is fire-and-forget and nothing may throw into the evaluation.
|
|
40859
41690
|
*/
|
|
40860
41691
|
runRuleActions(rule, subject, kind, controls) {
|
|
40861
|
-
const sequences = rule.actions?.onTrigger;
|
|
40862
|
-
if (sequences === void 0 || sequences.length === 0) return;
|
|
40863
41692
|
if (this.actionRunner === null || controls.runActions === false) return;
|
|
40864
|
-
this.
|
|
41693
|
+
const bindings = this.isTriggerOffSubject(subject) ? rule.actions?.onTriggerOff : rule.actions?.onTrigger;
|
|
41694
|
+
if (bindings === void 0 || bindings.length === 0) return;
|
|
41695
|
+
this.actionRunner.runBindings({
|
|
40865
41696
|
ruleId: rule.id,
|
|
40866
41697
|
ruleName: rule.name,
|
|
40867
41698
|
deviceId: subject.deviceId,
|
|
40868
|
-
|
|
41699
|
+
bindings,
|
|
41700
|
+
lookup: (id) => this.sequences.get(id),
|
|
40869
41701
|
origin: fireRefFor(rule, subject, kind)
|
|
40870
41702
|
});
|
|
40871
41703
|
}
|
|
@@ -41769,7 +42601,8 @@ var NotificationCenter = class {
|
|
|
41769
42601
|
snoozeDecision(input) {
|
|
41770
42602
|
const verdict = snoozeVerdict(input, {
|
|
41771
42603
|
activeFor: (query) => this.snoozes.activeFor(query),
|
|
41772
|
-
ruleFor: (ruleId) => this.rules.get(ruleId)
|
|
42604
|
+
ruleFor: (ruleId) => this.rules.get(ruleId),
|
|
42605
|
+
sequenceActionsOf: this.sequenceActionsOf
|
|
41773
42606
|
});
|
|
41774
42607
|
if (verdict.kind === "exempt") this.logger.info("snooze does not apply — alarm rules are never silenced", {
|
|
41775
42608
|
tags: { deviceId: input.deviceId },
|