@camstack/addon-post-analysis 1.2.305 → 1.2.306
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-t2ey3w0B.js → clip-model-registry-C67UM_q_.js} +128 -6
- package/dist/{clip-model-registry-EA6x7Vd5.mjs → clip-model-registry-C9Aw4qkh.mjs} +117 -7
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-yOiRjNnR.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-QE0Juzwb.mjs} +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-MwQBD526.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-6Km5THPt.mjs → hostInit-DzNrV8y8.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +330 -22
- package/dist/pipeline-analytics/index.mjs +330 -22
- 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-CieM-gpB.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-C67UM_q_.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");
|
|
@@ -10652,21 +10652,49 @@ function matchesPlate(label, values, maxDistance) {
|
|
|
10652
10652
|
return false;
|
|
10653
10653
|
}
|
|
10654
10654
|
/**
|
|
10655
|
+
* Subject kinds whose `classNames` is a real, operator-meaningful detection
|
|
10656
|
+
* class — the ones the `classes` condition can actually scope a rule to.
|
|
10657
|
+
* `device-event` and `system-event` carry NO class live (`classNames: []`,
|
|
10658
|
+
* always — see `subjectFromSensorEvent`); their OUTBOX `className` is a
|
|
10659
|
+
* display placeholder instead (the sensor kind, or the literal `'event'`,
|
|
10660
|
+
* `className: subject.classNames[0] ?? subject.sensorKind ?? 'event'`), which
|
|
10661
|
+
* `cooldownSubjectFromOutbox` would otherwise reconstruct as if it were a real
|
|
10662
|
+
* class. Gating here, not on the reconstructed value, keeps the live path and
|
|
10663
|
+
* the boot reseed addressing the SAME key regardless of what a display label
|
|
10664
|
+
* happens to say (2026-10-01, caught by `system-event-cooldown-direction.spec.ts`
|
|
10665
|
+
* when the default below flipped).
|
|
10666
|
+
*/
|
|
10667
|
+
var CLASS_BEARING_SUBJECT_KINDS = new Set([
|
|
10668
|
+
"object-event",
|
|
10669
|
+
"track-end",
|
|
10670
|
+
"package-event",
|
|
10671
|
+
"occupancy-event",
|
|
10672
|
+
"audio-event",
|
|
10673
|
+
"audio-window"
|
|
10674
|
+
]);
|
|
10675
|
+
/**
|
|
10655
10676
|
* Should this subject's class be part of the cooldown key?
|
|
10656
10677
|
*
|
|
10657
10678
|
* AUDIO is unconditional and predates the setting: a rule opted into several
|
|
10658
10679
|
* audio classes (dog + scream) must not have a safety-relevant scream
|
|
10659
10680
|
* swallowed by an unrelated bark's 60 s window.
|
|
10660
10681
|
*
|
|
10661
|
-
*
|
|
10662
|
-
*
|
|
10663
|
-
*
|
|
10664
|
-
*
|
|
10665
|
-
*
|
|
10682
|
+
* A `device-event` or `system-event` subject never reads a class — see
|
|
10683
|
+
* {@link CLASS_BEARING_SUBJECT_KINDS} — so the setting has nothing to key on
|
|
10684
|
+
* regardless of `granularity`.
|
|
10685
|
+
*
|
|
10686
|
+
* Every class-bearing subject gets the same treatment unless the rule
|
|
10687
|
+
* explicitly asks for `shared` — cat→dog fires at once, cat→cat still waits.
|
|
10688
|
+
* That is the operator's default since 2026-10-01 (D36's dated note): an
|
|
10689
|
+
* UNSET rule now reads exactly like an explicit `per-class` one, new rules and
|
|
10690
|
+
* existing rules alike. Only a `granularity: 'shared'` actually stored
|
|
10691
|
+
* reproduces the old class-agnostic key byte for byte — the one case where an
|
|
10692
|
+
* operator asked for one window across every class on purpose.
|
|
10666
10693
|
*/
|
|
10667
10694
|
function keysPerClass(rule, subject) {
|
|
10668
10695
|
if (subject.kind === "audio-event" || subject.kind === "audio-window") return true;
|
|
10669
|
-
|
|
10696
|
+
if (!CLASS_BEARING_SUBJECT_KINDS.has(subject.kind)) return false;
|
|
10697
|
+
return rule.throttle.granularity !== "shared";
|
|
10670
10698
|
}
|
|
10671
10699
|
/**
|
|
10672
10700
|
* The cooldown scope of ONE system event — subject AND direction.
|
|
@@ -10751,6 +10779,42 @@ function isCoolingDown(rule, lastFiredAt, now) {
|
|
|
10751
10779
|
return now - lastFiredAt < rule.throttle.cooldownSec * 1e3;
|
|
10752
10780
|
}
|
|
10753
10781
|
/**
|
|
10782
|
+
* The media policy EVERY sensor rule attaches: one whole-scene photograph,
|
|
10783
|
+
* never a selector. `attach: 'keyFrame'` is the existing "clean full scene
|
|
10784
|
+
* frame" ladder (`attachmentKindPreference`) — the closest existing concept
|
|
10785
|
+
* to a plain snapshot, as opposed to `'best'`/`'best-matching'`, which chase a
|
|
10786
|
+
* SUBJECT crop a sensor subject (a doorbell press, a contact change) has
|
|
10787
|
+
* none of to chase. No zone crop, no gif, no clip, no profile — a sensor
|
|
10788
|
+
* subject has no zone condition to crop to and no reason to animate.
|
|
10789
|
+
*/
|
|
10790
|
+
var NC_SENSOR_MEDIA_POLICY = { attach: "keyFrame" };
|
|
10791
|
+
/**
|
|
10792
|
+
* The media policy actually used to build a delivery, for a rule of this
|
|
10793
|
+
* KIND. A sensor rule's STORED `media` is never read at evaluation — the
|
|
10794
|
+
* editor offers no media control for one any more, and `media.attach` /
|
|
10795
|
+
* `.gif` / `.clip` / `.profile` on an existing sensor rule (authored before
|
|
10796
|
+
* this ruling, from the viewer or the API) are IGNORED here, never refused at
|
|
10797
|
+
* load. See {@link mediaPolicyMatchesSensorDefault} for the sampled-log gate.
|
|
10798
|
+
*/
|
|
10799
|
+
function effectiveMediaPolicy(rule, kind) {
|
|
10800
|
+
return kind === "sensor" ? NC_SENSOR_MEDIA_POLICY : rule.media;
|
|
10801
|
+
}
|
|
10802
|
+
/**
|
|
10803
|
+
* True when a stored media policy already reads as the sensor default — the
|
|
10804
|
+
* gate the boot/evaluation path uses to decide whether an existing sensor
|
|
10805
|
+
* rule's stored choice is being silently overridden (and so is worth a
|
|
10806
|
+
* sampled log line) or already matches and there is nothing to say.
|
|
10807
|
+
*/
|
|
10808
|
+
function mediaPolicyMatchesSensorDefault(media) {
|
|
10809
|
+
return media.attach === NC_SENSOR_MEDIA_POLICY.attach && media.frame === void 0 && media.zoneCrop !== true && media.gif !== true && media.clip !== true && media.profile === void 0;
|
|
10810
|
+
}
|
|
10811
|
+
/** `effectiveMediaPolicy`, resolving the kind itself — the call-site shortcut
|
|
10812
|
+
* every production caller uses; `effectiveMediaPolicy` stays the unit under
|
|
10813
|
+
* test because `ruleKindOf` needs no fixture of its own there. */
|
|
10814
|
+
function effectiveMediaPolicyForRule(rule) {
|
|
10815
|
+
return effectiveMediaPolicy(rule, require_clip_model_registry.ruleKindOf(rule));
|
|
10816
|
+
}
|
|
10817
|
+
/**
|
|
10754
10818
|
* Ordered media-kind preference for one attachment, per policy and owner.
|
|
10755
10819
|
* `best` favours the tight subject crop; `keyFrame` the clean scene frame
|
|
10756
10820
|
* (small variant first — the degrade engine enforces per-backend byte
|
|
@@ -14332,11 +14396,11 @@ function logRefusal$3(check, ctx) {
|
|
|
14332
14396
|
try {
|
|
14333
14397
|
check();
|
|
14334
14398
|
} catch (err) {
|
|
14335
|
-
if (err instanceof NcDevicesExcludeRefusal) warnRefusal$
|
|
14399
|
+
if (err instanceof NcDevicesExcludeRefusal) warnRefusal$3(ctx.logger, err, ctx);
|
|
14336
14400
|
throw err;
|
|
14337
14401
|
}
|
|
14338
14402
|
}
|
|
14339
|
-
function warnRefusal$
|
|
14403
|
+
function warnRefusal$3(logger, err, ctx) {
|
|
14340
14404
|
logger.warn("nc rule refused: devicesExclude on a system rule", { meta: {
|
|
14341
14405
|
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
14342
14406
|
by: ctx.by,
|
|
@@ -14359,6 +14423,184 @@ function guardPatchDevicesExclude(existing, patch, ctx) {
|
|
|
14359
14423
|
logRefusal$3(() => assertDevicesExcludeDelivery(merged), ctx);
|
|
14360
14424
|
}
|
|
14361
14425
|
//#endregion
|
|
14426
|
+
//#region src/notification-center/device-compat-guard.ts
|
|
14427
|
+
/**
|
|
14428
|
+
* A NEW or UPDATED rule's `conditions.devices` (the "Only these" INCLUDE list)
|
|
14429
|
+
* names only devices that can actually produce this rule's KIND of subject —
|
|
14430
|
+
* operator ruling, 2026-10-01: "the DEVICE picker of a rule must offer only
|
|
14431
|
+
* devices compatible with the rule type … the server should also refuse a
|
|
14432
|
+
* save whose deviceIds include an incompatible device".
|
|
14433
|
+
*
|
|
14434
|
+
* THE authority, same arrangement as `device-scope-guard.ts` beside it: the
|
|
14435
|
+
* cap-provider route (`notificationRules.createRule/updateRule`, index.ts)
|
|
14436
|
+
* and the rule bridge (`nc.createRule/updateRule`, nc-actions.ts) both call
|
|
14437
|
+
* `guardRuleDeviceCompat` / `guardPatchDeviceCompat`. Which devices a kind
|
|
14438
|
+
* may scope is `ruleDeviceCompatCapQueries` (`@camstack/types`) — the SAME
|
|
14439
|
+
* table the admin's Devices tab narrows by, so a save can never refuse a
|
|
14440
|
+
* device its own picker just offered. A `sensor` rule's set includes every
|
|
14441
|
+
* camera: a camera-scoped sensor rule is legitimate (D630 §6).
|
|
14442
|
+
*
|
|
14443
|
+
* ASYNC, unlike its sibling guards: it has to ask `deviceManager` which
|
|
14444
|
+
* devices carry the caps. `listDeviceIdsByCaps` is therefore OPTIONAL on the
|
|
14445
|
+
* context — absent on an older wiring, if the cross-addon call throws, or if
|
|
14446
|
+
* it answers EMPTY (the registry not yet warm at boot: `getAllBindings`
|
|
14447
|
+
* answers `[]` with only a warning, which is not a measurement — D315/D393),
|
|
14448
|
+
* the guard FAILS OPEN (never refuses) rather than blocking every save on a
|
|
14449
|
+
* read it cannot make (D49). It is never SILENT about it: an unjudged scoped
|
|
14450
|
+
* save logs a sampled WARN naming the rule and the reason, so a dropped wiring
|
|
14451
|
+
* is visible in production rather than a guard that quietly stopped existing.
|
|
14452
|
+
*
|
|
14453
|
+
* ON UPDATE only the ids the patch ADDS are judged. A device already stored on
|
|
14454
|
+
* the rule is never re-judged: the answer is not stable (switching detection
|
|
14455
|
+
* off on a camera removes its `detection-pipeline` binding, D62; an agent
|
|
14456
|
+
* camera's native caps are missing during the handshake), and re-judging
|
|
14457
|
+
* would make every rule naming that camera un-editable until the operator
|
|
14458
|
+
* removed it — losing a scope a later switch-on would not give back.
|
|
14459
|
+
*
|
|
14460
|
+
* NEVER called at LOAD. `NcRuleSchema.parse` and the store's own read path are
|
|
14461
|
+
* untouched — a rule saved before this ruling, or saved while the capability
|
|
14462
|
+
* read was unavailable, keeps loading and keeps firing exactly as it does
|
|
14463
|
+
* today; only a NEW save attempt is judged.
|
|
14464
|
+
*
|
|
14465
|
+
* `devicesExclude` is NOT checked: this guard judges what the rule actually
|
|
14466
|
+
* MATCHES ON (the include scope), not what it widens away from, and an
|
|
14467
|
+
* operator excluding an incompatible device from an "all except" rule is not
|
|
14468
|
+
* the mistake this exists to catch.
|
|
14469
|
+
*/
|
|
14470
|
+
/** How long one rule's "accepted unchecked" WARN stays quiet after it fired. */
|
|
14471
|
+
var UNCHECKED_WARN_WINDOW_MS = 10 * 6e4;
|
|
14472
|
+
/** Bound on remembered rule keys — cleared whole when exceeded. */
|
|
14473
|
+
var UNCHECKED_WARN_MEMO_MAX = 256;
|
|
14474
|
+
/**
|
|
14475
|
+
* Samples the "accepted unchecked" WARN per rule: an editor that autosaves, or
|
|
14476
|
+
* a script, must not turn one unreachable read into a log flood, but the first
|
|
14477
|
+
* save of every rule in a window is always reported.
|
|
14478
|
+
*/
|
|
14479
|
+
var NcDeviceCompatWarnSampler = class {
|
|
14480
|
+
lastWarnedAt = /* @__PURE__ */ new Map();
|
|
14481
|
+
shouldWarn(key, now) {
|
|
14482
|
+
const last = this.lastWarnedAt.get(key);
|
|
14483
|
+
if (last !== void 0 && now - last < UNCHECKED_WARN_WINDOW_MS) return false;
|
|
14484
|
+
if (this.lastWarnedAt.size >= UNCHECKED_WARN_MEMO_MAX) this.lastWarnedAt.clear();
|
|
14485
|
+
this.lastWarnedAt.set(key, now);
|
|
14486
|
+
return true;
|
|
14487
|
+
}
|
|
14488
|
+
};
|
|
14489
|
+
var NcDeviceCompatRefusal = class extends Error {
|
|
14490
|
+
kind;
|
|
14491
|
+
incompatibleIds;
|
|
14492
|
+
constructor(kind, incompatibleIds) {
|
|
14493
|
+
super(`rule refused: device(s) ${incompatibleIds.join(", ")} cannot produce a '${kind}' rule's subject`);
|
|
14494
|
+
this.name = "NcDeviceCompatRefusal";
|
|
14495
|
+
this.kind = kind;
|
|
14496
|
+
this.incompatibleIds = incompatibleIds;
|
|
14497
|
+
}
|
|
14498
|
+
};
|
|
14499
|
+
/** The union of every compat query's answer, or why there is none. */
|
|
14500
|
+
async function readCompatibleIds(queries, listDeviceIdsByCaps) {
|
|
14501
|
+
try {
|
|
14502
|
+
const answers = await Promise.all(queries.map((capNames) => listDeviceIdsByCaps(capNames)));
|
|
14503
|
+
return {
|
|
14504
|
+
ok: true,
|
|
14505
|
+
ids: new Set(answers.flat())
|
|
14506
|
+
};
|
|
14507
|
+
} catch (err) {
|
|
14508
|
+
return {
|
|
14509
|
+
ok: false,
|
|
14510
|
+
error: String(err)
|
|
14511
|
+
};
|
|
14512
|
+
}
|
|
14513
|
+
}
|
|
14514
|
+
/**
|
|
14515
|
+
* The check. `judged` is the ids to judge — the whole include list on a
|
|
14516
|
+
* create, only the ADDED ids on an update (see the file header).
|
|
14517
|
+
*
|
|
14518
|
+
* Resolves `not-judged` when the kind has no capability filter
|
|
14519
|
+
* (`ruleDeviceCompatCapQueries` is `null`) or there is nothing to judge —
|
|
14520
|
+
* "every device" is never refused here. Resolves `unchecked` (fail open) when
|
|
14521
|
+
* the read is not wired, throws, or answers nothing at all. Throws
|
|
14522
|
+
* {@link NcDeviceCompatRefusal} naming every incompatible id, never only the
|
|
14523
|
+
* first — an operator fixing the list needs the whole picture in one pass.
|
|
14524
|
+
*/
|
|
14525
|
+
async function assertRuleDeviceCompat(rule, listDeviceIdsByCaps, judged = rule.conditions.devices ?? []) {
|
|
14526
|
+
const kind = require_clip_model_registry.ruleKindOf(rule);
|
|
14527
|
+
const queries = require_clip_model_registry.ruleDeviceCompatCapQueries(kind);
|
|
14528
|
+
if (queries === null || judged.length === 0) return { outcome: "not-judged" };
|
|
14529
|
+
if (listDeviceIdsByCaps === void 0) return {
|
|
14530
|
+
outcome: "unchecked",
|
|
14531
|
+
reason: "unwired",
|
|
14532
|
+
ids: judged
|
|
14533
|
+
};
|
|
14534
|
+
const read = await readCompatibleIds(queries, listDeviceIdsByCaps);
|
|
14535
|
+
if (!read.ok) return {
|
|
14536
|
+
outcome: "unchecked",
|
|
14537
|
+
reason: "read-failed",
|
|
14538
|
+
ids: judged,
|
|
14539
|
+
error: read.error
|
|
14540
|
+
};
|
|
14541
|
+
if (read.ids.size === 0) return {
|
|
14542
|
+
outcome: "unchecked",
|
|
14543
|
+
reason: "answered-empty",
|
|
14544
|
+
ids: judged
|
|
14545
|
+
};
|
|
14546
|
+
const incompatible = judged.filter((id) => !read.ids.has(id));
|
|
14547
|
+
if (incompatible.length > 0) throw new NcDeviceCompatRefusal(kind, incompatible);
|
|
14548
|
+
return { outcome: "compatible" };
|
|
14549
|
+
}
|
|
14550
|
+
/**
|
|
14551
|
+
* Run the check and report what it could not do: a refusal is dropped work, so
|
|
14552
|
+
* it is logged before it is rethrown; an unchecked acceptance is a guard that
|
|
14553
|
+
* did not run, so it is logged (sampled) too.
|
|
14554
|
+
*/
|
|
14555
|
+
async function runAndReport(rule, judged, ctx) {
|
|
14556
|
+
let verdict;
|
|
14557
|
+
try {
|
|
14558
|
+
verdict = await assertRuleDeviceCompat(rule, ctx.listDeviceIdsByCaps, judged);
|
|
14559
|
+
} catch (err) {
|
|
14560
|
+
if (err instanceof NcDeviceCompatRefusal) warnRefusal$2(ctx.logger, err, ctx);
|
|
14561
|
+
throw err;
|
|
14562
|
+
}
|
|
14563
|
+
if (verdict.outcome === "unchecked") warnUnchecked(require_clip_model_registry.ruleKindOf(rule), verdict, ctx);
|
|
14564
|
+
}
|
|
14565
|
+
function warnRefusal$2(logger, err, ctx) {
|
|
14566
|
+
logger.warn("nc rule refused: device(s) incompatible with the rule kind", { meta: {
|
|
14567
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
14568
|
+
by: ctx.by,
|
|
14569
|
+
kind: err.kind,
|
|
14570
|
+
incompatibleIds: [...err.incompatibleIds]
|
|
14571
|
+
} });
|
|
14572
|
+
}
|
|
14573
|
+
function warnUnchecked(kind, verdict, ctx) {
|
|
14574
|
+
const now = (ctx.now ?? Date.now)();
|
|
14575
|
+
const key = `${ctx.ruleId ?? `new:${ctx.by}`}:${verdict.reason}`;
|
|
14576
|
+
if (ctx.sampler !== void 0 && !ctx.sampler.shouldWarn(key, now)) return;
|
|
14577
|
+
ctx.logger.warn("nc rule saved WITHOUT the device-compatibility check — accepted unchecked", { meta: {
|
|
14578
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
14579
|
+
by: ctx.by,
|
|
14580
|
+
kind,
|
|
14581
|
+
reason: verdict.reason,
|
|
14582
|
+
uncheckedIds: [...verdict.ids],
|
|
14583
|
+
...verdict.error !== void 0 ? { error: verdict.error } : {}
|
|
14584
|
+
} });
|
|
14585
|
+
}
|
|
14586
|
+
/** The create path: the whole include list is judged. */
|
|
14587
|
+
async function guardRuleDeviceCompat(rule, ctx) {
|
|
14588
|
+
await runAndReport(rule, rule.conditions.devices ?? [], ctx);
|
|
14589
|
+
}
|
|
14590
|
+
/**
|
|
14591
|
+
* The update path: the MERGED rule decides the kind, but only the ids the
|
|
14592
|
+
* patch ADDS to `conditions.devices` are judged — a stored id is never
|
|
14593
|
+
* re-judged (see the file header). A patch that adds nothing never pays for a
|
|
14594
|
+
* capability read.
|
|
14595
|
+
*/
|
|
14596
|
+
async function guardPatchDeviceCompat(existing, patch, ctx) {
|
|
14597
|
+
if (existing === null) return;
|
|
14598
|
+
const merged = mergedFamilySubject(existing, patch);
|
|
14599
|
+
if (merged === null) return;
|
|
14600
|
+
const stored = new Set(existing.conditions.devices ?? []);
|
|
14601
|
+
await runAndReport(merged, (merged.conditions.devices ?? []).filter((id) => !stored.has(id)), ctx);
|
|
14602
|
+
}
|
|
14603
|
+
//#endregion
|
|
14362
14604
|
//#region src/notification-center/rule-summary-guard.ts
|
|
14363
14605
|
/**
|
|
14364
14606
|
* The hub's half of D647: `systemEventDelivery: { mode: 'summary' }` belongs to
|
|
@@ -17218,6 +17460,8 @@ function requireAdmin(caller) {
|
|
|
17218
17460
|
return c;
|
|
17219
17461
|
}
|
|
17220
17462
|
function makeNcActionHandlers(deps) {
|
|
17463
|
+
/** One sampler per handler set: an unchecked save warns once per rule per window. */
|
|
17464
|
+
const deviceCompatWarnSampler = new NcDeviceCompatWarnSampler();
|
|
17221
17465
|
/** The alarm half, or a refusal by name — never a made-up "disarmed". */
|
|
17222
17466
|
const requireAlarm = (action) => {
|
|
17223
17467
|
if (deps.alarm === void 0) throw new Error(`${action}: the alarm is not wired on this node`);
|
|
@@ -17375,6 +17619,12 @@ function makeNcActionHandlers(deps) {
|
|
|
17375
17619
|
logger: deps.logger,
|
|
17376
17620
|
by: c.userId
|
|
17377
17621
|
});
|
|
17622
|
+
await guardRuleDeviceCompat(parsed, {
|
|
17623
|
+
logger: deps.logger,
|
|
17624
|
+
by: c.userId,
|
|
17625
|
+
listDeviceIdsByCaps: deps.listDeviceIdsByCaps,
|
|
17626
|
+
sampler: deviceCompatWarnSampler
|
|
17627
|
+
});
|
|
17378
17628
|
guardRuleSystemEventDelivery(parsed, {
|
|
17379
17629
|
logger: deps.logger,
|
|
17380
17630
|
by: c.userId
|
|
@@ -17417,6 +17667,13 @@ function makeNcActionHandlers(deps) {
|
|
|
17417
17667
|
by: c.userId,
|
|
17418
17668
|
ruleId: input.ruleId
|
|
17419
17669
|
});
|
|
17670
|
+
await guardPatchDeviceCompat(existing, patch, {
|
|
17671
|
+
logger: deps.logger,
|
|
17672
|
+
by: c.userId,
|
|
17673
|
+
ruleId: input.ruleId,
|
|
17674
|
+
listDeviceIdsByCaps: deps.listDeviceIdsByCaps,
|
|
17675
|
+
sampler: deviceCompatWarnSampler
|
|
17676
|
+
});
|
|
17420
17677
|
guardPatchSystemEventDelivery(existing, patch, clear, {
|
|
17421
17678
|
logger: deps.logger,
|
|
17422
17679
|
by: c.userId,
|
|
@@ -17918,9 +18175,9 @@ function makeNcActionHandlers(deps) {
|
|
|
17918
18175
|
* reproduce.
|
|
17919
18176
|
*/
|
|
17920
18177
|
var NC_CLIP_DISPATCH_MARGIN_SEC = 10;
|
|
17921
|
-
/** Does this
|
|
17922
|
-
function wantsFootage(
|
|
17923
|
-
return
|
|
18178
|
+
/** Does this media policy ever cut footage from the broker's clip ring? */
|
|
18179
|
+
function wantsFootage(media) {
|
|
18180
|
+
return media.gif === true || media.clip === true;
|
|
17924
18181
|
}
|
|
17925
18182
|
/** The ask ONE rule makes, or `null` when it needs no retained footage. */
|
|
17926
18183
|
function askSecondsFor(rule) {
|
|
@@ -17929,9 +18186,10 @@ function askSecondsFor(rule) {
|
|
|
17929
18186
|
seconds: require_clip_model_registry.NC_CLIP_RETENTION_MAX_SEC,
|
|
17930
18187
|
reason: "occupancy"
|
|
17931
18188
|
};
|
|
17932
|
-
|
|
17933
|
-
|
|
17934
|
-
const
|
|
18189
|
+
const media = effectiveMediaPolicyForRule(rule);
|
|
18190
|
+
if (!wantsFootage(media)) return null;
|
|
18191
|
+
const pre = media.clipPreRollSec ?? require_clip_model_registry.NC_CLIP_PRE_ROLL_DEFAULT_SEC;
|
|
18192
|
+
const post = media.clipPostRollSec ?? require_clip_model_registry.NC_CLIP_POST_ROLL_DEFAULT_SEC;
|
|
17935
18193
|
return {
|
|
17936
18194
|
seconds: Math.min(pre + post + NC_CLIP_DISPATCH_MARGIN_SEC, require_clip_model_registry.NC_CLIP_RETENTION_MAX_SEC),
|
|
17937
18195
|
reason: "window"
|
|
@@ -17950,12 +18208,13 @@ function deriveClipRetentionAsks(rules) {
|
|
|
17950
18208
|
for (const rule of rules) {
|
|
17951
18209
|
const ask = askSecondsFor(rule);
|
|
17952
18210
|
if (ask === null) continue;
|
|
18211
|
+
const profile = effectiveMediaPolicyForRule(rule).profile;
|
|
17953
18212
|
const base = {
|
|
17954
18213
|
seconds: ask.seconds,
|
|
17955
18214
|
reason: ask.reason,
|
|
17956
18215
|
ruleId: rule.id,
|
|
17957
18216
|
ruleName: rule.name,
|
|
17958
|
-
...
|
|
18217
|
+
...profile !== void 0 ? { profile } : {}
|
|
17959
18218
|
};
|
|
17960
18219
|
const devices = rule.conditions.devices;
|
|
17961
18220
|
if (devices === void 0 || devices.length === 0) {
|
|
@@ -31569,9 +31828,15 @@ var NC_ALARM_ANNOUNCE_RULE_ID = "nc-alarm-announce";
|
|
|
31569
31828
|
* A rule's media policy, as the fields a row freezes — ONE derivation, used by
|
|
31570
31829
|
* every row a rule addresses AND by the alarm's own announcement row when it is
|
|
31571
31830
|
* the COMBINED siren (the triggering rule's pictures on the alarm's words).
|
|
31831
|
+
*
|
|
31832
|
+
* `effectiveMediaPolicyForRule` is what actually resolves it: a SENSOR rule's
|
|
31833
|
+
* stored `media` is ignored here, in favour of the one-snapshot default
|
|
31834
|
+
* (operator ruling, 2026-10-01) — the rule still LOADS with whatever it has
|
|
31835
|
+
* stored, and `buildEntries` is where the sampled log fires when that stored
|
|
31836
|
+
* choice is about to be overridden.
|
|
31572
31837
|
*/
|
|
31573
31838
|
function mediaPolicyFields(mediaRule, cropZoneIds) {
|
|
31574
|
-
const media = mediaRule
|
|
31839
|
+
const media = effectiveMediaPolicyForRule(mediaRule);
|
|
31575
31840
|
return {
|
|
31576
31841
|
media: media.attach,
|
|
31577
31842
|
...media.zoneCrop === true && cropZoneIds !== void 0 ? { mediaZoneIds: [...cropZoneIds] } : {},
|
|
@@ -31865,13 +32130,14 @@ function cooldownSubjectKind(kind) {
|
|
|
31865
32130
|
* the same builder the live path does, so the two can never address different
|
|
31866
32131
|
* windows. Only the four fields the key is a function of are reconstructed —
|
|
31867
32132
|
* the class (for `per-class` and audio), the subject kind (which decides
|
|
31868
|
-
* whether
|
|
32133
|
+
* whether the class is keyed at all — audio always, occupancy and detections
|
|
32134
|
+
* unless `shared`), the system event (subject +
|
|
31869
32135
|
* direction) and the device.
|
|
31870
32136
|
*/
|
|
31871
32137
|
function cooldownSubjectFromOutbox(entry) {
|
|
31872
32138
|
const subject = entry.payload.subject;
|
|
31873
32139
|
return {
|
|
31874
|
-
kind: cooldownSubjectKind(entry.recordKind),
|
|
32140
|
+
kind: entry.recordKind === "device-event" && subject.occupancy !== void 0 ? "occupancy-event" : cooldownSubjectKind(entry.recordKind),
|
|
31875
32141
|
recordId: entry.recordId,
|
|
31876
32142
|
deviceId: entry.deviceId,
|
|
31877
32143
|
timestamp: subject.timestamp,
|
|
@@ -31950,6 +32216,9 @@ function renderAudioScope(devices, excluded) {
|
|
|
31950
32216
|
if (devices.length === 0) return excluded.length === 0 ? "@all" : `@all-except:${excluded.join("+")}`;
|
|
31951
32217
|
return devices.filter((id) => !excluded.includes(id)).join("+");
|
|
31952
32218
|
}
|
|
32219
|
+
/** {@link NotificationCenter.reportedSensorMediaIgnored} cap, same shape as
|
|
32220
|
+
* the dispatcher's own annotated-fallback memo. */
|
|
32221
|
+
var NC_SENSOR_MEDIA_IGNORED_MEMO_MAX = 5e3;
|
|
31953
32222
|
var NotificationCenter = class NotificationCenter {
|
|
31954
32223
|
logger;
|
|
31955
32224
|
rules;
|
|
@@ -31957,6 +32226,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31957
32226
|
dispatcher;
|
|
31958
32227
|
deps;
|
|
31959
32228
|
now;
|
|
32229
|
+
/** Rule ids whose stored (non-default) media policy has already been
|
|
32230
|
+
* reported as ignored at evaluation — ONE line per rule, not per fire. */
|
|
32231
|
+
reportedSensorMediaIgnored = /* @__PURE__ */ new Set();
|
|
32232
|
+
/** Samples the device-compat guard's "accepted unchecked" WARN per rule. */
|
|
32233
|
+
deviceCompatWarnSampler = new NcDeviceCompatWarnSampler();
|
|
31960
32234
|
/**
|
|
31961
32235
|
* Every word this centre can print, in the hub's language.
|
|
31962
32236
|
*
|
|
@@ -33737,7 +34011,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
33737
34011
|
const shown = this.unmutedCameras(cameraIds);
|
|
33738
34012
|
const involved = [sensorId, ...rowShown];
|
|
33739
34013
|
if (!this.rules.listEnabled("device-event").some((rule) => {
|
|
33740
|
-
if (rule.
|
|
34014
|
+
if (effectiveMediaPolicyForRule(rule).attach === "none") return false;
|
|
33741
34015
|
if (!deviceEventRuleMayFireOnKind(rule, sensorKind, sensorRaised)) return false;
|
|
33742
34016
|
const excluded = rule.conditions.devicesExclude;
|
|
33743
34017
|
if (excluded !== void 0 && deviceScopeExcludes(excluded, {
|
|
@@ -34034,7 +34308,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
34034
34308
|
ruleWantsOccupancyStill(edge) {
|
|
34035
34309
|
const subject = occupancySubjectFromEdge(edge);
|
|
34036
34310
|
for (const rule of this.rules.listEnabled("device-event")) {
|
|
34037
|
-
if (rule.
|
|
34311
|
+
if (effectiveMediaPolicyForRule(rule).attach === "none") continue;
|
|
34038
34312
|
const occ = rule.conditions.occupancy;
|
|
34039
34313
|
if (occ === void 0 || !matchesOccupancy(occ, subject)) continue;
|
|
34040
34314
|
const devices = rule.conditions.devices;
|
|
@@ -34301,6 +34575,12 @@ var NotificationCenter = class NotificationCenter {
|
|
|
34301
34575
|
logger: this.logger,
|
|
34302
34576
|
by: caller.userId
|
|
34303
34577
|
});
|
|
34578
|
+
await guardRuleDeviceCompat(rule, {
|
|
34579
|
+
logger: this.logger,
|
|
34580
|
+
by: caller.userId,
|
|
34581
|
+
listDeviceIdsByCaps: this.deps.listDeviceIdsByCaps,
|
|
34582
|
+
sampler: this.deviceCompatWarnSampler
|
|
34583
|
+
});
|
|
34304
34584
|
guardRuleSystemEventDelivery(rule, {
|
|
34305
34585
|
logger: this.logger,
|
|
34306
34586
|
by: caller.userId
|
|
@@ -34338,6 +34618,13 @@ var NotificationCenter = class NotificationCenter {
|
|
|
34338
34618
|
by: caller.userId,
|
|
34339
34619
|
ruleId
|
|
34340
34620
|
});
|
|
34621
|
+
await guardPatchDeviceCompat(existing, patch, {
|
|
34622
|
+
logger: this.logger,
|
|
34623
|
+
by: caller.userId,
|
|
34624
|
+
ruleId,
|
|
34625
|
+
listDeviceIdsByCaps: this.deps.listDeviceIdsByCaps,
|
|
34626
|
+
sampler: this.deviceCompatWarnSampler
|
|
34627
|
+
});
|
|
34341
34628
|
guardPatchSystemEventDelivery(existing, patch, clear ?? [], {
|
|
34342
34629
|
logger: this.logger,
|
|
34343
34630
|
by: caller.userId,
|
|
@@ -35385,7 +35672,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
35385
35672
|
buttons: unresolvable.join(", ")
|
|
35386
35673
|
}
|
|
35387
35674
|
});
|
|
35388
|
-
if (!noOwnerReported && kind === "device-event" && subject.trackId === void 0 && subject.linkedStills === void 0 && rule.
|
|
35675
|
+
if (!noOwnerReported && kind === "device-event" && subject.trackId === void 0 && subject.linkedStills === void 0 && effectiveMediaPolicyForRule(rule).attach !== "none") {
|
|
35389
35676
|
noOwnerReported = true;
|
|
35390
35677
|
const linked = subject.linkedCameraIds;
|
|
35391
35678
|
const meta = {
|
|
@@ -35763,6 +36050,24 @@ var NotificationCenter = class NotificationCenter {
|
|
|
35763
36050
|
}
|
|
35764
36051
|
});
|
|
35765
36052
|
}
|
|
36053
|
+
/**
|
|
36054
|
+
* ONE line per rule id, the first time a SENSOR rule's stored media policy
|
|
36055
|
+
* is about to be overridden by {@link effectiveMediaPolicyForRule}'s
|
|
36056
|
+
* one-snapshot default (operator ruling, 2026-10-01). Never refuses the
|
|
36057
|
+
* rule — it still loads and still fires — this only says the stored choice
|
|
36058
|
+
* is not the one being used.
|
|
36059
|
+
*/
|
|
36060
|
+
warnSensorMediaIgnoredOnce(rule) {
|
|
36061
|
+
if (mediaPolicyMatchesSensorDefault(rule.media)) return;
|
|
36062
|
+
if (this.reportedSensorMediaIgnored.has(rule.id)) return;
|
|
36063
|
+
if (this.reportedSensorMediaIgnored.size >= NC_SENSOR_MEDIA_IGNORED_MEMO_MAX) this.reportedSensorMediaIgnored.clear();
|
|
36064
|
+
this.reportedSensorMediaIgnored.add(rule.id);
|
|
36065
|
+
this.logger.info("sensor rule carries a media choice — ignored at evaluation, one snapshot is always attached", { meta: {
|
|
36066
|
+
ruleId: rule.id,
|
|
36067
|
+
rule: rule.name,
|
|
36068
|
+
storedMedia: rule.media
|
|
36069
|
+
} });
|
|
36070
|
+
}
|
|
35766
36071
|
buildEntries(rule, subject, kind, matchedOn, userTargets = [], origin = "pipeline", cooldown, upgrade, group) {
|
|
35767
36072
|
const hasEventMedia = kind === "object-event" || kind === "package-event";
|
|
35768
36073
|
const isTrackScoped = kind === "object-event" || kind === "track-end";
|
|
@@ -35770,6 +36075,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
35770
36075
|
const alarmSource = subject.systemEvent?.alarmSource;
|
|
35771
36076
|
const mediaRule = alarmSource !== void 0 ? this.rules.get(alarmSource.ruleId) ?? rule : rule;
|
|
35772
36077
|
const noMedia = isSystemEvent && alarmSource === void 0;
|
|
36078
|
+
if (require_clip_model_registry.ruleKindOf(mediaRule) === "sensor") this.warnSensorMediaIgnoredOnce(mediaRule);
|
|
35773
36079
|
const direct = new Set(rule.targets.map((t) => t.targetId));
|
|
35774
36080
|
const renderedZones = notificationZones(rule, subject);
|
|
35775
36081
|
const confirm = frozenConfirmPolicy(rule, isSystemEvent);
|
|
@@ -98994,6 +99300,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
98994
99300
|
listCallerTargetIds: async (userId) => {
|
|
98995
99301
|
return (await api.notificationOutput.listTargets.query({})).filter((t) => t.config["ownerUserId"] === userId).map((t) => t.id);
|
|
98996
99302
|
},
|
|
99303
|
+
listDeviceIdsByCaps: (capNames) => api.deviceManager.listDeviceIdsByCaps.query({ capNames: [...capNames] }),
|
|
98997
99304
|
dropRuleMosaics: async (ruleId) => {
|
|
98998
99305
|
const mediaStore = this.mediaStore;
|
|
98999
99306
|
if (mediaStore === null) return;
|
|
@@ -99910,6 +100217,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
99910
100217
|
addonId: device.addonId
|
|
99911
100218
|
};
|
|
99912
100219
|
},
|
|
100220
|
+
listDeviceIdsByCaps: (capNames) => api.deviceManager.listDeviceIdsByCaps.query({ capNames: [...capNames] }),
|
|
99913
100221
|
isDeviceDeleted: async (deviceId) => await api.deviceManager.getDevice.query({ deviceId }) === null,
|
|
99914
100222
|
readNodeDirectory: () => api.nodes.directory.query(),
|
|
99915
100223
|
getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
|