@camstack/addon-post-analysis 1.2.337 → 1.2.339
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-CD0UYoAb.mjs → clip-model-registry-BKVUtnRA.mjs} +299 -140
- package/dist/{clip-model-registry-CML1YOOa.js → clip-model-registry-DlJT7zn4.js} +299 -140
- 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-DGaCYQ8R.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CF8ypDMO.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BImFB0o5.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-BRmdbHeK.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-Dlhtn22B.mjs → hostInit-DYVlvRBm.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +494 -429
- package/dist/pipeline-analytics/index.mjs +494 -429
- 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-D7NMYqUM.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-bIOxWlPv.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-DlJT7zn4.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");
|
|
@@ -1408,6 +1408,12 @@ async function readAlarmOpenings(ids, loadRuntime, logger) {
|
|
|
1408
1408
|
function isPhase(value) {
|
|
1409
1409
|
return value === "open" || value === "closed";
|
|
1410
1410
|
}
|
|
1411
|
+
/**
|
|
1412
|
+
* A change time that cannot be a move: `0` is a placeholder and `1` is the
|
|
1413
|
+
* "known value, unknown change time" convention providers stamp on a replay or
|
|
1414
|
+
* a recovery (`HM_SEEDED_CHANGED_AT`, `HA_SEEDED_CHANGED_AT`).
|
|
1415
|
+
*/
|
|
1416
|
+
var NO_CHANGE_TIME = 1;
|
|
1411
1417
|
var NcAlarmExclusions = class {
|
|
1412
1418
|
entries = [];
|
|
1413
1419
|
/** Replace the whole set — one arm, one set of exclusions. */
|
|
@@ -1416,7 +1422,9 @@ var NcAlarmExclusions = class {
|
|
|
1416
1422
|
deviceId,
|
|
1417
1423
|
phase: "open",
|
|
1418
1424
|
at,
|
|
1419
|
-
announced: false
|
|
1425
|
+
announced: false,
|
|
1426
|
+
moved: false,
|
|
1427
|
+
lastMoveAt: 0
|
|
1420
1428
|
}));
|
|
1421
1429
|
}
|
|
1422
1430
|
/** Every excluded sensor, in the order it was excluded. */
|
|
@@ -1440,24 +1448,53 @@ var NcAlarmExclusions = class {
|
|
|
1440
1448
|
* An unreadable state changes nothing (D49): a failed read must never lift an
|
|
1441
1449
|
* exclusion the operator asked for, and must never swallow the close that
|
|
1442
1450
|
* would have.
|
|
1451
|
+
*
|
|
1452
|
+
* `changedAt` is the reading's OWN change time, when the caller has one (a
|
|
1453
|
+
* sensor event's slice; the reconcile tick has none). It decides only whether
|
|
1454
|
+
* an open in phase `open` really MOVED ({@link takeMoved}) — never a lift.
|
|
1443
1455
|
*/
|
|
1444
|
-
observe(deviceId, reading, _at) {
|
|
1456
|
+
observe(deviceId, reading, _at, changedAt) {
|
|
1445
1457
|
if (reading === void 0) return "unchanged";
|
|
1446
1458
|
const current = this.entries.find((e) => e.deviceId === deviceId);
|
|
1447
1459
|
if (current === void 0) return "unchanged";
|
|
1448
1460
|
if (reading === "secure") {
|
|
1449
|
-
if (current.phase === "closed") return
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1461
|
+
if (current.phase === "closed") return this.replace({
|
|
1462
|
+
...current,
|
|
1463
|
+
moved: false
|
|
1464
|
+
}, "unchanged");
|
|
1465
|
+
return this.replace({
|
|
1466
|
+
...current,
|
|
1467
|
+
phase: "closed",
|
|
1468
|
+
moved: false
|
|
1469
|
+
}, "closed");
|
|
1470
|
+
}
|
|
1471
|
+
if (current.phase === "open") {
|
|
1472
|
+
const fresh = changedAt !== void 0 && Number.isFinite(changedAt) && changedAt > Math.max(current.at, current.lastMoveAt, NO_CHANGE_TIME);
|
|
1473
|
+
const redelivered = current.moved && changedAt === current.lastMoveAt;
|
|
1474
|
+
return this.replace({
|
|
1475
|
+
...current,
|
|
1476
|
+
moved: fresh || redelivered,
|
|
1477
|
+
lastMoveAt: fresh ? changedAt : current.lastMoveAt
|
|
1478
|
+
}, "unchanged");
|
|
1455
1479
|
}
|
|
1456
|
-
if (current.phase === "open") return "unchanged";
|
|
1457
1480
|
this.entries = this.entries.filter((e) => e.deviceId !== deviceId);
|
|
1458
1481
|
return "rearmed";
|
|
1459
1482
|
}
|
|
1460
1483
|
/**
|
|
1484
|
+
* Did the last reading of this sensor really move it (D735)? Consumed: the
|
|
1485
|
+
* press it caused asks once, and a later press of the same reading is a
|
|
1486
|
+
* duplicate. False for a sensor that is not excluded.
|
|
1487
|
+
*/
|
|
1488
|
+
takeMoved(deviceId) {
|
|
1489
|
+
const current = this.entries.find((e) => e.deviceId === deviceId);
|
|
1490
|
+
if (current === void 0 || !current.moved) return false;
|
|
1491
|
+
this.replace({
|
|
1492
|
+
...current,
|
|
1493
|
+
moved: false
|
|
1494
|
+
}, "unchanged");
|
|
1495
|
+
return true;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1461
1498
|
* Mark this sensor's ignored trigger as announced — true only the FIRST time
|
|
1462
1499
|
* in this arm (the caller then announces and persists), false for a repeat
|
|
1463
1500
|
* or for a sensor that is not excluded.
|
|
@@ -1480,6 +1517,11 @@ var NcAlarmExclusions = class {
|
|
|
1480
1517
|
this.entries = this.entries.filter((e) => e.deviceId !== deviceId);
|
|
1481
1518
|
return true;
|
|
1482
1519
|
}
|
|
1520
|
+
/** Swap one entry for its next version — the set is replaced, never mutated. */
|
|
1521
|
+
replace(next, change) {
|
|
1522
|
+
this.entries = this.entries.map((e) => e.deviceId === next.deviceId ? next : e);
|
|
1523
|
+
return change;
|
|
1524
|
+
}
|
|
1483
1525
|
/** The arm this belongs to is over. */
|
|
1484
1526
|
clear() {
|
|
1485
1527
|
this.entries = [];
|
|
@@ -1511,7 +1553,9 @@ var NcAlarmExclusions = class {
|
|
|
1511
1553
|
deviceId: item,
|
|
1512
1554
|
phase: "open",
|
|
1513
1555
|
at: 0,
|
|
1514
|
-
announced: false
|
|
1556
|
+
announced: false,
|
|
1557
|
+
moved: false,
|
|
1558
|
+
lastMoveAt: 0
|
|
1515
1559
|
});
|
|
1516
1560
|
continue;
|
|
1517
1561
|
}
|
|
@@ -1525,7 +1569,9 @@ var NcAlarmExclusions = class {
|
|
|
1525
1569
|
deviceId,
|
|
1526
1570
|
phase,
|
|
1527
1571
|
at,
|
|
1528
|
-
announced: rec["announced"] === true
|
|
1572
|
+
announced: rec["announced"] === true,
|
|
1573
|
+
moved: false,
|
|
1574
|
+
lastMoveAt: 0
|
|
1529
1575
|
});
|
|
1530
1576
|
}
|
|
1531
1577
|
this.entries = out;
|
|
@@ -1616,7 +1662,8 @@ function modeDelaysFromPersisted(raw) {
|
|
|
1616
1662
|
}
|
|
1617
1663
|
if (item.notifyDuringEntryDelay !== void 0) kept.notifyDuringEntryDelay = item.notifyDuringEntryDelay;
|
|
1618
1664
|
if (item.notifySensorRewatched !== void 0) kept.notifySensorRewatched = item.notifySensorRewatched;
|
|
1619
|
-
if (
|
|
1665
|
+
if (item.notifySensorIgnored !== void 0) kept.notifySensorIgnored = item.notifySensorIgnored;
|
|
1666
|
+
if (kept.notifyDuringEntryDelay !== void 0 || kept.notifySensorRewatched !== void 0 || kept.notifySensorIgnored !== void 0 || kept.exitDelaySec !== void 0 || kept.entryDelaySec !== void 0 || kept.triggeredDurationSec !== void 0 || kept.postTriggeredAction !== void 0 || kept.schedule !== void 0) entries.push(kept);
|
|
1620
1667
|
}
|
|
1621
1668
|
return {
|
|
1622
1669
|
entries,
|
|
@@ -1827,6 +1874,10 @@ function alarmPushDecision(phase, heard) {
|
|
|
1827
1874
|
switch (phase.kind) {
|
|
1828
1875
|
case "none":
|
|
1829
1876
|
case "triggers-now": return { verdict: { kind: "plain" } };
|
|
1877
|
+
case "excluded": return { verdict: {
|
|
1878
|
+
kind: "suppress",
|
|
1879
|
+
reason: "alarm-sensor-excluded"
|
|
1880
|
+
} };
|
|
1830
1881
|
case "triggered": return { verdict: { kind: "intrusion" } };
|
|
1831
1882
|
case "starts-entry-delay": return heard.pending ? { verdict: {
|
|
1832
1883
|
kind: "suppress",
|
|
@@ -1869,17 +1920,6 @@ function intrusionDeviceOf(ref) {
|
|
|
1869
1920
|
//#endregion
|
|
1870
1921
|
//#region src/notification-center/alarm/alarm-transitions.ts
|
|
1871
1922
|
/**
|
|
1872
|
-
* How long a note handed to {@link NcAlarmAttribution.note} stays valid.
|
|
1873
|
-
*
|
|
1874
|
-
* The note is placed IMMEDIATELY before the `alarm-panel.trigger` call it
|
|
1875
|
-
* describes, in the same process, so the real gap is milliseconds. The bound
|
|
1876
|
-
* exists only so a sequence that failed before reaching its trigger step — or a
|
|
1877
|
-
* `wait` that outlived the incident — cannot leave an attribution lying around
|
|
1878
|
-
* for an unrelated manual trigger to pick up half an hour later.
|
|
1879
|
-
* Mis-attributing an alarm would put somebody else's photograph on it.
|
|
1880
|
-
*/
|
|
1881
|
-
var TRIGGER_ATTRIBUTION_TTL_MS = 3e4;
|
|
1882
|
-
/**
|
|
1883
1923
|
* How many followers one incident keeps (D705). The mosaic tiles at most
|
|
1884
1924
|
* `NC_GROUPED_MOSAIC_MAX_TILES` (6) and the sentence counts every member, so this
|
|
1885
1925
|
* bounds RAM and the frozen row, not the picture: a camera that keeps matching for
|
|
@@ -1895,88 +1935,41 @@ function subjectKeyOf(ref) {
|
|
|
1895
1935
|
* WHO the panel's current countdown belongs to.
|
|
1896
1936
|
*
|
|
1897
1937
|
* Small and separate because its whole content is a LIFETIME, and a lifetime
|
|
1898
|
-
* is what goes wrong: the
|
|
1899
|
-
*
|
|
1900
|
-
*
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
1938
|
+
* is what goes wrong: the trigger and the siren are up to `entryDelaySec`
|
|
1939
|
+
* apart, and in between the operator may disarm, re-arm, or press trigger by
|
|
1940
|
+
* hand. Each of those must drop the attribution, or tonight's alarm arrives
|
|
1941
|
+
* carrying last night's photograph.
|
|
1942
|
+
*
|
|
1943
|
+
* It holds NO pending press. A rule press carries its source IN the call
|
|
1944
|
+
* (`NcAlarmPanelDevice.triggerFromRule`), synchronously, so there is nothing
|
|
1945
|
+
* to hand over and nothing to overwrite. The single "next trigger is this
|
|
1946
|
+
* rule's" slot this class used to hold was written before an awaited cap call
|
|
1947
|
+
* and read when the call landed; two matches 1 ms apart overwrote it, one press
|
|
1948
|
+
* landed with no source, skipped the exclusion check and sounded the siren off
|
|
1949
|
+
* an excluded contact (2026-10-07 02:32Z, D735).
|
|
1903
1950
|
*/
|
|
1904
1951
|
var NcAlarmAttribution = class {
|
|
1905
|
-
noted = null;
|
|
1906
1952
|
claimed = null;
|
|
1907
1953
|
/** The incident's other subjects (D705) — replaced, never mutated. */
|
|
1908
1954
|
followed = [];
|
|
1909
|
-
/** "The next trigger is this rule's." Consumed by the trigger that follows:
|
|
1910
|
-
* {@link claim} when it is accepted, {@link follow} when an entry delay
|
|
1911
|
-
* already running refused it (D705), {@link dropPending} when an excluded
|
|
1912
|
-
* sensor made it. */
|
|
1913
|
-
note(source, now) {
|
|
1914
|
-
this.noted = {
|
|
1915
|
-
source,
|
|
1916
|
-
at: now
|
|
1917
|
-
};
|
|
1918
|
-
}
|
|
1919
|
-
/**
|
|
1920
|
-
* The note that has not been claimed yet — WITHOUT consuming it.
|
|
1921
|
-
*
|
|
1922
|
-
* The panel has to know which SENSOR is about to set the alarm off before it
|
|
1923
|
-
* decides whether to accept the trigger at all (an excluded contact does
|
|
1924
|
-
* not), and asking must not spend the note: a trigger that is accepted still
|
|
1925
|
-
* needs it to build the combined notification. Expired reads as absent, on
|
|
1926
|
-
* the same bound {@link claim} uses.
|
|
1927
|
-
*/
|
|
1928
|
-
pending(now) {
|
|
1929
|
-
const noted = this.noted;
|
|
1930
|
-
if (noted === null) return null;
|
|
1931
|
-
if (now - noted.at > TRIGGER_ATTRIBUTION_TTL_MS) return null;
|
|
1932
|
-
return noted.source;
|
|
1933
|
-
}
|
|
1934
|
-
/** Throw the un-claimed note away — the trigger it described was REFUSED, and
|
|
1935
|
-
* a note left behind would be inherited by the next real one. */
|
|
1936
|
-
dropPending() {
|
|
1937
|
-
this.noted = null;
|
|
1938
|
-
}
|
|
1939
1955
|
/**
|
|
1940
|
-
* Called by
|
|
1941
|
-
*
|
|
1942
|
-
* rather than inheriting it.
|
|
1956
|
+
* Called by EVERY accepted trigger with the source that pressed it — `null`
|
|
1957
|
+
* for a manual or external press, so it clears whatever a rule left behind
|
|
1958
|
+
* rather than inheriting it. A new countdown is a new incident: nothing the
|
|
1959
|
+
* last one collected rides it.
|
|
1943
1960
|
*/
|
|
1944
|
-
claim(
|
|
1945
|
-
|
|
1946
|
-
this.noted = null;
|
|
1961
|
+
claim(source) {
|
|
1962
|
+
this.claimed = source;
|
|
1947
1963
|
this.followed = [];
|
|
1948
|
-
if (noted === null) {
|
|
1949
|
-
this.claimed = null;
|
|
1950
|
-
return {
|
|
1951
|
-
source: null,
|
|
1952
|
-
expired: null
|
|
1953
|
-
};
|
|
1954
|
-
}
|
|
1955
|
-
if (now - noted.at > TRIGGER_ATTRIBUTION_TTL_MS) {
|
|
1956
|
-
this.claimed = null;
|
|
1957
|
-
return {
|
|
1958
|
-
source: null,
|
|
1959
|
-
expired: noted.source
|
|
1960
|
-
};
|
|
1961
|
-
}
|
|
1962
|
-
this.claimed = noted.source;
|
|
1963
|
-
return {
|
|
1964
|
-
source: noted.source,
|
|
1965
|
-
expired: null
|
|
1966
|
-
};
|
|
1967
1964
|
}
|
|
1968
1965
|
/**
|
|
1969
|
-
* A
|
|
1970
|
-
*
|
|
1971
|
-
* a note left behind would be claimed by the next, unrelated incident.
|
|
1966
|
+
* A rule press the panel REFUSED because its entry delay is already running
|
|
1967
|
+
* (D705): its source is another subject of the SAME incident.
|
|
1972
1968
|
*
|
|
1973
1969
|
* Only an incident a rule started collects followers: the combined row is the
|
|
1974
1970
|
* source rule's, and promoting a follower to source would change the sentence.
|
|
1975
1971
|
*/
|
|
1976
|
-
follow(
|
|
1977
|
-
const source = this.pending(now);
|
|
1978
|
-
this.noted = null;
|
|
1979
|
-
if (source === null) return { kind: "none" };
|
|
1972
|
+
follow(source) {
|
|
1980
1973
|
const claimed = this.claimed;
|
|
1981
1974
|
if (claimed === null) return {
|
|
1982
1975
|
kind: "no-incident",
|
|
@@ -2230,7 +2223,8 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
|
|
|
2230
2223
|
schedule: require_clip_model_registry.unknown().optional(),
|
|
2231
2224
|
/** D706 — the mode's notification switches; absent = off. */
|
|
2232
2225
|
notifyDuringEntryDelay: require_clip_model_registry.boolean().optional(),
|
|
2233
|
-
notifySensorRewatched: require_clip_model_registry.boolean().optional()
|
|
2226
|
+
notifySensorRewatched: require_clip_model_registry.boolean().optional(),
|
|
2227
|
+
notifySensorIgnored: require_clip_model_registry.boolean().optional()
|
|
2234
2228
|
})).default([]),
|
|
2235
2229
|
/**
|
|
2236
2230
|
* Active alarm configuration profile id. Empty / absent on a blob older than
|
|
@@ -2306,6 +2300,7 @@ function settingsBlobFromPersisted(raw) {
|
|
|
2306
2300
|
if (rec["schedule"] !== void 0) entry.schedule = rec["schedule"];
|
|
2307
2301
|
if (typeof rec["notifyDuringEntryDelay"] === "boolean") entry.notifyDuringEntryDelay = rec["notifyDuringEntryDelay"];
|
|
2308
2302
|
if (typeof rec["notifySensorRewatched"] === "boolean") entry.notifySensorRewatched = rec["notifySensorRewatched"];
|
|
2303
|
+
if (typeof rec["notifySensorIgnored"] === "boolean") entry.notifySensorIgnored = rec["notifySensorIgnored"];
|
|
2309
2304
|
modeDelaysLoose.push(entry);
|
|
2310
2305
|
}
|
|
2311
2306
|
const truncatedTriggered = Math.max(0, Math.min(3600, Math.trunc(triggered)));
|
|
@@ -2882,21 +2877,26 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2882
2877
|
this.onPublish = hook;
|
|
2883
2878
|
}
|
|
2884
2879
|
/**
|
|
2885
|
-
*
|
|
2880
|
+
* A RULE pressed the panel, and says which match it was — in the call itself.
|
|
2886
2881
|
*
|
|
2887
2882
|
* The join between a rule's notification and the alarm's. `alarm-panel.trigger`
|
|
2888
2883
|
* takes nothing but a device id — by design, since a Home Assistant panel
|
|
2889
|
-
* provides the same cap — so
|
|
2890
|
-
*
|
|
2891
|
-
*
|
|
2892
|
-
*
|
|
2893
|
-
*
|
|
2884
|
+
* provides the same cap — so a rule press on THIS panel never goes through the
|
|
2885
|
+
* cap: the notification centre calls this method, in-process and
|
|
2886
|
+
* synchronously, with the match that pressed it. The source is never parked
|
|
2887
|
+
* anywhere for a later call to pick up: a single "next trigger is this rule's"
|
|
2888
|
+
* slot, written before an awaited cap call and read when it landed, was
|
|
2889
|
+
* overwritten by a second match 1 ms later, and the press that found it empty
|
|
2890
|
+
* skipped the exclusion check and sounded the siren off an excluded contact
|
|
2891
|
+
* (2026-10-07 02:32Z, D735).
|
|
2892
|
+
*
|
|
2893
|
+
* The FIRST accepted press claims the source and owns the countdown; a rule
|
|
2894
2894
|
* pressing again while the entry delay runs is refused as a trigger and its
|
|
2895
|
-
*
|
|
2895
|
+
* source becomes a FOLLOWER of the same incident (D705) — the triggered
|
|
2896
2896
|
* notification tiles the source and every follower as one mosaic.
|
|
2897
2897
|
*/
|
|
2898
|
-
|
|
2899
|
-
this.
|
|
2898
|
+
triggerFromRule(source, now = Date.now()) {
|
|
2899
|
+
return this.press(source, now);
|
|
2900
2900
|
}
|
|
2901
2901
|
/**
|
|
2902
2902
|
* Where the alarm stands for ONE rule match, read BEFORE that match presses the panel
|
|
@@ -2905,14 +2905,19 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2905
2905
|
* sensor is ignored, a running countdown or a triggered alarm refuses the press, an armed
|
|
2906
2906
|
* panel starts the countdown (or fires at once when the mode's entry delay is 0).
|
|
2907
2907
|
*
|
|
2908
|
-
*
|
|
2909
|
-
*
|
|
2910
|
-
*
|
|
2911
|
-
*
|
|
2912
|
-
*
|
|
2908
|
+
* The subject that STARTED the countdown is still its starter on its later matches (a
|
|
2909
|
+
* person who stays in view is not "another intrusion during the delay"). A rule press
|
|
2910
|
+
* lands synchronously ({@link triggerFromRule}), so the machine this reads already holds
|
|
2911
|
+
* every earlier press of the same evaluation — there is no press "in flight" to guess at.
|
|
2912
|
+
*
|
|
2913
|
+
* A match of a sensor the current arm EXCLUDES is `excluded`: its press will be ignored,
|
|
2914
|
+
* and the rule's own push is not sent either (D735) — the panel's bypass logic speaks for it.
|
|
2913
2915
|
*/
|
|
2914
2916
|
classifyRuleMatch(ref, now = Date.now()) {
|
|
2915
|
-
if (this.exclusions.blocks(intrusionDeviceOf(ref))) return {
|
|
2917
|
+
if (this.exclusions.blocks(intrusionDeviceOf(ref))) return {
|
|
2918
|
+
kind: "excluded",
|
|
2919
|
+
sensorDeviceId: intrusionDeviceOf(ref)
|
|
2920
|
+
};
|
|
2916
2921
|
const mode = armModeOf(this.machine.target, NC_ALARM_MODES);
|
|
2917
2922
|
if (mode === null) return { kind: "none" };
|
|
2918
2923
|
const state = stateAt(this.machine, now);
|
|
@@ -2934,17 +2939,7 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2934
2939
|
};
|
|
2935
2940
|
}
|
|
2936
2941
|
if (!isArmed(this.machine, now)) return { kind: "none" };
|
|
2937
|
-
|
|
2938
|
-
const inFlight = this.attribution.pending(now);
|
|
2939
|
-
if (inFlight !== null && subjectKeyOf(inFlight) !== key) return immediate ? {
|
|
2940
|
-
kind: "triggered",
|
|
2941
|
-
mode
|
|
2942
|
-
} : {
|
|
2943
|
-
kind: "entry-delay",
|
|
2944
|
-
mode,
|
|
2945
|
-
notify: this.notifyPolicy(mode).notifyDuringEntryDelay
|
|
2946
|
-
};
|
|
2947
|
-
return immediate ? {
|
|
2942
|
+
return this.delaysFor(mode).entryDelaySec === 0 ? {
|
|
2948
2943
|
kind: "triggers-now",
|
|
2949
2944
|
mode
|
|
2950
2945
|
} : {
|
|
@@ -3125,7 +3120,7 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
3125
3120
|
* sensor is already lifted when the rule it fires reaches the panel.
|
|
3126
3121
|
*/
|
|
3127
3122
|
observeOpening(deviceId, reading, now = Date.now(), options = {}) {
|
|
3128
|
-
const change = this.exclusions.observe(deviceId, reading, now);
|
|
3123
|
+
const change = this.exclusions.observe(deviceId, reading, now, options.changedAt);
|
|
3129
3124
|
if (change === "unchanged") return "unchanged";
|
|
3130
3125
|
this.persist();
|
|
3131
3126
|
if (change === "closed") return "closed";
|
|
@@ -3175,13 +3170,12 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
3175
3170
|
event: "lifted"
|
|
3176
3171
|
}, now);
|
|
3177
3172
|
}
|
|
3178
|
-
/** Did THIS sensor start the running entry delay
|
|
3173
|
+
/** Did THIS sensor start the running entry delay? A rule press lands synchronously, so
|
|
3174
|
+
* a press of the evaluation that lifted it has already landed when this is asked. */
|
|
3179
3175
|
startsEntryDelay(deviceId, now) {
|
|
3180
|
-
|
|
3181
|
-
const
|
|
3182
|
-
|
|
3183
|
-
if (!isArmed(this.machine, now) || state === "triggered") return false;
|
|
3184
|
-
return sensorOf(this.attribution.pending(now)) === deviceId;
|
|
3176
|
+
if (stateAt(this.machine, now) !== "pending") return false;
|
|
3177
|
+
const starter = this.attribution.current();
|
|
3178
|
+
return starter !== null && intrusionDeviceOf(starter) === deviceId;
|
|
3185
3179
|
}
|
|
3186
3180
|
/**
|
|
3187
3181
|
* A DELETED device leaves the current arm's exclusions: it can never report
|
|
@@ -3407,64 +3401,78 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
3407
3401
|
},
|
|
3408
3402
|
disarm: async () => this.disarm(),
|
|
3409
3403
|
trigger: async () => {
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3404
|
+
this.press(null, Date.now());
|
|
3405
|
+
}
|
|
3406
|
+
});
|
|
3407
|
+
}
|
|
3408
|
+
/**
|
|
3409
|
+
* THE ENTRY DELAY LIVES OR DIES HERE. The one body every press runs — a rule's
|
|
3410
|
+
* ({@link triggerFromRule}, with its source) and a person's (the cap, `null`).
|
|
3411
|
+
*
|
|
3412
|
+
* It used to set `fired: true` outright, calling itself "a panic button", while
|
|
3413
|
+
* the entry-delay transition sat in a method NO caller ever reached — so the
|
|
3414
|
+
* panel shipped with `pending` unreachable. The doc comment describing the right
|
|
3415
|
+
* design is precisely what made it look implemented.
|
|
3416
|
+
*
|
|
3417
|
+
* Synchronous, end to end: the source is a parameter, never a slot, so two
|
|
3418
|
+
* presses cannot exchange their sources however close together they land.
|
|
3419
|
+
*/
|
|
3420
|
+
press(source, now) {
|
|
3421
|
+
const sensor = source !== null ? intrusionDeviceOf(source) : void 0;
|
|
3422
|
+
if (sensor !== void 0 && this.exclusions.blocks(sensor)) {
|
|
3423
|
+
const moved = this.exclusions.takeMoved(sensor);
|
|
3424
|
+
this.ctx.logger.info("alarm trigger ignored — the sensor was open when the alarm was armed", {
|
|
3425
|
+
tags: { deviceId: sensor },
|
|
3426
|
+
meta: {
|
|
3427
|
+
panelDeviceId: this.id,
|
|
3428
|
+
ruleId: source?.ruleId,
|
|
3429
|
+
rule: source?.ruleName,
|
|
3430
|
+
state: stateAt(this.machine, now),
|
|
3431
|
+
moved
|
|
3426
3432
|
}
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3433
|
+
});
|
|
3434
|
+
this.announceIgnoredTrigger(sensor, now, moved);
|
|
3435
|
+
return "ignored-excluded";
|
|
3436
|
+
}
|
|
3437
|
+
const armed = armModeOf(this.machine.target, NC_ALARM_MODES);
|
|
3438
|
+
const resolved = this.delaysFor(armed);
|
|
3439
|
+
const next = trigger(this.machine, resolved, now);
|
|
3440
|
+
if (next === this.machine) {
|
|
3441
|
+
const state = stateAt(this.machine, now);
|
|
3442
|
+
this.ctx.logger.info(ignoredTriggerMessage(state), {
|
|
3443
|
+
tags: { deviceId: this.id },
|
|
3444
|
+
meta: {
|
|
3445
|
+
state,
|
|
3446
|
+
...source !== null ? {
|
|
3447
|
+
ruleId: source.ruleId,
|
|
3448
|
+
rule: source.ruleName
|
|
3449
|
+
} : {}
|
|
3442
3450
|
}
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
});
|
|
3465
|
-
this.publish(now);
|
|
3451
|
+
});
|
|
3452
|
+
if (state === "pending" && source !== null) this.reportFollow(this.attribution.follow(source));
|
|
3453
|
+
return "refused";
|
|
3454
|
+
}
|
|
3455
|
+
this.attribution.claim(source);
|
|
3456
|
+
this.machine = next;
|
|
3457
|
+
this.ctx.logger.info("alarm delays applied", {
|
|
3458
|
+
tags: { deviceId: this.id },
|
|
3459
|
+
meta: {
|
|
3460
|
+
mode: armed,
|
|
3461
|
+
phase: "trigger",
|
|
3462
|
+
entryDelaySec: resolved.entryDelaySec,
|
|
3463
|
+
triggeredDurationSec: resolved.triggeredDurationSec,
|
|
3464
|
+
source: {
|
|
3465
|
+
entry: resolved.source.entryDelaySec,
|
|
3466
|
+
siren: resolved.source.triggeredDurationSec
|
|
3467
|
+
},
|
|
3468
|
+
...source !== null ? {
|
|
3469
|
+
ruleId: source.ruleId,
|
|
3470
|
+
rule: source.ruleName
|
|
3471
|
+
} : { by: "manual" }
|
|
3466
3472
|
}
|
|
3467
3473
|
});
|
|
3474
|
+
this.publish(now);
|
|
3475
|
+
return "accepted";
|
|
3468
3476
|
}
|
|
3469
3477
|
status(now = Date.now()) {
|
|
3470
3478
|
return {
|
|
@@ -3545,7 +3553,6 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
3545
3553
|
* stated: the drop is work the operator would otherwise never see go missing.
|
|
3546
3554
|
*/
|
|
3547
3555
|
reportFollow(outcome) {
|
|
3548
|
-
if (outcome.kind === "none") return;
|
|
3549
3556
|
const source = outcome.source;
|
|
3550
3557
|
const meta = {
|
|
3551
3558
|
panelDeviceId: this.id,
|
|
@@ -3574,13 +3581,15 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
3574
3581
|
});
|
|
3575
3582
|
}
|
|
3576
3583
|
/**
|
|
3577
|
-
*
|
|
3578
|
-
*
|
|
3579
|
-
*
|
|
3580
|
-
*
|
|
3581
|
-
*
|
|
3584
|
+
* "Ignored" is TOLD only when the excluded sensor really MOVED while armed and the
|
|
3585
|
+
* mode's `notifySensorIgnored` switch is on (D735, default off) — and then ONCE per
|
|
3586
|
+
* sensor per arm (the mark rides the persisted exclusion), and only while the panel is
|
|
3587
|
+
* armed: during the exit delay `trigger()` would have dropped it anyway, so the
|
|
3588
|
+
* exclusion is not what ignored it. A contact that was open all along and was merely
|
|
3589
|
+
* re-published (a provider reconnect, a Zigbee2MQTT restart) moved nothing, and four
|
|
3590
|
+
* such pushes reached the operator in 60 ms on 2026-10-07. Every refusal is a line.
|
|
3582
3591
|
*/
|
|
3583
|
-
announceIgnoredTrigger(deviceId, now) {
|
|
3592
|
+
announceIgnoredTrigger(deviceId, now, moved) {
|
|
3584
3593
|
const state = stateAt(this.machine, now);
|
|
3585
3594
|
if (!isArmed(this.machine, now)) {
|
|
3586
3595
|
this.ctx.logger.debug("alarm bypass not announced — the panel is not armed yet", {
|
|
@@ -3602,6 +3611,29 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
3602
3611
|
});
|
|
3603
3612
|
return;
|
|
3604
3613
|
}
|
|
3614
|
+
if (!moved) {
|
|
3615
|
+
this.ctx.logger.debug("alarm bypass not announced — the sensor did not move since the arm", {
|
|
3616
|
+
tags: { deviceId },
|
|
3617
|
+
meta: {
|
|
3618
|
+
panelDeviceId: this.id,
|
|
3619
|
+
state,
|
|
3620
|
+
reason: "not-moved"
|
|
3621
|
+
}
|
|
3622
|
+
});
|
|
3623
|
+
return;
|
|
3624
|
+
}
|
|
3625
|
+
const mode = armModeOf(this.machine.target, NC_ALARM_MODES);
|
|
3626
|
+
if (mode === null || !this.notifyPolicy(mode).notifySensorIgnored) {
|
|
3627
|
+
this.ctx.logger.info("alarm \"ignored\" not notified — the mode does not notify it", {
|
|
3628
|
+
tags: { deviceId },
|
|
3629
|
+
meta: {
|
|
3630
|
+
panelDeviceId: this.id,
|
|
3631
|
+
mode,
|
|
3632
|
+
reason: "ignored-notify-off"
|
|
3633
|
+
}
|
|
3634
|
+
});
|
|
3635
|
+
return;
|
|
3636
|
+
}
|
|
3605
3637
|
if (!this.exclusions.markAnnounced(deviceId)) {
|
|
3606
3638
|
this.ctx.logger.debug("alarm bypass already announced in this arm — repeat not sent", {
|
|
3607
3639
|
tags: { deviceId },
|
|
@@ -3759,7 +3791,7 @@ function ncAlarmPanelPort(panel) {
|
|
|
3759
3791
|
deleteProfile: (id) => panel.deleteProfile(id),
|
|
3760
3792
|
activateProfile: (id) => panel.activateProfile(id),
|
|
3761
3793
|
availableModes: () => NC_ALARM_MODES,
|
|
3762
|
-
|
|
3794
|
+
triggerFromRule: (source) => panel.triggerFromRule(source),
|
|
3763
3795
|
classifyRuleMatch: (source) => panel.classifyRuleMatch(source),
|
|
3764
3796
|
announceLifted: (deviceId, now) => panel.announceLifted(deviceId, now),
|
|
3765
3797
|
excludedDevices: () => panel.excludedDevices(),
|
|
@@ -4666,22 +4698,26 @@ var HUB_API_PORT = 4443;
|
|
|
4666
4698
|
* preferred LAN interface priority `0`, so a public candidate has to sit below
|
|
4667
4699
|
* zero to outrank it.
|
|
4668
4700
|
*/
|
|
4669
|
-
var
|
|
4701
|
+
var PRIORITY_PUBLIC_ENDPOINT = -400;
|
|
4670
4702
|
var PRIORITY_CONNECTED = -200;
|
|
4671
4703
|
var PRIORITY_EXTERNAL_LIST = -100;
|
|
4672
4704
|
/**
|
|
4673
|
-
* Build the ranked candidate list
|
|
4674
|
-
*
|
|
4675
|
-
* then
|
|
4676
|
-
*
|
|
4705
|
+
* Build the ranked candidate list for every link a notification carries
|
|
4706
|
+
* (buttons, attachments / media, admin links): the hub's PUBLIC ENDPOINT
|
|
4707
|
+
* (D734), then — only reached when that is unknown — the connected external
|
|
4708
|
+
* ingress (tunnels), then the LAN addresses. Every source is best-effort — one
|
|
4709
|
+
* that throws contributes nothing rather than costing the whole list.
|
|
4710
|
+
*
|
|
4711
|
+
* The notification-taps address is deliberately NOT an input: it decides only
|
|
4712
|
+
* where a tap opens, and its default (the app link) is no hub address.
|
|
4677
4713
|
*/
|
|
4678
4714
|
async function collectArtifactEndpoints(sources) {
|
|
4679
4715
|
const out = [];
|
|
4680
|
-
const
|
|
4681
|
-
if (
|
|
4682
|
-
baseUrl:
|
|
4716
|
+
const publicEndpoint = sources.publicEndpoint;
|
|
4717
|
+
if (publicEndpoint !== void 0 && publicEndpoint !== "" && !require_clip_model_registry.isAppLinkBase(publicEndpoint)) out.push({
|
|
4718
|
+
baseUrl: publicEndpoint,
|
|
4683
4719
|
kind: "public",
|
|
4684
|
-
priority:
|
|
4720
|
+
priority: PRIORITY_PUBLIC_ENDPOINT
|
|
4685
4721
|
});
|
|
4686
4722
|
try {
|
|
4687
4723
|
const connected = await sources.getConnected();
|
|
@@ -4916,6 +4952,49 @@ var NcArtifactPlane = class {
|
|
|
4916
4952
|
}
|
|
4917
4953
|
};
|
|
4918
4954
|
//#endregion
|
|
4955
|
+
//#region src/notification-center/link-endpoints.ts
|
|
4956
|
+
async function publicEndpointAddress(api, logger) {
|
|
4957
|
+
try {
|
|
4958
|
+
const { roles } = await api.localNetwork.getAddresses.query({});
|
|
4959
|
+
return roles.publicEndpoint.resolved ?? void 0;
|
|
4960
|
+
} catch (err) {
|
|
4961
|
+
logger.warn("nc links: public endpoint unreadable — using the ingress ladder", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
4962
|
+
return;
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
/** The ranked base-URL candidates for a notification's non-tap links. */
|
|
4966
|
+
async function notificationLinkEndpoints(api, logger) {
|
|
4967
|
+
return collectArtifactEndpoints({
|
|
4968
|
+
publicEndpoint: await publicEndpointAddress(api, logger),
|
|
4969
|
+
getConnected: async () => {
|
|
4970
|
+
const status = await api.networkAccess.getStatus.query();
|
|
4971
|
+
logger.debug("artifact base-url: connected ingress", { meta: {
|
|
4972
|
+
connected: status.connected,
|
|
4973
|
+
url: status.endpoint?.url ?? null,
|
|
4974
|
+
protocol: status.endpoint?.protocol ?? null
|
|
4975
|
+
} });
|
|
4976
|
+
return status.connected && status.endpoint !== null ? {
|
|
4977
|
+
url: status.endpoint.url,
|
|
4978
|
+
protocol: status.endpoint.protocol
|
|
4979
|
+
} : null;
|
|
4980
|
+
},
|
|
4981
|
+
listExternal: async () => {
|
|
4982
|
+
return (await api.networkAccess.listEndpoints.query()).map((e) => ({
|
|
4983
|
+
url: e.url,
|
|
4984
|
+
protocol: e.protocol
|
|
4985
|
+
}));
|
|
4986
|
+
},
|
|
4987
|
+
listLan: async (port) => {
|
|
4988
|
+
return (await api.localNetwork.getConnectionEndpoints.query({ port })).endpoints.flatMap((e) => e.kind === "app-link" || e.kind === "custom" ? [] : [{
|
|
4989
|
+
baseUrl: e.baseUrl,
|
|
4990
|
+
kind: e.kind,
|
|
4991
|
+
priority: e.priority
|
|
4992
|
+
}]);
|
|
4993
|
+
},
|
|
4994
|
+
logger
|
|
4995
|
+
});
|
|
4996
|
+
}
|
|
4997
|
+
//#endregion
|
|
4919
4998
|
//#region src/notification-center/artifact-read-plane.ts
|
|
4920
4999
|
/**
|
|
4921
5000
|
* Parse a single-range `Range` header against a known length.
|
|
@@ -5372,158 +5451,6 @@ function mimeFromExtension(file) {
|
|
|
5372
5451
|
return "application/octet-stream";
|
|
5373
5452
|
}
|
|
5374
5453
|
//#endregion
|
|
5375
|
-
//#region src/notification-center/system-event-tap-url.ts
|
|
5376
|
-
/** The admin UI's own routes, as `packages/addon-admin-ui/src/App.tsx` declares them. */
|
|
5377
|
-
var ADMIN_ADDONS_PATH = "/system/addons";
|
|
5378
|
-
var ADMIN_CLUSTER_PATH = "/system/cluster";
|
|
5379
|
-
var ADMIN_DEVICES_PATH = "/devices";
|
|
5380
|
-
/**
|
|
5381
|
-
* The query parameter naming a node.
|
|
5382
|
-
*
|
|
5383
|
-
* One spelling for both admin pages, mirrored by `NODE_PARAM` in
|
|
5384
|
-
* `packages/addon-admin-ui/src/lib/node-url-selection.ts` and pinned across
|
|
5385
|
-
* the two packages by `scripts/check-notification-tap-targets.ts`.
|
|
5386
|
-
*/
|
|
5387
|
-
var NODE_PARAM = "node";
|
|
5388
|
-
/** The device page's top-level tab parameter, read by `DeviceDetail`. */
|
|
5389
|
-
var TAB_PARAM = "tab";
|
|
5390
|
-
/** The Stream Broker tab id — a `WELL_KNOWN_TABS` member, hoisted `top-tab`. */
|
|
5391
|
-
var STREAM_BROKER_TAB = "stream-broker";
|
|
5392
|
-
/** `DeviceType.Camera`. Spelled here rather than imported so this stays a pure
|
|
5393
|
-
* string map with no device-model dependency; the guard pins it. */
|
|
5394
|
-
var CAMERA_DEVICE_TYPE = "camera";
|
|
5395
|
-
/** Kinds whose action is "go and apply an ADDON update". */
|
|
5396
|
-
var ADDON_UPDATE_KINDS = new Set(["addon-update-available"]);
|
|
5397
|
-
/** Kinds whose action is "go and apply a NODE's own (`@camstack/server`) update". */
|
|
5398
|
-
var NODE_UPDATE_KINDS = new Set(["server-update-available", "wrapper-update-available"]);
|
|
5399
|
-
/**
|
|
5400
|
-
* Kinds whose action is "go and look at this device's stream".
|
|
5401
|
-
*
|
|
5402
|
-
* BOTH DIRECTIONS. A recovery lands on the same page as the failure it undoes,
|
|
5403
|
-
* because "did it really come back, and what did it do while it was gone" is
|
|
5404
|
-
* an action and the page that answers it is the one the down-notification
|
|
5405
|
-
* already opened. On 2026-08-30 the operator's phone showed "Il dispositivo
|
|
5406
|
-
* Videocamera sala feste è online" with `tapAction.value: null` — the same
|
|
5407
|
-
* camera's `device-offline` an hour earlier opened the Stream Broker tab, and
|
|
5408
|
-
* the recovery opened nothing, purely because `device-online` had no entry
|
|
5409
|
-
* here.
|
|
5410
|
-
*
|
|
5411
|
-
* The legacy `camera-*` pair is here for the same reason both are still in the
|
|
5412
|
-
* enum: outbox and history rows enqueued before the rename carry them, and an
|
|
5413
|
-
* old row is exactly the one whose action has to keep working.
|
|
5414
|
-
*
|
|
5415
|
-
* Kinds NOT here are kinds whose DOWN direction has no action either
|
|
5416
|
-
* (`node-online`/`node-offline`, `device-enabled`/`device-disabled`) — a
|
|
5417
|
-
* recovery does not earn a destination its failure was never given.
|
|
5418
|
-
*/
|
|
5419
|
-
var DEVICE_STREAM_KINDS = new Set([
|
|
5420
|
-
"device-offline",
|
|
5421
|
-
"device-online",
|
|
5422
|
-
"stream-offline",
|
|
5423
|
-
"stream-online",
|
|
5424
|
-
"camera-offline",
|
|
5425
|
-
"camera-online"
|
|
5426
|
-
]);
|
|
5427
|
-
/**
|
|
5428
|
-
* Kinds whose subject is a camera BY CONSTRUCTION, whatever the row says.
|
|
5429
|
-
*
|
|
5430
|
-
* A stream belongs to a camera and nothing else. The legacy `camera-*` pair is
|
|
5431
|
-
* too — that name is precisely the claim it made, and it is why the kind was
|
|
5432
|
-
* split into `device-*` + a `deviceTypes` condition.
|
|
5433
|
-
*/
|
|
5434
|
-
var CAMERA_BY_CONSTRUCTION_KINDS = new Set([
|
|
5435
|
-
"stream-offline",
|
|
5436
|
-
"stream-online",
|
|
5437
|
-
"camera-offline",
|
|
5438
|
-
"camera-online"
|
|
5439
|
-
]);
|
|
5440
|
-
/**
|
|
5441
|
-
* Where a per-section STATE SUMMARY row lands (D708). The row spans nodes, so
|
|
5442
|
-
* no `?node=`: the unscoped page is the honest one. A section absent here taps
|
|
5443
|
-
* through its kind, like any other row — a device section carries the system
|
|
5444
|
-
* placeholder device and gets no link, and the crash-looped addons have none
|
|
5445
|
-
* (their instant kind has none either).
|
|
5446
|
-
*
|
|
5447
|
-
* `nodes` is the one destination no kind gives: a node going offline has no
|
|
5448
|
-
* instant action, but "which nodes are in trouble" is answered by the
|
|
5449
|
-
* Cluster page, and the summary row is about all of them at once.
|
|
5450
|
-
*/
|
|
5451
|
-
var SUMMARY_SECTION_PATHS = {
|
|
5452
|
-
nodes: ADMIN_CLUSTER_PATH,
|
|
5453
|
-
"addon-updates": ADMIN_ADDONS_PATH,
|
|
5454
|
-
updates: ADMIN_CLUSTER_PATH
|
|
5455
|
-
};
|
|
5456
|
-
function withParam(path, key, value) {
|
|
5457
|
-
if (value === void 0 || value.length === 0) return path;
|
|
5458
|
-
return `${path}?${key}=${encodeURIComponent(value)}`;
|
|
5459
|
-
}
|
|
5460
|
-
/**
|
|
5461
|
-
* The admin path (with its query) a system event taps through to, or `null`
|
|
5462
|
-
* when the kind has no action.
|
|
5463
|
-
*
|
|
5464
|
-
* Exported for the cross-package guard and for tests that want the path
|
|
5465
|
-
* without a base; the dispatcher goes through {@link systemEventTapUrl}.
|
|
5466
|
-
*/
|
|
5467
|
-
function systemEventTapPath(subject) {
|
|
5468
|
-
const sectionPath = subject.summarySection !== void 0 ? SUMMARY_SECTION_PATHS[subject.summarySection] : void 0;
|
|
5469
|
-
if (sectionPath !== void 0) return sectionPath;
|
|
5470
|
-
if (ADDON_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_ADDONS_PATH, NODE_PARAM, subject.nodeId);
|
|
5471
|
-
if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_CLUSTER_PATH, NODE_PARAM, subject.nodeId);
|
|
5472
|
-
if (DEVICE_STREAM_KINDS.has(subject.kind)) {
|
|
5473
|
-
const deviceId = subject.deviceId;
|
|
5474
|
-
if (deviceId === void 0 || !Number.isInteger(deviceId) || deviceId <= 0) return null;
|
|
5475
|
-
const devicePath = `${ADMIN_DEVICES_PATH}/${String(deviceId)}`;
|
|
5476
|
-
return CAMERA_BY_CONSTRUCTION_KINDS.has(subject.kind) || subject.deviceType === CAMERA_DEVICE_TYPE ? withParam(devicePath, TAB_PARAM, STREAM_BROKER_TAB) : devicePath;
|
|
5477
|
-
}
|
|
5478
|
-
return null;
|
|
5479
|
-
}
|
|
5480
|
-
/** Full URL, or `null` when the kind has no action or no base is reachable. */
|
|
5481
|
-
function systemEventTapUrl(subject, baseUrl) {
|
|
5482
|
-
if (baseUrl === null || baseUrl.length === 0) return null;
|
|
5483
|
-
const path = systemEventTapPath(subject);
|
|
5484
|
-
if (path === null) return null;
|
|
5485
|
-
return `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
5486
|
-
}
|
|
5487
|
-
//#endregion
|
|
5488
|
-
//#region src/notification-center/default-tap-url.ts
|
|
5489
|
-
/**
|
|
5490
|
-
* The app-link choice from the flagged notification address — present when
|
|
5491
|
-
* the effective address is the app link. Reads the answer through
|
|
5492
|
-
* `viewerLinkBaseOf`, the same reading the Network page's preview uses.
|
|
5493
|
-
*/
|
|
5494
|
-
function ncAppLinkChoiceOf(endpoint) {
|
|
5495
|
-
const base = require_clip_model_registry.viewerLinkBaseOf(endpoint);
|
|
5496
|
-
return base.form === "app" ? { hubBaseUrl: base.hubBaseUrl } : null;
|
|
5497
|
-
}
|
|
5498
|
-
/**
|
|
5499
|
-
* The tap URL, or null. In the app form a camera link is minted even with no
|
|
5500
|
-
* `hub=` — the central page reaches the hub through the app's (or the
|
|
5501
|
-
* browser's) own stored hub (central-link design §5). The hub form and every
|
|
5502
|
-
* admin destination still need a base.
|
|
5503
|
-
*/
|
|
5504
|
-
function defaultTapUrl(input) {
|
|
5505
|
-
const baseUrl = input.baseUrl === null || input.baseUrl.length === 0 ? null : input.baseUrl;
|
|
5506
|
-
if (input.systemEvent !== void 0) return baseUrl === null ? null : systemEventTapUrl(input.systemEvent, baseUrl);
|
|
5507
|
-
const destination = {
|
|
5508
|
-
kind: "camera",
|
|
5509
|
-
deviceId: input.deviceId,
|
|
5510
|
-
atMs: input.timestampMs
|
|
5511
|
-
};
|
|
5512
|
-
return input.appLink !== void 0 ? require_clip_model_registry.buildViewerLink({
|
|
5513
|
-
base: {
|
|
5514
|
-
form: "app",
|
|
5515
|
-
hubBaseUrl: input.appLink.hubBaseUrl
|
|
5516
|
-
},
|
|
5517
|
-
destination
|
|
5518
|
-
}) : require_clip_model_registry.buildViewerLink({
|
|
5519
|
-
base: {
|
|
5520
|
-
form: "hub",
|
|
5521
|
-
hubBaseUrl: baseUrl
|
|
5522
|
-
},
|
|
5523
|
-
destination
|
|
5524
|
-
});
|
|
5525
|
-
}
|
|
5526
|
-
//#endregion
|
|
5527
5454
|
//#region src/notification-center/prune-windows.ts
|
|
5528
5455
|
/**
|
|
5529
5456
|
* The operator-configurable prune windows, in days. One module, because the
|
|
@@ -14941,7 +14868,8 @@ var NcGroupMemberFateSchema = require_clip_model_registry.union([require_clip_mo
|
|
|
14941
14868
|
"already-existed",
|
|
14942
14869
|
"close-failed",
|
|
14943
14870
|
"alarm-entry-delay-started",
|
|
14944
|
-
"alarm-entry-delay"
|
|
14871
|
+
"alarm-entry-delay",
|
|
14872
|
+
"alarm-sensor-excluded"
|
|
14945
14873
|
]), require_clip_model_registry.templateLiteral(["failed-condition:", require_clip_model_registry.string()])]);
|
|
14946
14874
|
/** Read cap, so a client cannot ask for the whole ring in one call. */
|
|
14947
14875
|
var MAX_QUERY_LIMIT = 200;
|
|
@@ -23060,6 +22988,143 @@ function withSummaryBody(rule, section) {
|
|
|
23060
22988
|
};
|
|
23061
22989
|
}
|
|
23062
22990
|
//#endregion
|
|
22991
|
+
//#region src/notification-center/system-event-tap-url.ts
|
|
22992
|
+
/** The admin UI's own routes, as `packages/addon-admin-ui/src/App.tsx` declares them. */
|
|
22993
|
+
var ADMIN_ADDONS_PATH = "/system/addons";
|
|
22994
|
+
var ADMIN_CLUSTER_PATH = "/system/cluster";
|
|
22995
|
+
var ADMIN_DEVICES_PATH = "/devices";
|
|
22996
|
+
/**
|
|
22997
|
+
* The query parameter naming a node.
|
|
22998
|
+
*
|
|
22999
|
+
* One spelling for both admin pages, mirrored by `NODE_PARAM` in
|
|
23000
|
+
* `packages/addon-admin-ui/src/lib/node-url-selection.ts` and pinned across
|
|
23001
|
+
* the two packages by `scripts/check-notification-tap-targets.ts`.
|
|
23002
|
+
*/
|
|
23003
|
+
var NODE_PARAM = "node";
|
|
23004
|
+
/** The device page's top-level tab parameter, read by `DeviceDetail`. */
|
|
23005
|
+
var TAB_PARAM = "tab";
|
|
23006
|
+
/** The Stream Broker tab id — a `WELL_KNOWN_TABS` member, hoisted `top-tab`. */
|
|
23007
|
+
var STREAM_BROKER_TAB = "stream-broker";
|
|
23008
|
+
/** `DeviceType.Camera`. Spelled here rather than imported so this stays a pure
|
|
23009
|
+
* string map with no device-model dependency; the guard pins it. */
|
|
23010
|
+
var CAMERA_DEVICE_TYPE = "camera";
|
|
23011
|
+
/** Kinds whose action is "go and apply an ADDON update". */
|
|
23012
|
+
var ADDON_UPDATE_KINDS = new Set(["addon-update-available"]);
|
|
23013
|
+
/** Kinds whose action is "go and apply a NODE's own (`@camstack/server`) update". */
|
|
23014
|
+
var NODE_UPDATE_KINDS = new Set(["server-update-available", "wrapper-update-available"]);
|
|
23015
|
+
/**
|
|
23016
|
+
* Kinds whose action is "go and look at this device's stream".
|
|
23017
|
+
*
|
|
23018
|
+
* BOTH DIRECTIONS. A recovery lands on the same page as the failure it undoes,
|
|
23019
|
+
* because "did it really come back, and what did it do while it was gone" is
|
|
23020
|
+
* an action and the page that answers it is the one the down-notification
|
|
23021
|
+
* already opened. On 2026-08-30 the operator's phone showed "Il dispositivo
|
|
23022
|
+
* Videocamera sala feste è online" with `tapAction.value: null` — the same
|
|
23023
|
+
* camera's `device-offline` an hour earlier opened the Stream Broker tab, and
|
|
23024
|
+
* the recovery opened nothing, purely because `device-online` had no entry
|
|
23025
|
+
* here.
|
|
23026
|
+
*
|
|
23027
|
+
* The legacy `camera-*` pair is here for the same reason both are still in the
|
|
23028
|
+
* enum: outbox and history rows enqueued before the rename carry them, and an
|
|
23029
|
+
* old row is exactly the one whose action has to keep working.
|
|
23030
|
+
*
|
|
23031
|
+
* Kinds NOT here are kinds whose DOWN direction has no action either
|
|
23032
|
+
* (`node-online`/`node-offline`, `device-enabled`/`device-disabled`) — a
|
|
23033
|
+
* recovery does not earn a destination its failure was never given.
|
|
23034
|
+
*/
|
|
23035
|
+
var DEVICE_STREAM_KINDS = new Set([
|
|
23036
|
+
"device-offline",
|
|
23037
|
+
"device-online",
|
|
23038
|
+
"stream-offline",
|
|
23039
|
+
"stream-online",
|
|
23040
|
+
"camera-offline",
|
|
23041
|
+
"camera-online"
|
|
23042
|
+
]);
|
|
23043
|
+
/**
|
|
23044
|
+
* Kinds whose subject is a camera BY CONSTRUCTION, whatever the row says.
|
|
23045
|
+
*
|
|
23046
|
+
* A stream belongs to a camera and nothing else. The legacy `camera-*` pair is
|
|
23047
|
+
* too — that name is precisely the claim it made, and it is why the kind was
|
|
23048
|
+
* split into `device-*` + a `deviceTypes` condition.
|
|
23049
|
+
*/
|
|
23050
|
+
var CAMERA_BY_CONSTRUCTION_KINDS = new Set([
|
|
23051
|
+
"stream-offline",
|
|
23052
|
+
"stream-online",
|
|
23053
|
+
"camera-offline",
|
|
23054
|
+
"camera-online"
|
|
23055
|
+
]);
|
|
23056
|
+
/**
|
|
23057
|
+
* Where a per-section STATE SUMMARY row lands (D708). The row spans nodes, so
|
|
23058
|
+
* no `?node=`: the unscoped page is the honest one. A section absent here taps
|
|
23059
|
+
* through its kind, like any other row — a device section carries the system
|
|
23060
|
+
* placeholder device and gets no link, and the crash-looped addons have none
|
|
23061
|
+
* (their instant kind has none either).
|
|
23062
|
+
*
|
|
23063
|
+
* `nodes` is the one destination no kind gives: a node going offline has no
|
|
23064
|
+
* instant action, but "which nodes are in trouble" is answered by the
|
|
23065
|
+
* Cluster page, and the summary row is about all of them at once.
|
|
23066
|
+
*/
|
|
23067
|
+
var SUMMARY_SECTION_PATHS = {
|
|
23068
|
+
nodes: ADMIN_CLUSTER_PATH,
|
|
23069
|
+
"addon-updates": ADMIN_ADDONS_PATH,
|
|
23070
|
+
updates: ADMIN_CLUSTER_PATH
|
|
23071
|
+
};
|
|
23072
|
+
function withParam(path, key, value) {
|
|
23073
|
+
if (value === void 0 || value.length === 0) return path;
|
|
23074
|
+
return `${path}?${key}=${encodeURIComponent(value)}`;
|
|
23075
|
+
}
|
|
23076
|
+
/**
|
|
23077
|
+
* The admin path (with its query) a system event taps through to, or `null`
|
|
23078
|
+
* when the kind has no action.
|
|
23079
|
+
*
|
|
23080
|
+
* Exported for the cross-package guard and for tests that want the path
|
|
23081
|
+
* without a base; the dispatcher goes through {@link systemEventTapUrl}.
|
|
23082
|
+
*/
|
|
23083
|
+
function systemEventTapPath(subject) {
|
|
23084
|
+
const sectionPath = subject.summarySection !== void 0 ? SUMMARY_SECTION_PATHS[subject.summarySection] : void 0;
|
|
23085
|
+
if (sectionPath !== void 0) return sectionPath;
|
|
23086
|
+
if (ADDON_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_ADDONS_PATH, NODE_PARAM, subject.nodeId);
|
|
23087
|
+
if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_CLUSTER_PATH, NODE_PARAM, subject.nodeId);
|
|
23088
|
+
if (DEVICE_STREAM_KINDS.has(subject.kind)) {
|
|
23089
|
+
const deviceId = subject.deviceId;
|
|
23090
|
+
if (deviceId === void 0 || !Number.isInteger(deviceId) || deviceId <= 0) return null;
|
|
23091
|
+
const devicePath = `${ADMIN_DEVICES_PATH}/${String(deviceId)}`;
|
|
23092
|
+
return CAMERA_BY_CONSTRUCTION_KINDS.has(subject.kind) || subject.deviceType === CAMERA_DEVICE_TYPE ? withParam(devicePath, TAB_PARAM, STREAM_BROKER_TAB) : devicePath;
|
|
23093
|
+
}
|
|
23094
|
+
return null;
|
|
23095
|
+
}
|
|
23096
|
+
/** Full URL, or `null` when the kind has no action or no base is reachable. */
|
|
23097
|
+
function systemEventTapUrl(subject, baseUrl) {
|
|
23098
|
+
if (baseUrl === null || baseUrl.length === 0) return null;
|
|
23099
|
+
const path = systemEventTapPath(subject);
|
|
23100
|
+
if (path === null) return null;
|
|
23101
|
+
return `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
23102
|
+
}
|
|
23103
|
+
//#endregion
|
|
23104
|
+
//#region src/notification-center/default-tap-url.ts
|
|
23105
|
+
/**
|
|
23106
|
+
* The tap URL, or null. In the app form a camera link is minted even with no
|
|
23107
|
+
* `hub=` — the central page reaches the hub through the app's (or the
|
|
23108
|
+
* browser's) own stored hub (central-link design §5). The hub form and every
|
|
23109
|
+
* admin destination still need a base.
|
|
23110
|
+
*/
|
|
23111
|
+
function defaultTapUrl(input) {
|
|
23112
|
+
const publicBaseUrl = input.publicBaseUrl === null || input.publicBaseUrl.length === 0 ? null : input.publicBaseUrl;
|
|
23113
|
+
if (input.systemEvent !== void 0) return publicBaseUrl === null ? null : systemEventTapUrl(input.systemEvent, publicBaseUrl);
|
|
23114
|
+
const destination = {
|
|
23115
|
+
kind: "camera",
|
|
23116
|
+
deviceId: input.deviceId,
|
|
23117
|
+
atMs: input.timestampMs
|
|
23118
|
+
};
|
|
23119
|
+
return require_clip_model_registry.buildViewerLink({
|
|
23120
|
+
base: input.tap ?? {
|
|
23121
|
+
form: "hub",
|
|
23122
|
+
hubBaseUrl: publicBaseUrl
|
|
23123
|
+
},
|
|
23124
|
+
destination
|
|
23125
|
+
});
|
|
23126
|
+
}
|
|
23127
|
+
//#endregion
|
|
23063
23128
|
//#region src/notification-center/footage-source.ts
|
|
23064
23129
|
/**
|
|
23065
23130
|
* Decide where this window must be cut from. Pure and total.
|
|
@@ -25557,20 +25622,19 @@ var NcDispatcher = class {
|
|
|
25557
25622
|
* repo keeps paying for.
|
|
25558
25623
|
*/
|
|
25559
25624
|
/**
|
|
25560
|
-
* The
|
|
25561
|
-
*
|
|
25562
|
-
*
|
|
25563
|
-
*
|
|
25625
|
+
* The `notificationTaps` base of a camera tap (D734). A failing read is
|
|
25626
|
+
* `null` — the tap then opens the hub viewer on the public endpoint — and is
|
|
25627
|
+
* logged, so a tap that lands somewhere other than the flagged address has a
|
|
25628
|
+
* line saying why.
|
|
25564
25629
|
*/
|
|
25565
|
-
async
|
|
25566
|
-
const read = this.deps.
|
|
25567
|
-
if (read === void 0) return
|
|
25630
|
+
async resolveTapBase() {
|
|
25631
|
+
const read = this.deps.tapBase;
|
|
25632
|
+
if (read === void 0) return null;
|
|
25568
25633
|
try {
|
|
25569
|
-
|
|
25570
|
-
return choice === null ? {} : { appLink: choice };
|
|
25634
|
+
return await read();
|
|
25571
25635
|
} catch (err) {
|
|
25572
|
-
this.deps.logger.warn("notification
|
|
25573
|
-
return
|
|
25636
|
+
this.deps.logger.warn("notification tap address unreadable — tap uses the public endpoint", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
25637
|
+
return null;
|
|
25574
25638
|
}
|
|
25575
25639
|
}
|
|
25576
25640
|
async mintActions(entry, target) {
|
|
@@ -25664,11 +25728,12 @@ var NcDispatcher = class {
|
|
|
25664
25728
|
const resolvedMedia = await this.withArtifactUrls(entry, resolved.attachments);
|
|
25665
25729
|
const attachments = [...resolvedMedia.attachments, ...frozenArtifactAttachments(entry)];
|
|
25666
25730
|
const params = pickParams(entry.payload.params);
|
|
25667
|
-
const
|
|
25731
|
+
const publicUrlBase = this.deps.publicUrlBase;
|
|
25732
|
+
const tapUrl = params.clickUrl === void 0 && publicUrlBase !== void 0 ? defaultTapUrl({
|
|
25668
25733
|
deviceId: systemEvent !== void 0 ? 0 : subject.deviceId,
|
|
25669
25734
|
timestampMs: subject.timestamp,
|
|
25670
|
-
|
|
25671
|
-
|
|
25735
|
+
publicBaseUrl: await publicUrlBase().catch(() => null),
|
|
25736
|
+
tap: systemEvent === void 0 ? await this.resolveTapBase() : null,
|
|
25672
25737
|
...systemEvent !== void 0 ? { systemEvent: {
|
|
25673
25738
|
kind: systemEvent.kind,
|
|
25674
25739
|
...systemEvent.deviceId !== void 0 ? { deviceId: systemEvent.deviceId } : {},
|
|
@@ -39831,7 +39896,8 @@ var NotificationCenter = class {
|
|
|
39831
39896
|
ruleAiStats: () => ruleAiAnalyst.stats()
|
|
39832
39897
|
} : {},
|
|
39833
39898
|
releaseCooldown: (input) => this.releaseCooldown(input),
|
|
39834
|
-
...deps.dispatcher.
|
|
39899
|
+
...deps.dispatcher.publicUrlBase !== void 0 ? { publicUrlBase: deps.dispatcher.publicUrlBase } : {},
|
|
39900
|
+
...deps.dispatcher.tapBase !== void 0 ? { tapBase: deps.dispatcher.tapBase } : {}
|
|
39835
39901
|
});
|
|
39836
39902
|
this.outbox = new NcOutbox({
|
|
39837
39903
|
store: deps.store,
|
|
@@ -40829,7 +40895,11 @@ var NotificationCenter = class {
|
|
|
40829
40895
|
if (panel === null) return "unchanged";
|
|
40830
40896
|
const opening = alarmOpeningFromSensorEvent(event.kind, event.value);
|
|
40831
40897
|
if (opening.kind !== "reading") return "unchanged";
|
|
40832
|
-
|
|
40898
|
+
const changedAt = event.value?.["lastChangedAt"];
|
|
40899
|
+
return panel.observeOpening(event.sourceDeviceId, opening.reading, event.timestamp, {
|
|
40900
|
+
deferLiftAnnouncement: true,
|
|
40901
|
+
...typeof changedAt === "number" ? { changedAt } : {}
|
|
40902
|
+
});
|
|
40833
40903
|
}
|
|
40834
40904
|
/**
|
|
40835
40905
|
* "Watched again", once the event that lifted the sensor has been EVALUATED (D706).
|
|
@@ -42316,23 +42386,50 @@ var NotificationCenter = class {
|
|
|
42316
42386
|
});
|
|
42317
42387
|
}
|
|
42318
42388
|
/**
|
|
42319
|
-
*
|
|
42320
|
-
*
|
|
42389
|
+
* The one actuation seam of this centre — a rule's sequence step, a granted button.
|
|
42390
|
+
*
|
|
42391
|
+
* A press of THIS node's panel `trigger` never reaches the actuator: with a match
|
|
42392
|
+
* (`origin`) it is {@link NcAlarmPanelPort.triggerFromRule}, synchronously, the source
|
|
42393
|
+
* in the call. The join is HERE rather than in the runner: the runner actuates devices
|
|
42394
|
+
* generically and must not learn what an alarm is, while this class holds the panel.
|
|
42321
42395
|
*
|
|
42322
|
-
*
|
|
42323
|
-
*
|
|
42324
|
-
*
|
|
42325
|
-
*
|
|
42326
|
-
*
|
|
42396
|
+
* Without a match, the press has no sensor to check against the arm's exclusions, so
|
|
42397
|
+
* while the arm EXCLUDES anything it is REFUSED (fail-closed, D735): an anonymous press
|
|
42398
|
+
* from this seam is exactly what sounded the siren off an excluded contact on
|
|
42399
|
+
* 2026-10-07. A person's manual trigger (the panel's cap, from the UI) does not come
|
|
42400
|
+
* through here and is unaffected.
|
|
42327
42401
|
*/
|
|
42328
42402
|
async actuateWithAttribution(input) {
|
|
42403
|
+
const panel = this.alarmPanel;
|
|
42404
|
+
if (panel !== null && input.deviceId === panel.deviceId && input.cap === ALARM_PANEL_CAP && input.method === "trigger" && this.pressLocalPanel(panel, input.origin)) return;
|
|
42329
42405
|
const actuate = this.deps.actuate;
|
|
42330
42406
|
if (actuate === void 0) throw new Error("no actuator is wired on this node");
|
|
42331
|
-
const panel = this.alarmPanel;
|
|
42332
|
-
if (panel !== null && input.origin !== void 0 && input.deviceId === panel.deviceId && input.cap === ALARM_PANEL_CAP && input.method === "trigger") panel.noteRuleTrigger(input.origin);
|
|
42333
42407
|
await actuate(input);
|
|
42334
42408
|
}
|
|
42335
42409
|
/**
|
|
42410
|
+
* A press of this node's panel from the actuation seam; true when it was handled here.
|
|
42411
|
+
* With a source: the panel's rule press. Without one: refused while the arm excludes any
|
|
42412
|
+
* sensor — an ERROR line and a throw, so the sequence step reports failed instead of
|
|
42413
|
+
* looking done. Without one and nothing excluded: false, and the press takes the cap
|
|
42414
|
+
* like any manual press (there is no sensor whose exclusion it could skip).
|
|
42415
|
+
*/
|
|
42416
|
+
pressLocalPanel(panel, origin) {
|
|
42417
|
+
if (origin !== void 0) {
|
|
42418
|
+
panel.triggerFromRule(origin);
|
|
42419
|
+
return true;
|
|
42420
|
+
}
|
|
42421
|
+
const excluded = panel.excludedDevices();
|
|
42422
|
+
if (excluded.length === 0) return false;
|
|
42423
|
+
this.logger.error("alarm trigger REFUSED — a rule-action press carried no source while the arm excludes sensors", {
|
|
42424
|
+
tags: { deviceId: panel.deviceId },
|
|
42425
|
+
meta: {
|
|
42426
|
+
excluded: excluded.join(","),
|
|
42427
|
+
state: panel.currentState()
|
|
42428
|
+
}
|
|
42429
|
+
});
|
|
42430
|
+
throw new Error("alarm trigger refused: no source, and the current arm excludes sensors");
|
|
42431
|
+
}
|
|
42432
|
+
/**
|
|
42336
42433
|
* Device names from the in-process directory — no I/O, so the arm sentence
|
|
42337
42434
|
* can be composed on the publish path.
|
|
42338
42435
|
*
|
|
@@ -43220,13 +43317,9 @@ var NotificationCenter = class {
|
|
|
43220
43317
|
}
|
|
43221
43318
|
const origin = fireRefFor(rule, subject, kind);
|
|
43222
43319
|
const phase = panel.classifyRuleMatch(origin);
|
|
43223
|
-
|
|
43224
|
-
|
|
43225
|
-
|
|
43226
|
-
method: "trigger",
|
|
43227
|
-
args: {},
|
|
43228
|
-
origin
|
|
43229
|
-
}).catch((err) => {
|
|
43320
|
+
try {
|
|
43321
|
+
panel.triggerFromRule(origin);
|
|
43322
|
+
} catch (err) {
|
|
43230
43323
|
this.logger.warn("alarm panel auto-trigger failed", {
|
|
43231
43324
|
tags: { deviceId: subject.deviceId },
|
|
43232
43325
|
meta: {
|
|
@@ -43236,7 +43329,7 @@ var NotificationCenter = class {
|
|
|
43236
43329
|
error: err instanceof Error ? err.message : String(err)
|
|
43237
43330
|
}
|
|
43238
43331
|
});
|
|
43239
|
-
}
|
|
43332
|
+
}
|
|
43240
43333
|
return phase;
|
|
43241
43334
|
}
|
|
43242
43335
|
/**
|
|
@@ -101598,9 +101691,15 @@ async function projectSensorMarkers(deps, data, timestamp) {
|
|
|
101598
101691
|
*
|
|
101599
101692
|
* - The RECOVERY family. The Zigbee entity flaps `off → unavailable → off`
|
|
101600
101693
|
* (03:22:37→03:23:28, 03:56:51→03:58:19, 04:40:05→04:45:45). The HA
|
|
101601
|
-
* provider
|
|
101602
|
-
* `entryOpen`
|
|
101603
|
-
* `last_changed`. The mirror
|
|
101694
|
+
* provider dropped the `unavailable` push and wrote the recovery, whose
|
|
101695
|
+
* `entryOpen` was still `false` and whose `lastChangedAt` was HA's fresh
|
|
101696
|
+
* `last_changed`. The mirror diffed, and emitted. Since D735 the provider
|
|
101697
|
+
* keeps the change time of a value that did not change
|
|
101698
|
+
* (`addon-provider-homeassistant` `ha-change-stamp.ts`), because this gate
|
|
101699
|
+
* cannot fix the ACTIVE half: with its memory empty (a hub restart), a
|
|
101700
|
+
* recovery `on` with a fresh stamp is indistinguishable here from an
|
|
101701
|
+
* opening — that is how four excluded contacts pressed the armed panel on
|
|
101702
|
+
* 2026-10-07 02:32Z.
|
|
101604
101703
|
* - The COLD-SLICE family. `HaBinarySensorDevice`'s constructor writes
|
|
101605
101704
|
* `{ entryOpen: false, lastChangedAt: 0 }` on every device rebuild; the
|
|
101606
101705
|
* real state lands seconds later with a `lastChangedAt` hours old. Two
|
|
@@ -106690,51 +106789,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
106690
106789
|
};
|
|
106691
106790
|
}
|
|
106692
106791
|
}
|
|
106693
|
-
/** The operator's marked notification endpoint, or null (AUTO / unavailable). */
|
|
106694
|
-
async markedNotificationEndpoint() {
|
|
106695
|
-
try {
|
|
106696
|
-
return (await this.ctx.api.localNetwork.getNotificationEndpoint.query()).baseUrl ?? void 0;
|
|
106697
|
-
} catch {
|
|
106698
|
-
return;
|
|
106699
|
-
}
|
|
106700
|
-
}
|
|
106701
106792
|
/**
|
|
106702
|
-
* The ranked base-URL candidates a notification LINK may use
|
|
106793
|
+
* The ranked base-URL candidates a notification LINK may use — every link
|
|
106794
|
+
* except the tap: the hub's public endpoint (D734, `link-endpoints.ts`).
|
|
106703
106795
|
*
|
|
106704
|
-
* Shared by the artifact plane (media)
|
|
106705
|
-
*
|
|
106706
|
-
* minting links on different hosts
|
|
106796
|
+
* Shared by the artifact plane (media), the action plane (buttons) and the
|
|
106797
|
+
* admin page a system event taps into — one resolver, because a second copy
|
|
106798
|
+
* would drift and the planes would start minting links on different hosts
|
|
106799
|
+
* for the same notification.
|
|
106707
106800
|
*/
|
|
106708
106801
|
async ncEndpoints() {
|
|
106709
|
-
return
|
|
106710
|
-
markedBaseUrl: await this.markedNotificationEndpoint(),
|
|
106711
|
-
getConnected: async () => {
|
|
106712
|
-
const status = await this.ctx.api.networkAccess.getStatus.query();
|
|
106713
|
-
this.ctx.logger.debug("artifact base-url: connected ingress", { meta: {
|
|
106714
|
-
connected: status.connected,
|
|
106715
|
-
url: status.endpoint?.url ?? null,
|
|
106716
|
-
protocol: status.endpoint?.protocol ?? null
|
|
106717
|
-
} });
|
|
106718
|
-
return status.connected && status.endpoint !== null ? {
|
|
106719
|
-
url: status.endpoint.url,
|
|
106720
|
-
protocol: status.endpoint.protocol
|
|
106721
|
-
} : null;
|
|
106722
|
-
},
|
|
106723
|
-
listExternal: async () => {
|
|
106724
|
-
return (await this.ctx.api.networkAccess.listEndpoints.query()).map((e) => ({
|
|
106725
|
-
url: e.url,
|
|
106726
|
-
protocol: e.protocol
|
|
106727
|
-
}));
|
|
106728
|
-
},
|
|
106729
|
-
listLan: async (port) => {
|
|
106730
|
-
return (await this.ctx.api.localNetwork.getConnectionEndpoints.query({ port })).endpoints.flatMap((e) => e.kind === "app-link" ? [] : [{
|
|
106731
|
-
baseUrl: e.baseUrl,
|
|
106732
|
-
kind: e.kind,
|
|
106733
|
-
priority: e.priority
|
|
106734
|
-
}]);
|
|
106735
|
-
},
|
|
106736
|
-
logger: this.ctx.logger
|
|
106737
|
-
});
|
|
106802
|
+
return notificationLinkEndpoints(this.ctx.api, this.ctx.logger);
|
|
106738
106803
|
}
|
|
106739
106804
|
/**
|
|
106740
106805
|
* Serve the NC ACTION plane: the public route a notification button lands on.
|
|
@@ -109320,8 +109385,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
109320
109385
|
resolveFfmpeg: () => this.ctx.deps.ensureFfmpeg()
|
|
109321
109386
|
}), req),
|
|
109322
109387
|
publishArtifact: async (bytes, mime, deviceId) => await this.ncArtifactPlane?.publish(bytes, mime, deviceId) ?? null,
|
|
109323
|
-
|
|
109324
|
-
|
|
109388
|
+
publicUrlBase: async () => pickArtifactBaseUrl(await this.ncEndpoints()),
|
|
109389
|
+
tapBase: async () => require_clip_model_registry.viewerLinkBaseOf(await this.ctx.api.localNetwork.getNotificationEndpoint.query()),
|
|
109325
109390
|
indexArtifact: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? {
|
|
109326
109391
|
url: null,
|
|
109327
109392
|
artifactId: null
|