@camstack/addon-post-analysis 1.2.284 → 1.2.285
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/THIRD_PARTY_MODELS.md +233 -0
- package/dist/{dist-C770IfQJ.mjs → dist-D2tXUMfE.mjs} +1014 -713
- package/dist/{dist-Bpx5JMso.js → dist-ITESpHou.js} +1049 -712
- 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-Cc4KIQdo.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-7oasHfhD.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CjeM5Bph.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-DWPDYuJS.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BidXbeas.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BzoLW-k9.mjs → hostInit-B6m75Rnz.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +1860 -90
- package/dist/pipeline-analytics/index.mjs +1860 -90
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +3 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CZkZhroP.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_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-ITESpHou.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -4104,7 +4104,7 @@ function mimeFromExtension(file) {
|
|
|
4104
4104
|
* A value that is negative, NaN or absent falls back to the shipped default:
|
|
4105
4105
|
* an unreadable window must not become a window nobody chose.
|
|
4106
4106
|
*/
|
|
4107
|
-
var DAY_MS$
|
|
4107
|
+
var DAY_MS$4 = 24 * 36e5;
|
|
4108
4108
|
/**
|
|
4109
4109
|
* The shipped windows. Each one is the value that was hardcoded before this
|
|
4110
4110
|
* module existed, except the mosaic — see {@link PRUNE_SETTING_KEYS}.
|
|
@@ -4150,8 +4150,8 @@ var PRUNE_SETTING_KEYS = [
|
|
|
4150
4150
|
*/
|
|
4151
4151
|
function windowMs(raw, defaultDays) {
|
|
4152
4152
|
if (raw === 0) return null;
|
|
4153
|
-
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) return defaultDays * DAY_MS$
|
|
4154
|
-
return raw * DAY_MS$
|
|
4153
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) return defaultDays * DAY_MS$4;
|
|
4154
|
+
return raw * DAY_MS$4;
|
|
4155
4155
|
}
|
|
4156
4156
|
function resolvePruneWindows(settings) {
|
|
4157
4157
|
return {
|
|
@@ -8262,6 +8262,19 @@ var NcAlarmLiveReadLog = class {
|
|
|
8262
8262
|
};
|
|
8263
8263
|
//#endregion
|
|
8264
8264
|
//#region src/notification-center/alerts/alert-episode.ts
|
|
8265
|
+
/** The row with `detail` set (or removed). Unchanged rows are returned as is. */
|
|
8266
|
+
function withAlertDetail(row, detail) {
|
|
8267
|
+
if (row.detail === detail) return row;
|
|
8268
|
+
const { subject, alerting, since, lastNotifiedAt, reminders } = row;
|
|
8269
|
+
return {
|
|
8270
|
+
subject,
|
|
8271
|
+
alerting,
|
|
8272
|
+
since,
|
|
8273
|
+
lastNotifiedAt,
|
|
8274
|
+
reminders,
|
|
8275
|
+
...detail !== void 0 ? { detail } : {}
|
|
8276
|
+
};
|
|
8277
|
+
}
|
|
8265
8278
|
function decideAlert(input) {
|
|
8266
8279
|
const previous = input.previous;
|
|
8267
8280
|
if (previous === void 0) return input.alerting ? { kind: "raise" } : { kind: "seeded" };
|
|
@@ -8405,6 +8418,12 @@ var NC_ALERTS_COLUMNS = [
|
|
|
8405
8418
|
{
|
|
8406
8419
|
name: "deviceId",
|
|
8407
8420
|
type: "INTEGER"
|
|
8421
|
+
}),
|
|
8422
|
+
(
|
|
8423
|
+
/** What the open episode is — a tank's band (D647 review M6). Nullable, added in place. */
|
|
8424
|
+
{
|
|
8425
|
+
name: "detail",
|
|
8426
|
+
type: "TEXT"
|
|
8408
8427
|
})
|
|
8409
8428
|
];
|
|
8410
8429
|
var NC_ALERTS_INDEXES = [{
|
|
@@ -8427,7 +8446,8 @@ var NC_ALERTS_SPEC = {
|
|
|
8427
8446
|
since: row.since,
|
|
8428
8447
|
lastNotifiedAt: row.lastNotifiedAt,
|
|
8429
8448
|
reminders: row.reminders,
|
|
8430
|
-
...deviceIdOf(row.subject) !== void 0 ? { deviceId: deviceIdOf(row.subject) } : {}
|
|
8449
|
+
...deviceIdOf(row.subject) !== void 0 ? { deviceId: deviceIdOf(row.subject) } : {},
|
|
8450
|
+
...row.detail !== void 0 ? { detail: row.detail } : {}
|
|
8431
8451
|
}),
|
|
8432
8452
|
fromRecord: (key, data) => {
|
|
8433
8453
|
const since = data["since"];
|
|
@@ -8436,12 +8456,14 @@ var NC_ALERTS_SPEC = {
|
|
|
8436
8456
|
if (typeof since !== "number") return null;
|
|
8437
8457
|
if (typeof lastNotifiedAt !== "number") return null;
|
|
8438
8458
|
if (typeof reminders !== "number") return null;
|
|
8459
|
+
const detail = data["detail"];
|
|
8439
8460
|
return {
|
|
8440
8461
|
subject: key,
|
|
8441
8462
|
alerting: data["alerting"] === 1 || data["alerting"] === true,
|
|
8442
8463
|
since,
|
|
8443
8464
|
lastNotifiedAt,
|
|
8444
|
-
reminders
|
|
8465
|
+
reminders,
|
|
8466
|
+
...typeof detail === "string" ? { detail } : {}
|
|
8445
8467
|
};
|
|
8446
8468
|
},
|
|
8447
8469
|
equalFact: (held, incoming) => held.alerting === incoming.alerting,
|
|
@@ -8474,7 +8496,8 @@ var NcAlertStore = class {
|
|
|
8474
8496
|
this.ledger.put(row).catch(() => void 0);
|
|
8475
8497
|
},
|
|
8476
8498
|
all: () => this.ledger.snapshot(),
|
|
8477
|
-
remove: (subject) => this.ledger.forget(subject)
|
|
8499
|
+
remove: (subject) => this.ledger.forget(subject),
|
|
8500
|
+
answered: () => this.ledger.hasAnswered()
|
|
8478
8501
|
};
|
|
8479
8502
|
}
|
|
8480
8503
|
};
|
|
@@ -11922,6 +11945,80 @@ function vacuumTanksOf(data) {
|
|
|
11922
11945
|
};
|
|
11923
11946
|
}
|
|
11924
11947
|
//#endregion
|
|
11948
|
+
//#region src/notification-center/alerts/open-alert.ts
|
|
11949
|
+
/**
|
|
11950
|
+
* The raise of an episode whose reading has not arrived since the restart —
|
|
11951
|
+
* worded with what the row KNOWS: no invented percentage (the binary battery
|
|
11952
|
+
* sentence), the item key as its name, and a tank kind that only the filters
|
|
11953
|
+
* read (the summary words an unmeasured tank without a band).
|
|
11954
|
+
*/
|
|
11955
|
+
function unmeasuredEpisode(input) {
|
|
11956
|
+
const deviceId = alertSubjectDeviceId(input.subject);
|
|
11957
|
+
if (deviceId === void 0) return null;
|
|
11958
|
+
const identity = input.lookup(deviceId);
|
|
11959
|
+
const facts = input.facts;
|
|
11960
|
+
if (input.family === "battery") return incomingFromBatteryBand({
|
|
11961
|
+
facts,
|
|
11962
|
+
deviceId,
|
|
11963
|
+
band: "low",
|
|
11964
|
+
reading: {
|
|
11965
|
+
percentage: 0,
|
|
11966
|
+
lastUpdated: 0,
|
|
11967
|
+
binary: true
|
|
11968
|
+
},
|
|
11969
|
+
identity
|
|
11970
|
+
});
|
|
11971
|
+
const parts = parseConsumableSubject(input.subject);
|
|
11972
|
+
if (input.family === "consumable" && parts !== null) return incomingFromConsumableBand({
|
|
11973
|
+
facts,
|
|
11974
|
+
deviceId,
|
|
11975
|
+
band: "low",
|
|
11976
|
+
item: {
|
|
11977
|
+
key: parts.key,
|
|
11978
|
+
label: parts.key,
|
|
11979
|
+
level: null,
|
|
11980
|
+
status: "replace"
|
|
11981
|
+
},
|
|
11982
|
+
thresholdPercent: parts.thresholdPercent,
|
|
11983
|
+
identity
|
|
11984
|
+
});
|
|
11985
|
+
const tank = NC_TANK_KEYS.find((key) => input.subject === `tank:${String(deviceId)}:${key}`);
|
|
11986
|
+
if (tank === void 0) return null;
|
|
11987
|
+
return incomingFromTankBand({
|
|
11988
|
+
facts,
|
|
11989
|
+
deviceId,
|
|
11990
|
+
band: input.detail === "full" ? "full" : "low",
|
|
11991
|
+
tank,
|
|
11992
|
+
identity
|
|
11993
|
+
});
|
|
11994
|
+
}
|
|
11995
|
+
/**
|
|
11996
|
+
* One alerting row as an open episode, or `null` when it cannot be worded (the
|
|
11997
|
+
* caller logs it). A legacy consumable row is skipped: it is adopted or
|
|
11998
|
+
* retired first. A tank whose band the row remembers is worded by it
|
|
11999
|
+
* (review M6), even without a reading.
|
|
12000
|
+
*/
|
|
12001
|
+
function openAlertOf(row, input) {
|
|
12002
|
+
const parts = parseConsumableSubject(row.subject);
|
|
12003
|
+
if (parts?.legacy === true) return null;
|
|
12004
|
+
const family = parts !== null ? "consumable" : row.subject.startsWith("tank:") ? "tank" : "battery";
|
|
12005
|
+
const subject = (input.restated ?? unmeasuredEpisode({
|
|
12006
|
+
subject: row.subject,
|
|
12007
|
+
family,
|
|
12008
|
+
facts: input.facts,
|
|
12009
|
+
lookup: input.lookup,
|
|
12010
|
+
...row.detail !== void 0 ? { detail: row.detail } : {}
|
|
12011
|
+
}))?.subject.systemEvent;
|
|
12012
|
+
if (subject === void 0) return null;
|
|
12013
|
+
const measured = input.restated !== null || family === "tank" && row.detail !== void 0;
|
|
12014
|
+
return {
|
|
12015
|
+
family,
|
|
12016
|
+
since: row.since,
|
|
12017
|
+
subject,
|
|
12018
|
+
measured
|
|
12019
|
+
};
|
|
12020
|
+
}
|
|
12021
|
+
//#endregion
|
|
11925
12022
|
//#region src/notification-center/alerts/battery-band.ts
|
|
11926
12023
|
/**
|
|
11927
12024
|
* The stand-in a provider writes for a binary NORMAL. Anything below it, on a
|
|
@@ -12129,7 +12226,7 @@ var NcDeviceAlerts = class {
|
|
|
12129
12226
|
band: alerting ? band : "normal",
|
|
12130
12227
|
tank: tank.key,
|
|
12131
12228
|
identity: this.deps.lookup(deviceId)
|
|
12132
|
-
}));
|
|
12229
|
+
}), band === "normal" ? void 0 : band);
|
|
12133
12230
|
}
|
|
12134
12231
|
/**
|
|
12135
12232
|
* Has this tank band held long enough to move its episode? A band that
|
|
@@ -12247,6 +12344,37 @@ var NcDeviceAlerts = class {
|
|
|
12247
12344
|
}
|
|
12248
12345
|
}
|
|
12249
12346
|
/**
|
|
12347
|
+
* Every OPEN episode, restated (D647). `null` while the rows have not been
|
|
12348
|
+
* read. A legacy consumable row is skipped (it is adopted or retired first),
|
|
12349
|
+
* and so is a tank whose band is unknown and whose device the summary could
|
|
12350
|
+
* not word — never silently: each skip is a line.
|
|
12351
|
+
*/
|
|
12352
|
+
openEpisodes() {
|
|
12353
|
+
if (this.deps.rows.answered?.() === false) return null;
|
|
12354
|
+
const facts = {
|
|
12355
|
+
eventId: "state-summary",
|
|
12356
|
+
timestamp: this.now(),
|
|
12357
|
+
category: "state-summary",
|
|
12358
|
+
data: {}
|
|
12359
|
+
};
|
|
12360
|
+
const out = [];
|
|
12361
|
+
for (const row of this.deps.rows.all()) {
|
|
12362
|
+
if (!row.alerting) continue;
|
|
12363
|
+
const restated = this.restatementOf(row.subject);
|
|
12364
|
+
const open = openAlertOf(row, {
|
|
12365
|
+
facts,
|
|
12366
|
+
restated: restated === null ? null : restated(facts),
|
|
12367
|
+
lookup: this.deps.lookup
|
|
12368
|
+
});
|
|
12369
|
+
if (open !== null) out.push(open);
|
|
12370
|
+
else this.logger()?.debug("open alert left out of the state summary — it cannot be worded", {
|
|
12371
|
+
...tagsOf(row.subject),
|
|
12372
|
+
meta: { subject: row.subject }
|
|
12373
|
+
});
|
|
12374
|
+
}
|
|
12375
|
+
return out;
|
|
12376
|
+
}
|
|
12377
|
+
/**
|
|
12250
12378
|
* Every device that holds an alert row — what the deleted-device reconcile
|
|
12251
12379
|
* must look at besides the devices a rule names (a battery device no rule
|
|
12252
12380
|
* names still has a row).
|
|
@@ -12388,7 +12516,7 @@ var NcDeviceAlerts = class {
|
|
|
12388
12516
|
* a state we never recorded leaving is a state we can never announce
|
|
12389
12517
|
* entering again.
|
|
12390
12518
|
*/
|
|
12391
|
-
apply(subject, alerting, settings, build) {
|
|
12519
|
+
apply(subject, alerting, settings, build, detail) {
|
|
12392
12520
|
const nowMs = this.now();
|
|
12393
12521
|
const previous = this.deps.rows.get(subject);
|
|
12394
12522
|
const verdict = decideAlert({
|
|
@@ -12398,7 +12526,7 @@ var NcDeviceAlerts = class {
|
|
|
12398
12526
|
renotifyMs: settings.renotifyMinutes * 6e4,
|
|
12399
12527
|
notifyRecovery: settings.notifyRecovery
|
|
12400
12528
|
});
|
|
12401
|
-
const next = nextAlertRow(subject, previous, alerting, verdict, nowMs);
|
|
12529
|
+
const next = withAlertDetail(nextAlertRow(subject, previous, alerting, verdict, nowMs), alerting ? detail ?? previous?.detail : void 0);
|
|
12402
12530
|
if (next !== previous) this.deps.rows.put(next);
|
|
12403
12531
|
if (!speaks(verdict)) {
|
|
12404
12532
|
this.report(subject, verdict, alerting);
|
|
@@ -13033,7 +13161,31 @@ var en_default = {
|
|
|
13033
13161
|
},
|
|
13034
13162
|
"summary.body.detections": "{{detectionSummary}}",
|
|
13035
13163
|
"summary.body.truncated": "showing the {{shown}} most relevant of {{total}}",
|
|
13036
|
-
"summary.body.empty": "nothing was seen in this window"
|
|
13164
|
+
"summary.body.empty": "nothing was seen in this window",
|
|
13165
|
+
"system.state-summary.title": "{{rule}}",
|
|
13166
|
+
"system.state-summary.body": "{{summaryText}}",
|
|
13167
|
+
"summary.section.device-state": "Devices offline or switched off",
|
|
13168
|
+
"summary.section.streams": "Streams offline",
|
|
13169
|
+
"summary.section.battery": "Low batteries",
|
|
13170
|
+
"summary.section.consumables": "Consumables to replace",
|
|
13171
|
+
"summary.section.tanks": "Tanks",
|
|
13172
|
+
"summary.section.detection-blind": "Cameras detecting nothing",
|
|
13173
|
+
"summary.section.nodes": "Nodes",
|
|
13174
|
+
"summary.section.addons": "Addons",
|
|
13175
|
+
"summary.section.updates": "Server and container updates",
|
|
13176
|
+
"summary.section.unavailable": "{{section}}: unavailable — the state could not be read",
|
|
13177
|
+
"summary.line.device-offline": "{{deviceName}} — offline (since {{since}})",
|
|
13178
|
+
"summary.line.device-disabled": "{{deviceName}} — switched off (since {{since}})",
|
|
13179
|
+
"summary.line.stream-offline": "{{deviceName}} — stream offline (since {{since}})",
|
|
13180
|
+
"summary.line.stream-offline.profiled": "{{deviceName}} — {{profile}} stream offline (since {{since}})",
|
|
13181
|
+
"summary.line.detection-blind": "{{deviceName}} — detecting nothing (since {{since}})",
|
|
13182
|
+
"summary.line.node-offline": "{{nodeId}} — offline (since {{since}})",
|
|
13183
|
+
"summary.line.node-inference-unavailable": "{{nodeId}} — cannot run inference (since {{since}})",
|
|
13184
|
+
"summary.line.addon-crash-loop": "{{nodeId}} — {{addons}} stopped after repeated crashes (since {{since}})",
|
|
13185
|
+
"summary.line.update": "{{packageName}} {{currentVersion}} → {{latestVersion}}",
|
|
13186
|
+
"summary.line.update.onNode": "{{packageName}} {{currentVersion}} → {{latestVersion}} on {{nodeId}}",
|
|
13187
|
+
"summary.line.wrapper-update": "Container {{currentVersion}} → {{latestVersion}}",
|
|
13188
|
+
"summary.line.tank-unread": "{{deviceName}} — the {{tank}} tank needs attention"
|
|
13037
13189
|
};
|
|
13038
13190
|
//#endregion
|
|
13039
13191
|
//#region src/notification-center/texts/it.json
|
|
@@ -13337,7 +13489,31 @@ var it_default = {
|
|
|
13337
13489
|
},
|
|
13338
13490
|
"summary.body.detections": "{{detectionSummary}}",
|
|
13339
13491
|
"summary.body.truncated": "mostrati i {{shown}} più rilevanti su {{total}}",
|
|
13340
|
-
"summary.body.empty": "nessun evento in questa finestra"
|
|
13492
|
+
"summary.body.empty": "nessun evento in questa finestra",
|
|
13493
|
+
"system.state-summary.title": "{{rule}}",
|
|
13494
|
+
"system.state-summary.body": "{{summaryText}}",
|
|
13495
|
+
"summary.section.device-state": "Dispositivi offline o spenti",
|
|
13496
|
+
"summary.section.streams": "Stream offline",
|
|
13497
|
+
"summary.section.battery": "Batterie scariche",
|
|
13498
|
+
"summary.section.consumables": "Consumabili da sostituire",
|
|
13499
|
+
"summary.section.tanks": "Serbatoi",
|
|
13500
|
+
"summary.section.detection-blind": "Camere che non rilevano nulla",
|
|
13501
|
+
"summary.section.nodes": "Nodi",
|
|
13502
|
+
"summary.section.addons": "Addon",
|
|
13503
|
+
"summary.section.updates": "Aggiornamenti di server e container",
|
|
13504
|
+
"summary.section.unavailable": "{{section}}: non disponibile — lo stato non è leggibile",
|
|
13505
|
+
"summary.line.device-offline": "{{deviceName}} — offline (da {{since}})",
|
|
13506
|
+
"summary.line.device-disabled": "{{deviceName}} — spento (da {{since}})",
|
|
13507
|
+
"summary.line.stream-offline": "{{deviceName}} — stream offline (da {{since}})",
|
|
13508
|
+
"summary.line.stream-offline.profiled": "{{deviceName}} — stream {{profile}} offline (da {{since}})",
|
|
13509
|
+
"summary.line.detection-blind": "{{deviceName}} — non rileva nulla (da {{since}})",
|
|
13510
|
+
"summary.line.node-offline": "{{nodeId}} — offline (da {{since}})",
|
|
13511
|
+
"summary.line.node-inference-unavailable": "{{nodeId}} — non può eseguire l'inferenza (da {{since}})",
|
|
13512
|
+
"summary.line.addon-crash-loop": "{{nodeId}} — {{addons}} fermati dopo crash ripetuti (da {{since}})",
|
|
13513
|
+
"summary.line.update": "{{packageName}} {{currentVersion}} → {{latestVersion}}",
|
|
13514
|
+
"summary.line.update.onNode": "{{packageName}} {{currentVersion}} → {{latestVersion}} su {{nodeId}}",
|
|
13515
|
+
"summary.line.wrapper-update": "Container {{currentVersion}} → {{latestVersion}}",
|
|
13516
|
+
"summary.line.tank-unread": "{{deviceName}} — il serbatoio {{tank}} richiede attenzione"
|
|
13341
13517
|
};
|
|
13342
13518
|
//#endregion
|
|
13343
13519
|
//#region src/notification-center/text-catalog.ts
|
|
@@ -13755,7 +13931,7 @@ function assertPatchKeepsFamily(existing, patch) {
|
|
|
13755
13931
|
* the offending kind or filter). Then rethrow. A refused save is dropped work,
|
|
13756
13932
|
* and dropped work is never silent. Any other error passes through unlogged.
|
|
13757
13933
|
*/
|
|
13758
|
-
function logRefusal$
|
|
13934
|
+
function logRefusal$3(check, ctx) {
|
|
13759
13935
|
try {
|
|
13760
13936
|
check();
|
|
13761
13937
|
} catch (err) {
|
|
@@ -13771,11 +13947,11 @@ function logRefusal$2(check, ctx) {
|
|
|
13771
13947
|
}
|
|
13772
13948
|
/** {@link assertRuleFamily}, logged. The create path of both routes. */
|
|
13773
13949
|
function guardRuleFamily(rule, ctx) {
|
|
13774
|
-
logRefusal$
|
|
13950
|
+
logRefusal$3(() => assertRuleFamily(rule), ctx);
|
|
13775
13951
|
}
|
|
13776
13952
|
/** {@link assertPatchKeepsFamily}, logged. The update path of both routes. */
|
|
13777
13953
|
function guardPatchFamily(existing, patch, ctx) {
|
|
13778
|
-
logRefusal$
|
|
13954
|
+
logRefusal$3(() => assertPatchKeepsFamily(existing, patch), ctx);
|
|
13779
13955
|
}
|
|
13780
13956
|
//#endregion
|
|
13781
13957
|
//#region src/notification-center/device-scope-guard.ts
|
|
@@ -13800,7 +13976,7 @@ function assertDevicesExcludeDelivery(rule) {
|
|
|
13800
13976
|
throw new NcDevicesExcludeRefusal(rule.delivery, excluded);
|
|
13801
13977
|
}
|
|
13802
13978
|
/** Run the check; a refusal is dropped work, so it is logged before it is rethrown. */
|
|
13803
|
-
function logRefusal$
|
|
13979
|
+
function logRefusal$2(check, ctx) {
|
|
13804
13980
|
try {
|
|
13805
13981
|
check();
|
|
13806
13982
|
} catch (err) {
|
|
@@ -13818,7 +13994,7 @@ function warnRefusal$1(logger, err, ctx) {
|
|
|
13818
13994
|
}
|
|
13819
13995
|
/** The create path of both routes. */
|
|
13820
13996
|
function guardRuleDevicesExclude(rule, ctx) {
|
|
13821
|
-
logRefusal$
|
|
13997
|
+
logRefusal$2(() => assertDevicesExcludeDelivery(rule), ctx);
|
|
13822
13998
|
}
|
|
13823
13999
|
/**
|
|
13824
14000
|
* The update path of both routes: the MERGED rule is judged, and only when the
|
|
@@ -13828,7 +14004,86 @@ function guardPatchDevicesExclude(existing, patch, ctx) {
|
|
|
13828
14004
|
if (existing === null) return;
|
|
13829
14005
|
const merged = mergedFamilySubject(existing, patch);
|
|
13830
14006
|
if (merged === null) return;
|
|
13831
|
-
logRefusal$
|
|
14007
|
+
logRefusal$2(() => assertDevicesExcludeDelivery(merged), ctx);
|
|
14008
|
+
}
|
|
14009
|
+
//#endregion
|
|
14010
|
+
//#region src/notification-center/rule-summary-guard.ts
|
|
14011
|
+
/**
|
|
14012
|
+
* The hub's half of D647: `systemEventDelivery: { mode: 'summary' }` belongs to
|
|
14013
|
+
* a `device-events` or `system` rule, and nowhere else — the alarm stays
|
|
14014
|
+
* instant. Refused, loudly, on both save routes (the cap provider and the
|
|
14015
|
+
* `nc.*Rule` bridge), beside the D637 family guard.
|
|
14016
|
+
*
|
|
14017
|
+
* The rule is judged by `systemEventDeliveryProblems` (`@camstack/types`), the
|
|
14018
|
+
* one statement both editors read too. On an update it is the MERGED rule —
|
|
14019
|
+
* merged the way the store merges it, `clear` included — and only when the
|
|
14020
|
+
* patch changes something the verdict reads: a rename of a stored rule is
|
|
14021
|
+
* never refused for what it already was.
|
|
14022
|
+
*
|
|
14023
|
+
* Not in Zod, for the reason `rule-family-guard.ts` gives: a cross-field refine
|
|
14024
|
+
* would also run when the store LOADS a rule.
|
|
14025
|
+
*/
|
|
14026
|
+
var NcSystemEventDeliveryRefusal = class extends Error {
|
|
14027
|
+
problems;
|
|
14028
|
+
constructor(problems) {
|
|
14029
|
+
super(`rule refused: ${problems.map(require_dist.describeSystemEventDeliveryProblem).join("; ")}`);
|
|
14030
|
+
this.name = "NcSystemEventDeliveryRefusal";
|
|
14031
|
+
this.problems = problems;
|
|
14032
|
+
}
|
|
14033
|
+
};
|
|
14034
|
+
function assertSystemEventDelivery(rule) {
|
|
14035
|
+
const problems = require_dist.systemEventDeliveryProblems(rule);
|
|
14036
|
+
if (problems.length > 0) throw new NcSystemEventDeliveryRefusal(problems);
|
|
14037
|
+
}
|
|
14038
|
+
var JUDGED_KEYS = [
|
|
14039
|
+
"delivery",
|
|
14040
|
+
"conditions",
|
|
14041
|
+
"systemEventDelivery",
|
|
14042
|
+
"schedule",
|
|
14043
|
+
"actions"
|
|
14044
|
+
];
|
|
14045
|
+
/**
|
|
14046
|
+
* The rule the patch WOULD produce, or `null` when it changes nothing the
|
|
14047
|
+
* verdict reads. `clear` removes `schedule` / `actions` before the merge, as
|
|
14048
|
+
* the store does.
|
|
14049
|
+
*/
|
|
14050
|
+
function mergedDeliverySubject(existing, patch, clear) {
|
|
14051
|
+
const merged = {
|
|
14052
|
+
delivery: existing.delivery,
|
|
14053
|
+
conditions: existing.conditions,
|
|
14054
|
+
...existing.systemEventDelivery !== void 0 ? { systemEventDelivery: existing.systemEventDelivery } : {},
|
|
14055
|
+
...existing.schedule !== void 0 && !clear.includes("schedule") ? { schedule: existing.schedule } : {},
|
|
14056
|
+
...existing.actions !== void 0 && !clear.includes("actions") ? { actions: existing.actions } : {},
|
|
14057
|
+
...patch.delivery !== void 0 ? { delivery: patch.delivery } : {},
|
|
14058
|
+
...patch.conditions !== void 0 ? { conditions: patch.conditions } : {},
|
|
14059
|
+
...patch.systemEventDelivery !== void 0 ? { systemEventDelivery: patch.systemEventDelivery } : {},
|
|
14060
|
+
...patch.schedule !== void 0 ? { schedule: patch.schedule } : {},
|
|
14061
|
+
...patch.actions !== void 0 ? { actions: patch.actions } : {}
|
|
14062
|
+
};
|
|
14063
|
+
return JUDGED_KEYS.some((key) => require_dist.canonicalHash(merged[key] ?? null) !== require_dist.canonicalHash(existing[key] ?? null)) ? merged : null;
|
|
14064
|
+
}
|
|
14065
|
+
function logRefusal$1(check, ctx) {
|
|
14066
|
+
try {
|
|
14067
|
+
check();
|
|
14068
|
+
} catch (err) {
|
|
14069
|
+
if (err instanceof NcSystemEventDeliveryRefusal) ctx.logger.warn("nc rule refused: summary delivery not allowed here", { meta: {
|
|
14070
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
14071
|
+
by: ctx.by,
|
|
14072
|
+
problems: err.problems.map(require_dist.describeSystemEventDeliveryProblem)
|
|
14073
|
+
} });
|
|
14074
|
+
throw err;
|
|
14075
|
+
}
|
|
14076
|
+
}
|
|
14077
|
+
/** The create path of both routes. */
|
|
14078
|
+
function guardRuleSystemEventDelivery(rule, ctx) {
|
|
14079
|
+
logRefusal$1(() => assertSystemEventDelivery(rule), ctx);
|
|
14080
|
+
}
|
|
14081
|
+
/** The update path of both routes: the MERGED rule, when the patch changes what is judged. */
|
|
14082
|
+
function guardPatchSystemEventDelivery(existing, patch, clear, ctx) {
|
|
14083
|
+
if (existing === null) return;
|
|
14084
|
+
const merged = mergedDeliverySubject(existing, patch, clear);
|
|
14085
|
+
if (merged === null) return;
|
|
14086
|
+
logRefusal$1(() => assertSystemEventDelivery(merged), ctx);
|
|
13832
14087
|
}
|
|
13833
14088
|
//#endregion
|
|
13834
14089
|
//#region src/notification-center/sensor-transition-guard.ts
|
|
@@ -16636,6 +16891,10 @@ function makeNcActionHandlers(deps) {
|
|
|
16636
16891
|
logger: deps.logger,
|
|
16637
16892
|
by: c.userId
|
|
16638
16893
|
});
|
|
16894
|
+
guardRuleSystemEventDelivery(parsed, {
|
|
16895
|
+
logger: deps.logger,
|
|
16896
|
+
by: c.userId
|
|
16897
|
+
});
|
|
16639
16898
|
await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
|
|
16640
16899
|
const rule = await deps.ruleStore.create({
|
|
16641
16900
|
...parsed,
|
|
@@ -16669,6 +16928,11 @@ function makeNcActionHandlers(deps) {
|
|
|
16669
16928
|
by: c.userId,
|
|
16670
16929
|
ruleId: input.ruleId
|
|
16671
16930
|
});
|
|
16931
|
+
guardPatchSystemEventDelivery(existing, patch, clear, {
|
|
16932
|
+
logger: deps.logger,
|
|
16933
|
+
by: c.userId,
|
|
16934
|
+
ruleId: input.ruleId
|
|
16935
|
+
});
|
|
16672
16936
|
if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
|
|
16673
16937
|
const { ownerUserId: _owner, disabledTargetIds: _optOut, ...safe } = patch;
|
|
16674
16938
|
const rule = await deps.ruleStore.update(input.ruleId, safe, clear);
|
|
@@ -17094,6 +17358,87 @@ function makeNcActionHandlers(deps) {
|
|
|
17094
17358
|
};
|
|
17095
17359
|
}
|
|
17096
17360
|
//#endregion
|
|
17361
|
+
//#region src/notification-center/state-summary/state-summary-delivery.ts
|
|
17362
|
+
/**
|
|
17363
|
+
* A state summary as an ORDINARY system-event subject (D647), so it reaches the
|
|
17364
|
+
* phone the way every system rule does: `buildEntries` → the durable outbox →
|
|
17365
|
+
* the drain's snooze gate, retries and per-target history → the dispatcher,
|
|
17366
|
+
* which renders the rule's own template with the summary's variables.
|
|
17367
|
+
*
|
|
17368
|
+
* - `recordId` is `state-summary:<ruleId>:<windowEndMs>` — the outbox dedup
|
|
17369
|
+
* key, so a restart can never enqueue one window twice.
|
|
17370
|
+
* - The row files under the system device (0): a summary spans devices.
|
|
17371
|
+
* `targetUsers` therefore reach admins only — a user whose grant is
|
|
17372
|
+
* narrower than the whole installation must not be sent the names of
|
|
17373
|
+
* devices they cannot open.
|
|
17374
|
+
* - The words are FROZEN at enqueue (`textParams`), like the digest's: the
|
|
17375
|
+
* state is the state at the window's end, not at a retry hours later. The
|
|
17376
|
+
* LANGUAGE is frozen with them.
|
|
17377
|
+
* - `kind` is the rule's first kind: the history row needs one, and no kind
|
|
17378
|
+
* is "the" event of a summary — `data.stateSummary` says what the row is,
|
|
17379
|
+
* and `{{eventKind}}` renders empty on it.
|
|
17380
|
+
*/
|
|
17381
|
+
var NC_STATE_SUMMARY_TITLE_KEY = "system.state-summary.title";
|
|
17382
|
+
var NC_STATE_SUMMARY_BODY_KEY = "system.state-summary.body";
|
|
17383
|
+
function stateSummaryRecordId(ruleId, windowEndMs) {
|
|
17384
|
+
return `state-summary:${ruleId}:${String(windowEndMs)}`;
|
|
17385
|
+
}
|
|
17386
|
+
/** Is this system-event data the data of a state-summary row? */
|
|
17387
|
+
function isStateSummaryData(data) {
|
|
17388
|
+
return data["stateSummary"] === true;
|
|
17389
|
+
}
|
|
17390
|
+
/** `null` when the rule has no system-event condition (the guard refuses that). */
|
|
17391
|
+
function stateSummarySubject(input) {
|
|
17392
|
+
const kind = input.rule.conditions.systemEvent?.kinds[0];
|
|
17393
|
+
if (kind === void 0) return null;
|
|
17394
|
+
return {
|
|
17395
|
+
kind: "system-event",
|
|
17396
|
+
recordId: stateSummaryRecordId(input.rule.id, input.window.endMs),
|
|
17397
|
+
deviceId: 0,
|
|
17398
|
+
timestamp: input.window.endMs,
|
|
17399
|
+
classNames: [],
|
|
17400
|
+
zones: [],
|
|
17401
|
+
systemEvent: {
|
|
17402
|
+
kind,
|
|
17403
|
+
subject: `state-summary:${input.rule.id}`,
|
|
17404
|
+
titleKey: NC_STATE_SUMMARY_TITLE_KEY,
|
|
17405
|
+
bodyKey: NC_STATE_SUMMARY_BODY_KEY,
|
|
17406
|
+
textParams: {
|
|
17407
|
+
summaryText: input.text.text,
|
|
17408
|
+
summaryCount: String(input.text.count),
|
|
17409
|
+
...input.text.sectionVars
|
|
17410
|
+
},
|
|
17411
|
+
textCount: input.text.count,
|
|
17412
|
+
data: {
|
|
17413
|
+
stateSummary: true,
|
|
17414
|
+
windowStartMs: input.window.startMs,
|
|
17415
|
+
windowEndMs: input.window.endMs,
|
|
17416
|
+
sections: input.summary.sections.map((section) => ({
|
|
17417
|
+
id: section.id,
|
|
17418
|
+
status: section.status,
|
|
17419
|
+
items: section.items.length
|
|
17420
|
+
}))
|
|
17421
|
+
}
|
|
17422
|
+
}
|
|
17423
|
+
};
|
|
17424
|
+
}
|
|
17425
|
+
/**
|
|
17426
|
+
* The rule as its summary is ENQUEUED: a custom body that carries none of the
|
|
17427
|
+
* summary's content gets `{{summaryText}}` appended on its own line, so an
|
|
17428
|
+
* operator's body never sends an empty summary (review M3). `null` = unchanged.
|
|
17429
|
+
*/
|
|
17430
|
+
function withSummaryBody(rule) {
|
|
17431
|
+
const body = rule.template?.body;
|
|
17432
|
+
if (require_dist.templateCarriesSummary(body)) return null;
|
|
17433
|
+
return {
|
|
17434
|
+
...rule,
|
|
17435
|
+
template: {
|
|
17436
|
+
...rule.template,
|
|
17437
|
+
body: `${body ?? ""}\n{{summaryText}}`
|
|
17438
|
+
}
|
|
17439
|
+
};
|
|
17440
|
+
}
|
|
17441
|
+
//#endregion
|
|
17097
17442
|
//#region src/notification-center/system-event-tap-url.ts
|
|
17098
17443
|
/** The admin UI's own routes, as `packages/addon-admin-ui/src/App.tsx` declares them. */
|
|
17099
17444
|
var ADMIN_ADDONS_PATH = "/system/addons";
|
|
@@ -17520,6 +17865,32 @@ function selectTriggerZoneCandidates(input) {
|
|
|
17520
17865
|
//#endregion
|
|
17521
17866
|
//#region src/notification-center/dispatcher.ts
|
|
17522
17867
|
/**
|
|
17868
|
+
* NcDispatcher — turns a durable outbox entry into ONE canonical
|
|
17869
|
+
* `notification-output.send` RPC.
|
|
17870
|
+
*
|
|
17871
|
+
* Deps are injected tRPC-shaped closures (the wiring in
|
|
17872
|
+
* pipeline-analytics binds them to `ctx.api` / the media store), so the
|
|
17873
|
+
* dispatcher is unit-testable with fakes and stays extractable.
|
|
17874
|
+
*
|
|
17875
|
+
* Responsibilities:
|
|
17876
|
+
* - resolve the outbox entry's `targetId` against the live
|
|
17877
|
+
* `notification-output` target catalog (TTL-cached; the send call is
|
|
17878
|
+
* routed to the owning provider by the target's `addonId`);
|
|
17879
|
+
* - resolve the media attachment per the rule's policy from the media
|
|
17880
|
+
* store BY OWNER at SEND time — D-3 "best media AVAILABLE at the
|
|
17881
|
+
* moment": the drain runs after the persist moment, so late-landing
|
|
17882
|
+
* crops are picked up without ever delaying delivery;
|
|
17883
|
+
* - render the `{{var}}` template (title/body) over the frozen subject;
|
|
17884
|
+
* - map recognized per-target passthrough params onto canonical
|
|
17885
|
+
* Notification fields (level/sound/clickUrl/ttl) — everything else is
|
|
17886
|
+
* the degrade engine's job.
|
|
17887
|
+
*
|
|
17888
|
+
* Failure semantics: an UNKNOWN target (no longer in the catalog) ⇒ PERMANENT
|
|
17889
|
+
* failure (the outbox dead-letters the row — genuine misconfig); a globally
|
|
17890
|
+
* DISABLED target (operator kill-switch) ⇒ benign skip (no send, no dead row);
|
|
17891
|
+
* transport/backend errors ⇒ transient (retried with backoff).
|
|
17892
|
+
*/
|
|
17893
|
+
/**
|
|
17523
17894
|
* Where the model's sentence goes.
|
|
17524
17895
|
*
|
|
17525
17896
|
* Three cases, and the third is the one that would otherwise be reported as a
|
|
@@ -19087,7 +19458,7 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
|
|
|
19087
19458
|
count: occupancy !== void 0 ? `${occupancy.count}` : group !== void 0 && group.members.length > 1 ? `${group.members.length}` : "",
|
|
19088
19459
|
capacity: occupancy !== void 0 ? `${occupancy.capacity}` : "",
|
|
19089
19460
|
op: occupancy !== void 0 ? ncOccupancyOp(texts, occupancy.occupied) : "",
|
|
19090
|
-
eventKind: systemEvent
|
|
19461
|
+
eventKind: systemEvent === void 0 || isStateSummaryData(systemEvent.data) ? "" : systemEvent.kind,
|
|
19091
19462
|
node: systemEvent?.nodeId ?? "",
|
|
19092
19463
|
package: systemEvent?.packageName ?? "",
|
|
19093
19464
|
currentVersion: typeof systemEvent?.data["currentVersion"] === "string" ? systemEvent.data["currentVersion"] : "",
|
|
@@ -22108,6 +22479,10 @@ var NcLivenessLedger = class {
|
|
|
22108
22479
|
async load() {
|
|
22109
22480
|
return (await this.ledger.load()).length;
|
|
22110
22481
|
}
|
|
22482
|
+
/** Has a load READ the store once? Until then the mirror is not the truth (D647 reads it). */
|
|
22483
|
+
hasAnswered() {
|
|
22484
|
+
return this.ledger.hasAnswered();
|
|
22485
|
+
}
|
|
22111
22486
|
/**
|
|
22112
22487
|
* Boot seed from the DEVICE DIRECTORY — the second half of the reseed, and
|
|
22113
22488
|
* the fix for the outage this ledger used to swallow.
|
|
@@ -28434,6 +28809,1328 @@ var AddonUpdateCoalescer = class {
|
|
|
28434
28809
|
}
|
|
28435
28810
|
};
|
|
28436
28811
|
//#endregion
|
|
28812
|
+
//#region src/notification-center/state-summary/state-sources.ts
|
|
28813
|
+
/**
|
|
28814
|
+
* Where each section of a STATE SUMMARY reads its state (D647) — the same
|
|
28815
|
+
* mirrors the instant raises read, and nothing else:
|
|
28816
|
+
*
|
|
28817
|
+
* | section | mirror |
|
|
28818
|
+
* | --- | --- |
|
|
28819
|
+
* | battery, consumables, tanks | the open alert episodes (`NcDeviceAlerts`, durable rows) |
|
|
28820
|
+
* | device-state | the liveness ledger (offline) + the system-state ledger (switched off) |
|
|
28821
|
+
* | streams, detection-blind, addons, updates | the system-state ledger |
|
|
28822
|
+
* | nodes | the liveness ledger (offline) + the system-state ledger (cannot infer) |
|
|
28823
|
+
*
|
|
28824
|
+
* Every source is RAM. A source that has not answered (its durable store never
|
|
28825
|
+
* read) makes the section UNAVAILABLE — never empty (D49/D315).
|
|
28826
|
+
*
|
|
28827
|
+
* A device offline for less than the liveness hold-off is left out: the instant
|
|
28828
|
+
* path would not have announced it yet either, and a summary must not report a
|
|
28829
|
+
* blip the rule itself would have stayed silent about.
|
|
28830
|
+
*/
|
|
28831
|
+
var UNREAD_ALERTS = "the battery / consumable ledger has not been read yet";
|
|
28832
|
+
var UNREAD_LIVENESS = "the liveness ledger has not been read yet";
|
|
28833
|
+
var UNREAD_STATE = "the system-state ledger has not been read yet";
|
|
28834
|
+
var UNREAD_DEVICES = "the device directory has not answered yet";
|
|
28835
|
+
var UNREAD_NODES = "the node directory did not answer completely";
|
|
28836
|
+
/** Sections whose items name a node: the node directory must have answered. */
|
|
28837
|
+
var NODE_SECTIONS = [
|
|
28838
|
+
"nodes",
|
|
28839
|
+
"addons",
|
|
28840
|
+
"updates"
|
|
28841
|
+
];
|
|
28842
|
+
function deviceParams(deviceId, identity) {
|
|
28843
|
+
const name = identity?.name ?? "";
|
|
28844
|
+
return {
|
|
28845
|
+
deviceId: String(deviceId),
|
|
28846
|
+
deviceName: name.length > 0 ? name : String(deviceId)
|
|
28847
|
+
};
|
|
28848
|
+
}
|
|
28849
|
+
function alertItem(section, open) {
|
|
28850
|
+
const params = open.subject.textParams ?? {};
|
|
28851
|
+
const unreadTank = open.family === "tank" && !open.measured;
|
|
28852
|
+
return {
|
|
28853
|
+
section,
|
|
28854
|
+
subject: open.subject,
|
|
28855
|
+
lineKey: unreadTank ? "summary.line.tank-unread" : open.subject.bodyKey ?? "",
|
|
28856
|
+
params,
|
|
28857
|
+
since: open.since
|
|
28858
|
+
};
|
|
28859
|
+
}
|
|
28860
|
+
function livenessItem(row, lookup) {
|
|
28861
|
+
if (row.subject.startsWith("node:")) {
|
|
28862
|
+
const nodeId = row.subject.slice(5);
|
|
28863
|
+
return {
|
|
28864
|
+
section: "nodes",
|
|
28865
|
+
subject: {
|
|
28866
|
+
kind: "node-offline",
|
|
28867
|
+
subject: row.subject,
|
|
28868
|
+
nodeId,
|
|
28869
|
+
textParams: { nodeId },
|
|
28870
|
+
data: {}
|
|
28871
|
+
},
|
|
28872
|
+
lineKey: "summary.line.node-offline",
|
|
28873
|
+
params: { nodeId },
|
|
28874
|
+
since: row.updatedAt
|
|
28875
|
+
};
|
|
28876
|
+
}
|
|
28877
|
+
if (row.deviceId === void 0) return null;
|
|
28878
|
+
const identity = lookup(row.deviceId);
|
|
28879
|
+
const params = deviceParams(row.deviceId, identity);
|
|
28880
|
+
return {
|
|
28881
|
+
section: "device-state",
|
|
28882
|
+
subject: {
|
|
28883
|
+
kind: "device-offline",
|
|
28884
|
+
subject: row.subject,
|
|
28885
|
+
deviceId: row.deviceId,
|
|
28886
|
+
...identity !== void 0 ? { deviceType: identity.type } : {},
|
|
28887
|
+
textParams: params,
|
|
28888
|
+
data: {}
|
|
28889
|
+
},
|
|
28890
|
+
lineKey: "summary.line.device-offline",
|
|
28891
|
+
params,
|
|
28892
|
+
since: row.updatedAt
|
|
28893
|
+
};
|
|
28894
|
+
}
|
|
28895
|
+
/** The section and the line of an open system-state row. */
|
|
28896
|
+
function stateLine(row) {
|
|
28897
|
+
switch (row.kind) {
|
|
28898
|
+
case "device-disabled": return {
|
|
28899
|
+
section: "device-state",
|
|
28900
|
+
lineKey: "summary.line.device-disabled"
|
|
28901
|
+
};
|
|
28902
|
+
case "stream-offline": return {
|
|
28903
|
+
section: "streams",
|
|
28904
|
+
lineKey: row.params["profile"] !== void 0 ? "summary.line.stream-offline.profiled" : "summary.line.stream-offline"
|
|
28905
|
+
};
|
|
28906
|
+
case "detection-blind": return {
|
|
28907
|
+
section: "detection-blind",
|
|
28908
|
+
lineKey: "summary.line.detection-blind"
|
|
28909
|
+
};
|
|
28910
|
+
case "node-inference-unavailable": return {
|
|
28911
|
+
section: "nodes",
|
|
28912
|
+
lineKey: "summary.line.node-inference-unavailable"
|
|
28913
|
+
};
|
|
28914
|
+
case "addon-crash-loop": return {
|
|
28915
|
+
section: "addons",
|
|
28916
|
+
lineKey: "summary.line.addon-crash-loop"
|
|
28917
|
+
};
|
|
28918
|
+
case "wrapper-update-available": return {
|
|
28919
|
+
section: "updates",
|
|
28920
|
+
lineKey: "summary.line.wrapper-update"
|
|
28921
|
+
};
|
|
28922
|
+
default: {
|
|
28923
|
+
const onNode = row.params["nodeId"] !== void 0;
|
|
28924
|
+
return {
|
|
28925
|
+
section: row.kind === "addon-update-available" ? "addons" : "updates",
|
|
28926
|
+
lineKey: onNode ? "summary.line.update.onNode" : "summary.line.update"
|
|
28927
|
+
};
|
|
28928
|
+
}
|
|
28929
|
+
}
|
|
28930
|
+
}
|
|
28931
|
+
function stateItem(row, lookup) {
|
|
28932
|
+
const identity = row.deviceId !== void 0 ? lookup(row.deviceId) : void 0;
|
|
28933
|
+
const params = row.deviceId !== void 0 ? {
|
|
28934
|
+
...row.params,
|
|
28935
|
+
...deviceParams(row.deviceId, identity)
|
|
28936
|
+
} : row.params;
|
|
28937
|
+
const deviceType = identity?.type ?? row.deviceType;
|
|
28938
|
+
const subject = {
|
|
28939
|
+
kind: row.kind,
|
|
28940
|
+
subject: row.key,
|
|
28941
|
+
...row.deviceId !== void 0 ? { deviceId: row.deviceId } : {},
|
|
28942
|
+
...deviceType !== void 0 ? { deviceType } : {},
|
|
28943
|
+
...row.nodeId !== void 0 ? { nodeId: row.nodeId } : {},
|
|
28944
|
+
...row.packageName !== void 0 ? {
|
|
28945
|
+
packageName: row.packageName,
|
|
28946
|
+
packageNames: [row.packageName]
|
|
28947
|
+
} : {},
|
|
28948
|
+
textParams: params,
|
|
28949
|
+
data: {}
|
|
28950
|
+
};
|
|
28951
|
+
return {
|
|
28952
|
+
...stateLine(row),
|
|
28953
|
+
subject,
|
|
28954
|
+
params,
|
|
28955
|
+
since: row.since
|
|
28956
|
+
};
|
|
28957
|
+
}
|
|
28958
|
+
/**
|
|
28959
|
+
* THE RECONCILE (D647 review I2, D8): a ledger row is a memory, never the
|
|
28960
|
+
* truth. A device item is listed only while the device EXISTS in the directory
|
|
28961
|
+
* and is not an accessory — and an "offline" one only while the device
|
|
28962
|
+
* manager does not call it online. A node item only while the node is known
|
|
28963
|
+
* to a complete node directory (a forgotten node is gone), and an "offline"
|
|
28964
|
+
* one only while the directory does not call it online.
|
|
28965
|
+
*/
|
|
28966
|
+
function stillTrue(item, ports) {
|
|
28967
|
+
const deviceId = item.subject.deviceId;
|
|
28968
|
+
if (deviceId !== void 0) {
|
|
28969
|
+
const identity = ports.lookup(deviceId);
|
|
28970
|
+
if (identity === void 0 || isAccessoryIdentity(identity)) return false;
|
|
28971
|
+
if (item.subject.kind === "device-offline" && identity.online === true) return false;
|
|
28972
|
+
}
|
|
28973
|
+
const nodeId = item.subject.nodeId;
|
|
28974
|
+
if (nodeId !== void 0 && ports.nodes !== null) {
|
|
28975
|
+
const online = ports.nodes.online(nodeId);
|
|
28976
|
+
if (online === void 0) return false;
|
|
28977
|
+
if (item.subject.kind === "node-offline" && online) return false;
|
|
28978
|
+
}
|
|
28979
|
+
return true;
|
|
28980
|
+
}
|
|
28981
|
+
/** Why a section cannot be reconciled right now, or `null` when it can. */
|
|
28982
|
+
function unreconcilable(section, ports) {
|
|
28983
|
+
if (NODE_SECTIONS.includes(section) && (ports.nodes === null || !ports.nodes.complete)) return UNREAD_NODES;
|
|
28984
|
+
if (!NODE_SECTIONS.includes(section) && !ports.devicesAnswered()) return UNREAD_DEVICES;
|
|
28985
|
+
return null;
|
|
28986
|
+
}
|
|
28987
|
+
/** The section, read and reconciled. */
|
|
28988
|
+
function readSection(section, ports, nowMs) {
|
|
28989
|
+
const blocked = unreconcilable(section, ports);
|
|
28990
|
+
if (blocked !== null) return {
|
|
28991
|
+
answered: false,
|
|
28992
|
+
reason: blocked
|
|
28993
|
+
};
|
|
28994
|
+
const read = readRaw(section, ports, nowMs);
|
|
28995
|
+
if (!read.answered) return read;
|
|
28996
|
+
return {
|
|
28997
|
+
answered: true,
|
|
28998
|
+
items: read.items.filter((item) => stillTrue(item, ports))
|
|
28999
|
+
};
|
|
29000
|
+
}
|
|
29001
|
+
/** Which ports a section needs, and why each one is unavailable. */
|
|
29002
|
+
function readRaw(section, ports, nowMs) {
|
|
29003
|
+
if (section === "battery" || section === "consumables" || section === "tanks") {
|
|
29004
|
+
const alerts = ports.alerts();
|
|
29005
|
+
if (alerts === null) return {
|
|
29006
|
+
answered: false,
|
|
29007
|
+
reason: UNREAD_ALERTS
|
|
29008
|
+
};
|
|
29009
|
+
const family = section === "battery" ? "battery" : section === "tanks" ? "tank" : "consumable";
|
|
29010
|
+
return {
|
|
29011
|
+
answered: true,
|
|
29012
|
+
items: alerts.filter((open) => open.family === family).map((open) => alertItem(section, open))
|
|
29013
|
+
};
|
|
29014
|
+
}
|
|
29015
|
+
const state = ports.state();
|
|
29016
|
+
if (state === null) return {
|
|
29017
|
+
answered: false,
|
|
29018
|
+
reason: UNREAD_STATE
|
|
29019
|
+
};
|
|
29020
|
+
const items = [];
|
|
29021
|
+
if (section === "device-state" || section === "nodes") {
|
|
29022
|
+
const liveness = ports.liveness();
|
|
29023
|
+
if (liveness === null) return {
|
|
29024
|
+
answered: false,
|
|
29025
|
+
reason: UNREAD_LIVENESS
|
|
29026
|
+
};
|
|
29027
|
+
for (const row of liveness) {
|
|
29028
|
+
if (row.state !== "offline" || nowMs - row.updatedAt < ports.livenessHoldoffMs) continue;
|
|
29029
|
+
const item = livenessItem(row, ports.lookup);
|
|
29030
|
+
if (item !== null && item.section === section) items.push(item);
|
|
29031
|
+
}
|
|
29032
|
+
}
|
|
29033
|
+
for (const row of state) {
|
|
29034
|
+
const item = stateItem(row, ports.lookup);
|
|
29035
|
+
if (item.section === section) items.push(item);
|
|
29036
|
+
}
|
|
29037
|
+
return {
|
|
29038
|
+
answered: true,
|
|
29039
|
+
items
|
|
29040
|
+
};
|
|
29041
|
+
}
|
|
29042
|
+
function stateSources(ports) {
|
|
29043
|
+
return { read: (section, nowMs) => readSection(section, ports, nowMs) };
|
|
29044
|
+
}
|
|
29045
|
+
//#endregion
|
|
29046
|
+
//#region src/notification-center/state-summary/state-model.ts
|
|
29047
|
+
/**
|
|
29048
|
+
* The STATE SUMMARY's vocabulary (D647): its sections, and what one reported
|
|
29049
|
+
* item is.
|
|
29050
|
+
*
|
|
29051
|
+
* A section is one of the D637 kind GROUPS (`NC_SYSTEM_EVENT_KIND_GROUP_SPECS`)
|
|
29052
|
+
* — the same grouping both editors list kinds under — so "the rule selected a
|
|
29053
|
+
* battery kind" and "the summary has a battery section" are the same fact.
|
|
29054
|
+
* `backups-exports` is the one group with no section: a backup or an export is
|
|
29055
|
+
* a job that COMPLETED, and the state it leaves behind is not a condition that
|
|
29056
|
+
* is still true. The ADR records why it is excluded rather than approximated.
|
|
29057
|
+
*/
|
|
29058
|
+
var NC_STATE_SUMMARY_SECTION_IDS = [
|
|
29059
|
+
"device-state",
|
|
29060
|
+
"streams",
|
|
29061
|
+
"battery",
|
|
29062
|
+
"consumables",
|
|
29063
|
+
"tanks",
|
|
29064
|
+
"detection-blind",
|
|
29065
|
+
"nodes",
|
|
29066
|
+
"addons",
|
|
29067
|
+
"updates"
|
|
29068
|
+
];
|
|
29069
|
+
/** The kind groups that have no section, and why — said on the preview. */
|
|
29070
|
+
var NC_STATE_SUMMARY_EXCLUDED_GROUPS = { "backups-exports": "a backup or an export is a completed job, not a state that is still true" };
|
|
29071
|
+
/** The template variable that carries one section's lines (`template-vars.ts`). */
|
|
29072
|
+
var NC_STATE_SECTION_VAR = {
|
|
29073
|
+
"device-state": "summaryDevices",
|
|
29074
|
+
streams: "summaryStreams",
|
|
29075
|
+
battery: "summaryBattery",
|
|
29076
|
+
consumables: "summaryConsumables",
|
|
29077
|
+
tanks: "summaryTanks",
|
|
29078
|
+
"detection-blind": "summaryDetection",
|
|
29079
|
+
nodes: "summaryNodes",
|
|
29080
|
+
addons: "summaryAddons",
|
|
29081
|
+
updates: "summaryUpdates"
|
|
29082
|
+
};
|
|
29083
|
+
/** The kinds of a section — its kind group. */
|
|
29084
|
+
function sectionKinds(section) {
|
|
29085
|
+
return require_dist.NC_SYSTEM_EVENT_KIND_GROUP_SPECS.find((group) => group.id === section)?.kinds ?? [];
|
|
29086
|
+
}
|
|
29087
|
+
/**
|
|
29088
|
+
* The sections a rule reports, in section order: every section whose kind
|
|
29089
|
+
* group shares a kind with the rule. A rule that selected only
|
|
29090
|
+
* `device-battery-low` still reports the battery STATE — the state has no
|
|
29091
|
+
* direction, only the event did.
|
|
29092
|
+
*/
|
|
29093
|
+
function sectionsOfRule(condition) {
|
|
29094
|
+
return NC_STATE_SUMMARY_SECTION_IDS.filter((section) => sectionKinds(section).some((kind) => condition.kinds.includes(kind)));
|
|
29095
|
+
}
|
|
29096
|
+
/** The kind groups the rule selected that have no section. */
|
|
29097
|
+
function excludedGroupsOfRule(condition) {
|
|
29098
|
+
return require_dist.NC_SYSTEM_EVENT_KIND_GROUP_SPECS.filter((group) => NC_STATE_SUMMARY_EXCLUDED_GROUPS[group.id] !== void 0 && group.kinds.some((kind) => condition.kinds.includes(kind))).map((group) => group.id);
|
|
29099
|
+
}
|
|
29100
|
+
/**
|
|
29101
|
+
* The condition the summary judges its items with: the rule's own, with its
|
|
29102
|
+
* kinds widened to every kind of every section it reports — so a rule that
|
|
29103
|
+
* named the raise is not told "not my kind" about the state the raise left
|
|
29104
|
+
* behind. Every FILTER is the rule's, untouched.
|
|
29105
|
+
*/
|
|
29106
|
+
function summaryCondition(condition) {
|
|
29107
|
+
const kinds = sectionsOfRule(condition).flatMap((section) => sectionKinds(section));
|
|
29108
|
+
return {
|
|
29109
|
+
...condition,
|
|
29110
|
+
kinds: [...new Set([...condition.kinds, ...kinds])]
|
|
29111
|
+
};
|
|
29112
|
+
}
|
|
29113
|
+
//#endregion
|
|
29114
|
+
//#region src/notification-center/state-summary/state-summary.ts
|
|
29115
|
+
/** Nothing to say: no item, and every section READ. Such a summary is not sent. */
|
|
29116
|
+
function isEmptyStateSummary(summary) {
|
|
29117
|
+
return summary.itemCount === 0 && summary.unavailableCount === 0;
|
|
29118
|
+
}
|
|
29119
|
+
function collectStateSummary(input) {
|
|
29120
|
+
const condition = input.rule.conditions.systemEvent;
|
|
29121
|
+
if (condition === void 0) return {
|
|
29122
|
+
sections: [],
|
|
29123
|
+
itemCount: 0,
|
|
29124
|
+
unavailableCount: 0,
|
|
29125
|
+
dropped: {},
|
|
29126
|
+
excludedGroups: []
|
|
29127
|
+
};
|
|
29128
|
+
const judge = {
|
|
29129
|
+
...input.rule,
|
|
29130
|
+
conditions: {
|
|
29131
|
+
...input.rule.conditions,
|
|
29132
|
+
systemEvent: summaryCondition(condition)
|
|
29133
|
+
},
|
|
29134
|
+
schedule: void 0,
|
|
29135
|
+
systemEventDelivery: void 0
|
|
29136
|
+
};
|
|
29137
|
+
const dropped = {};
|
|
29138
|
+
const drop = (reason) => {
|
|
29139
|
+
dropped[reason] = (dropped[reason] ?? 0) + 1;
|
|
29140
|
+
};
|
|
29141
|
+
const sections = [];
|
|
29142
|
+
for (const id of sectionsOfRule(condition)) {
|
|
29143
|
+
const read = input.sources.read(id, input.nowMs);
|
|
29144
|
+
if (!read.answered) {
|
|
29145
|
+
sections.push({
|
|
29146
|
+
id,
|
|
29147
|
+
status: "unavailable",
|
|
29148
|
+
items: [],
|
|
29149
|
+
reason: read.reason
|
|
29150
|
+
});
|
|
29151
|
+
continue;
|
|
29152
|
+
}
|
|
29153
|
+
const items = read.items.filter((item) => {
|
|
29154
|
+
const deviceId = item.subject.deviceId;
|
|
29155
|
+
if (deviceId !== void 0 && input.isMuted(deviceId)) {
|
|
29156
|
+
drop("muted");
|
|
29157
|
+
return false;
|
|
29158
|
+
}
|
|
29159
|
+
const verdict = evaluateRule(judge, ruleSubjectOf(item, input.nowMs));
|
|
29160
|
+
if (verdict.matched) return true;
|
|
29161
|
+
drop(verdict.excludedBySystemEventFilter ?? verdict.failedCondition ?? "unknown");
|
|
29162
|
+
return false;
|
|
29163
|
+
});
|
|
29164
|
+
sections.push({
|
|
29165
|
+
id,
|
|
29166
|
+
status: "read",
|
|
29167
|
+
items: items.toSorted(bySince)
|
|
29168
|
+
});
|
|
29169
|
+
}
|
|
29170
|
+
return {
|
|
29171
|
+
sections,
|
|
29172
|
+
itemCount: sections.reduce((sum, section) => sum + section.items.length, 0),
|
|
29173
|
+
unavailableCount: sections.filter((section) => section.status === "unavailable").length,
|
|
29174
|
+
dropped,
|
|
29175
|
+
excludedGroups: excludedGroupsOfRule(condition)
|
|
29176
|
+
};
|
|
29177
|
+
}
|
|
29178
|
+
function bySince(a, b) {
|
|
29179
|
+
return (a.since ?? 0) - (b.since ?? 0);
|
|
29180
|
+
}
|
|
29181
|
+
function ruleSubjectOf(item, nowMs) {
|
|
29182
|
+
return {
|
|
29183
|
+
kind: "system-event",
|
|
29184
|
+
recordId: `state:${item.subject.subject}`,
|
|
29185
|
+
deviceId: item.subject.deviceId ?? 0,
|
|
29186
|
+
timestamp: nowMs,
|
|
29187
|
+
classNames: [],
|
|
29188
|
+
zones: [],
|
|
29189
|
+
systemEvent: item.subject
|
|
29190
|
+
};
|
|
29191
|
+
}
|
|
29192
|
+
var DAY_MS$3 = 864e5;
|
|
29193
|
+
/** "07:12" when it began in the last day, "25 Sep, 07:12" before that. */
|
|
29194
|
+
function sinceText(texts, sinceMs, nowMs, timeZone) {
|
|
29195
|
+
if (nowMs - sinceMs < DAY_MS$3) return ncClockText(texts.language, sinceMs, timeZone);
|
|
29196
|
+
return new Intl.DateTimeFormat(texts.language, {
|
|
29197
|
+
day: "numeric",
|
|
29198
|
+
month: "short",
|
|
29199
|
+
hour: "2-digit",
|
|
29200
|
+
minute: "2-digit",
|
|
29201
|
+
...timeZone !== void 0 ? { timeZone } : {}
|
|
29202
|
+
}).format(new Date(sinceMs));
|
|
29203
|
+
}
|
|
29204
|
+
function lineOf(input, item) {
|
|
29205
|
+
const since = item.since !== void 0 ? sinceText(input.texts, item.since, input.nowMs, input.timeZone) : "";
|
|
29206
|
+
return input.texts.text({
|
|
29207
|
+
key: item.lineKey,
|
|
29208
|
+
vars: {
|
|
29209
|
+
...item.params,
|
|
29210
|
+
since
|
|
29211
|
+
}
|
|
29212
|
+
});
|
|
29213
|
+
}
|
|
29214
|
+
function blockOf(input, section) {
|
|
29215
|
+
const title = input.texts.text({ key: `summary.section.${section.id}` });
|
|
29216
|
+
if (section.status === "unavailable") return input.texts.text({
|
|
29217
|
+
key: "summary.section.unavailable",
|
|
29218
|
+
vars: { section: title }
|
|
29219
|
+
});
|
|
29220
|
+
if (section.items.length === 0) return null;
|
|
29221
|
+
const lines = section.items.map((item) => `• ${lineOf(input, item)}`);
|
|
29222
|
+
return [`${title}:`, ...lines].join("\n");
|
|
29223
|
+
}
|
|
29224
|
+
function stateSummaryText(input) {
|
|
29225
|
+
const sectionVars = {};
|
|
29226
|
+
const blocks = [];
|
|
29227
|
+
for (const section of input.summary.sections) {
|
|
29228
|
+
const block = blockOf(input, section);
|
|
29229
|
+
sectionVars[NC_STATE_SECTION_VAR[section.id]] = block ?? "";
|
|
29230
|
+
if (block !== null) blocks.push(block);
|
|
29231
|
+
}
|
|
29232
|
+
return {
|
|
29233
|
+
text: blocks.join("\n"),
|
|
29234
|
+
count: input.summary.itemCount,
|
|
29235
|
+
sectionVars
|
|
29236
|
+
};
|
|
29237
|
+
}
|
|
29238
|
+
//#endregion
|
|
29239
|
+
//#region src/notification-center/state-summary/zoned-clock.ts
|
|
29240
|
+
var WEEKDAYS = {
|
|
29241
|
+
Sun: 0,
|
|
29242
|
+
Mon: 1,
|
|
29243
|
+
Tue: 2,
|
|
29244
|
+
Wed: 3,
|
|
29245
|
+
Thu: 4,
|
|
29246
|
+
Fri: 5,
|
|
29247
|
+
Sat: 6
|
|
29248
|
+
};
|
|
29249
|
+
var FORMATTERS = /* @__PURE__ */ new Map();
|
|
29250
|
+
function formatterFor(timezone) {
|
|
29251
|
+
const held = FORMATTERS.get(timezone);
|
|
29252
|
+
if (held !== void 0) return held;
|
|
29253
|
+
let made;
|
|
29254
|
+
try {
|
|
29255
|
+
made = new Intl.DateTimeFormat("en-US", {
|
|
29256
|
+
timeZone: timezone,
|
|
29257
|
+
year: "numeric",
|
|
29258
|
+
month: "numeric",
|
|
29259
|
+
day: "numeric",
|
|
29260
|
+
weekday: "short",
|
|
29261
|
+
hour: "numeric",
|
|
29262
|
+
minute: "numeric",
|
|
29263
|
+
hourCycle: "h23"
|
|
29264
|
+
});
|
|
29265
|
+
} catch {
|
|
29266
|
+
made = null;
|
|
29267
|
+
}
|
|
29268
|
+
FORMATTERS.set(timezone, made);
|
|
29269
|
+
return made;
|
|
29270
|
+
}
|
|
29271
|
+
function wallOf(atMs, timezone) {
|
|
29272
|
+
const date = new Date(atMs);
|
|
29273
|
+
const formatter = timezone !== void 0 ? formatterFor(timezone) : null;
|
|
29274
|
+
if (formatter !== null) {
|
|
29275
|
+
const parts = new Map(formatter.formatToParts(date).map((p) => [p.type, p.value]));
|
|
29276
|
+
const weekday = WEEKDAYS[parts.get("weekday") ?? ""];
|
|
29277
|
+
const wall = {
|
|
29278
|
+
year: Number(parts.get("year")),
|
|
29279
|
+
month: Number(parts.get("month")),
|
|
29280
|
+
day: Number(parts.get("day")),
|
|
29281
|
+
hour: Number(parts.get("hour")),
|
|
29282
|
+
minute: Number(parts.get("minute"))
|
|
29283
|
+
};
|
|
29284
|
+
if (weekday !== void 0 && Object.values(wall).every(Number.isFinite)) return {
|
|
29285
|
+
...wall,
|
|
29286
|
+
weekday
|
|
29287
|
+
};
|
|
29288
|
+
}
|
|
29289
|
+
return {
|
|
29290
|
+
year: date.getFullYear(),
|
|
29291
|
+
month: date.getMonth() + 1,
|
|
29292
|
+
day: date.getDate(),
|
|
29293
|
+
weekday: date.getDay(),
|
|
29294
|
+
hour: date.getHours(),
|
|
29295
|
+
minute: date.getMinutes()
|
|
29296
|
+
};
|
|
29297
|
+
}
|
|
29298
|
+
/** The calendar day `atMs` falls on, in the timezone. */
|
|
29299
|
+
function zonedDayOf(atMs, timezone) {
|
|
29300
|
+
const { year, month, day, weekday } = wallOf(atMs, timezone);
|
|
29301
|
+
return {
|
|
29302
|
+
year,
|
|
29303
|
+
month,
|
|
29304
|
+
day,
|
|
29305
|
+
weekday
|
|
29306
|
+
};
|
|
29307
|
+
}
|
|
29308
|
+
/** The day `offset` calendar days after `day`. Pure calendar arithmetic. */
|
|
29309
|
+
function addDays(day, offset) {
|
|
29310
|
+
const shifted = new Date(Date.UTC(day.year, day.month - 1, day.day + offset));
|
|
29311
|
+
return {
|
|
29312
|
+
year: shifted.getUTCFullYear(),
|
|
29313
|
+
month: shifted.getUTCMonth() + 1,
|
|
29314
|
+
day: shifted.getUTCDate(),
|
|
29315
|
+
weekday: ((day.weekday + offset) % 7 + 7) % 7
|
|
29316
|
+
};
|
|
29317
|
+
}
|
|
29318
|
+
/**
|
|
29319
|
+
* The instant of `minuteOfDay` on `day`, in the timezone. Two passes of the
|
|
29320
|
+
* offset read settle a DST change; a wall time that does not exist (the spring
|
|
29321
|
+
* gap) lands next to it, and the caller verifies every instant it produces.
|
|
29322
|
+
*/
|
|
29323
|
+
function zonedInstant(day, minuteOfDay, timezone) {
|
|
29324
|
+
const hour = Math.floor(minuteOfDay / 60);
|
|
29325
|
+
const minute = minuteOfDay % 60;
|
|
29326
|
+
if (timezone === void 0 || formatterFor(timezone) === null) return new Date(day.year, day.month - 1, day.day, hour, minute).getTime();
|
|
29327
|
+
const asUtc = Date.UTC(day.year, day.month - 1, day.day, hour, minute);
|
|
29328
|
+
const offsetAt = (atMs) => {
|
|
29329
|
+
const wall = wallOf(atMs, timezone);
|
|
29330
|
+
return Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute) - atMs;
|
|
29331
|
+
};
|
|
29332
|
+
return asUtc - offsetAt(asUtc - offsetAt(asUtc));
|
|
29333
|
+
}
|
|
29334
|
+
/** The widest look back: a weekly schedule closes at least once in eight days. */
|
|
29335
|
+
var NC_STATE_SUMMARY_MAX_LOOKBACK_MS = 8 * 864e5;
|
|
29336
|
+
/** A close found by the tick right after it is on time; later than this is `late`. */
|
|
29337
|
+
var ON_TIME_MS = 2 * 6e4;
|
|
29338
|
+
var MINUTE_MS = 6e4;
|
|
29339
|
+
var DAY_MS$2 = 864e5;
|
|
29340
|
+
/**
|
|
29341
|
+
* Every verified transition of `kind` in `(fromMs, toMs]`, ascending.
|
|
29342
|
+
* Candidates are each window's start and end minute on each day of the range
|
|
29343
|
+
* (the day before `fromMs` included, for a window crossing midnight).
|
|
29344
|
+
*/
|
|
29345
|
+
function scheduleTransitions(schedule, fromMs, toMs, kind, probe = require_dist.isScheduleActive) {
|
|
29346
|
+
if (toMs <= fromMs) return [];
|
|
29347
|
+
const first = zonedDayOf(fromMs - DAY_MS$2, schedule.timezone);
|
|
29348
|
+
const days = Math.ceil((toMs - fromMs) / DAY_MS$2) + 2;
|
|
29349
|
+
const minutes = /* @__PURE__ */ new Set();
|
|
29350
|
+
for (const window of schedule.windows) {
|
|
29351
|
+
minutes.add(window.startMinute);
|
|
29352
|
+
minutes.add(window.endMinute);
|
|
29353
|
+
}
|
|
29354
|
+
const found = /* @__PURE__ */ new Set();
|
|
29355
|
+
for (let offset = 0; offset <= days; offset += 1) {
|
|
29356
|
+
const day = addDays(first, offset);
|
|
29357
|
+
for (const minute of minutes) {
|
|
29358
|
+
const at = zonedInstant(day, minute, schedule.timezone);
|
|
29359
|
+
if (at <= fromMs || at > toMs || found.has(at)) continue;
|
|
29360
|
+
const before = probe(schedule, at - MINUTE_MS);
|
|
29361
|
+
const after = probe(schedule, at);
|
|
29362
|
+
if (kind === "close" ? before && !after : !before && after) found.add(at);
|
|
29363
|
+
}
|
|
29364
|
+
}
|
|
29365
|
+
return [...found].toSorted((a, b) => a - b);
|
|
29366
|
+
}
|
|
29367
|
+
/** The first window close after `afterMs`, within `horizonMs`, or `null`. */
|
|
29368
|
+
function nextStateSummaryClose(schedule, afterMs, horizonMs = NC_STATE_SUMMARY_MAX_LOOKBACK_MS, probe) {
|
|
29369
|
+
return scheduleTransitions(schedule, afterMs, afterMs + horizonMs, "close", probe)[0] ?? null;
|
|
29370
|
+
}
|
|
29371
|
+
function decideStateSummary(input) {
|
|
29372
|
+
const from = Math.max(input.floorMs, input.nowMs - NC_STATE_SUMMARY_MAX_LOOKBACK_MS);
|
|
29373
|
+
const endMs = scheduleTransitions(input.schedule, from, input.nowMs, "close", input.probe).at(-1);
|
|
29374
|
+
if (endMs === void 0) return { kind: "idle" };
|
|
29375
|
+
const window = {
|
|
29376
|
+
startMs: scheduleTransitions(input.schedule, endMs - DAY_MS$2, endMs, "open", input.probe).at(-1) ?? endMs,
|
|
29377
|
+
endMs
|
|
29378
|
+
};
|
|
29379
|
+
const overdueMs = input.nowMs - endMs;
|
|
29380
|
+
if (overdueMs > (input.graceMs ?? 108e5)) return {
|
|
29381
|
+
kind: "missed",
|
|
29382
|
+
window
|
|
29383
|
+
};
|
|
29384
|
+
return {
|
|
29385
|
+
kind: "send",
|
|
29386
|
+
window,
|
|
29387
|
+
late: overdueMs > ON_TIME_MS
|
|
29388
|
+
};
|
|
29389
|
+
}
|
|
29390
|
+
//#endregion
|
|
29391
|
+
//#region src/notification-center/state-summary/state-summary-scheduler.ts
|
|
29392
|
+
/**
|
|
29393
|
+
* The STATE SUMMARY's tick (D647): for every enabled summary rule, is a
|
|
29394
|
+
* window owed, and if so collect → word → enqueue → stamp.
|
|
29395
|
+
*
|
|
29396
|
+
* Driven by the centre's existing rule-reload tick — no timer of its own — and
|
|
29397
|
+
* single-flight. It reads RAM only: the rule mirror, the stamps and the state
|
|
29398
|
+
* mirrors (D447: nothing on this schedule queries SQLite).
|
|
29399
|
+
*
|
|
29400
|
+
* Every branch that sends nothing SAYS so: an empty state (INFO), a missed
|
|
29401
|
+
* window (WARN), a stamp ledger that has not been read (WARN — nothing is sent
|
|
29402
|
+
* until it has, because without it a restart could send a window twice), a
|
|
29403
|
+
* delivery that failed (WARN, retried on the next tick inside the grace).
|
|
29404
|
+
*
|
|
29405
|
+
* The stamp is LAST, as in the digest: a window whose enqueue failed stays owed.
|
|
29406
|
+
*/
|
|
29407
|
+
var NcStateSummaryScheduler = class {
|
|
29408
|
+
deps;
|
|
29409
|
+
inFlight = false;
|
|
29410
|
+
/** Per rule: the next window close, valid for one floor and one schedule. */
|
|
29411
|
+
nextClose = /* @__PURE__ */ new Map();
|
|
29412
|
+
/** One WARN per hold, not one per tick. */
|
|
29413
|
+
holdReported = false;
|
|
29414
|
+
constructor(deps) {
|
|
29415
|
+
this.deps = deps;
|
|
29416
|
+
}
|
|
29417
|
+
async tick() {
|
|
29418
|
+
const report = {
|
|
29419
|
+
decisions: 0,
|
|
29420
|
+
sent: 0,
|
|
29421
|
+
empty: 0,
|
|
29422
|
+
missed: 0,
|
|
29423
|
+
failed: 0,
|
|
29424
|
+
held: false
|
|
29425
|
+
};
|
|
29426
|
+
if (this.inFlight) return report;
|
|
29427
|
+
this.inFlight = true;
|
|
29428
|
+
try {
|
|
29429
|
+
const rules = this.deps.rules().filter((rule) => require_dist.systemEventSummaryScheduleOf(rule) !== null);
|
|
29430
|
+
if (rules.length === 0) return report;
|
|
29431
|
+
if (!this.deps.stamps.answered()) {
|
|
29432
|
+
report.held = true;
|
|
29433
|
+
if (!this.holdReported) {
|
|
29434
|
+
this.holdReported = true;
|
|
29435
|
+
this.deps.logger.warn("state summaries held — the send ledger has not been read yet", { meta: { rules: rules.map((rule) => rule.id) } });
|
|
29436
|
+
}
|
|
29437
|
+
return report;
|
|
29438
|
+
}
|
|
29439
|
+
this.holdReported = false;
|
|
29440
|
+
const nowMs = this.deps.now();
|
|
29441
|
+
for (const rule of rules) {
|
|
29442
|
+
const schedule = require_dist.systemEventSummaryScheduleOf(rule);
|
|
29443
|
+
if (schedule !== null) await this.tickRule(rule, schedule, nowMs, report);
|
|
29444
|
+
}
|
|
29445
|
+
return report;
|
|
29446
|
+
} finally {
|
|
29447
|
+
this.inFlight = false;
|
|
29448
|
+
}
|
|
29449
|
+
}
|
|
29450
|
+
async tickRule(rule, schedule, nowMs, report) {
|
|
29451
|
+
const stamp = await this.armed(rule, schedule, nowMs);
|
|
29452
|
+
if (stamp === null) return;
|
|
29453
|
+
const floorMs = Math.max(stamp.armedAt, stamp.lastWindowEnd);
|
|
29454
|
+
const cached = this.nextClose.get(rule.id);
|
|
29455
|
+
if (cached !== void 0 && cached.floorMs === floorMs && cached.scheduleKey === stamp.scheduleKey && nowMs < cached.atMs) return;
|
|
29456
|
+
report.decisions += 1;
|
|
29457
|
+
const decision = decideStateSummary({
|
|
29458
|
+
schedule,
|
|
29459
|
+
nowMs,
|
|
29460
|
+
floorMs,
|
|
29461
|
+
...this.deps.graceMs !== void 0 ? { graceMs: this.deps.graceMs } : {}
|
|
29462
|
+
});
|
|
29463
|
+
if (decision.kind === "idle") {
|
|
29464
|
+
const next = nextStateSummaryClose(schedule, nowMs);
|
|
29465
|
+
this.nextClose.set(rule.id, {
|
|
29466
|
+
floorMs,
|
|
29467
|
+
scheduleKey: stamp.scheduleKey,
|
|
29468
|
+
atMs: next ?? nowMs + 6912e5
|
|
29469
|
+
});
|
|
29470
|
+
return;
|
|
29471
|
+
}
|
|
29472
|
+
this.nextClose.delete(rule.id);
|
|
29473
|
+
const meta = {
|
|
29474
|
+
ruleId: rule.id,
|
|
29475
|
+
rule: rule.name,
|
|
29476
|
+
windowStart: decision.window.startMs,
|
|
29477
|
+
windowEnd: decision.window.endMs
|
|
29478
|
+
};
|
|
29479
|
+
if (decision.kind === "missed") {
|
|
29480
|
+
report.missed += 1;
|
|
29481
|
+
this.deps.logger.warn("state summary window MISSED — closed too long ago to send now", { meta: {
|
|
29482
|
+
...meta,
|
|
29483
|
+
overdueMs: nowMs - decision.window.endMs
|
|
29484
|
+
} });
|
|
29485
|
+
await this.stamp(stamp, decision.window.endMs);
|
|
29486
|
+
return;
|
|
29487
|
+
}
|
|
29488
|
+
const summary = await this.deps.collect(rule, nowMs);
|
|
29489
|
+
if (isEmptyStateSummary(summary)) {
|
|
29490
|
+
report.empty += 1;
|
|
29491
|
+
this.deps.logger.info("state summary has nothing to report — not sent", { meta: {
|
|
29492
|
+
...meta,
|
|
29493
|
+
dropped: summary.dropped
|
|
29494
|
+
} });
|
|
29495
|
+
await this.stamp(stamp, decision.window.endMs);
|
|
29496
|
+
return;
|
|
29497
|
+
}
|
|
29498
|
+
let inserted;
|
|
29499
|
+
try {
|
|
29500
|
+
inserted = await this.deps.deliver(rule, summary, decision.window);
|
|
29501
|
+
} catch (err) {
|
|
29502
|
+
report.failed += 1;
|
|
29503
|
+
this.deps.logger.warn("state summary could not be enqueued — retried on the next tick", { meta: {
|
|
29504
|
+
...meta,
|
|
29505
|
+
error: err instanceof Error ? err.message : String(err)
|
|
29506
|
+
} });
|
|
29507
|
+
return;
|
|
29508
|
+
}
|
|
29509
|
+
report.sent += 1;
|
|
29510
|
+
this.deps.logger.info("state summary enqueued", { meta: {
|
|
29511
|
+
...meta,
|
|
29512
|
+
late: decision.late,
|
|
29513
|
+
items: summary.itemCount,
|
|
29514
|
+
unavailable: summary.unavailableCount,
|
|
29515
|
+
sections: summary.sections.map((section) => `${section.id}:${section.status}`),
|
|
29516
|
+
dropped: summary.dropped,
|
|
29517
|
+
inserted
|
|
29518
|
+
} });
|
|
29519
|
+
await this.stamp(stamp, decision.window.endMs);
|
|
29520
|
+
}
|
|
29521
|
+
/**
|
|
29522
|
+
* The rule's stamp, armed NOW when the rule is new to summary mode or its
|
|
29523
|
+
* schedule changed. `null` when arming could not be written — nothing is
|
|
29524
|
+
* sent on an unarmed rule.
|
|
29525
|
+
*/
|
|
29526
|
+
async armed(rule, schedule, nowMs) {
|
|
29527
|
+
const scheduleKey = require_dist.canonicalHash(schedule);
|
|
29528
|
+
const held = this.deps.stamps.get(rule.id);
|
|
29529
|
+
if (held !== void 0 && held.scheduleKey === scheduleKey) return held;
|
|
29530
|
+
const armed = {
|
|
29531
|
+
ruleId: rule.id,
|
|
29532
|
+
armedAt: nowMs,
|
|
29533
|
+
lastWindowEnd: held?.lastWindowEnd ?? 0,
|
|
29534
|
+
scheduleKey
|
|
29535
|
+
};
|
|
29536
|
+
try {
|
|
29537
|
+
await this.deps.stamps.put(armed);
|
|
29538
|
+
} catch (err) {
|
|
29539
|
+
this.deps.logger.warn("state summary rule could not be armed — nothing is sent until it is", { meta: {
|
|
29540
|
+
ruleId: rule.id,
|
|
29541
|
+
error: err instanceof Error ? err.message : String(err)
|
|
29542
|
+
} });
|
|
29543
|
+
return null;
|
|
29544
|
+
}
|
|
29545
|
+
this.deps.logger.info("state summary rule armed — windows closing from now on are sent", { meta: {
|
|
29546
|
+
ruleId: rule.id,
|
|
29547
|
+
rule: rule.name,
|
|
29548
|
+
rearmed: held !== void 0
|
|
29549
|
+
} });
|
|
29550
|
+
return armed;
|
|
29551
|
+
}
|
|
29552
|
+
async stamp(held, windowEndMs) {
|
|
29553
|
+
try {
|
|
29554
|
+
await this.deps.stamps.put({
|
|
29555
|
+
...held,
|
|
29556
|
+
lastWindowEnd: Math.max(held.lastWindowEnd, windowEndMs)
|
|
29557
|
+
});
|
|
29558
|
+
} catch (err) {
|
|
29559
|
+
this.deps.logger.warn("state summary stamp did not land — the outbox dedup still holds", { meta: {
|
|
29560
|
+
ruleId: held.ruleId,
|
|
29561
|
+
windowEnd: windowEndMs,
|
|
29562
|
+
error: err instanceof Error ? err.message : String(err)
|
|
29563
|
+
} });
|
|
29564
|
+
}
|
|
29565
|
+
}
|
|
29566
|
+
};
|
|
29567
|
+
var SPEC = {
|
|
29568
|
+
collection: "notification-center:state-summary-stamps",
|
|
29569
|
+
columns: [
|
|
29570
|
+
{
|
|
29571
|
+
name: "ruleId",
|
|
29572
|
+
type: "TEXT",
|
|
29573
|
+
primaryKey: true,
|
|
29574
|
+
notNull: true
|
|
29575
|
+
},
|
|
29576
|
+
{
|
|
29577
|
+
name: "armedAt",
|
|
29578
|
+
type: "INTEGER",
|
|
29579
|
+
notNull: true
|
|
29580
|
+
},
|
|
29581
|
+
{
|
|
29582
|
+
name: "lastWindowEnd",
|
|
29583
|
+
type: "INTEGER",
|
|
29584
|
+
notNull: true
|
|
29585
|
+
},
|
|
29586
|
+
{
|
|
29587
|
+
name: "scheduleKey",
|
|
29588
|
+
type: "TEXT",
|
|
29589
|
+
notNull: true
|
|
29590
|
+
}
|
|
29591
|
+
],
|
|
29592
|
+
writeMode: "write-through",
|
|
29593
|
+
keyOf: (row) => row.ruleId,
|
|
29594
|
+
toValue: (row) => ({
|
|
29595
|
+
armedAt: row.armedAt,
|
|
29596
|
+
lastWindowEnd: row.lastWindowEnd,
|
|
29597
|
+
scheduleKey: row.scheduleKey
|
|
29598
|
+
}),
|
|
29599
|
+
fromRecord: (key, data) => {
|
|
29600
|
+
const armedAt = data["armedAt"];
|
|
29601
|
+
const lastWindowEnd = data["lastWindowEnd"];
|
|
29602
|
+
const scheduleKey = data["scheduleKey"];
|
|
29603
|
+
if (typeof armedAt !== "number" || typeof lastWindowEnd !== "number") return null;
|
|
29604
|
+
if (typeof scheduleKey !== "string") return null;
|
|
29605
|
+
return {
|
|
29606
|
+
ruleId: key,
|
|
29607
|
+
armedAt,
|
|
29608
|
+
lastWindowEnd,
|
|
29609
|
+
scheduleKey
|
|
29610
|
+
};
|
|
29611
|
+
},
|
|
29612
|
+
loadLimit: 1e4
|
|
29613
|
+
};
|
|
29614
|
+
var NcStateSummaryStampStore = class {
|
|
29615
|
+
ledger;
|
|
29616
|
+
constructor(deps) {
|
|
29617
|
+
this.ledger = new DurableLedger({
|
|
29618
|
+
spec: SPEC,
|
|
29619
|
+
store: deps.store,
|
|
29620
|
+
logger: deps.logger
|
|
29621
|
+
});
|
|
29622
|
+
}
|
|
29623
|
+
static declare(store) {
|
|
29624
|
+
return DurableLedger.declare(store, SPEC);
|
|
29625
|
+
}
|
|
29626
|
+
async load() {
|
|
29627
|
+
return (await this.ledger.load()).length;
|
|
29628
|
+
}
|
|
29629
|
+
port() {
|
|
29630
|
+
return {
|
|
29631
|
+
get: (ruleId) => this.ledger.get(ruleId),
|
|
29632
|
+
put: (row) => this.ledger.put(row),
|
|
29633
|
+
answered: () => this.ledger.hasAnswered()
|
|
29634
|
+
};
|
|
29635
|
+
}
|
|
29636
|
+
};
|
|
29637
|
+
//#endregion
|
|
29638
|
+
//#region src/notification-center/state-summary/system-state-ledger.ts
|
|
29639
|
+
/**
|
|
29640
|
+
* @durable class=ledger owner=notification-center
|
|
29641
|
+
* write="a system state the centre accepted OPENING (a stream offline, a device switched off, a camera detecting nothing, an update available, a node that cannot infer, a crash-looped runner); its closing event deletes the row"
|
|
29642
|
+
* retention="none — a row lives exactly as long as its state is open. Bounded by the installation: streams, devices, nodes and installed packages."
|
|
29643
|
+
*/
|
|
29644
|
+
var NC_SYSTEM_STATE_COLLECTION = "notification-center:system-state";
|
|
29645
|
+
var NC_SYSTEM_STATE_COLUMNS = [
|
|
29646
|
+
(
|
|
29647
|
+
/** The state's subject (`stream:<id>`, `device-switch:<id>`,
|
|
29648
|
+
* `update:addon:<node>:<package>`, ...). */
|
|
29649
|
+
{
|
|
29650
|
+
name: "key",
|
|
29651
|
+
type: "TEXT",
|
|
29652
|
+
primaryKey: true,
|
|
29653
|
+
notNull: true
|
|
29654
|
+
}),
|
|
29655
|
+
(
|
|
29656
|
+
/** The OPENING kind. */
|
|
29657
|
+
{
|
|
29658
|
+
name: "kind",
|
|
29659
|
+
type: "TEXT",
|
|
29660
|
+
notNull: true
|
|
29661
|
+
}),
|
|
29662
|
+
{
|
|
29663
|
+
name: "since",
|
|
29664
|
+
type: "INTEGER",
|
|
29665
|
+
notNull: true
|
|
29666
|
+
},
|
|
29667
|
+
(
|
|
29668
|
+
/** Indexed: every question about a device is asked per device. */
|
|
29669
|
+
{
|
|
29670
|
+
name: "deviceId",
|
|
29671
|
+
type: "INTEGER"
|
|
29672
|
+
}),
|
|
29673
|
+
{
|
|
29674
|
+
name: "nodeId",
|
|
29675
|
+
type: "TEXT"
|
|
29676
|
+
},
|
|
29677
|
+
{
|
|
29678
|
+
name: "packageName",
|
|
29679
|
+
type: "TEXT"
|
|
29680
|
+
},
|
|
29681
|
+
(
|
|
29682
|
+
/** The words' facts (device name, profile, versions, addons). */
|
|
29683
|
+
{
|
|
29684
|
+
name: "params",
|
|
29685
|
+
type: "JSON",
|
|
29686
|
+
notNull: true
|
|
29687
|
+
})
|
|
29688
|
+
];
|
|
29689
|
+
var NC_SYSTEM_STATE_INDEXES = [{
|
|
29690
|
+
name: "idx_nc_system_state_device",
|
|
29691
|
+
columns: ["deviceId"]
|
|
29692
|
+
}];
|
|
29693
|
+
/** The kinds that OPEN a row. */
|
|
29694
|
+
var NC_SYSTEM_STATE_OPENING_KINDS = [
|
|
29695
|
+
"stream-offline",
|
|
29696
|
+
"device-disabled",
|
|
29697
|
+
"detection-blind",
|
|
29698
|
+
"node-inference-unavailable",
|
|
29699
|
+
"addon-crash-loop",
|
|
29700
|
+
"addon-update-available",
|
|
29701
|
+
"server-update-available",
|
|
29702
|
+
"wrapper-update-available"
|
|
29703
|
+
];
|
|
29704
|
+
function isOpeningKind(value) {
|
|
29705
|
+
return NC_SYSTEM_STATE_OPENING_KINDS.some((kind) => kind === value);
|
|
29706
|
+
}
|
|
29707
|
+
function stringRecord(value) {
|
|
29708
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
29709
|
+
const out = {};
|
|
29710
|
+
for (const [key, entry] of Object.entries(value)) if (typeof entry === "string") out[key] = entry;
|
|
29711
|
+
return out;
|
|
29712
|
+
}
|
|
29713
|
+
var NC_SYSTEM_STATE_SPEC = {
|
|
29714
|
+
collection: NC_SYSTEM_STATE_COLLECTION,
|
|
29715
|
+
columns: NC_SYSTEM_STATE_COLUMNS,
|
|
29716
|
+
indexes: NC_SYSTEM_STATE_INDEXES,
|
|
29717
|
+
writeMode: "write-behind",
|
|
29718
|
+
keyOf: (row) => row.key,
|
|
29719
|
+
toValue: (row) => ({
|
|
29720
|
+
kind: row.kind,
|
|
29721
|
+
since: row.since,
|
|
29722
|
+
...row.deviceId !== void 0 ? { deviceId: row.deviceId } : {},
|
|
29723
|
+
...row.nodeId !== void 0 ? { nodeId: row.nodeId } : {},
|
|
29724
|
+
...row.packageName !== void 0 ? { packageName: row.packageName } : {},
|
|
29725
|
+
params: {
|
|
29726
|
+
...row.params,
|
|
29727
|
+
...row.deviceType !== void 0 ? { deviceType: row.deviceType } : {}
|
|
29728
|
+
}
|
|
29729
|
+
}),
|
|
29730
|
+
fromRecord: (key, data) => {
|
|
29731
|
+
const kind = data["kind"];
|
|
29732
|
+
const since = data["since"];
|
|
29733
|
+
const params = stringRecord(data["params"]);
|
|
29734
|
+
if (!isOpeningKind(kind) || typeof since !== "number" || params === null) return null;
|
|
29735
|
+
const deviceId = data["deviceId"];
|
|
29736
|
+
const nodeId = data["nodeId"];
|
|
29737
|
+
const packageName = data["packageName"];
|
|
29738
|
+
const { deviceType, ...words } = params;
|
|
29739
|
+
return {
|
|
29740
|
+
key,
|
|
29741
|
+
kind,
|
|
29742
|
+
since,
|
|
29743
|
+
...typeof deviceId === "number" ? { deviceId } : {},
|
|
29744
|
+
...deviceType !== void 0 ? { deviceType } : {},
|
|
29745
|
+
...typeof nodeId === "string" ? { nodeId } : {},
|
|
29746
|
+
...typeof packageName === "string" ? { packageName } : {},
|
|
29747
|
+
params: words
|
|
29748
|
+
};
|
|
29749
|
+
},
|
|
29750
|
+
deviceIdOf: (row) => row.deviceId
|
|
29751
|
+
};
|
|
29752
|
+
function isUpdateTarget(value) {
|
|
29753
|
+
return value === "addon" || value === "server" || value === "wrapper";
|
|
29754
|
+
}
|
|
29755
|
+
function updateKind(target) {
|
|
29756
|
+
return target === "addon" ? "addon-update-available" : target === "server" ? "server-update-available" : "wrapper-update-available";
|
|
29757
|
+
}
|
|
29758
|
+
function updateStateKey(target, nodeId, packageName) {
|
|
29759
|
+
return `update:${target}:${nodeId ?? "hub"}:${packageName}`;
|
|
29760
|
+
}
|
|
29761
|
+
var NcSystemStateLedger = class {
|
|
29762
|
+
deps;
|
|
29763
|
+
constructor(deps) {
|
|
29764
|
+
this.deps = deps;
|
|
29765
|
+
}
|
|
29766
|
+
/** The open rows, or `null` while the store has never answered. */
|
|
29767
|
+
openRows() {
|
|
29768
|
+
return this.deps.rows.answered() ? this.deps.rows.all() : null;
|
|
29769
|
+
}
|
|
29770
|
+
/**
|
|
29771
|
+
* One system subject the intake accepted. The update-available kinds are
|
|
29772
|
+
* not read here: {@link observeUpdateList} replaces them from the raw event.
|
|
29773
|
+
*/
|
|
29774
|
+
observe(system, atMs) {
|
|
29775
|
+
switch (system.kind) {
|
|
29776
|
+
case "stream-offline":
|
|
29777
|
+
case "device-disabled":
|
|
29778
|
+
case "detection-blind":
|
|
29779
|
+
case "node-inference-unavailable":
|
|
29780
|
+
case "addon-crash-loop":
|
|
29781
|
+
this.open(system, atMs);
|
|
29782
|
+
return;
|
|
29783
|
+
case "stream-online":
|
|
29784
|
+
case "device-enabled":
|
|
29785
|
+
this.close(system.subject, system.kind);
|
|
29786
|
+
return;
|
|
29787
|
+
case "node-online":
|
|
29788
|
+
case "node-offline":
|
|
29789
|
+
if (system.nodeId !== void 0) this.closeNodeFaults(system.nodeId, system.kind);
|
|
29790
|
+
return;
|
|
29791
|
+
case "server-updated":
|
|
29792
|
+
if (system.nodeId !== void 0) this.closeServerUpdate(system.nodeId);
|
|
29793
|
+
return;
|
|
29794
|
+
default: return;
|
|
29795
|
+
}
|
|
29796
|
+
}
|
|
29797
|
+
/** A detection on a camera: it is not blind. Called for every detection — a Map lookup. */
|
|
29798
|
+
observeDetection(deviceId) {
|
|
29799
|
+
const key = `detection-blind:${String(deviceId)}`;
|
|
29800
|
+
if (this.deps.rows.get(key) === void 0) return;
|
|
29801
|
+
this.close(key, "detection");
|
|
29802
|
+
}
|
|
29803
|
+
/**
|
|
29804
|
+
* Any `stream.online`, whatever its reason (D647 review I1). The intake gate
|
|
29805
|
+
* drops a `first-packet` online — a recreated broker's — because it is not
|
|
29806
|
+
* a recovery worth a notification; it is still proof the stream is up.
|
|
29807
|
+
*/
|
|
29808
|
+
observeStreamOnline(camStreamId) {
|
|
29809
|
+
this.close(`stream:${camStreamId}`, "stream-online");
|
|
29810
|
+
}
|
|
29811
|
+
/** One `update.available` list: REPLACE the rows of every node it names. */
|
|
29812
|
+
observeUpdateList(list, atMs) {
|
|
29813
|
+
const keep = new Set(list.rows.map((pkg) => updateStateKey(list.target, pkg.nodeId, pkg.packageName)));
|
|
29814
|
+
for (const nodeId of list.nodeIds) {
|
|
29815
|
+
const prefix = `update:${list.target}:${nodeId}:`;
|
|
29816
|
+
for (const row of this.deps.rows.all()) if (row.key.startsWith(prefix) && !keep.has(row.key)) this.close(row.key, "update-list");
|
|
29817
|
+
}
|
|
29818
|
+
for (const pkg of list.rows) this.openUpdate(list.target, pkg, atMs);
|
|
29819
|
+
}
|
|
29820
|
+
/**
|
|
29821
|
+
* An addon STARTED on `nodeId` (D647 review I5): the runner hosting it is
|
|
29822
|
+
* up again, so a crash-loop row naming it on THAT node is over — and only
|
|
29823
|
+
* on that node.
|
|
29824
|
+
*/
|
|
29825
|
+
observeAddonStarted(addonId, nodeId) {
|
|
29826
|
+
for (const row of this.deps.rows.all()) {
|
|
29827
|
+
if (row.kind !== "addon-crash-loop") continue;
|
|
29828
|
+
if (row.nodeId !== nodeId) continue;
|
|
29829
|
+
if ((row.params["addons"] ?? "").split(",").map((id) => id.trim()).includes(addonId)) this.close(row.key, "addon-started");
|
|
29830
|
+
}
|
|
29831
|
+
}
|
|
29832
|
+
/**
|
|
29833
|
+
* The balancer placed a camera on a node (not pinned): placement skips a
|
|
29834
|
+
* node that cannot infer, so the node can infer again (D647 review I5).
|
|
29835
|
+
*/
|
|
29836
|
+
observeCameraPlaced(nodeId) {
|
|
29837
|
+
this.close(`node-inference:${nodeId}`, "camera-placed");
|
|
29838
|
+
}
|
|
29839
|
+
/** `addon.updated` — the package is current on that node. */
|
|
29840
|
+
observeAddonUpdated(update) {
|
|
29841
|
+
this.close(updateStateKey("addon", update.nodeId, update.packageName), "addon-updated");
|
|
29842
|
+
}
|
|
29843
|
+
/** Every device that holds an open state — the deleted-device reconcile reads it. */
|
|
29844
|
+
deviceIds() {
|
|
29845
|
+
const ids = /* @__PURE__ */ new Set();
|
|
29846
|
+
for (const row of this.deps.rows.all()) if (row.deviceId !== void 0) ids.add(row.deviceId);
|
|
29847
|
+
return [...ids];
|
|
29848
|
+
}
|
|
29849
|
+
/** A deleted device leaves (D633). Returns how many rows went. */
|
|
29850
|
+
forgetDevice(deviceId) {
|
|
29851
|
+
const keys = this.deps.rows.all().filter((row) => row.deviceId === deviceId).map((row) => row.key);
|
|
29852
|
+
for (const key of keys) this.deps.rows.remove(key);
|
|
29853
|
+
return keys.length;
|
|
29854
|
+
}
|
|
29855
|
+
open(system, atMs) {
|
|
29856
|
+
const held = this.deps.rows.get(system.subject);
|
|
29857
|
+
const since = held !== void 0 && held.kind === system.kind ? held.since : atMs;
|
|
29858
|
+
this.deps.rows.put({
|
|
29859
|
+
key: system.subject,
|
|
29860
|
+
kind: system.kind,
|
|
29861
|
+
since,
|
|
29862
|
+
...system.deviceId !== void 0 ? { deviceId: system.deviceId } : {},
|
|
29863
|
+
...system.deviceType !== void 0 ? { deviceType: system.deviceType } : {},
|
|
29864
|
+
...system.nodeId !== void 0 ? { nodeId: system.nodeId } : {},
|
|
29865
|
+
params: system.textParams ?? {}
|
|
29866
|
+
});
|
|
29867
|
+
}
|
|
29868
|
+
openUpdate(target, pkg, atMs) {
|
|
29869
|
+
const key = updateStateKey(target, pkg.nodeId, pkg.packageName);
|
|
29870
|
+
const held = this.deps.rows.get(key);
|
|
29871
|
+
const kind = updateKind(target);
|
|
29872
|
+
this.deps.rows.put({
|
|
29873
|
+
key,
|
|
29874
|
+
kind,
|
|
29875
|
+
since: held?.since ?? atMs,
|
|
29876
|
+
...pkg.nodeId !== void 0 ? { nodeId: pkg.nodeId } : {},
|
|
29877
|
+
packageName: pkg.packageName,
|
|
29878
|
+
params: {
|
|
29879
|
+
packageName: pkg.packageName,
|
|
29880
|
+
currentVersion: pkg.currentVersion,
|
|
29881
|
+
latestVersion: pkg.latestVersion,
|
|
29882
|
+
...pkg.nodeId !== void 0 ? { nodeId: pkg.nodeId } : {}
|
|
29883
|
+
}
|
|
29884
|
+
});
|
|
29885
|
+
}
|
|
29886
|
+
close(key, by) {
|
|
29887
|
+
const held = this.deps.rows.get(key);
|
|
29888
|
+
if (held === void 0) return;
|
|
29889
|
+
this.deps.rows.remove(key);
|
|
29890
|
+
this.deps.logger?.debug("system state closed", {
|
|
29891
|
+
...held.deviceId !== void 0 ? { tags: { deviceId: held.deviceId } } : {},
|
|
29892
|
+
meta: {
|
|
29893
|
+
key,
|
|
29894
|
+
kind: held.kind,
|
|
29895
|
+
by,
|
|
29896
|
+
since: held.since
|
|
29897
|
+
}
|
|
29898
|
+
});
|
|
29899
|
+
}
|
|
29900
|
+
closeNodeFaults(nodeId, by) {
|
|
29901
|
+
for (const row of this.deps.rows.all()) {
|
|
29902
|
+
if (row.nodeId !== nodeId) continue;
|
|
29903
|
+
if (row.kind === "node-inference-unavailable" || row.kind === "addon-crash-loop") this.close(row.key, by);
|
|
29904
|
+
}
|
|
29905
|
+
}
|
|
29906
|
+
closeServerUpdate(nodeId) {
|
|
29907
|
+
for (const row of this.deps.rows.all()) {
|
|
29908
|
+
if ((row.nodeId ?? "hub") !== nodeId) continue;
|
|
29909
|
+
if (row.kind === "server-update-available" || row.kind === "wrapper-update-available" || row.kind === "addon-crash-loop" || row.kind === "node-inference-unavailable") this.close(row.key, "server-updated");
|
|
29910
|
+
}
|
|
29911
|
+
}
|
|
29912
|
+
};
|
|
29913
|
+
/** The durable home of the rows. */
|
|
29914
|
+
var NcSystemStateStore = class {
|
|
29915
|
+
ledger;
|
|
29916
|
+
constructor(deps) {
|
|
29917
|
+
this.ledger = new DurableLedger({
|
|
29918
|
+
spec: NC_SYSTEM_STATE_SPEC,
|
|
29919
|
+
store: deps.store,
|
|
29920
|
+
logger: deps.logger
|
|
29921
|
+
});
|
|
29922
|
+
}
|
|
29923
|
+
static declare(store) {
|
|
29924
|
+
return DurableLedger.declare(store, NC_SYSTEM_STATE_SPEC);
|
|
29925
|
+
}
|
|
29926
|
+
/** Boot reseed. A failed load keeps the mirror and leaves `answered` false. */
|
|
29927
|
+
async load() {
|
|
29928
|
+
return (await this.ledger.load()).length;
|
|
29929
|
+
}
|
|
29930
|
+
port() {
|
|
29931
|
+
return {
|
|
29932
|
+
get: (key) => this.ledger.get(key),
|
|
29933
|
+
put: (row) => {
|
|
29934
|
+
this.ledger.put(row).catch(() => void 0);
|
|
29935
|
+
},
|
|
29936
|
+
remove: (key) => {
|
|
29937
|
+
this.ledger.forget(key);
|
|
29938
|
+
},
|
|
29939
|
+
all: () => this.ledger.snapshot(),
|
|
29940
|
+
answered: () => this.ledger.hasAnswered()
|
|
29941
|
+
};
|
|
29942
|
+
}
|
|
29943
|
+
};
|
|
29944
|
+
//#endregion
|
|
29945
|
+
//#region src/notification-center/state-summary/state-summary-service.ts
|
|
29946
|
+
function stringOf(data, key) {
|
|
29947
|
+
const value = data[key];
|
|
29948
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
29949
|
+
}
|
|
29950
|
+
/**
|
|
29951
|
+
* The full list an `update.available` event carries, and the nodes it speaks
|
|
29952
|
+
* for: every node a row names, plus the event's own `nodeId`. The emitter
|
|
29953
|
+
* never sends an empty list (it emits only when a `latestVersion` changes), so
|
|
29954
|
+
* the empty case is defensive, not a path that clears anything today — see
|
|
29955
|
+
* {@link NcUpdateList} for the limit that leaves.
|
|
29956
|
+
*/
|
|
29957
|
+
function updateListOf(data) {
|
|
29958
|
+
const target = data["target"];
|
|
29959
|
+
if (!isUpdateTarget(target)) return null;
|
|
29960
|
+
const rows = updatePackages(data);
|
|
29961
|
+
const eventNode = stringOf(data, "nodeId");
|
|
29962
|
+
const nodes = new Set(rows.map((row) => row.nodeId ?? eventNode ?? "hub"));
|
|
29963
|
+
if (eventNode !== void 0) nodes.add(eventNode);
|
|
29964
|
+
const withNode = rows.map((row) => row.nodeId === void 0 && eventNode !== void 0 ? {
|
|
29965
|
+
...row,
|
|
29966
|
+
nodeId: eventNode
|
|
29967
|
+
} : row);
|
|
29968
|
+
return {
|
|
29969
|
+
target,
|
|
29970
|
+
nodeIds: [...nodes],
|
|
29971
|
+
rows: withNode
|
|
29972
|
+
};
|
|
29973
|
+
}
|
|
29974
|
+
var NcStateSummaryService = class {
|
|
29975
|
+
deps;
|
|
29976
|
+
stateStore;
|
|
29977
|
+
stampStore;
|
|
29978
|
+
ledger;
|
|
29979
|
+
scheduler;
|
|
29980
|
+
constructor(deps) {
|
|
29981
|
+
this.deps = deps;
|
|
29982
|
+
this.stateStore = new NcSystemStateStore({
|
|
29983
|
+
store: deps.store,
|
|
29984
|
+
logger: deps.logger
|
|
29985
|
+
});
|
|
29986
|
+
this.stampStore = new NcStateSummaryStampStore({
|
|
29987
|
+
store: deps.store,
|
|
29988
|
+
logger: deps.logger
|
|
29989
|
+
});
|
|
29990
|
+
this.ledger = new NcSystemStateLedger({
|
|
29991
|
+
rows: this.stateStore.port(),
|
|
29992
|
+
logger: deps.logger
|
|
29993
|
+
});
|
|
29994
|
+
this.scheduler = new NcStateSummaryScheduler({
|
|
29995
|
+
rules: deps.rules,
|
|
29996
|
+
stamps: this.stampStore.port(),
|
|
29997
|
+
collect: (rule, nowMs) => this.collect(rule, nowMs),
|
|
29998
|
+
deliver: (rule, summary, window) => this.deliver(rule, summary, window),
|
|
29999
|
+
now: deps.now,
|
|
30000
|
+
logger: deps.logger
|
|
30001
|
+
});
|
|
30002
|
+
}
|
|
30003
|
+
static async declare(store) {
|
|
30004
|
+
await NcSystemStateStore.declare(store);
|
|
30005
|
+
await NcStateSummaryStampStore.declare(store);
|
|
30006
|
+
}
|
|
30007
|
+
/** Boot back-fill. Each load that fails leaves its mirror unanswered — the summary then says so. */
|
|
30008
|
+
async load() {
|
|
30009
|
+
return {
|
|
30010
|
+
states: await this.stateStore.load(),
|
|
30011
|
+
stamps: await this.stampStore.load()
|
|
30012
|
+
};
|
|
30013
|
+
}
|
|
30014
|
+
/** One system subject the intake accepted. */
|
|
30015
|
+
observeSystem(system, atMs) {
|
|
30016
|
+
this.ledger.observe(system, atMs);
|
|
30017
|
+
}
|
|
30018
|
+
/**
|
|
30019
|
+
* The RAW bus event, before the intake's gates (D647 review I1/I3/I5): the
|
|
30020
|
+
* facts that close or replace a state even when no notification is owed —
|
|
30021
|
+
* any `stream.online`, the full `update.available` list of a node, an addon
|
|
30022
|
+
* that started, a camera the balancer placed.
|
|
30023
|
+
*/
|
|
30024
|
+
observeRawEvent(category, data, atMs, sourceNodeId) {
|
|
30025
|
+
switch (category) {
|
|
30026
|
+
case "stream.online": {
|
|
30027
|
+
const camStreamId = stringOf(data, "camStreamId");
|
|
30028
|
+
if (camStreamId !== void 0) this.ledger.observeStreamOnline(camStreamId);
|
|
30029
|
+
return;
|
|
30030
|
+
}
|
|
30031
|
+
case "update.available": {
|
|
30032
|
+
const list = updateListOf(data);
|
|
30033
|
+
if (list !== null) this.ledger.observeUpdateList(list, atMs);
|
|
30034
|
+
return;
|
|
30035
|
+
}
|
|
30036
|
+
case "addon.started":
|
|
30037
|
+
case "addon.restarted": {
|
|
30038
|
+
const addonId = stringOf(data, "addonId");
|
|
30039
|
+
if (addonId !== void 0) this.ledger.observeAddonStarted(addonId, sourceNodeId ?? "hub");
|
|
30040
|
+
return;
|
|
30041
|
+
}
|
|
30042
|
+
case "pipeline.camera-assigned": {
|
|
30043
|
+
const nodeId = stringOf(data, "agentNodeId");
|
|
30044
|
+
const placed = data["pinned"] === false && data["reason"] !== "unassigned";
|
|
30045
|
+
if (nodeId !== void 0 && placed) this.ledger.observeCameraPlaced(nodeId);
|
|
30046
|
+
return;
|
|
30047
|
+
}
|
|
30048
|
+
default: return;
|
|
30049
|
+
}
|
|
30050
|
+
}
|
|
30051
|
+
observeDetection(deviceId) {
|
|
30052
|
+
this.ledger.observeDetection(deviceId);
|
|
30053
|
+
}
|
|
30054
|
+
observeAddonUpdated(update) {
|
|
30055
|
+
this.ledger.observeAddonUpdated(update);
|
|
30056
|
+
}
|
|
30057
|
+
tick() {
|
|
30058
|
+
return this.scheduler.tick();
|
|
30059
|
+
}
|
|
30060
|
+
/** The summary this rule would send NOW (`previewStateSummary`) — sends nothing, stamps nothing. */
|
|
30061
|
+
async preview(rule) {
|
|
30062
|
+
const nowMs = this.deps.now();
|
|
30063
|
+
const summary = await this.collect(rule, nowMs);
|
|
30064
|
+
const text = this.word(summary, nowMs);
|
|
30065
|
+
return {
|
|
30066
|
+
empty: summary.itemCount === 0 && summary.unavailableCount === 0,
|
|
30067
|
+
count: text.count,
|
|
30068
|
+
text: text.text,
|
|
30069
|
+
sections: summary.sections.map((section) => ({
|
|
30070
|
+
id: section.id,
|
|
30071
|
+
status: section.status,
|
|
30072
|
+
items: section.items.length,
|
|
30073
|
+
...section.reason !== void 0 ? { reason: section.reason } : {}
|
|
30074
|
+
})),
|
|
30075
|
+
dropped: { ...summary.dropped },
|
|
30076
|
+
excludedGroups: [...summary.excludedGroups]
|
|
30077
|
+
};
|
|
30078
|
+
}
|
|
30079
|
+
async collect(rule, nowMs) {
|
|
30080
|
+
const nodes = await this.nodeView();
|
|
30081
|
+
return collectStateSummary({
|
|
30082
|
+
rule,
|
|
30083
|
+
nowMs,
|
|
30084
|
+
isMuted: this.deps.isMuted,
|
|
30085
|
+
sources: stateSources({
|
|
30086
|
+
alerts: this.deps.alerts,
|
|
30087
|
+
liveness: this.deps.liveness,
|
|
30088
|
+
state: () => this.ledger.openRows(),
|
|
30089
|
+
lookup: this.deps.lookup,
|
|
30090
|
+
devicesAnswered: this.deps.devicesAnswered,
|
|
30091
|
+
nodes,
|
|
30092
|
+
livenessHoldoffMs: this.deps.livenessHoldoffMs
|
|
30093
|
+
})
|
|
30094
|
+
});
|
|
30095
|
+
}
|
|
30096
|
+
/** The node directory for one summary, or `null` (logged) when it did not answer. */
|
|
30097
|
+
async nodeView() {
|
|
30098
|
+
const read = this.deps.readNodes;
|
|
30099
|
+
if (read === void 0) return null;
|
|
30100
|
+
try {
|
|
30101
|
+
const answer = await read();
|
|
30102
|
+
const online = new Map(answer.nodes.map((node) => [node.id, node.isOnline]));
|
|
30103
|
+
return {
|
|
30104
|
+
complete: answer.complete,
|
|
30105
|
+
online: (nodeId) => online.get(nodeId)
|
|
30106
|
+
};
|
|
30107
|
+
} catch (err) {
|
|
30108
|
+
this.deps.logger.warn("state summary: the node directory did not answer — node sections unavailable", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
30109
|
+
return null;
|
|
30110
|
+
}
|
|
30111
|
+
}
|
|
30112
|
+
word(summary, nowMs) {
|
|
30113
|
+
return stateSummaryText({
|
|
30114
|
+
texts: this.deps.texts(),
|
|
30115
|
+
summary,
|
|
30116
|
+
nowMs
|
|
30117
|
+
});
|
|
30118
|
+
}
|
|
30119
|
+
async deliver(rule, summary, window) {
|
|
30120
|
+
const subject = stateSummarySubject({
|
|
30121
|
+
rule,
|
|
30122
|
+
summary,
|
|
30123
|
+
text: this.word(summary, window.endMs),
|
|
30124
|
+
window
|
|
30125
|
+
});
|
|
30126
|
+
if (subject === null) {
|
|
30127
|
+
this.deps.logger.warn("state summary rule has no system-event condition — not sent", { meta: { ruleId: rule.id } });
|
|
30128
|
+
return 0;
|
|
30129
|
+
}
|
|
30130
|
+
return this.deps.enqueue(rule, subject);
|
|
30131
|
+
}
|
|
30132
|
+
};
|
|
30133
|
+
//#endregion
|
|
28437
30134
|
//#region src/notification-center/zone-owner-cache.ts
|
|
28438
30135
|
var NcZoneOwnershipCache = class {
|
|
28439
30136
|
deps;
|
|
@@ -28991,6 +30688,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
28991
30688
|
alertStore;
|
|
28992
30689
|
/** Battery band → notification, and the reminder sweep. */
|
|
28993
30690
|
deviceAlerts;
|
|
30691
|
+
/** The scheduled STATE SUMMARY of system rules (D647): its ledgers and its tick. */
|
|
30692
|
+
stateSummary;
|
|
28994
30693
|
/** States the `deviceState` rule gate reads. Refreshed on the reload tick;
|
|
28995
30694
|
* read with no I/O on the persist hook. */
|
|
28996
30695
|
deviceStates;
|
|
@@ -29884,6 +31583,21 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29884
31583
|
logger: this.logger.child("device-alerts"),
|
|
29885
31584
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
29886
31585
|
});
|
|
31586
|
+
this.stateSummary = new NcStateSummaryService({
|
|
31587
|
+
store: deps.store,
|
|
31588
|
+
logger: this.logger.child("state-summary"),
|
|
31589
|
+
now: () => this.now(),
|
|
31590
|
+
rules: () => this.rules.listEnabled("system-event"),
|
|
31591
|
+
alerts: () => this.deviceAlerts.openEpisodes(),
|
|
31592
|
+
liveness: () => this.liveness.hasAnswered() ? this.liveness.snapshot() : null,
|
|
31593
|
+
lookup: (deviceId) => this.deviceDirectory.get(deviceId),
|
|
31594
|
+
devicesAnswered: () => this.deviceDirectory.isWarm(),
|
|
31595
|
+
...deps.readNodeDirectory !== void 0 ? { readNodes: deps.readNodeDirectory } : {},
|
|
31596
|
+
livenessHoldoffMs: deps.livenessHoldoffMs ?? 3e5,
|
|
31597
|
+
isMuted: (deviceId) => this.deviceMutes.isMuted(deviceId),
|
|
31598
|
+
texts: () => this.texts,
|
|
31599
|
+
enqueue: (rule, subject) => this.enqueueStateSummary(rule, subject)
|
|
31600
|
+
});
|
|
29887
31601
|
this.confirmGate = deps.generateVision !== void 0 ? new NcConfirmGate({
|
|
29888
31602
|
logger: this.logger.child("confirm"),
|
|
29889
31603
|
generateVision: deps.generateVision,
|
|
@@ -29981,7 +31695,10 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29981
31695
|
summary: this.summaryRules,
|
|
29982
31696
|
mutes: this.deviceMutes,
|
|
29983
31697
|
alarm: () => this.alarmPanel,
|
|
29984
|
-
alerts:
|
|
31698
|
+
alerts: {
|
|
31699
|
+
deviceIds: () => [...new Set([...this.deviceAlerts.deviceIds(), ...this.stateSummary.ledger.deviceIds()])],
|
|
31700
|
+
forget: async (deviceId) => await this.deviceAlerts.forget(deviceId) + this.stateSummary.ledger.forgetDevice(deviceId)
|
|
31701
|
+
},
|
|
29985
31702
|
directory: this.deviceDirectory,
|
|
29986
31703
|
isDeviceDeleted: deps.isDeviceDeleted,
|
|
29987
31704
|
targetCatalog: () => this.dispatcher.catalogSnapshot()
|
|
@@ -30092,6 +31809,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30092
31809
|
await OccupancyStore.declare(store);
|
|
30093
31810
|
await NcLivenessLedger.declare(store);
|
|
30094
31811
|
await NcAlertStore.declare(store);
|
|
31812
|
+
await NcStateSummaryService.declare(store);
|
|
30095
31813
|
await NcSnoozeStore.declare(store);
|
|
30096
31814
|
await NcDeviceMuteStore.declare(store);
|
|
30097
31815
|
await TimelapseStore.declare(store);
|
|
@@ -30285,6 +32003,10 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30285
32003
|
const rows = await this.alertStore.load();
|
|
30286
32004
|
this.logger.info("device alert ledger reseeded", { meta: { rows } });
|
|
30287
32005
|
})) return;
|
|
32006
|
+
if (!await phase("stateSummary", async () => {
|
|
32007
|
+
const loaded = await this.stateSummary.load();
|
|
32008
|
+
this.logger.info("state summary ledgers reseeded", { meta: loaded });
|
|
32009
|
+
})) return;
|
|
30288
32010
|
if (!await phase("occupancy", () => this.hydrateOccupancy())) return;
|
|
30289
32011
|
if (!await phase("outbox", () => this.outbox.load())) return;
|
|
30290
32012
|
if (!await phase("cooldowns", () => this.seedCooldowns())) return;
|
|
@@ -30590,6 +32312,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30590
32312
|
atMs: event.timestamp,
|
|
30591
32313
|
...event.trackId !== void 0 ? { trackId: event.trackId } : {}
|
|
30592
32314
|
});
|
|
32315
|
+
if (this.evaluationActive) this.stateSummary.observeDetection(event.deviceId);
|
|
30593
32316
|
this.consumeEvent(incomingFromObjectEvent(event));
|
|
30594
32317
|
}
|
|
30595
32318
|
/**
|
|
@@ -30973,9 +32696,15 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30973
32696
|
if (!this.evaluationActive) return;
|
|
30974
32697
|
if (event.category === "addon.updated") {
|
|
30975
32698
|
const update = parseAddonUpdateComplete(event.data);
|
|
30976
|
-
if (update !== null)
|
|
32699
|
+
if (update !== null) {
|
|
32700
|
+
this.addonUpdates.observe(update);
|
|
32701
|
+
this.stateSummary.observeAddonUpdated(update);
|
|
32702
|
+
}
|
|
30977
32703
|
return;
|
|
30978
32704
|
}
|
|
32705
|
+
this.stateSummary.observeRawEvent(event.category, event.data, this.now(), event.source.nodeId);
|
|
32706
|
+
if (event.category === "addon.started" || event.category === "addon.restarted") return;
|
|
32707
|
+
if (event.category === "pipeline.camera-assigned") return;
|
|
30979
32708
|
const battery = batterySliceOf(event);
|
|
30980
32709
|
if (battery !== null) {
|
|
30981
32710
|
this.deviceAlerts.observeBattery(battery.deviceId, battery.reading);
|
|
@@ -31005,6 +32734,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31005
32734
|
return;
|
|
31006
32735
|
}
|
|
31007
32736
|
const system = incoming.subject.systemEvent;
|
|
32737
|
+
if (system !== void 0) this.stateSummary.observeSystem(system, this.now());
|
|
31008
32738
|
const observation = system !== void 0 ? ledgeredLiveness(system) : null;
|
|
31009
32739
|
if (observation !== null) {
|
|
31010
32740
|
const previous = this.liveness.stateOf(observation.subject);
|
|
@@ -31059,6 +32789,10 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31059
32789
|
logger: this.logger,
|
|
31060
32790
|
by: caller.userId
|
|
31061
32791
|
});
|
|
32792
|
+
guardRuleSystemEventDelivery(rule, {
|
|
32793
|
+
logger: this.logger,
|
|
32794
|
+
by: caller.userId
|
|
32795
|
+
});
|
|
31062
32796
|
await this.validateTargetRefs(rule.targets.map((t) => t.targetId));
|
|
31063
32797
|
const created = await this.rules.create(rule, caller.userId);
|
|
31064
32798
|
this.logger.info("notification rule created", { meta: {
|
|
@@ -31087,6 +32821,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31087
32821
|
by: caller.userId,
|
|
31088
32822
|
ruleId
|
|
31089
32823
|
});
|
|
32824
|
+
guardPatchSystemEventDelivery(existing, patch, clear ?? [], {
|
|
32825
|
+
logger: this.logger,
|
|
32826
|
+
by: caller.userId,
|
|
32827
|
+
ruleId
|
|
32828
|
+
});
|
|
31090
32829
|
const { disabledTargetIds: _optOut, ...safePatch } = patch;
|
|
31091
32830
|
const updated = await this.rules.update(ruleId, safePatch, clear ?? []);
|
|
31092
32831
|
this.logger.info("notification rule updated", { meta: {
|
|
@@ -31105,6 +32844,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31105
32844
|
return { success: true };
|
|
31106
32845
|
},
|
|
31107
32846
|
testRule: async ({ rule, lookbackMinutes }) => ({ results: [...await this.dryRun(rule, lookbackMinutes)] }),
|
|
32847
|
+
previewStateSummary: async ({ rule }) => this.stateSummary.preview(rule),
|
|
31108
32848
|
getConditionCatalog: async () => ({
|
|
31109
32849
|
catalog: [...require_dist.NC_CONDITION_CATALOG],
|
|
31110
32850
|
taxonomy: require_dist.NC_TAXONOMY
|
|
@@ -31986,6 +33726,29 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31986
33726
|
});
|
|
31987
33727
|
continue;
|
|
31988
33728
|
}
|
|
33729
|
+
if (require_dist.isSystemEventSummaryRule(rule)) {
|
|
33730
|
+
this.logger.debug("rule matched — held for its scheduled state summary", {
|
|
33731
|
+
tags: { deviceId: subject.deviceId },
|
|
33732
|
+
meta: {
|
|
33733
|
+
ruleId: rule.id,
|
|
33734
|
+
rule: rule.name,
|
|
33735
|
+
kind: subject.systemEvent?.kind,
|
|
33736
|
+
eventId: subject.recordId
|
|
33737
|
+
}
|
|
33738
|
+
});
|
|
33739
|
+
outcomes.push({
|
|
33740
|
+
ruleId: rule.id,
|
|
33741
|
+
ruleName: rule.name,
|
|
33742
|
+
matched: true,
|
|
33743
|
+
...evaluation.matchedOn !== void 0 ? { matchedOn: [...evaluation.matchedOn] } : {},
|
|
33744
|
+
cooldown: "n/a",
|
|
33745
|
+
enqueued: 0,
|
|
33746
|
+
targetIds: [],
|
|
33747
|
+
heldForSummary: true
|
|
33748
|
+
});
|
|
33749
|
+
anyMatched = true;
|
|
33750
|
+
continue;
|
|
33751
|
+
}
|
|
31989
33752
|
this.sceneGateReported.delete(rule.id);
|
|
31990
33753
|
this.runRuleActions(rule, subject, kind, controls);
|
|
31991
33754
|
const admission = this.admitToGroup(rule, subject, now);
|
|
@@ -32177,6 +33940,31 @@ var NotificationCenter = class NotificationCenter {
|
|
|
32177
33940
|
return report({ candidates: candidates.length });
|
|
32178
33941
|
}
|
|
32179
33942
|
/**
|
|
33943
|
+
* Enqueue ONE state summary (D647) through the rule's ordinary rows: its
|
|
33944
|
+
* targets, its users (admins only — the row files under the system device),
|
|
33945
|
+
* its template, and the drain's snooze gate and retries. The record id is
|
|
33946
|
+
* the dedup key, so a window is enqueued once whatever the restarts.
|
|
33947
|
+
*/
|
|
33948
|
+
async enqueueStateSummary(rule, subject) {
|
|
33949
|
+
const appended = withSummaryBody(rule);
|
|
33950
|
+
if (appended !== null) this.logger.warn("state summary rule body names no summary content — {{summaryText}} appended", { meta: {
|
|
33951
|
+
ruleId: rule.id,
|
|
33952
|
+
rule: rule.name
|
|
33953
|
+
} });
|
|
33954
|
+
const sent = appended ?? rule;
|
|
33955
|
+
const userTargets = await this.resolveUserTargets(sent, subject.deviceId);
|
|
33956
|
+
const entries = this.buildEntries(sent, subject, "system-event", ["systemEvent"], userTargets, "system");
|
|
33957
|
+
if (entries.length === 0) {
|
|
33958
|
+
this.logger.warn("state summary reached NO target — its users are not admins, and it has no targets", { meta: {
|
|
33959
|
+
ruleId: rule.id,
|
|
33960
|
+
rule: rule.name,
|
|
33961
|
+
targetUsers: rule.targetUsers ?? []
|
|
33962
|
+
} });
|
|
33963
|
+
return 0;
|
|
33964
|
+
}
|
|
33965
|
+
return this.outbox.enqueue(entries);
|
|
33966
|
+
}
|
|
33967
|
+
/**
|
|
32180
33968
|
* The rules a trigger may fire, which is `listEnabled(delivery)` plus and
|
|
32181
33969
|
* minus exactly one thing: `waitForEnhancement`.
|
|
32182
33970
|
*
|
|
@@ -32919,6 +34707,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
32919
34707
|
async reloadRules() {
|
|
32920
34708
|
await this.rules.load();
|
|
32921
34709
|
if (this.evaluationActive) this.deviceAlerts.tick();
|
|
34710
|
+
if (this.evaluationActive) this.stateSummary.tick().catch((err) => {
|
|
34711
|
+
this.logger.warn("state summary tick failed", { meta: { error: String(err) } });
|
|
34712
|
+
});
|
|
32922
34713
|
await this.timelapseRules.load();
|
|
32923
34714
|
await this.summaryRules.load();
|
|
32924
34715
|
await this.ruleAi.load();
|
|
@@ -47279,6 +49070,16 @@ var FrameProcessor = class {
|
|
|
47279
49070
|
};
|
|
47280
49071
|
}
|
|
47281
49072
|
};
|
|
49073
|
+
//#endregion
|
|
49074
|
+
//#region src/pipeline-analytics/pipeline/track-provenance.ts
|
|
49075
|
+
function trackProvenanceOf(frame) {
|
|
49076
|
+
if (!(frame.width > 0 && frame.height > 0)) return void 0;
|
|
49077
|
+
return {
|
|
49078
|
+
...frame.rootModelId !== void 0 && frame.rootModelId.length > 0 ? { modelId: frame.rootModelId } : {},
|
|
49079
|
+
frameWidth: frame.width,
|
|
49080
|
+
frameHeight: frame.height
|
|
49081
|
+
};
|
|
49082
|
+
}
|
|
47282
49083
|
/** Bounded trackId → pending-upgrade map. Insertion-ordered, so the cap
|
|
47283
49084
|
* evicts the oldest record. */
|
|
47284
49085
|
var PendingPreviewUpgrades = class {
|
|
@@ -50100,51 +51901,6 @@ var PlateGalleryProvider = class {
|
|
|
50100
51901
|
}
|
|
50101
51902
|
};
|
|
50102
51903
|
//#endregion
|
|
50103
|
-
//#region src/pipeline-analytics/replay/frame-processor-pass.ts
|
|
50104
|
-
/**
|
|
50105
|
-
* Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
|
|
50106
|
-
* return the tracks it produced.
|
|
50107
|
-
*
|
|
50108
|
-
* No `TrackStore`, no bus, no media — a track here is a summary built
|
|
50109
|
-
* ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
|
|
50110
|
-
*/
|
|
50111
|
-
function runFrameProcessorPass(input) {
|
|
50112
|
-
const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
|
|
50113
|
-
if (input.zones !== void 0) processor.setZones(input.zones);
|
|
50114
|
-
if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
|
|
50115
|
-
if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
|
|
50116
|
-
const accum = /* @__PURE__ */ new Map();
|
|
50117
|
-
for (const { timestamp, frame } of input.frames) {
|
|
50118
|
-
const result = processor.process({
|
|
50119
|
-
timestamp,
|
|
50120
|
-
frame
|
|
50121
|
-
});
|
|
50122
|
-
for (const td of result.tracked) {
|
|
50123
|
-
const existing = accum.get(td.trackId);
|
|
50124
|
-
const matched = td.matchedThisFrame ? 1 : 0;
|
|
50125
|
-
if (existing === void 0) accum.set(td.trackId, {
|
|
50126
|
-
className: td.className,
|
|
50127
|
-
firstSeenMs: timestamp,
|
|
50128
|
-
lastSeenMs: timestamp,
|
|
50129
|
-
bbox: { ...td.bbox },
|
|
50130
|
-
framesMatched: matched
|
|
50131
|
-
});
|
|
50132
|
-
else {
|
|
50133
|
-
existing.className = td.className;
|
|
50134
|
-
existing.lastSeenMs = timestamp;
|
|
50135
|
-
existing.framesMatched += matched;
|
|
50136
|
-
}
|
|
50137
|
-
}
|
|
50138
|
-
}
|
|
50139
|
-
return { tracks: [...accum.values()].map((t) => ({
|
|
50140
|
-
className: t.className,
|
|
50141
|
-
firstSeenMs: t.firstSeenMs,
|
|
50142
|
-
lastSeenMs: t.lastSeenMs,
|
|
50143
|
-
bbox: t.bbox,
|
|
50144
|
-
framesMatched: t.framesMatched
|
|
50145
|
-
})) };
|
|
50146
|
-
}
|
|
50147
|
-
//#endregion
|
|
50148
51904
|
//#region src/pipeline-analytics/retention/disk-reconcile.ts
|
|
50149
51905
|
/** Is this an event-owned media row — `motion` / `object` / `audio` (D476;
|
|
50150
51906
|
* `event` used to cover all three under one owner kind). */
|
|
@@ -52415,6 +54171,10 @@ var TRACKS_COLUMNS = [
|
|
|
52415
54171
|
name: "frameHeight",
|
|
52416
54172
|
type: "INTEGER"
|
|
52417
54173
|
},
|
|
54174
|
+
{
|
|
54175
|
+
name: "modelId",
|
|
54176
|
+
type: "TEXT"
|
|
54177
|
+
},
|
|
52418
54178
|
{
|
|
52419
54179
|
name: "closedReason",
|
|
52420
54180
|
type: "TEXT"
|
|
@@ -52579,7 +54339,12 @@ function cloneTrack(t) {
|
|
|
52579
54339
|
...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
|
|
52580
54340
|
...t.hasEmbeddedFace !== void 0 ? { hasEmbeddedFace: t.hasEmbeddedFace } : {},
|
|
52581
54341
|
...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {},
|
|
52582
|
-
...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {}
|
|
54342
|
+
...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {},
|
|
54343
|
+
...t.provenance?.modelId !== void 0 ? { modelId: t.provenance.modelId } : {},
|
|
54344
|
+
...t.provenance !== void 0 ? {
|
|
54345
|
+
frameWidth: t.provenance.frameWidth,
|
|
54346
|
+
frameHeight: t.provenance.frameHeight
|
|
54347
|
+
} : {}
|
|
52583
54348
|
};
|
|
52584
54349
|
}
|
|
52585
54350
|
var TrackStore = class {
|
|
@@ -52676,7 +54441,8 @@ var TrackStore = class {
|
|
|
52676
54441
|
totalDistance: 0,
|
|
52677
54442
|
state: params.state,
|
|
52678
54443
|
active: true,
|
|
52679
|
-
lastSnapshotAt: 0
|
|
54444
|
+
lastSnapshotAt: 0,
|
|
54445
|
+
...params.provenance !== void 0 ? { provenance: params.provenance } : {}
|
|
52680
54446
|
};
|
|
52681
54447
|
this.active.set(params.trackId, fresh);
|
|
52682
54448
|
return fresh;
|
|
@@ -54137,7 +55903,10 @@ var TrackStore = class {
|
|
|
54137
55903
|
});
|
|
54138
55904
|
}
|
|
54139
55905
|
async persistCompleted(t) {
|
|
54140
|
-
const dims =
|
|
55906
|
+
const dims = t.frameWidth !== void 0 && t.frameHeight !== void 0 ? {
|
|
55907
|
+
w: t.frameWidth,
|
|
55908
|
+
h: t.frameHeight
|
|
55909
|
+
} : this.frameDims?.(t.deviceId);
|
|
54141
55910
|
const envelope = computeTrackEnvelope(t.positions, dims?.w, dims?.h);
|
|
54142
55911
|
await this.store.set.mutate({
|
|
54143
55912
|
collection: TRACKS_COLLECTION,
|
|
@@ -54173,14 +55942,17 @@ var TrackStore = class {
|
|
|
54173
55942
|
...t.hasEmbeddedFace !== void 0 ? { hasEmbeddedFace: t.hasEmbeddedFace } : {},
|
|
54174
55943
|
...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {},
|
|
54175
55944
|
...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {},
|
|
54176
|
-
...envelope !== null
|
|
55945
|
+
...envelope !== null ? {
|
|
54177
55946
|
envMinX: envelope.minX,
|
|
54178
55947
|
envMinY: envelope.minY,
|
|
54179
55948
|
envMaxX: envelope.maxX,
|
|
54180
|
-
envMaxY: envelope.maxY
|
|
55949
|
+
envMaxY: envelope.maxY
|
|
55950
|
+
} : {},
|
|
55951
|
+
...dims !== void 0 ? {
|
|
54181
55952
|
frameWidth: dims.w,
|
|
54182
55953
|
frameHeight: dims.h
|
|
54183
|
-
} : {}
|
|
55954
|
+
} : {},
|
|
55955
|
+
...t.modelId !== void 0 ? { modelId: t.modelId } : {}
|
|
54184
55956
|
}
|
|
54185
55957
|
});
|
|
54186
55958
|
}
|
|
@@ -54240,6 +56012,9 @@ var TrackStore = class {
|
|
|
54240
56012
|
/** Free-form composed token line (see `composePreviewMissReason`) — a
|
|
54241
56013
|
* string read, absent stays absent. */
|
|
54242
56014
|
const previewMissReason = data["previewMissReason"];
|
|
56015
|
+
const modelId = data["modelId"];
|
|
56016
|
+
const frameWidth = data["frameWidth"];
|
|
56017
|
+
const frameHeight = data["frameHeight"];
|
|
54243
56018
|
const envMinX = data["envMinX"];
|
|
54244
56019
|
const envMinY = data["envMinY"];
|
|
54245
56020
|
const envMaxX = data["envMaxX"];
|
|
@@ -54283,7 +56058,12 @@ var TrackStore = class {
|
|
|
54283
56058
|
...closedReason !== void 0 ? { closedReason } : {},
|
|
54284
56059
|
...typeof previewMissReason === "string" && previewMissReason.length > 0 ? { previewMissReason } : {},
|
|
54285
56060
|
...observations !== void 0 ? { observations } : {},
|
|
54286
|
-
...envelope !== null ? { envelope } : {}
|
|
56061
|
+
...envelope !== null ? { envelope } : {},
|
|
56062
|
+
...typeof modelId === "string" && modelId.length > 0 ? { modelId } : {},
|
|
56063
|
+
...typeof frameWidth === "number" && typeof frameHeight === "number" ? {
|
|
56064
|
+
frameWidth,
|
|
56065
|
+
frameHeight
|
|
56066
|
+
} : {}
|
|
54287
56067
|
});
|
|
54288
56068
|
}
|
|
54289
56069
|
};
|
|
@@ -59736,7 +61516,6 @@ var DEFAULT_EMBEDDING_CONFIG = {
|
|
|
59736
61516
|
enabled: true,
|
|
59737
61517
|
modelId: "clip-vit-b32",
|
|
59738
61518
|
agentId: "local",
|
|
59739
|
-
runtime: "node",
|
|
59740
61519
|
backend: "cpu",
|
|
59741
61520
|
classes: [],
|
|
59742
61521
|
minConfidence: .5,
|
|
@@ -77674,6 +79453,9 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
|
|
|
77674
79453
|
require_dist.EventCategory.PipelineNodeInferenceUnavailable,
|
|
77675
79454
|
require_dist.EventCategory.PipelineDetectionBlind,
|
|
77676
79455
|
require_dist.EventCategory.AddonRunnerFailed,
|
|
79456
|
+
require_dist.EventCategory.AddonStarted,
|
|
79457
|
+
require_dist.EventCategory.AddonRestarted,
|
|
79458
|
+
require_dist.EventCategory.PipelineCameraAssigned,
|
|
77677
79459
|
require_dist.EventCategory.UpdateAvailable,
|
|
77678
79460
|
require_dist.EventCategory.AddonUpdated,
|
|
77679
79461
|
require_dist.EventCategory.SystemRestartCompleted,
|
|
@@ -87296,6 +89078,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
87296
89078
|
};
|
|
87297
89079
|
},
|
|
87298
89080
|
isDeviceDeleted: async (deviceId) => await api.deviceManager.getDevice.query({ deviceId }) === null,
|
|
89081
|
+
readNodeDirectory: () => api.nodes.directory.query(),
|
|
87299
89082
|
getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
|
|
87300
89083
|
...buildLinkedSnapshotReads(api.snapshot),
|
|
87301
89084
|
identityIdsByName: async () => {
|
|
@@ -88022,21 +89805,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
88022
89805
|
...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
|
|
88023
89806
|
});
|
|
88024
89807
|
}
|
|
88025
|
-
/**
|
|
88026
|
-
* The FrameProcessor pass of a replay run — see the cap doc on
|
|
88027
|
-
* `runReplayFrameProcessor` for why this is not the `processFrame` method
|
|
88028
|
-
* this addon deliberately does not have.
|
|
88029
|
-
*
|
|
88030
|
-
* `runFrameProcessorPass` is pure w.r.t. this addon's own state: it
|
|
88031
|
-
* constructs a FRESH `FrameProcessor` and does not touch `this.trackStore`,
|
|
88032
|
-
* `this.stationaryRegistry` or any other instance field — unlike
|
|
88033
|
-
* `getOrCreateProcessor` (the production path), which wires both.
|
|
88034
|
-
* `scripts/check-replay-isolation.ts` holds that boundary on the
|
|
88035
|
-
* `replay/` directory this delegates into.
|
|
88036
|
-
*/
|
|
88037
|
-
async runReplayFrameProcessor(input) {
|
|
88038
|
-
return runFrameProcessorPass(input);
|
|
88039
|
-
}
|
|
88040
89808
|
async listRetrainAnnotations(input) {
|
|
88041
89809
|
return this.requireRetrain().listAnnotations(input.trackId);
|
|
88042
89810
|
}
|
|
@@ -89142,6 +90910,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
89142
90910
|
});
|
|
89143
90911
|
const currentTrackIds = /* @__PURE__ */ new Set();
|
|
89144
90912
|
const positionsCountById = /* @__PURE__ */ new Map();
|
|
90913
|
+
const provenance = trackProvenanceOf(frame);
|
|
89145
90914
|
for (const t of result.tracked) {
|
|
89146
90915
|
currentTrackIds.add(t.trackId);
|
|
89147
90916
|
if (this.suppressedBirths.isRejected(key, t.trackId)) continue;
|
|
@@ -89165,7 +90934,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
89165
90934
|
score: t.confidence
|
|
89166
90935
|
},
|
|
89167
90936
|
zones: t.zones,
|
|
89168
|
-
state: t.state
|
|
90937
|
+
state: t.state,
|
|
90938
|
+
...provenance !== void 0 ? { provenance } : {}
|
|
89169
90939
|
});
|
|
89170
90940
|
positionsCountById.set(t.trackId, upserted.positions.length);
|
|
89171
90941
|
}
|