@camstack/addon-post-analysis 1.2.274 → 1.2.275
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/{dist-B9V4PRZO.js → dist-Br0p61E2.js} +1047 -24
- package/dist/{dist-DDko6srJ.mjs → dist-D3Yub96m.mjs} +1006 -25
- 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-CNrdmO1k.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Byz9Hzo0.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B7oKeWXX.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-D-4roee-.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BZetaopl.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BDwkPDdw.mjs → hostInit-Dza3GRpt.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +337 -50
- package/dist/pipeline-analytics/index.mjs +337 -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-kX2Fkx3d.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as SCENE_DIVERGED, $t as hydrateSchema, A as NcConditionDescriptorSchema, At as occupancyScope, B as NcSystemEventKindSchema, Bt as storageOccupancyCapability, C as MediaFileKindEnum, Ct as isKnownTemplateVar, Dt as kebabToCamel, E as NC_DEFAULT_SNOOZE_MINUTES, Et as isSourceCap, F as NcRuleUpdateInputSchema, Ft as readDeviceStateFrom, G as OpsLogEntrySchema, Gt as zoneAnalyticsCapability, H as NcTemplatePreviewInputSchema, Ht as systemEventFilterApplies, I as NcScheduleSchema, It as readTimelapseGeneratedAt, J as RECORDING_SOURCE_CAMSTACK_ADDON, Jt as CamProfileSchema, Kt as errMsg$1, L as NcSnoozeInputSchema, Lt as resolveLocationMode, M as NcRulePatchSchema, Mt as pickClusterStepModels, N as NcRuleSchema, Nt as pipelineAnalyticsCapability, O as NC_TAXONOMY, Ot as mayWriteToLocation, P as NcRuleTargetSchema, Pt as plateGalleryCapability, Q as SCENE_DEFAULT_UNCOVERED_POLICY, Qt as defineCustomActions, R as NcSnoozeSchema, S as MOTION_CLOSE_AFTER_MS, St as isEventOwnerType, T as NC_CONDITION_CATALOG, Tt as isScheduleActive, U as NcTemplatePreviewSchema, Ut as templateVarsFor, V as NcTaxonomySchema, Vt as subKindsOf, W as NcTemplateVarDescriptorSchema, Wt as vectorDimFromBase64, Xt as createEvent, Y as RetrainStatusSchema, Yt as DeviceType, Z as SCENE_DEFAULT_ANCHOR_THRESHOLD, Zt as customAction, _ as FailureCounters, _t as evictionPolicyOfLocation, a as CLUSTER_MODEL_SCOPED_STEPS, an as boolean, at as TrackSourceSchema, b as MAX_BIRTH_DECISION_RECORDS, cn as number, ct as assertTimelapseCadences, dn as record, dt as buildEventKindDescriptor, en as isDeviceScopedCap, et as SceneMonitorSchema, f as DeclaredDevices, fn as string, ft as cosineSimilarity$1, g as FULL_IMAGE_BBOX, gt as evaluateSensorEdge, h as FIRST_LEVEL_MACRO_CLASSES, ht as encodeVectorBase64, i as BirthDecisionRecordSchema, in as array, it as TimelapseRuleSchema, j as NcRuleInputSchema, k as NC_TEMPLATE_VARS, kt as notificationRulesCapability, ln as object, lt as audioMetricsCapability, m as EVENT_OWNER_TYPES, mn as EventCategory, n as ArchivedDebugNoteSchema, nn as sleep$1, nt as TimelapseRuleInputSchema, o as COCO_TO_MACRO, on as discriminatedUnion, ot as addonWidgetsSourceCapability, p as EVENT_KIND_BY_CAP, pn as unknown, pt as deriveRecordingMode, q as RECORDING_EXPORT_MAX_READ_BYTES, qt as BaseAddon, r as BaseDevice, rn as _enum, rt as TimelapseRulePatchSchema, s as DEFAULT_EVENT_COLOR, sn as literal, st as alarmPanelCapability, t as AUDIO_MACRO_LABELS, tn as nodePin, tt as TIMELAPSE_DENSE_FLOOR_SEC, u as DETECTION_MACRO_CLASSES, un as partialRecord, ut as audioModeOf, v as LabelAttributionSchema, vt as faceGalleryCapability, w as NC_ALARM_SYSTEM_EVENT_KINDS, wt as isMediaOwnerType, xt as isDetectionMacroClass, y as MAX_ARCHIVED_DEBUG_NOTES, yt as failureContributionCapability, z as NcSnoozeSuppressedSchema, zt as sceneMonitorCapability } from "../dist-D3Yub96m.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -1454,7 +1454,8 @@ var NcAlarmPanelDevice = class extends BaseDevice {
|
|
|
1454
1454
|
return {
|
|
1455
1455
|
...this.readDelays(),
|
|
1456
1456
|
announceArm: this.config.get("announceArm"),
|
|
1457
|
-
announceTargets: [...this.config.get("announceTargets")]
|
|
1457
|
+
announceTargets: [...this.config.get("announceTargets")],
|
|
1458
|
+
nonBlocking: []
|
|
1458
1459
|
};
|
|
1459
1460
|
}
|
|
1460
1461
|
/**
|
|
@@ -10815,6 +10816,23 @@ function requireCapCaller(method, caller) {
|
|
|
10815
10816
|
return caller;
|
|
10816
10817
|
}
|
|
10817
10818
|
//#endregion
|
|
10819
|
+
//#region src/notification-center/clock-text.ts
|
|
10820
|
+
/**
|
|
10821
|
+
* `{{time}}` — hours and minutes in the NOTIFICATION language.
|
|
10822
|
+
*
|
|
10823
|
+
* It used to be `toLocaleTimeString()` in the server's own locale, so an
|
|
10824
|
+
* Italian hub running an English container sent "8:05:13 PM" inside an
|
|
10825
|
+
* Italian sentence. The time zone is the host's unless a caller (a spec)
|
|
10826
|
+
* pins one.
|
|
10827
|
+
*/
|
|
10828
|
+
function ncClockText(language, atMs, timeZone) {
|
|
10829
|
+
return new Intl.DateTimeFormat(language, {
|
|
10830
|
+
hour: "2-digit",
|
|
10831
|
+
minute: "2-digit",
|
|
10832
|
+
...timeZone !== void 0 ? { timeZone } : {}
|
|
10833
|
+
}).format(new Date(atMs));
|
|
10834
|
+
}
|
|
10835
|
+
//#endregion
|
|
10818
10836
|
//#region src/notification-center/system-event-tap-url.ts
|
|
10819
10837
|
/** The admin UI's own routes, as `packages/addon-admin-ui/src/App.tsx` declares them. */
|
|
10820
10838
|
var ADMIN_ADDONS_PATH = "/system/addons";
|
|
@@ -12783,8 +12801,7 @@ var NcDispatcher = class {
|
|
|
12783
12801
|
async buildNotification(entry, target) {
|
|
12784
12802
|
const subject = entry.payload.subject;
|
|
12785
12803
|
const systemEvent = subject.systemEvent;
|
|
12786
|
-
const
|
|
12787
|
-
const deviceName = systemEvent !== void 0 && systemEvent.deviceId === void 0 ? "system" : await this.deps.getDeviceName(displayDeviceId).catch(() => null) ?? `camera ${displayDeviceId}`;
|
|
12804
|
+
const deviceName = await notificationDeviceName(entry, this.deps.getDeviceName);
|
|
12788
12805
|
const alarmSource = systemEvent?.alarmSource;
|
|
12789
12806
|
const zoneLabels = systemEvent === void 0 ? await resolveZoneLabels(this.deps.getZoneNames, subject.deviceId, subject.zones) : alarmSource?.zones !== void 0 && alarmSource.zones.length > 0 ? await resolveZoneLabels(this.deps.getZoneNames, alarmSource.deviceId, alarmSource.zones) : [];
|
|
12790
12807
|
const vars = buildTemplateVars(this.texts, entry, deviceName, zoneLabels);
|
|
@@ -13538,17 +13555,25 @@ async function resolveZoneLabels(getZoneNames, deviceId, zoneIds) {
|
|
|
13538
13555
|
function percentOf(confidence) {
|
|
13539
13556
|
return confidence !== void 0 ? `${Math.round(confidence * 100)}%` : "";
|
|
13540
13557
|
}
|
|
13558
|
+
/** What `{{camera}}` renders on a system row that names no device. */
|
|
13559
|
+
var NC_DEVICELESS_CAMERA_NAME = "system";
|
|
13541
13560
|
/**
|
|
13542
|
-
* The
|
|
13543
|
-
*
|
|
13561
|
+
* The name `{{camera}}` renders for a row — the ONE derivation, shared by the
|
|
13562
|
+
* dispatcher and the template-variable parity spec.
|
|
13544
13563
|
*
|
|
13545
|
-
*
|
|
13546
|
-
*
|
|
13547
|
-
*
|
|
13548
|
-
*
|
|
13549
|
-
*
|
|
13550
|
-
* outrank the live vocabulary for exactly those names.
|
|
13564
|
+
* A system row that names no device (a node, a backup, an update) renders the
|
|
13565
|
+
* filler {@link NC_DEVICELESS_CAMERA_NAME}: there is no camera to name, which
|
|
13566
|
+
* is why the catalog offers `{{camera}}` on a system rule only for the kinds
|
|
13567
|
+
* that carry a device (template-vars.ts). Everything else asks the directory,
|
|
13568
|
+
* and falls back to `camera <id>` when it cannot answer.
|
|
13551
13569
|
*/
|
|
13570
|
+
async function notificationDeviceName(entry, getDeviceName) {
|
|
13571
|
+
const subject = entry.payload.subject;
|
|
13572
|
+
const systemEvent = subject.systemEvent;
|
|
13573
|
+
if (systemEvent !== void 0 && systemEvent.deviceId === void 0) return NC_DEVICELESS_CAMERA_NAME;
|
|
13574
|
+
const displayDeviceId = systemEvent?.deviceId ?? subject.deviceId;
|
|
13575
|
+
return await getDeviceName(displayDeviceId).catch(() => null) ?? `camera ${displayDeviceId}`;
|
|
13576
|
+
}
|
|
13552
13577
|
/** Class composition of a group, busiest classes in first-seen order. */
|
|
13553
13578
|
function classCounts(members) {
|
|
13554
13579
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -13558,7 +13583,18 @@ function classCounts(members) {
|
|
|
13558
13583
|
count
|
|
13559
13584
|
}));
|
|
13560
13585
|
}
|
|
13561
|
-
|
|
13586
|
+
/**
|
|
13587
|
+
* The variable map ONE render pass sees: scalars, then the composites built
|
|
13588
|
+
* from them, then the params a system-event row froze at intake.
|
|
13589
|
+
*
|
|
13590
|
+
* The three groups are layered in that order deliberately. A composite
|
|
13591
|
+
* (`{{subject}}`, `{{inZones}}`, `{{op}}`) is a finished phrase and must not be
|
|
13592
|
+
* visible to the fragments it was built from — that is the depth bound, and it
|
|
13593
|
+
* is enforced by `text-compose.ts` never being handed this map. The frozen
|
|
13594
|
+
* params go last because they are the row's own truth about a system event and
|
|
13595
|
+
* outrank the live vocabulary for exactly those names.
|
|
13596
|
+
*/
|
|
13597
|
+
function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
|
|
13562
13598
|
const subject = entry.payload.subject;
|
|
13563
13599
|
const occupancy = subject.occupancy;
|
|
13564
13600
|
const systemEvent = subject.systemEvent;
|
|
@@ -13566,7 +13602,7 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels) {
|
|
|
13566
13602
|
const group = entry.payload.group;
|
|
13567
13603
|
return {
|
|
13568
13604
|
camera: deviceName,
|
|
13569
|
-
class: detection?.className ?? subject.className,
|
|
13605
|
+
class: detection?.className ?? (systemEvent !== void 0 ? "" : subject.className),
|
|
13570
13606
|
label: detection?.label ?? subject.label ?? "",
|
|
13571
13607
|
zones: zoneLabels.join(", "),
|
|
13572
13608
|
subject: detection?.className !== void 0 ? ncSubjectText(texts, {
|
|
@@ -13579,10 +13615,14 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels) {
|
|
|
13579
13615
|
}),
|
|
13580
13616
|
detectionSummary: group !== void 0 && group.members.length > 1 ? ncDetectionSummary(texts, classCounts(group.members)) : "",
|
|
13581
13617
|
inZones: ncInZonesText(texts, zoneLabels),
|
|
13582
|
-
scope: occupancy
|
|
13618
|
+
scope: occupancy !== void 0 ? occupancy.zone ?? deviceName : "",
|
|
13583
13619
|
zone: occupancy?.zone ?? zoneLabels[0] ?? "",
|
|
13584
13620
|
confidence: percentOf(detection?.confidence ?? subject.confidence),
|
|
13585
|
-
time:
|
|
13621
|
+
time: ncClockText(texts.language, subject.timestamp, timeZone),
|
|
13622
|
+
sounds: subject.audio?.labels.join(", ") ?? "",
|
|
13623
|
+
hitPercent: subject.audio?.hitPercent !== void 0 ? `${Math.round(subject.audio.hitPercent)}` : "",
|
|
13624
|
+
db: subject.audio?.peakDbfs !== void 0 ? `${Math.round(subject.audio.peakDbfs)}` : "",
|
|
13625
|
+
eventType: subject.eventType ?? "",
|
|
13586
13626
|
rule: entry.payload.ruleName,
|
|
13587
13627
|
count: occupancy !== void 0 ? `${occupancy.count}` : group !== void 0 && group.members.length > 1 ? `${group.members.length}` : "",
|
|
13588
13628
|
capacity: occupancy !== void 0 ? `${occupancy.capacity}` : "",
|
|
@@ -17413,12 +17453,22 @@ var NcRuleStore = class {
|
|
|
17413
17453
|
await this.ledger.put(rule);
|
|
17414
17454
|
return rule;
|
|
17415
17455
|
}
|
|
17416
|
-
/**
|
|
17417
|
-
|
|
17456
|
+
/**
|
|
17457
|
+
* Apply a partial patch. Immutable: returns the NEW rule object.
|
|
17458
|
+
*
|
|
17459
|
+
* `clear` names optional fields to REMOVE before the merge — an omitted key
|
|
17460
|
+
* keeps its stored value, so this is how an editor empties one. Setting and
|
|
17461
|
+
* clearing the same key is a contradiction and is refused rather than
|
|
17462
|
+
* resolved by ordering.
|
|
17463
|
+
*/
|
|
17464
|
+
async update(ruleId, patch, clear = []) {
|
|
17418
17465
|
const existing = this.ledger.get(ruleId);
|
|
17419
17466
|
if (!existing) throw new Error(`notification rule not found: ${ruleId}`);
|
|
17467
|
+
const conflict = clear.find((key) => patch[key] !== void 0);
|
|
17468
|
+
if (conflict !== void 0) throw new Error(`notification rule patch both sets and clears '${conflict}'`);
|
|
17469
|
+
const cleared = new Set(clear);
|
|
17420
17470
|
const candidate = {
|
|
17421
|
-
...existing,
|
|
17471
|
+
...Object.fromEntries(Object.entries(existing).filter(([key]) => !cleared.has(key))),
|
|
17422
17472
|
...patch,
|
|
17423
17473
|
id: existing.id,
|
|
17424
17474
|
createdBy: existing.createdBy,
|
|
@@ -19326,13 +19376,18 @@ function clockOf$2(atMs) {
|
|
|
19326
19376
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
19327
19377
|
}
|
|
19328
19378
|
/** Template vocabulary. Deliberately the SAME names the detection and timelapse
|
|
19329
|
-
* bodies use (`rule`, `from`, `to`, `time`) plus the digest-only ones.
|
|
19330
|
-
|
|
19379
|
+
* bodies use (`rule`, `from`, `to`, `time`) plus the digest-only ones.
|
|
19380
|
+
*
|
|
19381
|
+
* Exported as `summaryTemplateVars` — not just an implementation detail of
|
|
19382
|
+
* {@link buildSummaryOutboxInputs} — so the parity spec
|
|
19383
|
+
* (`template-var-parity.spec.ts`) can assert this map directly against the
|
|
19384
|
+
* catalog, the same way `buildTemplateVars` is asserted for rule rows. */
|
|
19385
|
+
function summaryTemplateVars(input) {
|
|
19331
19386
|
return {
|
|
19332
19387
|
rule: input.ruleName,
|
|
19333
19388
|
from: clockOf$2(input.window.startMs),
|
|
19334
19389
|
to: clockOf$2(input.window.endMs),
|
|
19335
|
-
time:
|
|
19390
|
+
time: ncClockText(input.texts.language, input.generatedAt),
|
|
19336
19391
|
events: String(input.tileCount),
|
|
19337
19392
|
cameras: String(input.cameraCount),
|
|
19338
19393
|
matched: String(input.matchedCount),
|
|
@@ -19439,7 +19494,7 @@ function summaryCaptionText(input) {
|
|
|
19439
19494
|
* @returns rows ready for `NcOutbox.enqueue` — never sent from here.
|
|
19440
19495
|
*/
|
|
19441
19496
|
function buildSummaryOutboxInputs(input) {
|
|
19442
|
-
const vars =
|
|
19497
|
+
const vars = summaryTemplateVars(input);
|
|
19443
19498
|
const optedOut = new Set(input.disabledTargetIds ?? []);
|
|
19444
19499
|
const title = renderTemplate(input.template?.title, vars) ?? input.texts.text({
|
|
19445
19500
|
key: "summary.title",
|
|
@@ -21663,6 +21718,67 @@ async function deriveSyntheticMedia(jpeg, bbox, timestamp) {
|
|
|
21663
21718
|
return files;
|
|
21664
21719
|
}
|
|
21665
21720
|
//#endregion
|
|
21721
|
+
//#region src/notification-center/template-preview.ts
|
|
21722
|
+
/**
|
|
21723
|
+
* `previewTemplate` — render a rule's draft title/body against EXAMPLE values,
|
|
21724
|
+
* so an operator sees `{{scope}}: {{count}}/{{capacity}}` become
|
|
21725
|
+
* `Cancello: 3/2` before saving the rule, not after the first real event.
|
|
21726
|
+
*
|
|
21727
|
+
* Pure with respect to the caller: it never sends anything, and the only
|
|
21728
|
+
* side-channel it reads is the live `NcTextCatalog` handed in, so a preview
|
|
21729
|
+
* always speaks the hub's CURRENT language and CURRENT overrides — the same
|
|
21730
|
+
* words a delivered notification would use.
|
|
21731
|
+
*/
|
|
21732
|
+
/**
|
|
21733
|
+
* The instant `{{time}}` is previewed at — fixed, and in UTC, so the example
|
|
21734
|
+
* is the same on every host: "20:05" in Italian, "08:05 PM" in English.
|
|
21735
|
+
*/
|
|
21736
|
+
var PREVIEW_INSTANT_MS = Date.UTC(2026, 8, 24, 20, 5);
|
|
21737
|
+
var PREVIEW_TIME_ZONE = "UTC";
|
|
21738
|
+
/**
|
|
21739
|
+
* The example map: every declared example, with the COMPOSITES rendered by the
|
|
21740
|
+
* real composers so the preview speaks the hub's language exactly as a
|
|
21741
|
+
* delivered notification would.
|
|
21742
|
+
*/
|
|
21743
|
+
function exampleVars(texts) {
|
|
21744
|
+
return {
|
|
21745
|
+
...Object.fromEntries(NC_TEMPLATE_VARS.filter((d) => d.dynamic === void 0).map((d) => [d.name, d.example])),
|
|
21746
|
+
subject: ncSubjectText(texts, {
|
|
21747
|
+
className: "person",
|
|
21748
|
+
label: "Mario",
|
|
21749
|
+
labelKind: "identity"
|
|
21750
|
+
}),
|
|
21751
|
+
inZones: ncInZonesText(texts, ["Cancello"]),
|
|
21752
|
+
op: ncOccupancyOp(texts, true),
|
|
21753
|
+
time: ncClockText(texts.language, PREVIEW_INSTANT_MS, PREVIEW_TIME_ZONE),
|
|
21754
|
+
detectionSummary: ncDetectionSummary(texts, [{
|
|
21755
|
+
className: "person",
|
|
21756
|
+
count: 2
|
|
21757
|
+
}, {
|
|
21758
|
+
className: "car",
|
|
21759
|
+
count: 1
|
|
21760
|
+
}])
|
|
21761
|
+
};
|
|
21762
|
+
}
|
|
21763
|
+
var DYNAMIC_EXAMPLE = "3";
|
|
21764
|
+
function previewTemplate(texts, input) {
|
|
21765
|
+
const ctx = input.context;
|
|
21766
|
+
const allowed = new Set(templateVarsFor(ctx).map((d) => d.name));
|
|
21767
|
+
const examples = exampleVars(texts);
|
|
21768
|
+
const used = [input.template.title ?? "", input.template.body ?? ""].flatMap((t) => [...templatePlaceholders(t)]);
|
|
21769
|
+
const unknown = [...new Set(used.filter((name) => !isKnownTemplateVar(name, ctx)))];
|
|
21770
|
+
const vars = {};
|
|
21771
|
+
for (const name of used) {
|
|
21772
|
+
if (!isKnownTemplateVar(name, ctx)) continue;
|
|
21773
|
+
vars[name] = allowed.has(name) ? examples[name] ?? "" : DYNAMIC_EXAMPLE;
|
|
21774
|
+
}
|
|
21775
|
+
return {
|
|
21776
|
+
title: renderTemplate(input.template.title, vars),
|
|
21777
|
+
body: renderTemplate(input.template.body, vars),
|
|
21778
|
+
unknown
|
|
21779
|
+
};
|
|
21780
|
+
}
|
|
21781
|
+
//#endregion
|
|
21666
21782
|
//#region src/notification-center/text-catalog-view.ts
|
|
21667
21783
|
/**
|
|
21668
21784
|
* The text catalog, projected for an EDITOR — Phase 2's read model, pure.
|
|
@@ -22102,14 +22218,19 @@ function clockOf(atMs) {
|
|
|
22102
22218
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
22103
22219
|
}
|
|
22104
22220
|
/** Template vocabulary. Deliberately the same NAMES the rule templates use
|
|
22105
|
-
* (`camera`, `rule`, `time`) plus the timelapse-only ones.
|
|
22106
|
-
|
|
22221
|
+
* (`camera`, `rule`, `time`) plus the timelapse-only ones.
|
|
22222
|
+
*
|
|
22223
|
+
* Exported as `timelapseTemplateVars` — not just an implementation detail of
|
|
22224
|
+
* {@link buildTimelapseOutboxInputs} — so the parity spec
|
|
22225
|
+
* (`template-var-parity.spec.ts`) can assert this map directly against the
|
|
22226
|
+
* catalog, the same way `buildTemplateVars` is asserted for rule rows. */
|
|
22227
|
+
function timelapseTemplateVars(input) {
|
|
22107
22228
|
return {
|
|
22108
22229
|
camera: input.deviceName ?? `camera ${input.deviceId}`,
|
|
22109
22230
|
rule: input.ruleName,
|
|
22110
22231
|
from: clockOf(input.window.startMs),
|
|
22111
22232
|
to: clockOf(input.window.endMs),
|
|
22112
|
-
time:
|
|
22233
|
+
time: ncClockText(input.texts.language, input.generatedAt),
|
|
22113
22234
|
coverage: String(input.coverage.percent),
|
|
22114
22235
|
events: String(input.denseRanges),
|
|
22115
22236
|
...detectionTemplateVars(input.texts, input.detections ?? NO_DETECTIONS)
|
|
@@ -22190,7 +22311,7 @@ function timelapseRecordId(deviceId, windowEndMs) {
|
|
|
22190
22311
|
* @returns rows ready for `NcOutbox.enqueue` — never sent from here.
|
|
22191
22312
|
*/
|
|
22192
22313
|
function buildTimelapseOutboxInputs(input) {
|
|
22193
|
-
const vars =
|
|
22314
|
+
const vars = timelapseTemplateVars(input);
|
|
22194
22315
|
const optedOut = new Set(input.disabledTargetIds ?? []);
|
|
22195
22316
|
const title = renderTemplate(input.template?.title, vars) ?? input.texts.text({
|
|
22196
22317
|
key: "timelapse.title",
|
|
@@ -23173,7 +23294,8 @@ function consumablesSliceOf(event) {
|
|
|
23173
23294
|
var NC_ALARM_SETTINGS_FALLBACK = {
|
|
23174
23295
|
...DEFAULT_DELAYS,
|
|
23175
23296
|
announceArm: false,
|
|
23176
|
-
announceTargets: []
|
|
23297
|
+
announceTargets: [],
|
|
23298
|
+
nonBlocking: []
|
|
23177
23299
|
};
|
|
23178
23300
|
/**
|
|
23179
23301
|
* How often the "matched NO rule" report may fire per device. Long enough that
|
|
@@ -24423,6 +24545,10 @@ var NotificationCenter = class NotificationCenter {
|
|
|
24423
24545
|
get textCatalogEditor() {
|
|
24424
24546
|
return this.textEditor;
|
|
24425
24547
|
}
|
|
24548
|
+
/** The live catalog every producer renders with — the preview must use the same one. */
|
|
24549
|
+
get textCatalog() {
|
|
24550
|
+
return this.texts;
|
|
24551
|
+
}
|
|
24426
24552
|
/**
|
|
24427
24553
|
* Produce the last closed window for one camera of one timelapse rule, now.
|
|
24428
24554
|
*
|
|
@@ -25285,17 +25411,19 @@ var NotificationCenter = class NotificationCenter {
|
|
|
25285
25411
|
} });
|
|
25286
25412
|
return { rule: created };
|
|
25287
25413
|
},
|
|
25288
|
-
updateRule: async ({ ruleId, patch, caller }) => {
|
|
25414
|
+
updateRule: async ({ ruleId, patch, clear, caller }) => {
|
|
25289
25415
|
if (patch.targets !== void 0) await this.validateTargetRefs(patch.targets.map((t) => t.targetId));
|
|
25290
|
-
|
|
25416
|
+
const clearsUsers = clear?.includes("targetUsers") ?? false;
|
|
25417
|
+
if (patch.targets !== void 0 || patch.targetUsers !== void 0 || clearsUsers) {
|
|
25291
25418
|
const existing = this.rules.get(ruleId);
|
|
25292
|
-
NotificationCenter.assertHasAddressee(patch.targets ?? existing?.targets ?? [], patch.targetUsers ?? existing?.targetUsers);
|
|
25419
|
+
NotificationCenter.assertHasAddressee(patch.targets ?? existing?.targets ?? [], clearsUsers ? void 0 : patch.targetUsers ?? existing?.targetUsers);
|
|
25293
25420
|
}
|
|
25294
25421
|
const { disabledTargetIds: _optOut, ...safePatch } = patch;
|
|
25295
|
-
const updated = await this.rules.update(ruleId, safePatch);
|
|
25422
|
+
const updated = await this.rules.update(ruleId, safePatch, clear ?? []);
|
|
25296
25423
|
this.logger.info("notification rule updated", { meta: {
|
|
25297
25424
|
ruleId,
|
|
25298
|
-
by: caller.userId
|
|
25425
|
+
by: caller.userId,
|
|
25426
|
+
...clear && clear.length > 0 ? { cleared: clear } : {}
|
|
25299
25427
|
} });
|
|
25300
25428
|
return { rule: updated };
|
|
25301
25429
|
},
|
|
@@ -25312,6 +25440,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
25312
25440
|
catalog: [...NC_CONDITION_CATALOG],
|
|
25313
25441
|
taxonomy: NC_TAXONOMY
|
|
25314
25442
|
}),
|
|
25443
|
+
getTemplateCatalog: async () => ({ vars: [...NC_TEMPLATE_VARS] }),
|
|
25444
|
+
previewTemplate: async (input) => previewTemplate(this.texts, input),
|
|
25315
25445
|
getHistory: async ({ filter }) => {
|
|
25316
25446
|
const entries = await this.outbox.queryHistory({
|
|
25317
25447
|
...filter.ruleId !== void 0 ? { ruleId: filter.ruleId } : {},
|
|
@@ -26499,6 +26629,12 @@ var NotificationCenter = class NotificationCenter {
|
|
|
26499
26629
|
...subject.observedAt !== void 0 ? { observedAt: subject.observedAt } : {},
|
|
26500
26630
|
...subject.occupancy !== void 0 ? { occupancy: frozenOccupancy(rule, subject.occupancy) } : {},
|
|
26501
26631
|
...subject.packagePhase !== void 0 ? { packagePhase: subject.packagePhase } : {},
|
|
26632
|
+
...subject.audioWindow !== void 0 ? { audio: {
|
|
26633
|
+
labels: subject.audioWindow.labels,
|
|
26634
|
+
...subject.audioWindow.mode === "level" ? { hitPercent: subject.audioWindow.hitPercent } : {},
|
|
26635
|
+
...subject.audioWindow.peakDbfs !== void 0 ? { peakDbfs: subject.audioWindow.peakDbfs } : {}
|
|
26636
|
+
} } : {},
|
|
26637
|
+
...subject.eventType !== void 0 ? { eventType: subject.eventType } : {},
|
|
26502
26638
|
...subject.systemEvent !== void 0 ? { systemEvent: subject.systemEvent } : {}
|
|
26503
26639
|
}
|
|
26504
26640
|
};
|
|
@@ -27687,14 +27823,13 @@ var ncActions = defineCustomActions({
|
|
|
27687
27823
|
catalog: array(NcConditionDescriptorSchema),
|
|
27688
27824
|
taxonomy: NcTaxonomySchema
|
|
27689
27825
|
})),
|
|
27826
|
+
"nc.getTemplateCatalog": customAction(object({}), object({ vars: array(NcTemplateVarDescriptorSchema) })),
|
|
27827
|
+
"nc.previewTemplate": customAction(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, { kind: "mutation" }),
|
|
27690
27828
|
"nc.createRule": customAction(object({ rule: NcRuleInputSchema }), object({ rule: NcRuleSchema }), {
|
|
27691
27829
|
kind: "mutation",
|
|
27692
27830
|
caller: "required"
|
|
27693
27831
|
}),
|
|
27694
|
-
"nc.updateRule": customAction(object({
|
|
27695
|
-
ruleId: string(),
|
|
27696
|
-
patch: NcRulePatchSchema
|
|
27697
|
-
}), object({ rule: NcRuleSchema }), {
|
|
27832
|
+
"nc.updateRule": customAction(NcRuleUpdateInputSchema, object({ rule: NcRuleSchema }), {
|
|
27698
27833
|
kind: "mutation",
|
|
27699
27834
|
caller: "required"
|
|
27700
27835
|
}),
|
|
@@ -27713,14 +27848,17 @@ var ncActions = defineCustomActions({
|
|
|
27713
27848
|
/**
|
|
27714
27849
|
* THE AI SECTION of the ordinary rules — read and write.
|
|
27715
27850
|
*
|
|
27716
|
-
* A pair of its own rather than two more keys on
|
|
27717
|
-
*
|
|
27718
|
-
*
|
|
27719
|
-
*
|
|
27720
|
-
*
|
|
27721
|
-
*
|
|
27722
|
-
* (`
|
|
27723
|
-
* `
|
|
27851
|
+
* A pair of its own rather than two more keys on the rule CRUD, and that is
|
|
27852
|
+
* the whole point of the design. The addon builds self-contained and INLINES
|
|
27853
|
+
* `@camstack/types`, so its own copy of `NcRuleInputSchema` is never the
|
|
27854
|
+
* problem and nothing here fails at load. The coupling is the HUB: the cap
|
|
27855
|
+
* route (`notificationRules.createRule` / `updateRule`, which the admin
|
|
27856
|
+
* editor uses) validates input against the copy of `@camstack/types` that
|
|
27857
|
+
* `@camstack/system` inlines (`bundleTypesMainEntry`), so a new key on
|
|
27858
|
+
* `NcRuleInputSchema` is SILENTLY STRIPPED there until a `@camstack/server`
|
|
27859
|
+
* release carrying it is installed. These two validate only here, against a
|
|
27860
|
+
* schema that lives in the addon (`rule-ai.ts`), so a field added to it is
|
|
27861
|
+
* live on the next `camstack deploy packages/addon-post-analysis`.
|
|
27724
27862
|
*
|
|
27725
27863
|
* Scoped exactly as the rule CRUD is: reading needs a rule the caller can
|
|
27726
27864
|
* SEE, writing needs one they own (admins bypass ownership, never the caller
|
|
@@ -28212,6 +28350,12 @@ function makeNcActionHandlers(deps) {
|
|
|
28212
28350
|
catalog: [...NC_CONDITION_CATALOG],
|
|
28213
28351
|
taxonomy: NC_TAXONOMY
|
|
28214
28352
|
}),
|
|
28353
|
+
"nc.getTemplateCatalog": async () => ({ vars: [...NC_TEMPLATE_VARS] }),
|
|
28354
|
+
"nc.previewTemplate": async (input) => {
|
|
28355
|
+
const texts = deps.previewTexts;
|
|
28356
|
+
if (texts === void 0) throw new Error("nc.previewTemplate: text catalog not wired");
|
|
28357
|
+
return previewTemplate(texts, input);
|
|
28358
|
+
},
|
|
28215
28359
|
"nc.createRule": async (input, caller) => {
|
|
28216
28360
|
const c = requireCaller$1(caller);
|
|
28217
28361
|
const parsed = NcRuleInputSchema.parse(input.rule);
|
|
@@ -28232,10 +28376,11 @@ function makeNcActionHandlers(deps) {
|
|
|
28232
28376
|
const patch = NcRulePatchSchema.parse(input.patch);
|
|
28233
28377
|
if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
|
|
28234
28378
|
const { ownerUserId: _owner, disabledTargetIds: _optOut, ...safe } = patch;
|
|
28235
|
-
const rule = await deps.ruleStore.update(input.ruleId, safe);
|
|
28379
|
+
const rule = await deps.ruleStore.update(input.ruleId, safe, input.clear ?? []);
|
|
28236
28380
|
deps.logger.info("nc rule updated", { meta: {
|
|
28237
28381
|
ruleId: rule.id,
|
|
28238
|
-
owner: c.userId
|
|
28382
|
+
owner: c.userId,
|
|
28383
|
+
...input.clear && input.clear.length > 0 ? { cleared: input.clear } : {}
|
|
28239
28384
|
} });
|
|
28240
28385
|
return { rule };
|
|
28241
28386
|
},
|
|
@@ -71662,9 +71807,111 @@ async function projectSensorMarkers(deps, data, timestamp) {
|
|
|
71662
71807
|
}
|
|
71663
71808
|
return landed;
|
|
71664
71809
|
}
|
|
71810
|
+
//#endregion
|
|
71811
|
+
//#region src/pipeline-analytics/services/sensor-transition-gate.ts
|
|
71812
|
+
/**
|
|
71813
|
+
* The gate between "a `DeviceStateChanged` arrived for a mapped sensor cap"
|
|
71814
|
+
* and "the sensor actually CHANGED".
|
|
71815
|
+
*
|
|
71816
|
+
* ## What this fixes
|
|
71817
|
+
*
|
|
71818
|
+
* `handleSensorStateChanged` treated the ARRIVAL of the bus event as the
|
|
71819
|
+
* assertion, and never looked at the slice. It cannot: the kernel mirror
|
|
71820
|
+
* (`device-state-mirror.ts` `applySingleCapUpdate`) diffs the whole SLICE, and
|
|
71821
|
+
* a contact slice carries `lastChangedAt` beside `entryOpen`. Anything that
|
|
71822
|
+
* moves the timestamp without moving the boolean is a `DeviceStateChanged`,
|
|
71823
|
+
* and downstream it became a `SensorEvent` row and a synthetic `contact` track
|
|
71824
|
+
* on the linked camera's timeline.
|
|
71825
|
+
*
|
|
71826
|
+
* Measured on camera 615 / contact sensor 4131, 2026-08-29→30: **30 of 35**
|
|
71827
|
+
* contact events were one of two non-events.
|
|
71828
|
+
*
|
|
71829
|
+
* - The RECOVERY family. The Zigbee entity flaps `off → unavailable → off`
|
|
71830
|
+
* (03:22:37→03:23:28, 03:56:51→03:58:19, 04:40:05→04:45:45). The HA
|
|
71831
|
+
* provider drops the `unavailable` push and writes the recovery, whose
|
|
71832
|
+
* `entryOpen` is still `false` and whose `lastChangedAt` is HA's fresh
|
|
71833
|
+
* `last_changed`. The mirror diffs, and emits.
|
|
71834
|
+
* - The COLD-SLICE family. `HaBinarySensorDevice`'s constructor writes
|
|
71835
|
+
* `{ entryOpen: false, lastChangedAt: 0 }` on every device rebuild; the
|
|
71836
|
+
* real state lands seconds later with a `lastChangedAt` hours old. Two
|
|
71837
|
+
* emissions, no transition.
|
|
71838
|
+
*
|
|
71839
|
+
* ## Why the predicate is imported and not written here
|
|
71840
|
+
*
|
|
71841
|
+
* `evaluateSensorEdge` (`@camstack/types`) is already THE edge predicate: the
|
|
71842
|
+
* virtual doorbell feeds it (`builtins/doorbell/trigger-engine.ts`) and so does
|
|
71843
|
+
* the recorder's sensor trigger (`addon-pipeline` `recorder/sensor-trigger.ts`).
|
|
71844
|
+
* Post-analysis was the third consumer of the same bus event and the only one
|
|
71845
|
+
* that did not. A second predicate here would diverge on the first cap added —
|
|
71846
|
+
* the exact failure [D62](../../../../../docs/decisions/adr-0062.md) names.
|
|
71847
|
+
*
|
|
71848
|
+
* ## Where it deliberately differs from the other two
|
|
71849
|
+
*
|
|
71850
|
+
* The doorbell and the recorder want the RISING edge only. A timeline marker
|
|
71851
|
+
* is not a doorbell: a door CLOSING is an event an operator wants to see, and
|
|
71852
|
+
* the taxonomy has one `contact` kind for both directions. So this gate accepts
|
|
71853
|
+
* `edge: 'rising'` **and** the `falling-edge` verdict, and rejects only the
|
|
71854
|
+
* verdicts that mean "nothing moved" — which is precisely the 30.
|
|
71855
|
+
*
|
|
71856
|
+
* ## Caps with no boolean level pass through untouched
|
|
71857
|
+
*
|
|
71858
|
+
* `EVENT_KIND_BY_CAP` covers sixteen caps; `SOURCE_CAP_ACTIVE_FIELD` covers the
|
|
71859
|
+
* ten that have a boolean whose edge means something. `doorbell`, `button`,
|
|
71860
|
+
* `lock-control`, `presence`, `enum-sensor` and `event-emitter` are NOT levels
|
|
71861
|
+
* — their ARRIVAL is the event, and three presses in a row are three markers.
|
|
71862
|
+
* Gating them would re-ship the failure of 2026-08-08, when camera 615's press
|
|
71863
|
+
* produced a row, a ring, a matched rule and no marker.
|
|
71864
|
+
*
|
|
71865
|
+
* ## A cover is a level whose level is a WORD
|
|
71866
|
+
*
|
|
71867
|
+
* `cover` is the sixteenth cap, and it is neither of the above. Its level is
|
|
71868
|
+
* the `state` word (`open`, `opening`, `closing`, `closed`, `stopped`), and its
|
|
71869
|
+
* slice also carries `position`, which ticks all the way through a travel and
|
|
71870
|
+
* is republished unchanged by providers that poll. Passed through, every tick
|
|
71871
|
+
* and every republish would write a sensor-event row per linked camera and an
|
|
71872
|
+
* NC evaluation — so it transitions on the WORD only (`WORD_LEVEL_FIELD`):
|
|
71873
|
+
* `closed → opening → open` is two events, a position tick or a same-word
|
|
71874
|
+
* republish is a `no-change` drop.
|
|
71875
|
+
*
|
|
71876
|
+
* Its FIRST sighting follows the booleans' rule (`evaluateSensorEdge`), with
|
|
71877
|
+
* `closed` as the inactive word: `closed` seeds the baseline and is dropped
|
|
71878
|
+
* (`baseline-seeded-inactive`); any other word is an EVENT when the slice's
|
|
71879
|
+
* `lastChangedAt` clears the same floor, `max(startedAt, now - freshness)`,
|
|
71880
|
+
* and is otherwise seeded and dropped (`baseline-seeded-stale-active`). It has
|
|
71881
|
+
* to emit: after a runner respawn the memory is empty and the next slice is
|
|
71882
|
+
* usually the next REAL change, and on a two-word cover (Velux, most HA
|
|
71883
|
+
* covers: `closed`/`open` only) seeding it would lose the whole opening.
|
|
71884
|
+
*
|
|
71885
|
+
* Residual edges, both erring towards silence on the ambiguous case like the
|
|
71886
|
+
* booleans do: a cover that was already open and whose `lastChangedAt` is
|
|
71887
|
+
* older than the floor is not reported (a respawn cannot tell it from a
|
|
71888
|
+
* hydration); and a first sighting whose `lastChangedAt` is fresh only
|
|
71889
|
+
* because a POSITION tick moved it (the slice timestamp is not the word's)
|
|
71890
|
+
* is reported once as if the word had just changed. The alarm needs covers
|
|
71891
|
+
* to be eventful to see one open (D630).
|
|
71892
|
+
*
|
|
71893
|
+
* ## Nothing here logs, and no drop is silent
|
|
71894
|
+
*
|
|
71895
|
+
* The gate is pure towards the world (no bus, no logger, no clock of its own —
|
|
71896
|
+
* the caller brings `now`), like the recorder's router. But it sits on a path
|
|
71897
|
+
* that carries every camera's native `motion` slice at motion rate — ~0.6/s
|
|
71898
|
+
* fleet-wide — so a line per drop would be its own outage. It accumulates
|
|
71899
|
+
* instead, per `(deviceId, capName, reason)`, and the caller drains one summary
|
|
71900
|
+
* per window. A `debug` at 3.3/s that nobody read is how a 100 %-rejection
|
|
71901
|
+
* regime survived three months here.
|
|
71902
|
+
*/
|
|
71903
|
+
/**
|
|
71904
|
+
* Caps whose level is a string word rather than a boolean: cap → slice field.
|
|
71905
|
+
* See "A cover is a level whose level is a WORD" above.
|
|
71906
|
+
*/
|
|
71907
|
+
var WORD_LEVEL_FIELD = { cover: "state" };
|
|
71908
|
+
/** The word a word-level cap rests in — its `false`, for the first-sighting rule. */
|
|
71909
|
+
var WORD_LEVEL_INACTIVE = { cover: "closed" };
|
|
71665
71910
|
var SensorTransitionGate = class {
|
|
71666
71911
|
/** Last observed boolean, keyed by `${deviceId}:${capName}`. */
|
|
71667
71912
|
lastValues = /* @__PURE__ */ new Map();
|
|
71913
|
+
/** Last observed word of a word-level cap, keyed by `${deviceId}:${capName}`. */
|
|
71914
|
+
lastWords = /* @__PURE__ */ new Map();
|
|
71668
71915
|
/** Drops since the window opened, keyed by `${deviceId}:${capName}:${reason}`. */
|
|
71669
71916
|
drops = /* @__PURE__ */ new Map();
|
|
71670
71917
|
windowStartedAt;
|
|
@@ -71682,10 +71929,13 @@ var SensorTransitionGate = class {
|
|
|
71682
71929
|
/**
|
|
71683
71930
|
* Feed one `DeviceStateChanged` slice for a cap `EVENT_KIND_BY_CAP` maps.
|
|
71684
71931
|
*
|
|
71685
|
-
* A cap with no
|
|
71686
|
-
*
|
|
71932
|
+
* A cap with no level passes through unconditionally. A cap that HAS one
|
|
71933
|
+
* passes only when its level moved — a boolean in either direction, or a
|
|
71934
|
+
* word-level cap's word changing.
|
|
71687
71935
|
*/
|
|
71688
71936
|
observe(deviceId, capName, slice) {
|
|
71937
|
+
const wordField = WORD_LEVEL_FIELD[capName];
|
|
71938
|
+
if (wordField !== void 0) return this.observeWord(deviceId, capName, slice, wordField);
|
|
71689
71939
|
if (!isSourceCap(capName)) return { changed: true };
|
|
71690
71940
|
const key = `${deviceId}:${capName}`;
|
|
71691
71941
|
const verdict = evaluateSensorEdge({
|
|
@@ -71723,6 +71973,40 @@ var SensorTransitionGate = class {
|
|
|
71723
71973
|
this.drops.clear();
|
|
71724
71974
|
return out;
|
|
71725
71975
|
}
|
|
71976
|
+
/**
|
|
71977
|
+
* A word-level cap: a transition is the word changing. The memory is
|
|
71978
|
+
* written only when the slice CARRIED a word, for the same reason as the
|
|
71979
|
+
* booleans — a malformed slice must not become a baseline.
|
|
71980
|
+
*/
|
|
71981
|
+
observeWord(deviceId, capName, slice, wordField) {
|
|
71982
|
+
const raw = slice[wordField];
|
|
71983
|
+
if (typeof raw !== "string" || raw.length === 0) return this.drop(deviceId, capName, "non-word-value");
|
|
71984
|
+
const key = `${deviceId}:${capName}`;
|
|
71985
|
+
const prior = this.lastWords.get(key);
|
|
71986
|
+
this.lastWords.set(key, raw);
|
|
71987
|
+
if (prior === void 0) return this.firstWordSighting(deviceId, capName, slice, raw);
|
|
71988
|
+
if (prior === raw) return this.drop(deviceId, capName, "no-change");
|
|
71989
|
+
return { changed: true };
|
|
71990
|
+
}
|
|
71991
|
+
/**
|
|
71992
|
+
* The booleans' first-sighting rule, on a word: the resting word seeds; any
|
|
71993
|
+
* other word is an event only when the slice's own `lastChangedAt` clears
|
|
71994
|
+
* the SAME floor `evaluateSensorEdge` uses, from the same two knobs.
|
|
71995
|
+
*/
|
|
71996
|
+
firstWordSighting(deviceId, capName, slice, word) {
|
|
71997
|
+
if (word === WORD_LEVEL_INACTIVE[capName]) return this.drop(deviceId, capName, "baseline-seeded-inactive");
|
|
71998
|
+
const changedAt = slice["lastChangedAt"];
|
|
71999
|
+
const floor = Math.max(this.startedAtMs, this.now() - this.freshnessMs);
|
|
72000
|
+
if (!(typeof changedAt === "number" && Number.isFinite(changedAt) && changedAt > 0 && changedAt >= floor)) return this.drop(deviceId, capName, "baseline-seeded-stale-active");
|
|
72001
|
+
return { changed: true };
|
|
72002
|
+
}
|
|
72003
|
+
drop(deviceId, capName, reason) {
|
|
72004
|
+
this.noteDrop(deviceId, capName, reason);
|
|
72005
|
+
return {
|
|
72006
|
+
changed: false,
|
|
72007
|
+
reason
|
|
72008
|
+
};
|
|
72009
|
+
}
|
|
71726
72010
|
noteDrop(deviceId, capName, reason) {
|
|
71727
72011
|
const key = `${deviceId}:${capName}:${reason}`;
|
|
71728
72012
|
const prior = this.drops.get(key);
|
|
@@ -78309,6 +78593,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
78309
78593
|
runNow: (input) => center.runSummaryNow(input)
|
|
78310
78594
|
},
|
|
78311
78595
|
texts: center.textCatalogEditor,
|
|
78596
|
+
previewTexts: center.textCatalog,
|
|
78312
78597
|
...this.ncArtifactIndex !== null ? { artifacts: this.ncArtifactIndex } : {},
|
|
78313
78598
|
artifactShelf: { remove: async (id) => await this.ncArtifactStore?.remove(id) ?? false },
|
|
78314
78599
|
ruleStatus: {
|
|
@@ -85148,7 +85433,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
85148
85433
|
* A cap with no boolean level (`doorbell`, `button`, `lock-control`,
|
|
85149
85434
|
* `presence`, `enum-sensor`, `event-emitter`) always passes — its ARRIVAL is
|
|
85150
85435
|
* the event, and gating it would re-ship the silence of 2026-08-08. A cap
|
|
85151
|
-
* that HAS one passes only when the boolean moved, in either direction
|
|
85436
|
+
* that HAS one passes only when the boolean moved, in either direction — or,
|
|
85437
|
+
* for `cover`, whose level is its `state` word, only when the word changed
|
|
85438
|
+
* (a position tick or a same-word republish is a drop).
|
|
85152
85439
|
*
|
|
85153
85440
|
* A slice that is not an object at all is refused rather than defaulted to
|
|
85154
85441
|
* `{}`: `evaluateSensorEdge` would read `non-boolean-value` from an empty
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Byz9Hzo0.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.275",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|