@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as NcTemplatePreviewSchema, A as NC_DEFAULT_SNOOZE_MINUTES, An as literal, At as evictionPolicyOfLocation, B as NcConditionsSchema, Bt as isSystemDelivery, C as MAX_BIRTH_DECISION_RECORDS, Cn as isDeviceScopedCap, Ct as cosineSimilarity$1, D as NC_ALARM_SYSTEM_EVENT_KINDS, Dn as array, E as MediaFileKindEnum, En as _enum, Et as describeSystemEventFamilyProblem, F as NC_TAXONOMY, Fn as string, Ft as isEventOwnerType, G as NcRuleUpdateInputSchema, Gt as occupancyScope, H as NcRulePatchSchema, Ht as kebabToCamel, I as NC_TEMPLATE_VARS, In as unknown, It as isKnownTemplateVar, J as NcSnoozeSchema, Jt as pipelineAnalyticsCapability, K as NcScheduleSchema, L as NcAlarmConfigSchema, Ln as EventCategory, Lt as isMediaOwnerType, Mn as object, Mt as failureContributionCapability, N as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, Nn as partialRecord, O as NC_CONDITION_CATALOG, On as boolean, Ot as encodeVectorBase64, P as NC_TANK_SYSTEM_EVENT_KINDS, Pn as record, Pt as isDetectionMacroClass, Q as NcTemplatePreviewInputSchema, Qt as resolveLocationMode, R as NcAlarmSettingsPatchSchema, Rt as isScheduleActive, S as MAX_ARCHIVED_DEBUG_NOTES, Sn as hydrateSchema, St as consumableLowPercentOf, T as MOTION_CLOSE_AFTER_MS, Tn as sleep$1, Tt as describeSystemEventDeliveryProblem, U as NcRuleSchema, Ut as mayWriteToLocation, V as NcRuleInputSchema, Vt as isSystemEventSummaryRule, W as NcRuleTargetSchema, Wt as notificationRulesCapability, X as NcSystemEventKindSchema, Xt as readDeviceStateFrom, Y as NcSnoozeSuppressedSchema, Yt as plateGalleryCapability, Z as NcTaxonomySchema, Zt as readTimelapseGeneratedAt, _ as FIRST_LEVEL_MACRO_CLASSES, _n as CamProfileSchema, _t as alarmPanelCapability, a as BirthDecisionRecordSchema, an as subKindsOf, at as RetrainStatusSchema, b as LabelAttributionSchema, bn as customAction, bt as audioModeOf, c as CoverStateSchema, cn as systemEventFilterApplies, ct as SCENE_DEFAULT_UNCOVERED_POLICY, dn as templateVarsFor, dt as TIMELAPSE_DENSE_FLOOR_SEC, en as ruleBelongsToAlarm$1, et as NcTemplateVarDescriptorSchema, f as DETECTION_MACRO_CLASSES, fn as vectorDimFromBase64, ft as TimelapseRuleInputSchema, g as EVENT_OWNER_TYPES, gn as BaseAddon, gt as addonWidgetsSourceCapability, h as EVENT_KIND_BY_CAP, hn as canonicalHash, ht as TrackSourceSchema, i as BaseDevice, in as storageOccupancyCapability, it as RECORDING_SOURCE_CAMSTACK_ADDON, j as NC_SENSOR_TRANSITION_KINDS, jn as number, jt as faceGalleryCapability, k as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, kn as discriminatedUnion, kt as evaluateSensorEdge, l as DEFAULT_EVENT_COLOR, ln as systemEventSummaryScheduleOf, lt as SCENE_DIVERGED, m as DeclaredDevices, mn as errMsg$1, mt as TimelapseRuleSchema, n as AlarmArmModeSchema, nn as sensorRaisedOf, o as CLUSTER_MODEL_SCOPED_STEPS, on as systemEventDeliveryProblems, pn as zoneAnalyticsCapability, pt as TimelapseRulePatchSchema, q as NcSnoozeInputSchema, qt as pickClusterStepModels, r as ArchivedDebugNoteSchema, rn as servedTemplateVars, rt as RECORDING_EXPORT_MAX_READ_BYTES, s as COCO_TO_MACRO, sn as systemEventFamilyProblems, st as SCENE_DEFAULT_ANCHOR_THRESHOLD, t as AUDIO_MACRO_LABELS, tn as sceneMonitorCapability, tt as OpsLogEntrySchema, un as templateCarriesSummary, ut as SceneMonitorSchema, v as FULL_IMAGE_BBOX, vn as DeviceType, vt as assertTimelapseCadences, wn as nodePin, wt as deriveRecordingMode, x as LockStateSchema, xn as defineCustomActions, xt as buildEventKindDescriptor, y as FailureCounters, yn as createEvent, yt as audioMetricsCapability, z as NcConditionDescriptorSchema, zt as isSourceCap } from "../dist-D2tXUMfE.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -4097,7 +4097,7 @@ function mimeFromExtension(file) {
|
|
|
4097
4097
|
* A value that is negative, NaN or absent falls back to the shipped default:
|
|
4098
4098
|
* an unreadable window must not become a window nobody chose.
|
|
4099
4099
|
*/
|
|
4100
|
-
var DAY_MS$
|
|
4100
|
+
var DAY_MS$4 = 24 * 36e5;
|
|
4101
4101
|
/**
|
|
4102
4102
|
* The shipped windows. Each one is the value that was hardcoded before this
|
|
4103
4103
|
* module existed, except the mosaic — see {@link PRUNE_SETTING_KEYS}.
|
|
@@ -4143,8 +4143,8 @@ var PRUNE_SETTING_KEYS = [
|
|
|
4143
4143
|
*/
|
|
4144
4144
|
function windowMs(raw, defaultDays) {
|
|
4145
4145
|
if (raw === 0) return null;
|
|
4146
|
-
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) return defaultDays * DAY_MS$
|
|
4147
|
-
return raw * DAY_MS$
|
|
4146
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) return defaultDays * DAY_MS$4;
|
|
4147
|
+
return raw * DAY_MS$4;
|
|
4148
4148
|
}
|
|
4149
4149
|
function resolvePruneWindows(settings) {
|
|
4150
4150
|
return {
|
|
@@ -8255,6 +8255,19 @@ var NcAlarmLiveReadLog = class {
|
|
|
8255
8255
|
};
|
|
8256
8256
|
//#endregion
|
|
8257
8257
|
//#region src/notification-center/alerts/alert-episode.ts
|
|
8258
|
+
/** The row with `detail` set (or removed). Unchanged rows are returned as is. */
|
|
8259
|
+
function withAlertDetail(row, detail) {
|
|
8260
|
+
if (row.detail === detail) return row;
|
|
8261
|
+
const { subject, alerting, since, lastNotifiedAt, reminders } = row;
|
|
8262
|
+
return {
|
|
8263
|
+
subject,
|
|
8264
|
+
alerting,
|
|
8265
|
+
since,
|
|
8266
|
+
lastNotifiedAt,
|
|
8267
|
+
reminders,
|
|
8268
|
+
...detail !== void 0 ? { detail } : {}
|
|
8269
|
+
};
|
|
8270
|
+
}
|
|
8258
8271
|
function decideAlert(input) {
|
|
8259
8272
|
const previous = input.previous;
|
|
8260
8273
|
if (previous === void 0) return input.alerting ? { kind: "raise" } : { kind: "seeded" };
|
|
@@ -8398,6 +8411,12 @@ var NC_ALERTS_COLUMNS = [
|
|
|
8398
8411
|
{
|
|
8399
8412
|
name: "deviceId",
|
|
8400
8413
|
type: "INTEGER"
|
|
8414
|
+
}),
|
|
8415
|
+
(
|
|
8416
|
+
/** What the open episode is — a tank's band (D647 review M6). Nullable, added in place. */
|
|
8417
|
+
{
|
|
8418
|
+
name: "detail",
|
|
8419
|
+
type: "TEXT"
|
|
8401
8420
|
})
|
|
8402
8421
|
];
|
|
8403
8422
|
var NC_ALERTS_INDEXES = [{
|
|
@@ -8420,7 +8439,8 @@ var NC_ALERTS_SPEC = {
|
|
|
8420
8439
|
since: row.since,
|
|
8421
8440
|
lastNotifiedAt: row.lastNotifiedAt,
|
|
8422
8441
|
reminders: row.reminders,
|
|
8423
|
-
...deviceIdOf(row.subject) !== void 0 ? { deviceId: deviceIdOf(row.subject) } : {}
|
|
8442
|
+
...deviceIdOf(row.subject) !== void 0 ? { deviceId: deviceIdOf(row.subject) } : {},
|
|
8443
|
+
...row.detail !== void 0 ? { detail: row.detail } : {}
|
|
8424
8444
|
}),
|
|
8425
8445
|
fromRecord: (key, data) => {
|
|
8426
8446
|
const since = data["since"];
|
|
@@ -8429,12 +8449,14 @@ var NC_ALERTS_SPEC = {
|
|
|
8429
8449
|
if (typeof since !== "number") return null;
|
|
8430
8450
|
if (typeof lastNotifiedAt !== "number") return null;
|
|
8431
8451
|
if (typeof reminders !== "number") return null;
|
|
8452
|
+
const detail = data["detail"];
|
|
8432
8453
|
return {
|
|
8433
8454
|
subject: key,
|
|
8434
8455
|
alerting: data["alerting"] === 1 || data["alerting"] === true,
|
|
8435
8456
|
since,
|
|
8436
8457
|
lastNotifiedAt,
|
|
8437
|
-
reminders
|
|
8458
|
+
reminders,
|
|
8459
|
+
...typeof detail === "string" ? { detail } : {}
|
|
8438
8460
|
};
|
|
8439
8461
|
},
|
|
8440
8462
|
equalFact: (held, incoming) => held.alerting === incoming.alerting,
|
|
@@ -8467,7 +8489,8 @@ var NcAlertStore = class {
|
|
|
8467
8489
|
this.ledger.put(row).catch(() => void 0);
|
|
8468
8490
|
},
|
|
8469
8491
|
all: () => this.ledger.snapshot(),
|
|
8470
|
-
remove: (subject) => this.ledger.forget(subject)
|
|
8492
|
+
remove: (subject) => this.ledger.forget(subject),
|
|
8493
|
+
answered: () => this.ledger.hasAnswered()
|
|
8471
8494
|
};
|
|
8472
8495
|
}
|
|
8473
8496
|
};
|
|
@@ -11915,6 +11938,80 @@ function vacuumTanksOf(data) {
|
|
|
11915
11938
|
};
|
|
11916
11939
|
}
|
|
11917
11940
|
//#endregion
|
|
11941
|
+
//#region src/notification-center/alerts/open-alert.ts
|
|
11942
|
+
/**
|
|
11943
|
+
* The raise of an episode whose reading has not arrived since the restart —
|
|
11944
|
+
* worded with what the row KNOWS: no invented percentage (the binary battery
|
|
11945
|
+
* sentence), the item key as its name, and a tank kind that only the filters
|
|
11946
|
+
* read (the summary words an unmeasured tank without a band).
|
|
11947
|
+
*/
|
|
11948
|
+
function unmeasuredEpisode(input) {
|
|
11949
|
+
const deviceId = alertSubjectDeviceId(input.subject);
|
|
11950
|
+
if (deviceId === void 0) return null;
|
|
11951
|
+
const identity = input.lookup(deviceId);
|
|
11952
|
+
const facts = input.facts;
|
|
11953
|
+
if (input.family === "battery") return incomingFromBatteryBand({
|
|
11954
|
+
facts,
|
|
11955
|
+
deviceId,
|
|
11956
|
+
band: "low",
|
|
11957
|
+
reading: {
|
|
11958
|
+
percentage: 0,
|
|
11959
|
+
lastUpdated: 0,
|
|
11960
|
+
binary: true
|
|
11961
|
+
},
|
|
11962
|
+
identity
|
|
11963
|
+
});
|
|
11964
|
+
const parts = parseConsumableSubject(input.subject);
|
|
11965
|
+
if (input.family === "consumable" && parts !== null) return incomingFromConsumableBand({
|
|
11966
|
+
facts,
|
|
11967
|
+
deviceId,
|
|
11968
|
+
band: "low",
|
|
11969
|
+
item: {
|
|
11970
|
+
key: parts.key,
|
|
11971
|
+
label: parts.key,
|
|
11972
|
+
level: null,
|
|
11973
|
+
status: "replace"
|
|
11974
|
+
},
|
|
11975
|
+
thresholdPercent: parts.thresholdPercent,
|
|
11976
|
+
identity
|
|
11977
|
+
});
|
|
11978
|
+
const tank = NC_TANK_KEYS.find((key) => input.subject === `tank:${String(deviceId)}:${key}`);
|
|
11979
|
+
if (tank === void 0) return null;
|
|
11980
|
+
return incomingFromTankBand({
|
|
11981
|
+
facts,
|
|
11982
|
+
deviceId,
|
|
11983
|
+
band: input.detail === "full" ? "full" : "low",
|
|
11984
|
+
tank,
|
|
11985
|
+
identity
|
|
11986
|
+
});
|
|
11987
|
+
}
|
|
11988
|
+
/**
|
|
11989
|
+
* One alerting row as an open episode, or `null` when it cannot be worded (the
|
|
11990
|
+
* caller logs it). A legacy consumable row is skipped: it is adopted or
|
|
11991
|
+
* retired first. A tank whose band the row remembers is worded by it
|
|
11992
|
+
* (review M6), even without a reading.
|
|
11993
|
+
*/
|
|
11994
|
+
function openAlertOf(row, input) {
|
|
11995
|
+
const parts = parseConsumableSubject(row.subject);
|
|
11996
|
+
if (parts?.legacy === true) return null;
|
|
11997
|
+
const family = parts !== null ? "consumable" : row.subject.startsWith("tank:") ? "tank" : "battery";
|
|
11998
|
+
const subject = (input.restated ?? unmeasuredEpisode({
|
|
11999
|
+
subject: row.subject,
|
|
12000
|
+
family,
|
|
12001
|
+
facts: input.facts,
|
|
12002
|
+
lookup: input.lookup,
|
|
12003
|
+
...row.detail !== void 0 ? { detail: row.detail } : {}
|
|
12004
|
+
}))?.subject.systemEvent;
|
|
12005
|
+
if (subject === void 0) return null;
|
|
12006
|
+
const measured = input.restated !== null || family === "tank" && row.detail !== void 0;
|
|
12007
|
+
return {
|
|
12008
|
+
family,
|
|
12009
|
+
since: row.since,
|
|
12010
|
+
subject,
|
|
12011
|
+
measured
|
|
12012
|
+
};
|
|
12013
|
+
}
|
|
12014
|
+
//#endregion
|
|
11918
12015
|
//#region src/notification-center/alerts/battery-band.ts
|
|
11919
12016
|
/**
|
|
11920
12017
|
* The stand-in a provider writes for a binary NORMAL. Anything below it, on a
|
|
@@ -12122,7 +12219,7 @@ var NcDeviceAlerts = class {
|
|
|
12122
12219
|
band: alerting ? band : "normal",
|
|
12123
12220
|
tank: tank.key,
|
|
12124
12221
|
identity: this.deps.lookup(deviceId)
|
|
12125
|
-
}));
|
|
12222
|
+
}), band === "normal" ? void 0 : band);
|
|
12126
12223
|
}
|
|
12127
12224
|
/**
|
|
12128
12225
|
* Has this tank band held long enough to move its episode? A band that
|
|
@@ -12240,6 +12337,37 @@ var NcDeviceAlerts = class {
|
|
|
12240
12337
|
}
|
|
12241
12338
|
}
|
|
12242
12339
|
/**
|
|
12340
|
+
* Every OPEN episode, restated (D647). `null` while the rows have not been
|
|
12341
|
+
* read. A legacy consumable row is skipped (it is adopted or retired first),
|
|
12342
|
+
* and so is a tank whose band is unknown and whose device the summary could
|
|
12343
|
+
* not word — never silently: each skip is a line.
|
|
12344
|
+
*/
|
|
12345
|
+
openEpisodes() {
|
|
12346
|
+
if (this.deps.rows.answered?.() === false) return null;
|
|
12347
|
+
const facts = {
|
|
12348
|
+
eventId: "state-summary",
|
|
12349
|
+
timestamp: this.now(),
|
|
12350
|
+
category: "state-summary",
|
|
12351
|
+
data: {}
|
|
12352
|
+
};
|
|
12353
|
+
const out = [];
|
|
12354
|
+
for (const row of this.deps.rows.all()) {
|
|
12355
|
+
if (!row.alerting) continue;
|
|
12356
|
+
const restated = this.restatementOf(row.subject);
|
|
12357
|
+
const open = openAlertOf(row, {
|
|
12358
|
+
facts,
|
|
12359
|
+
restated: restated === null ? null : restated(facts),
|
|
12360
|
+
lookup: this.deps.lookup
|
|
12361
|
+
});
|
|
12362
|
+
if (open !== null) out.push(open);
|
|
12363
|
+
else this.logger()?.debug("open alert left out of the state summary — it cannot be worded", {
|
|
12364
|
+
...tagsOf(row.subject),
|
|
12365
|
+
meta: { subject: row.subject }
|
|
12366
|
+
});
|
|
12367
|
+
}
|
|
12368
|
+
return out;
|
|
12369
|
+
}
|
|
12370
|
+
/**
|
|
12243
12371
|
* Every device that holds an alert row — what the deleted-device reconcile
|
|
12244
12372
|
* must look at besides the devices a rule names (a battery device no rule
|
|
12245
12373
|
* names still has a row).
|
|
@@ -12381,7 +12509,7 @@ var NcDeviceAlerts = class {
|
|
|
12381
12509
|
* a state we never recorded leaving is a state we can never announce
|
|
12382
12510
|
* entering again.
|
|
12383
12511
|
*/
|
|
12384
|
-
apply(subject, alerting, settings, build) {
|
|
12512
|
+
apply(subject, alerting, settings, build, detail) {
|
|
12385
12513
|
const nowMs = this.now();
|
|
12386
12514
|
const previous = this.deps.rows.get(subject);
|
|
12387
12515
|
const verdict = decideAlert({
|
|
@@ -12391,7 +12519,7 @@ var NcDeviceAlerts = class {
|
|
|
12391
12519
|
renotifyMs: settings.renotifyMinutes * 6e4,
|
|
12392
12520
|
notifyRecovery: settings.notifyRecovery
|
|
12393
12521
|
});
|
|
12394
|
-
const next = nextAlertRow(subject, previous, alerting, verdict, nowMs);
|
|
12522
|
+
const next = withAlertDetail(nextAlertRow(subject, previous, alerting, verdict, nowMs), alerting ? detail ?? previous?.detail : void 0);
|
|
12395
12523
|
if (next !== previous) this.deps.rows.put(next);
|
|
12396
12524
|
if (!speaks(verdict)) {
|
|
12397
12525
|
this.report(subject, verdict, alerting);
|
|
@@ -13026,7 +13154,31 @@ var en_default = {
|
|
|
13026
13154
|
},
|
|
13027
13155
|
"summary.body.detections": "{{detectionSummary}}",
|
|
13028
13156
|
"summary.body.truncated": "showing the {{shown}} most relevant of {{total}}",
|
|
13029
|
-
"summary.body.empty": "nothing was seen in this window"
|
|
13157
|
+
"summary.body.empty": "nothing was seen in this window",
|
|
13158
|
+
"system.state-summary.title": "{{rule}}",
|
|
13159
|
+
"system.state-summary.body": "{{summaryText}}",
|
|
13160
|
+
"summary.section.device-state": "Devices offline or switched off",
|
|
13161
|
+
"summary.section.streams": "Streams offline",
|
|
13162
|
+
"summary.section.battery": "Low batteries",
|
|
13163
|
+
"summary.section.consumables": "Consumables to replace",
|
|
13164
|
+
"summary.section.tanks": "Tanks",
|
|
13165
|
+
"summary.section.detection-blind": "Cameras detecting nothing",
|
|
13166
|
+
"summary.section.nodes": "Nodes",
|
|
13167
|
+
"summary.section.addons": "Addons",
|
|
13168
|
+
"summary.section.updates": "Server and container updates",
|
|
13169
|
+
"summary.section.unavailable": "{{section}}: unavailable — the state could not be read",
|
|
13170
|
+
"summary.line.device-offline": "{{deviceName}} — offline (since {{since}})",
|
|
13171
|
+
"summary.line.device-disabled": "{{deviceName}} — switched off (since {{since}})",
|
|
13172
|
+
"summary.line.stream-offline": "{{deviceName}} — stream offline (since {{since}})",
|
|
13173
|
+
"summary.line.stream-offline.profiled": "{{deviceName}} — {{profile}} stream offline (since {{since}})",
|
|
13174
|
+
"summary.line.detection-blind": "{{deviceName}} — detecting nothing (since {{since}})",
|
|
13175
|
+
"summary.line.node-offline": "{{nodeId}} — offline (since {{since}})",
|
|
13176
|
+
"summary.line.node-inference-unavailable": "{{nodeId}} — cannot run inference (since {{since}})",
|
|
13177
|
+
"summary.line.addon-crash-loop": "{{nodeId}} — {{addons}} stopped after repeated crashes (since {{since}})",
|
|
13178
|
+
"summary.line.update": "{{packageName}} {{currentVersion}} → {{latestVersion}}",
|
|
13179
|
+
"summary.line.update.onNode": "{{packageName}} {{currentVersion}} → {{latestVersion}} on {{nodeId}}",
|
|
13180
|
+
"summary.line.wrapper-update": "Container {{currentVersion}} → {{latestVersion}}",
|
|
13181
|
+
"summary.line.tank-unread": "{{deviceName}} — the {{tank}} tank needs attention"
|
|
13030
13182
|
};
|
|
13031
13183
|
//#endregion
|
|
13032
13184
|
//#region src/notification-center/texts/it.json
|
|
@@ -13330,7 +13482,31 @@ var it_default = {
|
|
|
13330
13482
|
},
|
|
13331
13483
|
"summary.body.detections": "{{detectionSummary}}",
|
|
13332
13484
|
"summary.body.truncated": "mostrati i {{shown}} più rilevanti su {{total}}",
|
|
13333
|
-
"summary.body.empty": "nessun evento in questa finestra"
|
|
13485
|
+
"summary.body.empty": "nessun evento in questa finestra",
|
|
13486
|
+
"system.state-summary.title": "{{rule}}",
|
|
13487
|
+
"system.state-summary.body": "{{summaryText}}",
|
|
13488
|
+
"summary.section.device-state": "Dispositivi offline o spenti",
|
|
13489
|
+
"summary.section.streams": "Stream offline",
|
|
13490
|
+
"summary.section.battery": "Batterie scariche",
|
|
13491
|
+
"summary.section.consumables": "Consumabili da sostituire",
|
|
13492
|
+
"summary.section.tanks": "Serbatoi",
|
|
13493
|
+
"summary.section.detection-blind": "Camere che non rilevano nulla",
|
|
13494
|
+
"summary.section.nodes": "Nodi",
|
|
13495
|
+
"summary.section.addons": "Addon",
|
|
13496
|
+
"summary.section.updates": "Aggiornamenti di server e container",
|
|
13497
|
+
"summary.section.unavailable": "{{section}}: non disponibile — lo stato non è leggibile",
|
|
13498
|
+
"summary.line.device-offline": "{{deviceName}} — offline (da {{since}})",
|
|
13499
|
+
"summary.line.device-disabled": "{{deviceName}} — spento (da {{since}})",
|
|
13500
|
+
"summary.line.stream-offline": "{{deviceName}} — stream offline (da {{since}})",
|
|
13501
|
+
"summary.line.stream-offline.profiled": "{{deviceName}} — stream {{profile}} offline (da {{since}})",
|
|
13502
|
+
"summary.line.detection-blind": "{{deviceName}} — non rileva nulla (da {{since}})",
|
|
13503
|
+
"summary.line.node-offline": "{{nodeId}} — offline (da {{since}})",
|
|
13504
|
+
"summary.line.node-inference-unavailable": "{{nodeId}} — non può eseguire l'inferenza (da {{since}})",
|
|
13505
|
+
"summary.line.addon-crash-loop": "{{nodeId}} — {{addons}} fermati dopo crash ripetuti (da {{since}})",
|
|
13506
|
+
"summary.line.update": "{{packageName}} {{currentVersion}} → {{latestVersion}}",
|
|
13507
|
+
"summary.line.update.onNode": "{{packageName}} {{currentVersion}} → {{latestVersion}} su {{nodeId}}",
|
|
13508
|
+
"summary.line.wrapper-update": "Container {{currentVersion}} → {{latestVersion}}",
|
|
13509
|
+
"summary.line.tank-unread": "{{deviceName}} — il serbatoio {{tank}} richiede attenzione"
|
|
13334
13510
|
};
|
|
13335
13511
|
//#endregion
|
|
13336
13512
|
//#region src/notification-center/text-catalog.ts
|
|
@@ -13748,7 +13924,7 @@ function assertPatchKeepsFamily(existing, patch) {
|
|
|
13748
13924
|
* the offending kind or filter). Then rethrow. A refused save is dropped work,
|
|
13749
13925
|
* and dropped work is never silent. Any other error passes through unlogged.
|
|
13750
13926
|
*/
|
|
13751
|
-
function logRefusal$
|
|
13927
|
+
function logRefusal$3(check, ctx) {
|
|
13752
13928
|
try {
|
|
13753
13929
|
check();
|
|
13754
13930
|
} catch (err) {
|
|
@@ -13764,11 +13940,11 @@ function logRefusal$2(check, ctx) {
|
|
|
13764
13940
|
}
|
|
13765
13941
|
/** {@link assertRuleFamily}, logged. The create path of both routes. */
|
|
13766
13942
|
function guardRuleFamily(rule, ctx) {
|
|
13767
|
-
logRefusal$
|
|
13943
|
+
logRefusal$3(() => assertRuleFamily(rule), ctx);
|
|
13768
13944
|
}
|
|
13769
13945
|
/** {@link assertPatchKeepsFamily}, logged. The update path of both routes. */
|
|
13770
13946
|
function guardPatchFamily(existing, patch, ctx) {
|
|
13771
|
-
logRefusal$
|
|
13947
|
+
logRefusal$3(() => assertPatchKeepsFamily(existing, patch), ctx);
|
|
13772
13948
|
}
|
|
13773
13949
|
//#endregion
|
|
13774
13950
|
//#region src/notification-center/device-scope-guard.ts
|
|
@@ -13793,7 +13969,7 @@ function assertDevicesExcludeDelivery(rule) {
|
|
|
13793
13969
|
throw new NcDevicesExcludeRefusal(rule.delivery, excluded);
|
|
13794
13970
|
}
|
|
13795
13971
|
/** Run the check; a refusal is dropped work, so it is logged before it is rethrown. */
|
|
13796
|
-
function logRefusal$
|
|
13972
|
+
function logRefusal$2(check, ctx) {
|
|
13797
13973
|
try {
|
|
13798
13974
|
check();
|
|
13799
13975
|
} catch (err) {
|
|
@@ -13811,7 +13987,7 @@ function warnRefusal$1(logger, err, ctx) {
|
|
|
13811
13987
|
}
|
|
13812
13988
|
/** The create path of both routes. */
|
|
13813
13989
|
function guardRuleDevicesExclude(rule, ctx) {
|
|
13814
|
-
logRefusal$
|
|
13990
|
+
logRefusal$2(() => assertDevicesExcludeDelivery(rule), ctx);
|
|
13815
13991
|
}
|
|
13816
13992
|
/**
|
|
13817
13993
|
* The update path of both routes: the MERGED rule is judged, and only when the
|
|
@@ -13821,7 +13997,86 @@ function guardPatchDevicesExclude(existing, patch, ctx) {
|
|
|
13821
13997
|
if (existing === null) return;
|
|
13822
13998
|
const merged = mergedFamilySubject(existing, patch);
|
|
13823
13999
|
if (merged === null) return;
|
|
13824
|
-
logRefusal$
|
|
14000
|
+
logRefusal$2(() => assertDevicesExcludeDelivery(merged), ctx);
|
|
14001
|
+
}
|
|
14002
|
+
//#endregion
|
|
14003
|
+
//#region src/notification-center/rule-summary-guard.ts
|
|
14004
|
+
/**
|
|
14005
|
+
* The hub's half of D647: `systemEventDelivery: { mode: 'summary' }` belongs to
|
|
14006
|
+
* a `device-events` or `system` rule, and nowhere else — the alarm stays
|
|
14007
|
+
* instant. Refused, loudly, on both save routes (the cap provider and the
|
|
14008
|
+
* `nc.*Rule` bridge), beside the D637 family guard.
|
|
14009
|
+
*
|
|
14010
|
+
* The rule is judged by `systemEventDeliveryProblems` (`@camstack/types`), the
|
|
14011
|
+
* one statement both editors read too. On an update it is the MERGED rule —
|
|
14012
|
+
* merged the way the store merges it, `clear` included — and only when the
|
|
14013
|
+
* patch changes something the verdict reads: a rename of a stored rule is
|
|
14014
|
+
* never refused for what it already was.
|
|
14015
|
+
*
|
|
14016
|
+
* Not in Zod, for the reason `rule-family-guard.ts` gives: a cross-field refine
|
|
14017
|
+
* would also run when the store LOADS a rule.
|
|
14018
|
+
*/
|
|
14019
|
+
var NcSystemEventDeliveryRefusal = class extends Error {
|
|
14020
|
+
problems;
|
|
14021
|
+
constructor(problems) {
|
|
14022
|
+
super(`rule refused: ${problems.map(describeSystemEventDeliveryProblem).join("; ")}`);
|
|
14023
|
+
this.name = "NcSystemEventDeliveryRefusal";
|
|
14024
|
+
this.problems = problems;
|
|
14025
|
+
}
|
|
14026
|
+
};
|
|
14027
|
+
function assertSystemEventDelivery(rule) {
|
|
14028
|
+
const problems = systemEventDeliveryProblems(rule);
|
|
14029
|
+
if (problems.length > 0) throw new NcSystemEventDeliveryRefusal(problems);
|
|
14030
|
+
}
|
|
14031
|
+
var JUDGED_KEYS = [
|
|
14032
|
+
"delivery",
|
|
14033
|
+
"conditions",
|
|
14034
|
+
"systemEventDelivery",
|
|
14035
|
+
"schedule",
|
|
14036
|
+
"actions"
|
|
14037
|
+
];
|
|
14038
|
+
/**
|
|
14039
|
+
* The rule the patch WOULD produce, or `null` when it changes nothing the
|
|
14040
|
+
* verdict reads. `clear` removes `schedule` / `actions` before the merge, as
|
|
14041
|
+
* the store does.
|
|
14042
|
+
*/
|
|
14043
|
+
function mergedDeliverySubject(existing, patch, clear) {
|
|
14044
|
+
const merged = {
|
|
14045
|
+
delivery: existing.delivery,
|
|
14046
|
+
conditions: existing.conditions,
|
|
14047
|
+
...existing.systemEventDelivery !== void 0 ? { systemEventDelivery: existing.systemEventDelivery } : {},
|
|
14048
|
+
...existing.schedule !== void 0 && !clear.includes("schedule") ? { schedule: existing.schedule } : {},
|
|
14049
|
+
...existing.actions !== void 0 && !clear.includes("actions") ? { actions: existing.actions } : {},
|
|
14050
|
+
...patch.delivery !== void 0 ? { delivery: patch.delivery } : {},
|
|
14051
|
+
...patch.conditions !== void 0 ? { conditions: patch.conditions } : {},
|
|
14052
|
+
...patch.systemEventDelivery !== void 0 ? { systemEventDelivery: patch.systemEventDelivery } : {},
|
|
14053
|
+
...patch.schedule !== void 0 ? { schedule: patch.schedule } : {},
|
|
14054
|
+
...patch.actions !== void 0 ? { actions: patch.actions } : {}
|
|
14055
|
+
};
|
|
14056
|
+
return JUDGED_KEYS.some((key) => canonicalHash(merged[key] ?? null) !== canonicalHash(existing[key] ?? null)) ? merged : null;
|
|
14057
|
+
}
|
|
14058
|
+
function logRefusal$1(check, ctx) {
|
|
14059
|
+
try {
|
|
14060
|
+
check();
|
|
14061
|
+
} catch (err) {
|
|
14062
|
+
if (err instanceof NcSystemEventDeliveryRefusal) ctx.logger.warn("nc rule refused: summary delivery not allowed here", { meta: {
|
|
14063
|
+
...ctx.ruleId !== void 0 ? { ruleId: ctx.ruleId } : {},
|
|
14064
|
+
by: ctx.by,
|
|
14065
|
+
problems: err.problems.map(describeSystemEventDeliveryProblem)
|
|
14066
|
+
} });
|
|
14067
|
+
throw err;
|
|
14068
|
+
}
|
|
14069
|
+
}
|
|
14070
|
+
/** The create path of both routes. */
|
|
14071
|
+
function guardRuleSystemEventDelivery(rule, ctx) {
|
|
14072
|
+
logRefusal$1(() => assertSystemEventDelivery(rule), ctx);
|
|
14073
|
+
}
|
|
14074
|
+
/** The update path of both routes: the MERGED rule, when the patch changes what is judged. */
|
|
14075
|
+
function guardPatchSystemEventDelivery(existing, patch, clear, ctx) {
|
|
14076
|
+
if (existing === null) return;
|
|
14077
|
+
const merged = mergedDeliverySubject(existing, patch, clear);
|
|
14078
|
+
if (merged === null) return;
|
|
14079
|
+
logRefusal$1(() => assertSystemEventDelivery(merged), ctx);
|
|
13825
14080
|
}
|
|
13826
14081
|
//#endregion
|
|
13827
14082
|
//#region src/notification-center/sensor-transition-guard.ts
|
|
@@ -16614,6 +16869,10 @@ function makeNcActionHandlers(deps) {
|
|
|
16614
16869
|
logger: deps.logger,
|
|
16615
16870
|
by: c.userId
|
|
16616
16871
|
});
|
|
16872
|
+
guardRuleSystemEventDelivery(parsed, {
|
|
16873
|
+
logger: deps.logger,
|
|
16874
|
+
by: c.userId
|
|
16875
|
+
});
|
|
16617
16876
|
await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
|
|
16618
16877
|
const rule = await deps.ruleStore.create({
|
|
16619
16878
|
...parsed,
|
|
@@ -16647,6 +16906,11 @@ function makeNcActionHandlers(deps) {
|
|
|
16647
16906
|
by: c.userId,
|
|
16648
16907
|
ruleId: input.ruleId
|
|
16649
16908
|
});
|
|
16909
|
+
guardPatchSystemEventDelivery(existing, patch, clear, {
|
|
16910
|
+
logger: deps.logger,
|
|
16911
|
+
by: c.userId,
|
|
16912
|
+
ruleId: input.ruleId
|
|
16913
|
+
});
|
|
16650
16914
|
if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
|
|
16651
16915
|
const { ownerUserId: _owner, disabledTargetIds: _optOut, ...safe } = patch;
|
|
16652
16916
|
const rule = await deps.ruleStore.update(input.ruleId, safe, clear);
|
|
@@ -17072,6 +17336,87 @@ function makeNcActionHandlers(deps) {
|
|
|
17072
17336
|
};
|
|
17073
17337
|
}
|
|
17074
17338
|
//#endregion
|
|
17339
|
+
//#region src/notification-center/state-summary/state-summary-delivery.ts
|
|
17340
|
+
/**
|
|
17341
|
+
* A state summary as an ORDINARY system-event subject (D647), so it reaches the
|
|
17342
|
+
* phone the way every system rule does: `buildEntries` → the durable outbox →
|
|
17343
|
+
* the drain's snooze gate, retries and per-target history → the dispatcher,
|
|
17344
|
+
* which renders the rule's own template with the summary's variables.
|
|
17345
|
+
*
|
|
17346
|
+
* - `recordId` is `state-summary:<ruleId>:<windowEndMs>` — the outbox dedup
|
|
17347
|
+
* key, so a restart can never enqueue one window twice.
|
|
17348
|
+
* - The row files under the system device (0): a summary spans devices.
|
|
17349
|
+
* `targetUsers` therefore reach admins only — a user whose grant is
|
|
17350
|
+
* narrower than the whole installation must not be sent the names of
|
|
17351
|
+
* devices they cannot open.
|
|
17352
|
+
* - The words are FROZEN at enqueue (`textParams`), like the digest's: the
|
|
17353
|
+
* state is the state at the window's end, not at a retry hours later. The
|
|
17354
|
+
* LANGUAGE is frozen with them.
|
|
17355
|
+
* - `kind` is the rule's first kind: the history row needs one, and no kind
|
|
17356
|
+
* is "the" event of a summary — `data.stateSummary` says what the row is,
|
|
17357
|
+
* and `{{eventKind}}` renders empty on it.
|
|
17358
|
+
*/
|
|
17359
|
+
var NC_STATE_SUMMARY_TITLE_KEY = "system.state-summary.title";
|
|
17360
|
+
var NC_STATE_SUMMARY_BODY_KEY = "system.state-summary.body";
|
|
17361
|
+
function stateSummaryRecordId(ruleId, windowEndMs) {
|
|
17362
|
+
return `state-summary:${ruleId}:${String(windowEndMs)}`;
|
|
17363
|
+
}
|
|
17364
|
+
/** Is this system-event data the data of a state-summary row? */
|
|
17365
|
+
function isStateSummaryData(data) {
|
|
17366
|
+
return data["stateSummary"] === true;
|
|
17367
|
+
}
|
|
17368
|
+
/** `null` when the rule has no system-event condition (the guard refuses that). */
|
|
17369
|
+
function stateSummarySubject(input) {
|
|
17370
|
+
const kind = input.rule.conditions.systemEvent?.kinds[0];
|
|
17371
|
+
if (kind === void 0) return null;
|
|
17372
|
+
return {
|
|
17373
|
+
kind: "system-event",
|
|
17374
|
+
recordId: stateSummaryRecordId(input.rule.id, input.window.endMs),
|
|
17375
|
+
deviceId: 0,
|
|
17376
|
+
timestamp: input.window.endMs,
|
|
17377
|
+
classNames: [],
|
|
17378
|
+
zones: [],
|
|
17379
|
+
systemEvent: {
|
|
17380
|
+
kind,
|
|
17381
|
+
subject: `state-summary:${input.rule.id}`,
|
|
17382
|
+
titleKey: NC_STATE_SUMMARY_TITLE_KEY,
|
|
17383
|
+
bodyKey: NC_STATE_SUMMARY_BODY_KEY,
|
|
17384
|
+
textParams: {
|
|
17385
|
+
summaryText: input.text.text,
|
|
17386
|
+
summaryCount: String(input.text.count),
|
|
17387
|
+
...input.text.sectionVars
|
|
17388
|
+
},
|
|
17389
|
+
textCount: input.text.count,
|
|
17390
|
+
data: {
|
|
17391
|
+
stateSummary: true,
|
|
17392
|
+
windowStartMs: input.window.startMs,
|
|
17393
|
+
windowEndMs: input.window.endMs,
|
|
17394
|
+
sections: input.summary.sections.map((section) => ({
|
|
17395
|
+
id: section.id,
|
|
17396
|
+
status: section.status,
|
|
17397
|
+
items: section.items.length
|
|
17398
|
+
}))
|
|
17399
|
+
}
|
|
17400
|
+
}
|
|
17401
|
+
};
|
|
17402
|
+
}
|
|
17403
|
+
/**
|
|
17404
|
+
* The rule as its summary is ENQUEUED: a custom body that carries none of the
|
|
17405
|
+
* summary's content gets `{{summaryText}}` appended on its own line, so an
|
|
17406
|
+
* operator's body never sends an empty summary (review M3). `null` = unchanged.
|
|
17407
|
+
*/
|
|
17408
|
+
function withSummaryBody(rule) {
|
|
17409
|
+
const body = rule.template?.body;
|
|
17410
|
+
if (templateCarriesSummary(body)) return null;
|
|
17411
|
+
return {
|
|
17412
|
+
...rule,
|
|
17413
|
+
template: {
|
|
17414
|
+
...rule.template,
|
|
17415
|
+
body: `${body ?? ""}\n{{summaryText}}`
|
|
17416
|
+
}
|
|
17417
|
+
};
|
|
17418
|
+
}
|
|
17419
|
+
//#endregion
|
|
17075
17420
|
//#region src/notification-center/system-event-tap-url.ts
|
|
17076
17421
|
/** The admin UI's own routes, as `packages/addon-admin-ui/src/App.tsx` declares them. */
|
|
17077
17422
|
var ADMIN_ADDONS_PATH = "/system/addons";
|
|
@@ -17498,6 +17843,32 @@ function selectTriggerZoneCandidates(input) {
|
|
|
17498
17843
|
//#endregion
|
|
17499
17844
|
//#region src/notification-center/dispatcher.ts
|
|
17500
17845
|
/**
|
|
17846
|
+
* NcDispatcher — turns a durable outbox entry into ONE canonical
|
|
17847
|
+
* `notification-output.send` RPC.
|
|
17848
|
+
*
|
|
17849
|
+
* Deps are injected tRPC-shaped closures (the wiring in
|
|
17850
|
+
* pipeline-analytics binds them to `ctx.api` / the media store), so the
|
|
17851
|
+
* dispatcher is unit-testable with fakes and stays extractable.
|
|
17852
|
+
*
|
|
17853
|
+
* Responsibilities:
|
|
17854
|
+
* - resolve the outbox entry's `targetId` against the live
|
|
17855
|
+
* `notification-output` target catalog (TTL-cached; the send call is
|
|
17856
|
+
* routed to the owning provider by the target's `addonId`);
|
|
17857
|
+
* - resolve the media attachment per the rule's policy from the media
|
|
17858
|
+
* store BY OWNER at SEND time — D-3 "best media AVAILABLE at the
|
|
17859
|
+
* moment": the drain runs after the persist moment, so late-landing
|
|
17860
|
+
* crops are picked up without ever delaying delivery;
|
|
17861
|
+
* - render the `{{var}}` template (title/body) over the frozen subject;
|
|
17862
|
+
* - map recognized per-target passthrough params onto canonical
|
|
17863
|
+
* Notification fields (level/sound/clickUrl/ttl) — everything else is
|
|
17864
|
+
* the degrade engine's job.
|
|
17865
|
+
*
|
|
17866
|
+
* Failure semantics: an UNKNOWN target (no longer in the catalog) ⇒ PERMANENT
|
|
17867
|
+
* failure (the outbox dead-letters the row — genuine misconfig); a globally
|
|
17868
|
+
* DISABLED target (operator kill-switch) ⇒ benign skip (no send, no dead row);
|
|
17869
|
+
* transport/backend errors ⇒ transient (retried with backoff).
|
|
17870
|
+
*/
|
|
17871
|
+
/**
|
|
17501
17872
|
* Where the model's sentence goes.
|
|
17502
17873
|
*
|
|
17503
17874
|
* Three cases, and the third is the one that would otherwise be reported as a
|
|
@@ -19065,7 +19436,7 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
|
|
|
19065
19436
|
count: occupancy !== void 0 ? `${occupancy.count}` : group !== void 0 && group.members.length > 1 ? `${group.members.length}` : "",
|
|
19066
19437
|
capacity: occupancy !== void 0 ? `${occupancy.capacity}` : "",
|
|
19067
19438
|
op: occupancy !== void 0 ? ncOccupancyOp(texts, occupancy.occupied) : "",
|
|
19068
|
-
eventKind: systemEvent
|
|
19439
|
+
eventKind: systemEvent === void 0 || isStateSummaryData(systemEvent.data) ? "" : systemEvent.kind,
|
|
19069
19440
|
node: systemEvent?.nodeId ?? "",
|
|
19070
19441
|
package: systemEvent?.packageName ?? "",
|
|
19071
19442
|
currentVersion: typeof systemEvent?.data["currentVersion"] === "string" ? systemEvent.data["currentVersion"] : "",
|
|
@@ -22086,6 +22457,10 @@ var NcLivenessLedger = class {
|
|
|
22086
22457
|
async load() {
|
|
22087
22458
|
return (await this.ledger.load()).length;
|
|
22088
22459
|
}
|
|
22460
|
+
/** Has a load READ the store once? Until then the mirror is not the truth (D647 reads it). */
|
|
22461
|
+
hasAnswered() {
|
|
22462
|
+
return this.ledger.hasAnswered();
|
|
22463
|
+
}
|
|
22089
22464
|
/**
|
|
22090
22465
|
* Boot seed from the DEVICE DIRECTORY — the second half of the reseed, and
|
|
22091
22466
|
* the fix for the outage this ledger used to swallow.
|
|
@@ -28412,6 +28787,1328 @@ var AddonUpdateCoalescer = class {
|
|
|
28412
28787
|
}
|
|
28413
28788
|
};
|
|
28414
28789
|
//#endregion
|
|
28790
|
+
//#region src/notification-center/state-summary/state-sources.ts
|
|
28791
|
+
/**
|
|
28792
|
+
* Where each section of a STATE SUMMARY reads its state (D647) — the same
|
|
28793
|
+
* mirrors the instant raises read, and nothing else:
|
|
28794
|
+
*
|
|
28795
|
+
* | section | mirror |
|
|
28796
|
+
* | --- | --- |
|
|
28797
|
+
* | battery, consumables, tanks | the open alert episodes (`NcDeviceAlerts`, durable rows) |
|
|
28798
|
+
* | device-state | the liveness ledger (offline) + the system-state ledger (switched off) |
|
|
28799
|
+
* | streams, detection-blind, addons, updates | the system-state ledger |
|
|
28800
|
+
* | nodes | the liveness ledger (offline) + the system-state ledger (cannot infer) |
|
|
28801
|
+
*
|
|
28802
|
+
* Every source is RAM. A source that has not answered (its durable store never
|
|
28803
|
+
* read) makes the section UNAVAILABLE — never empty (D49/D315).
|
|
28804
|
+
*
|
|
28805
|
+
* A device offline for less than the liveness hold-off is left out: the instant
|
|
28806
|
+
* path would not have announced it yet either, and a summary must not report a
|
|
28807
|
+
* blip the rule itself would have stayed silent about.
|
|
28808
|
+
*/
|
|
28809
|
+
var UNREAD_ALERTS = "the battery / consumable ledger has not been read yet";
|
|
28810
|
+
var UNREAD_LIVENESS = "the liveness ledger has not been read yet";
|
|
28811
|
+
var UNREAD_STATE = "the system-state ledger has not been read yet";
|
|
28812
|
+
var UNREAD_DEVICES = "the device directory has not answered yet";
|
|
28813
|
+
var UNREAD_NODES = "the node directory did not answer completely";
|
|
28814
|
+
/** Sections whose items name a node: the node directory must have answered. */
|
|
28815
|
+
var NODE_SECTIONS = [
|
|
28816
|
+
"nodes",
|
|
28817
|
+
"addons",
|
|
28818
|
+
"updates"
|
|
28819
|
+
];
|
|
28820
|
+
function deviceParams(deviceId, identity) {
|
|
28821
|
+
const name = identity?.name ?? "";
|
|
28822
|
+
return {
|
|
28823
|
+
deviceId: String(deviceId),
|
|
28824
|
+
deviceName: name.length > 0 ? name : String(deviceId)
|
|
28825
|
+
};
|
|
28826
|
+
}
|
|
28827
|
+
function alertItem(section, open) {
|
|
28828
|
+
const params = open.subject.textParams ?? {};
|
|
28829
|
+
const unreadTank = open.family === "tank" && !open.measured;
|
|
28830
|
+
return {
|
|
28831
|
+
section,
|
|
28832
|
+
subject: open.subject,
|
|
28833
|
+
lineKey: unreadTank ? "summary.line.tank-unread" : open.subject.bodyKey ?? "",
|
|
28834
|
+
params,
|
|
28835
|
+
since: open.since
|
|
28836
|
+
};
|
|
28837
|
+
}
|
|
28838
|
+
function livenessItem(row, lookup) {
|
|
28839
|
+
if (row.subject.startsWith("node:")) {
|
|
28840
|
+
const nodeId = row.subject.slice(5);
|
|
28841
|
+
return {
|
|
28842
|
+
section: "nodes",
|
|
28843
|
+
subject: {
|
|
28844
|
+
kind: "node-offline",
|
|
28845
|
+
subject: row.subject,
|
|
28846
|
+
nodeId,
|
|
28847
|
+
textParams: { nodeId },
|
|
28848
|
+
data: {}
|
|
28849
|
+
},
|
|
28850
|
+
lineKey: "summary.line.node-offline",
|
|
28851
|
+
params: { nodeId },
|
|
28852
|
+
since: row.updatedAt
|
|
28853
|
+
};
|
|
28854
|
+
}
|
|
28855
|
+
if (row.deviceId === void 0) return null;
|
|
28856
|
+
const identity = lookup(row.deviceId);
|
|
28857
|
+
const params = deviceParams(row.deviceId, identity);
|
|
28858
|
+
return {
|
|
28859
|
+
section: "device-state",
|
|
28860
|
+
subject: {
|
|
28861
|
+
kind: "device-offline",
|
|
28862
|
+
subject: row.subject,
|
|
28863
|
+
deviceId: row.deviceId,
|
|
28864
|
+
...identity !== void 0 ? { deviceType: identity.type } : {},
|
|
28865
|
+
textParams: params,
|
|
28866
|
+
data: {}
|
|
28867
|
+
},
|
|
28868
|
+
lineKey: "summary.line.device-offline",
|
|
28869
|
+
params,
|
|
28870
|
+
since: row.updatedAt
|
|
28871
|
+
};
|
|
28872
|
+
}
|
|
28873
|
+
/** The section and the line of an open system-state row. */
|
|
28874
|
+
function stateLine(row) {
|
|
28875
|
+
switch (row.kind) {
|
|
28876
|
+
case "device-disabled": return {
|
|
28877
|
+
section: "device-state",
|
|
28878
|
+
lineKey: "summary.line.device-disabled"
|
|
28879
|
+
};
|
|
28880
|
+
case "stream-offline": return {
|
|
28881
|
+
section: "streams",
|
|
28882
|
+
lineKey: row.params["profile"] !== void 0 ? "summary.line.stream-offline.profiled" : "summary.line.stream-offline"
|
|
28883
|
+
};
|
|
28884
|
+
case "detection-blind": return {
|
|
28885
|
+
section: "detection-blind",
|
|
28886
|
+
lineKey: "summary.line.detection-blind"
|
|
28887
|
+
};
|
|
28888
|
+
case "node-inference-unavailable": return {
|
|
28889
|
+
section: "nodes",
|
|
28890
|
+
lineKey: "summary.line.node-inference-unavailable"
|
|
28891
|
+
};
|
|
28892
|
+
case "addon-crash-loop": return {
|
|
28893
|
+
section: "addons",
|
|
28894
|
+
lineKey: "summary.line.addon-crash-loop"
|
|
28895
|
+
};
|
|
28896
|
+
case "wrapper-update-available": return {
|
|
28897
|
+
section: "updates",
|
|
28898
|
+
lineKey: "summary.line.wrapper-update"
|
|
28899
|
+
};
|
|
28900
|
+
default: {
|
|
28901
|
+
const onNode = row.params["nodeId"] !== void 0;
|
|
28902
|
+
return {
|
|
28903
|
+
section: row.kind === "addon-update-available" ? "addons" : "updates",
|
|
28904
|
+
lineKey: onNode ? "summary.line.update.onNode" : "summary.line.update"
|
|
28905
|
+
};
|
|
28906
|
+
}
|
|
28907
|
+
}
|
|
28908
|
+
}
|
|
28909
|
+
function stateItem(row, lookup) {
|
|
28910
|
+
const identity = row.deviceId !== void 0 ? lookup(row.deviceId) : void 0;
|
|
28911
|
+
const params = row.deviceId !== void 0 ? {
|
|
28912
|
+
...row.params,
|
|
28913
|
+
...deviceParams(row.deviceId, identity)
|
|
28914
|
+
} : row.params;
|
|
28915
|
+
const deviceType = identity?.type ?? row.deviceType;
|
|
28916
|
+
const subject = {
|
|
28917
|
+
kind: row.kind,
|
|
28918
|
+
subject: row.key,
|
|
28919
|
+
...row.deviceId !== void 0 ? { deviceId: row.deviceId } : {},
|
|
28920
|
+
...deviceType !== void 0 ? { deviceType } : {},
|
|
28921
|
+
...row.nodeId !== void 0 ? { nodeId: row.nodeId } : {},
|
|
28922
|
+
...row.packageName !== void 0 ? {
|
|
28923
|
+
packageName: row.packageName,
|
|
28924
|
+
packageNames: [row.packageName]
|
|
28925
|
+
} : {},
|
|
28926
|
+
textParams: params,
|
|
28927
|
+
data: {}
|
|
28928
|
+
};
|
|
28929
|
+
return {
|
|
28930
|
+
...stateLine(row),
|
|
28931
|
+
subject,
|
|
28932
|
+
params,
|
|
28933
|
+
since: row.since
|
|
28934
|
+
};
|
|
28935
|
+
}
|
|
28936
|
+
/**
|
|
28937
|
+
* THE RECONCILE (D647 review I2, D8): a ledger row is a memory, never the
|
|
28938
|
+
* truth. A device item is listed only while the device EXISTS in the directory
|
|
28939
|
+
* and is not an accessory — and an "offline" one only while the device
|
|
28940
|
+
* manager does not call it online. A node item only while the node is known
|
|
28941
|
+
* to a complete node directory (a forgotten node is gone), and an "offline"
|
|
28942
|
+
* one only while the directory does not call it online.
|
|
28943
|
+
*/
|
|
28944
|
+
function stillTrue(item, ports) {
|
|
28945
|
+
const deviceId = item.subject.deviceId;
|
|
28946
|
+
if (deviceId !== void 0) {
|
|
28947
|
+
const identity = ports.lookup(deviceId);
|
|
28948
|
+
if (identity === void 0 || isAccessoryIdentity(identity)) return false;
|
|
28949
|
+
if (item.subject.kind === "device-offline" && identity.online === true) return false;
|
|
28950
|
+
}
|
|
28951
|
+
const nodeId = item.subject.nodeId;
|
|
28952
|
+
if (nodeId !== void 0 && ports.nodes !== null) {
|
|
28953
|
+
const online = ports.nodes.online(nodeId);
|
|
28954
|
+
if (online === void 0) return false;
|
|
28955
|
+
if (item.subject.kind === "node-offline" && online) return false;
|
|
28956
|
+
}
|
|
28957
|
+
return true;
|
|
28958
|
+
}
|
|
28959
|
+
/** Why a section cannot be reconciled right now, or `null` when it can. */
|
|
28960
|
+
function unreconcilable(section, ports) {
|
|
28961
|
+
if (NODE_SECTIONS.includes(section) && (ports.nodes === null || !ports.nodes.complete)) return UNREAD_NODES;
|
|
28962
|
+
if (!NODE_SECTIONS.includes(section) && !ports.devicesAnswered()) return UNREAD_DEVICES;
|
|
28963
|
+
return null;
|
|
28964
|
+
}
|
|
28965
|
+
/** The section, read and reconciled. */
|
|
28966
|
+
function readSection(section, ports, nowMs) {
|
|
28967
|
+
const blocked = unreconcilable(section, ports);
|
|
28968
|
+
if (blocked !== null) return {
|
|
28969
|
+
answered: false,
|
|
28970
|
+
reason: blocked
|
|
28971
|
+
};
|
|
28972
|
+
const read = readRaw(section, ports, nowMs);
|
|
28973
|
+
if (!read.answered) return read;
|
|
28974
|
+
return {
|
|
28975
|
+
answered: true,
|
|
28976
|
+
items: read.items.filter((item) => stillTrue(item, ports))
|
|
28977
|
+
};
|
|
28978
|
+
}
|
|
28979
|
+
/** Which ports a section needs, and why each one is unavailable. */
|
|
28980
|
+
function readRaw(section, ports, nowMs) {
|
|
28981
|
+
if (section === "battery" || section === "consumables" || section === "tanks") {
|
|
28982
|
+
const alerts = ports.alerts();
|
|
28983
|
+
if (alerts === null) return {
|
|
28984
|
+
answered: false,
|
|
28985
|
+
reason: UNREAD_ALERTS
|
|
28986
|
+
};
|
|
28987
|
+
const family = section === "battery" ? "battery" : section === "tanks" ? "tank" : "consumable";
|
|
28988
|
+
return {
|
|
28989
|
+
answered: true,
|
|
28990
|
+
items: alerts.filter((open) => open.family === family).map((open) => alertItem(section, open))
|
|
28991
|
+
};
|
|
28992
|
+
}
|
|
28993
|
+
const state = ports.state();
|
|
28994
|
+
if (state === null) return {
|
|
28995
|
+
answered: false,
|
|
28996
|
+
reason: UNREAD_STATE
|
|
28997
|
+
};
|
|
28998
|
+
const items = [];
|
|
28999
|
+
if (section === "device-state" || section === "nodes") {
|
|
29000
|
+
const liveness = ports.liveness();
|
|
29001
|
+
if (liveness === null) return {
|
|
29002
|
+
answered: false,
|
|
29003
|
+
reason: UNREAD_LIVENESS
|
|
29004
|
+
};
|
|
29005
|
+
for (const row of liveness) {
|
|
29006
|
+
if (row.state !== "offline" || nowMs - row.updatedAt < ports.livenessHoldoffMs) continue;
|
|
29007
|
+
const item = livenessItem(row, ports.lookup);
|
|
29008
|
+
if (item !== null && item.section === section) items.push(item);
|
|
29009
|
+
}
|
|
29010
|
+
}
|
|
29011
|
+
for (const row of state) {
|
|
29012
|
+
const item = stateItem(row, ports.lookup);
|
|
29013
|
+
if (item.section === section) items.push(item);
|
|
29014
|
+
}
|
|
29015
|
+
return {
|
|
29016
|
+
answered: true,
|
|
29017
|
+
items
|
|
29018
|
+
};
|
|
29019
|
+
}
|
|
29020
|
+
function stateSources(ports) {
|
|
29021
|
+
return { read: (section, nowMs) => readSection(section, ports, nowMs) };
|
|
29022
|
+
}
|
|
29023
|
+
//#endregion
|
|
29024
|
+
//#region src/notification-center/state-summary/state-model.ts
|
|
29025
|
+
/**
|
|
29026
|
+
* The STATE SUMMARY's vocabulary (D647): its sections, and what one reported
|
|
29027
|
+
* item is.
|
|
29028
|
+
*
|
|
29029
|
+
* A section is one of the D637 kind GROUPS (`NC_SYSTEM_EVENT_KIND_GROUP_SPECS`)
|
|
29030
|
+
* — the same grouping both editors list kinds under — so "the rule selected a
|
|
29031
|
+
* battery kind" and "the summary has a battery section" are the same fact.
|
|
29032
|
+
* `backups-exports` is the one group with no section: a backup or an export is
|
|
29033
|
+
* a job that COMPLETED, and the state it leaves behind is not a condition that
|
|
29034
|
+
* is still true. The ADR records why it is excluded rather than approximated.
|
|
29035
|
+
*/
|
|
29036
|
+
var NC_STATE_SUMMARY_SECTION_IDS = [
|
|
29037
|
+
"device-state",
|
|
29038
|
+
"streams",
|
|
29039
|
+
"battery",
|
|
29040
|
+
"consumables",
|
|
29041
|
+
"tanks",
|
|
29042
|
+
"detection-blind",
|
|
29043
|
+
"nodes",
|
|
29044
|
+
"addons",
|
|
29045
|
+
"updates"
|
|
29046
|
+
];
|
|
29047
|
+
/** The kind groups that have no section, and why — said on the preview. */
|
|
29048
|
+
var NC_STATE_SUMMARY_EXCLUDED_GROUPS = { "backups-exports": "a backup or an export is a completed job, not a state that is still true" };
|
|
29049
|
+
/** The template variable that carries one section's lines (`template-vars.ts`). */
|
|
29050
|
+
var NC_STATE_SECTION_VAR = {
|
|
29051
|
+
"device-state": "summaryDevices",
|
|
29052
|
+
streams: "summaryStreams",
|
|
29053
|
+
battery: "summaryBattery",
|
|
29054
|
+
consumables: "summaryConsumables",
|
|
29055
|
+
tanks: "summaryTanks",
|
|
29056
|
+
"detection-blind": "summaryDetection",
|
|
29057
|
+
nodes: "summaryNodes",
|
|
29058
|
+
addons: "summaryAddons",
|
|
29059
|
+
updates: "summaryUpdates"
|
|
29060
|
+
};
|
|
29061
|
+
/** The kinds of a section — its kind group. */
|
|
29062
|
+
function sectionKinds(section) {
|
|
29063
|
+
return NC_SYSTEM_EVENT_KIND_GROUP_SPECS.find((group) => group.id === section)?.kinds ?? [];
|
|
29064
|
+
}
|
|
29065
|
+
/**
|
|
29066
|
+
* The sections a rule reports, in section order: every section whose kind
|
|
29067
|
+
* group shares a kind with the rule. A rule that selected only
|
|
29068
|
+
* `device-battery-low` still reports the battery STATE — the state has no
|
|
29069
|
+
* direction, only the event did.
|
|
29070
|
+
*/
|
|
29071
|
+
function sectionsOfRule(condition) {
|
|
29072
|
+
return NC_STATE_SUMMARY_SECTION_IDS.filter((section) => sectionKinds(section).some((kind) => condition.kinds.includes(kind)));
|
|
29073
|
+
}
|
|
29074
|
+
/** The kind groups the rule selected that have no section. */
|
|
29075
|
+
function excludedGroupsOfRule(condition) {
|
|
29076
|
+
return 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);
|
|
29077
|
+
}
|
|
29078
|
+
/**
|
|
29079
|
+
* The condition the summary judges its items with: the rule's own, with its
|
|
29080
|
+
* kinds widened to every kind of every section it reports — so a rule that
|
|
29081
|
+
* named the raise is not told "not my kind" about the state the raise left
|
|
29082
|
+
* behind. Every FILTER is the rule's, untouched.
|
|
29083
|
+
*/
|
|
29084
|
+
function summaryCondition(condition) {
|
|
29085
|
+
const kinds = sectionsOfRule(condition).flatMap((section) => sectionKinds(section));
|
|
29086
|
+
return {
|
|
29087
|
+
...condition,
|
|
29088
|
+
kinds: [...new Set([...condition.kinds, ...kinds])]
|
|
29089
|
+
};
|
|
29090
|
+
}
|
|
29091
|
+
//#endregion
|
|
29092
|
+
//#region src/notification-center/state-summary/state-summary.ts
|
|
29093
|
+
/** Nothing to say: no item, and every section READ. Such a summary is not sent. */
|
|
29094
|
+
function isEmptyStateSummary(summary) {
|
|
29095
|
+
return summary.itemCount === 0 && summary.unavailableCount === 0;
|
|
29096
|
+
}
|
|
29097
|
+
function collectStateSummary(input) {
|
|
29098
|
+
const condition = input.rule.conditions.systemEvent;
|
|
29099
|
+
if (condition === void 0) return {
|
|
29100
|
+
sections: [],
|
|
29101
|
+
itemCount: 0,
|
|
29102
|
+
unavailableCount: 0,
|
|
29103
|
+
dropped: {},
|
|
29104
|
+
excludedGroups: []
|
|
29105
|
+
};
|
|
29106
|
+
const judge = {
|
|
29107
|
+
...input.rule,
|
|
29108
|
+
conditions: {
|
|
29109
|
+
...input.rule.conditions,
|
|
29110
|
+
systemEvent: summaryCondition(condition)
|
|
29111
|
+
},
|
|
29112
|
+
schedule: void 0,
|
|
29113
|
+
systemEventDelivery: void 0
|
|
29114
|
+
};
|
|
29115
|
+
const dropped = {};
|
|
29116
|
+
const drop = (reason) => {
|
|
29117
|
+
dropped[reason] = (dropped[reason] ?? 0) + 1;
|
|
29118
|
+
};
|
|
29119
|
+
const sections = [];
|
|
29120
|
+
for (const id of sectionsOfRule(condition)) {
|
|
29121
|
+
const read = input.sources.read(id, input.nowMs);
|
|
29122
|
+
if (!read.answered) {
|
|
29123
|
+
sections.push({
|
|
29124
|
+
id,
|
|
29125
|
+
status: "unavailable",
|
|
29126
|
+
items: [],
|
|
29127
|
+
reason: read.reason
|
|
29128
|
+
});
|
|
29129
|
+
continue;
|
|
29130
|
+
}
|
|
29131
|
+
const items = read.items.filter((item) => {
|
|
29132
|
+
const deviceId = item.subject.deviceId;
|
|
29133
|
+
if (deviceId !== void 0 && input.isMuted(deviceId)) {
|
|
29134
|
+
drop("muted");
|
|
29135
|
+
return false;
|
|
29136
|
+
}
|
|
29137
|
+
const verdict = evaluateRule(judge, ruleSubjectOf(item, input.nowMs));
|
|
29138
|
+
if (verdict.matched) return true;
|
|
29139
|
+
drop(verdict.excludedBySystemEventFilter ?? verdict.failedCondition ?? "unknown");
|
|
29140
|
+
return false;
|
|
29141
|
+
});
|
|
29142
|
+
sections.push({
|
|
29143
|
+
id,
|
|
29144
|
+
status: "read",
|
|
29145
|
+
items: items.toSorted(bySince)
|
|
29146
|
+
});
|
|
29147
|
+
}
|
|
29148
|
+
return {
|
|
29149
|
+
sections,
|
|
29150
|
+
itemCount: sections.reduce((sum, section) => sum + section.items.length, 0),
|
|
29151
|
+
unavailableCount: sections.filter((section) => section.status === "unavailable").length,
|
|
29152
|
+
dropped,
|
|
29153
|
+
excludedGroups: excludedGroupsOfRule(condition)
|
|
29154
|
+
};
|
|
29155
|
+
}
|
|
29156
|
+
function bySince(a, b) {
|
|
29157
|
+
return (a.since ?? 0) - (b.since ?? 0);
|
|
29158
|
+
}
|
|
29159
|
+
function ruleSubjectOf(item, nowMs) {
|
|
29160
|
+
return {
|
|
29161
|
+
kind: "system-event",
|
|
29162
|
+
recordId: `state:${item.subject.subject}`,
|
|
29163
|
+
deviceId: item.subject.deviceId ?? 0,
|
|
29164
|
+
timestamp: nowMs,
|
|
29165
|
+
classNames: [],
|
|
29166
|
+
zones: [],
|
|
29167
|
+
systemEvent: item.subject
|
|
29168
|
+
};
|
|
29169
|
+
}
|
|
29170
|
+
var DAY_MS$3 = 864e5;
|
|
29171
|
+
/** "07:12" when it began in the last day, "25 Sep, 07:12" before that. */
|
|
29172
|
+
function sinceText(texts, sinceMs, nowMs, timeZone) {
|
|
29173
|
+
if (nowMs - sinceMs < DAY_MS$3) return ncClockText(texts.language, sinceMs, timeZone);
|
|
29174
|
+
return new Intl.DateTimeFormat(texts.language, {
|
|
29175
|
+
day: "numeric",
|
|
29176
|
+
month: "short",
|
|
29177
|
+
hour: "2-digit",
|
|
29178
|
+
minute: "2-digit",
|
|
29179
|
+
...timeZone !== void 0 ? { timeZone } : {}
|
|
29180
|
+
}).format(new Date(sinceMs));
|
|
29181
|
+
}
|
|
29182
|
+
function lineOf(input, item) {
|
|
29183
|
+
const since = item.since !== void 0 ? sinceText(input.texts, item.since, input.nowMs, input.timeZone) : "";
|
|
29184
|
+
return input.texts.text({
|
|
29185
|
+
key: item.lineKey,
|
|
29186
|
+
vars: {
|
|
29187
|
+
...item.params,
|
|
29188
|
+
since
|
|
29189
|
+
}
|
|
29190
|
+
});
|
|
29191
|
+
}
|
|
29192
|
+
function blockOf(input, section) {
|
|
29193
|
+
const title = input.texts.text({ key: `summary.section.${section.id}` });
|
|
29194
|
+
if (section.status === "unavailable") return input.texts.text({
|
|
29195
|
+
key: "summary.section.unavailable",
|
|
29196
|
+
vars: { section: title }
|
|
29197
|
+
});
|
|
29198
|
+
if (section.items.length === 0) return null;
|
|
29199
|
+
const lines = section.items.map((item) => `• ${lineOf(input, item)}`);
|
|
29200
|
+
return [`${title}:`, ...lines].join("\n");
|
|
29201
|
+
}
|
|
29202
|
+
function stateSummaryText(input) {
|
|
29203
|
+
const sectionVars = {};
|
|
29204
|
+
const blocks = [];
|
|
29205
|
+
for (const section of input.summary.sections) {
|
|
29206
|
+
const block = blockOf(input, section);
|
|
29207
|
+
sectionVars[NC_STATE_SECTION_VAR[section.id]] = block ?? "";
|
|
29208
|
+
if (block !== null) blocks.push(block);
|
|
29209
|
+
}
|
|
29210
|
+
return {
|
|
29211
|
+
text: blocks.join("\n"),
|
|
29212
|
+
count: input.summary.itemCount,
|
|
29213
|
+
sectionVars
|
|
29214
|
+
};
|
|
29215
|
+
}
|
|
29216
|
+
//#endregion
|
|
29217
|
+
//#region src/notification-center/state-summary/zoned-clock.ts
|
|
29218
|
+
var WEEKDAYS = {
|
|
29219
|
+
Sun: 0,
|
|
29220
|
+
Mon: 1,
|
|
29221
|
+
Tue: 2,
|
|
29222
|
+
Wed: 3,
|
|
29223
|
+
Thu: 4,
|
|
29224
|
+
Fri: 5,
|
|
29225
|
+
Sat: 6
|
|
29226
|
+
};
|
|
29227
|
+
var FORMATTERS = /* @__PURE__ */ new Map();
|
|
29228
|
+
function formatterFor(timezone) {
|
|
29229
|
+
const held = FORMATTERS.get(timezone);
|
|
29230
|
+
if (held !== void 0) return held;
|
|
29231
|
+
let made;
|
|
29232
|
+
try {
|
|
29233
|
+
made = new Intl.DateTimeFormat("en-US", {
|
|
29234
|
+
timeZone: timezone,
|
|
29235
|
+
year: "numeric",
|
|
29236
|
+
month: "numeric",
|
|
29237
|
+
day: "numeric",
|
|
29238
|
+
weekday: "short",
|
|
29239
|
+
hour: "numeric",
|
|
29240
|
+
minute: "numeric",
|
|
29241
|
+
hourCycle: "h23"
|
|
29242
|
+
});
|
|
29243
|
+
} catch {
|
|
29244
|
+
made = null;
|
|
29245
|
+
}
|
|
29246
|
+
FORMATTERS.set(timezone, made);
|
|
29247
|
+
return made;
|
|
29248
|
+
}
|
|
29249
|
+
function wallOf(atMs, timezone) {
|
|
29250
|
+
const date = new Date(atMs);
|
|
29251
|
+
const formatter = timezone !== void 0 ? formatterFor(timezone) : null;
|
|
29252
|
+
if (formatter !== null) {
|
|
29253
|
+
const parts = new Map(formatter.formatToParts(date).map((p) => [p.type, p.value]));
|
|
29254
|
+
const weekday = WEEKDAYS[parts.get("weekday") ?? ""];
|
|
29255
|
+
const wall = {
|
|
29256
|
+
year: Number(parts.get("year")),
|
|
29257
|
+
month: Number(parts.get("month")),
|
|
29258
|
+
day: Number(parts.get("day")),
|
|
29259
|
+
hour: Number(parts.get("hour")),
|
|
29260
|
+
minute: Number(parts.get("minute"))
|
|
29261
|
+
};
|
|
29262
|
+
if (weekday !== void 0 && Object.values(wall).every(Number.isFinite)) return {
|
|
29263
|
+
...wall,
|
|
29264
|
+
weekday
|
|
29265
|
+
};
|
|
29266
|
+
}
|
|
29267
|
+
return {
|
|
29268
|
+
year: date.getFullYear(),
|
|
29269
|
+
month: date.getMonth() + 1,
|
|
29270
|
+
day: date.getDate(),
|
|
29271
|
+
weekday: date.getDay(),
|
|
29272
|
+
hour: date.getHours(),
|
|
29273
|
+
minute: date.getMinutes()
|
|
29274
|
+
};
|
|
29275
|
+
}
|
|
29276
|
+
/** The calendar day `atMs` falls on, in the timezone. */
|
|
29277
|
+
function zonedDayOf(atMs, timezone) {
|
|
29278
|
+
const { year, month, day, weekday } = wallOf(atMs, timezone);
|
|
29279
|
+
return {
|
|
29280
|
+
year,
|
|
29281
|
+
month,
|
|
29282
|
+
day,
|
|
29283
|
+
weekday
|
|
29284
|
+
};
|
|
29285
|
+
}
|
|
29286
|
+
/** The day `offset` calendar days after `day`. Pure calendar arithmetic. */
|
|
29287
|
+
function addDays(day, offset) {
|
|
29288
|
+
const shifted = new Date(Date.UTC(day.year, day.month - 1, day.day + offset));
|
|
29289
|
+
return {
|
|
29290
|
+
year: shifted.getUTCFullYear(),
|
|
29291
|
+
month: shifted.getUTCMonth() + 1,
|
|
29292
|
+
day: shifted.getUTCDate(),
|
|
29293
|
+
weekday: ((day.weekday + offset) % 7 + 7) % 7
|
|
29294
|
+
};
|
|
29295
|
+
}
|
|
29296
|
+
/**
|
|
29297
|
+
* The instant of `minuteOfDay` on `day`, in the timezone. Two passes of the
|
|
29298
|
+
* offset read settle a DST change; a wall time that does not exist (the spring
|
|
29299
|
+
* gap) lands next to it, and the caller verifies every instant it produces.
|
|
29300
|
+
*/
|
|
29301
|
+
function zonedInstant(day, minuteOfDay, timezone) {
|
|
29302
|
+
const hour = Math.floor(minuteOfDay / 60);
|
|
29303
|
+
const minute = minuteOfDay % 60;
|
|
29304
|
+
if (timezone === void 0 || formatterFor(timezone) === null) return new Date(day.year, day.month - 1, day.day, hour, minute).getTime();
|
|
29305
|
+
const asUtc = Date.UTC(day.year, day.month - 1, day.day, hour, minute);
|
|
29306
|
+
const offsetAt = (atMs) => {
|
|
29307
|
+
const wall = wallOf(atMs, timezone);
|
|
29308
|
+
return Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute) - atMs;
|
|
29309
|
+
};
|
|
29310
|
+
return asUtc - offsetAt(asUtc - offsetAt(asUtc));
|
|
29311
|
+
}
|
|
29312
|
+
/** The widest look back: a weekly schedule closes at least once in eight days. */
|
|
29313
|
+
var NC_STATE_SUMMARY_MAX_LOOKBACK_MS = 8 * 864e5;
|
|
29314
|
+
/** A close found by the tick right after it is on time; later than this is `late`. */
|
|
29315
|
+
var ON_TIME_MS = 2 * 6e4;
|
|
29316
|
+
var MINUTE_MS = 6e4;
|
|
29317
|
+
var DAY_MS$2 = 864e5;
|
|
29318
|
+
/**
|
|
29319
|
+
* Every verified transition of `kind` in `(fromMs, toMs]`, ascending.
|
|
29320
|
+
* Candidates are each window's start and end minute on each day of the range
|
|
29321
|
+
* (the day before `fromMs` included, for a window crossing midnight).
|
|
29322
|
+
*/
|
|
29323
|
+
function scheduleTransitions(schedule, fromMs, toMs, kind, probe = isScheduleActive) {
|
|
29324
|
+
if (toMs <= fromMs) return [];
|
|
29325
|
+
const first = zonedDayOf(fromMs - DAY_MS$2, schedule.timezone);
|
|
29326
|
+
const days = Math.ceil((toMs - fromMs) / DAY_MS$2) + 2;
|
|
29327
|
+
const minutes = /* @__PURE__ */ new Set();
|
|
29328
|
+
for (const window of schedule.windows) {
|
|
29329
|
+
minutes.add(window.startMinute);
|
|
29330
|
+
minutes.add(window.endMinute);
|
|
29331
|
+
}
|
|
29332
|
+
const found = /* @__PURE__ */ new Set();
|
|
29333
|
+
for (let offset = 0; offset <= days; offset += 1) {
|
|
29334
|
+
const day = addDays(first, offset);
|
|
29335
|
+
for (const minute of minutes) {
|
|
29336
|
+
const at = zonedInstant(day, minute, schedule.timezone);
|
|
29337
|
+
if (at <= fromMs || at > toMs || found.has(at)) continue;
|
|
29338
|
+
const before = probe(schedule, at - MINUTE_MS);
|
|
29339
|
+
const after = probe(schedule, at);
|
|
29340
|
+
if (kind === "close" ? before && !after : !before && after) found.add(at);
|
|
29341
|
+
}
|
|
29342
|
+
}
|
|
29343
|
+
return [...found].toSorted((a, b) => a - b);
|
|
29344
|
+
}
|
|
29345
|
+
/** The first window close after `afterMs`, within `horizonMs`, or `null`. */
|
|
29346
|
+
function nextStateSummaryClose(schedule, afterMs, horizonMs = NC_STATE_SUMMARY_MAX_LOOKBACK_MS, probe) {
|
|
29347
|
+
return scheduleTransitions(schedule, afterMs, afterMs + horizonMs, "close", probe)[0] ?? null;
|
|
29348
|
+
}
|
|
29349
|
+
function decideStateSummary(input) {
|
|
29350
|
+
const from = Math.max(input.floorMs, input.nowMs - NC_STATE_SUMMARY_MAX_LOOKBACK_MS);
|
|
29351
|
+
const endMs = scheduleTransitions(input.schedule, from, input.nowMs, "close", input.probe).at(-1);
|
|
29352
|
+
if (endMs === void 0) return { kind: "idle" };
|
|
29353
|
+
const window = {
|
|
29354
|
+
startMs: scheduleTransitions(input.schedule, endMs - DAY_MS$2, endMs, "open", input.probe).at(-1) ?? endMs,
|
|
29355
|
+
endMs
|
|
29356
|
+
};
|
|
29357
|
+
const overdueMs = input.nowMs - endMs;
|
|
29358
|
+
if (overdueMs > (input.graceMs ?? 108e5)) return {
|
|
29359
|
+
kind: "missed",
|
|
29360
|
+
window
|
|
29361
|
+
};
|
|
29362
|
+
return {
|
|
29363
|
+
kind: "send",
|
|
29364
|
+
window,
|
|
29365
|
+
late: overdueMs > ON_TIME_MS
|
|
29366
|
+
};
|
|
29367
|
+
}
|
|
29368
|
+
//#endregion
|
|
29369
|
+
//#region src/notification-center/state-summary/state-summary-scheduler.ts
|
|
29370
|
+
/**
|
|
29371
|
+
* The STATE SUMMARY's tick (D647): for every enabled summary rule, is a
|
|
29372
|
+
* window owed, and if so collect → word → enqueue → stamp.
|
|
29373
|
+
*
|
|
29374
|
+
* Driven by the centre's existing rule-reload tick — no timer of its own — and
|
|
29375
|
+
* single-flight. It reads RAM only: the rule mirror, the stamps and the state
|
|
29376
|
+
* mirrors (D447: nothing on this schedule queries SQLite).
|
|
29377
|
+
*
|
|
29378
|
+
* Every branch that sends nothing SAYS so: an empty state (INFO), a missed
|
|
29379
|
+
* window (WARN), a stamp ledger that has not been read (WARN — nothing is sent
|
|
29380
|
+
* until it has, because without it a restart could send a window twice), a
|
|
29381
|
+
* delivery that failed (WARN, retried on the next tick inside the grace).
|
|
29382
|
+
*
|
|
29383
|
+
* The stamp is LAST, as in the digest: a window whose enqueue failed stays owed.
|
|
29384
|
+
*/
|
|
29385
|
+
var NcStateSummaryScheduler = class {
|
|
29386
|
+
deps;
|
|
29387
|
+
inFlight = false;
|
|
29388
|
+
/** Per rule: the next window close, valid for one floor and one schedule. */
|
|
29389
|
+
nextClose = /* @__PURE__ */ new Map();
|
|
29390
|
+
/** One WARN per hold, not one per tick. */
|
|
29391
|
+
holdReported = false;
|
|
29392
|
+
constructor(deps) {
|
|
29393
|
+
this.deps = deps;
|
|
29394
|
+
}
|
|
29395
|
+
async tick() {
|
|
29396
|
+
const report = {
|
|
29397
|
+
decisions: 0,
|
|
29398
|
+
sent: 0,
|
|
29399
|
+
empty: 0,
|
|
29400
|
+
missed: 0,
|
|
29401
|
+
failed: 0,
|
|
29402
|
+
held: false
|
|
29403
|
+
};
|
|
29404
|
+
if (this.inFlight) return report;
|
|
29405
|
+
this.inFlight = true;
|
|
29406
|
+
try {
|
|
29407
|
+
const rules = this.deps.rules().filter((rule) => systemEventSummaryScheduleOf(rule) !== null);
|
|
29408
|
+
if (rules.length === 0) return report;
|
|
29409
|
+
if (!this.deps.stamps.answered()) {
|
|
29410
|
+
report.held = true;
|
|
29411
|
+
if (!this.holdReported) {
|
|
29412
|
+
this.holdReported = true;
|
|
29413
|
+
this.deps.logger.warn("state summaries held — the send ledger has not been read yet", { meta: { rules: rules.map((rule) => rule.id) } });
|
|
29414
|
+
}
|
|
29415
|
+
return report;
|
|
29416
|
+
}
|
|
29417
|
+
this.holdReported = false;
|
|
29418
|
+
const nowMs = this.deps.now();
|
|
29419
|
+
for (const rule of rules) {
|
|
29420
|
+
const schedule = systemEventSummaryScheduleOf(rule);
|
|
29421
|
+
if (schedule !== null) await this.tickRule(rule, schedule, nowMs, report);
|
|
29422
|
+
}
|
|
29423
|
+
return report;
|
|
29424
|
+
} finally {
|
|
29425
|
+
this.inFlight = false;
|
|
29426
|
+
}
|
|
29427
|
+
}
|
|
29428
|
+
async tickRule(rule, schedule, nowMs, report) {
|
|
29429
|
+
const stamp = await this.armed(rule, schedule, nowMs);
|
|
29430
|
+
if (stamp === null) return;
|
|
29431
|
+
const floorMs = Math.max(stamp.armedAt, stamp.lastWindowEnd);
|
|
29432
|
+
const cached = this.nextClose.get(rule.id);
|
|
29433
|
+
if (cached !== void 0 && cached.floorMs === floorMs && cached.scheduleKey === stamp.scheduleKey && nowMs < cached.atMs) return;
|
|
29434
|
+
report.decisions += 1;
|
|
29435
|
+
const decision = decideStateSummary({
|
|
29436
|
+
schedule,
|
|
29437
|
+
nowMs,
|
|
29438
|
+
floorMs,
|
|
29439
|
+
...this.deps.graceMs !== void 0 ? { graceMs: this.deps.graceMs } : {}
|
|
29440
|
+
});
|
|
29441
|
+
if (decision.kind === "idle") {
|
|
29442
|
+
const next = nextStateSummaryClose(schedule, nowMs);
|
|
29443
|
+
this.nextClose.set(rule.id, {
|
|
29444
|
+
floorMs,
|
|
29445
|
+
scheduleKey: stamp.scheduleKey,
|
|
29446
|
+
atMs: next ?? nowMs + 6912e5
|
|
29447
|
+
});
|
|
29448
|
+
return;
|
|
29449
|
+
}
|
|
29450
|
+
this.nextClose.delete(rule.id);
|
|
29451
|
+
const meta = {
|
|
29452
|
+
ruleId: rule.id,
|
|
29453
|
+
rule: rule.name,
|
|
29454
|
+
windowStart: decision.window.startMs,
|
|
29455
|
+
windowEnd: decision.window.endMs
|
|
29456
|
+
};
|
|
29457
|
+
if (decision.kind === "missed") {
|
|
29458
|
+
report.missed += 1;
|
|
29459
|
+
this.deps.logger.warn("state summary window MISSED — closed too long ago to send now", { meta: {
|
|
29460
|
+
...meta,
|
|
29461
|
+
overdueMs: nowMs - decision.window.endMs
|
|
29462
|
+
} });
|
|
29463
|
+
await this.stamp(stamp, decision.window.endMs);
|
|
29464
|
+
return;
|
|
29465
|
+
}
|
|
29466
|
+
const summary = await this.deps.collect(rule, nowMs);
|
|
29467
|
+
if (isEmptyStateSummary(summary)) {
|
|
29468
|
+
report.empty += 1;
|
|
29469
|
+
this.deps.logger.info("state summary has nothing to report — not sent", { meta: {
|
|
29470
|
+
...meta,
|
|
29471
|
+
dropped: summary.dropped
|
|
29472
|
+
} });
|
|
29473
|
+
await this.stamp(stamp, decision.window.endMs);
|
|
29474
|
+
return;
|
|
29475
|
+
}
|
|
29476
|
+
let inserted;
|
|
29477
|
+
try {
|
|
29478
|
+
inserted = await this.deps.deliver(rule, summary, decision.window);
|
|
29479
|
+
} catch (err) {
|
|
29480
|
+
report.failed += 1;
|
|
29481
|
+
this.deps.logger.warn("state summary could not be enqueued — retried on the next tick", { meta: {
|
|
29482
|
+
...meta,
|
|
29483
|
+
error: err instanceof Error ? err.message : String(err)
|
|
29484
|
+
} });
|
|
29485
|
+
return;
|
|
29486
|
+
}
|
|
29487
|
+
report.sent += 1;
|
|
29488
|
+
this.deps.logger.info("state summary enqueued", { meta: {
|
|
29489
|
+
...meta,
|
|
29490
|
+
late: decision.late,
|
|
29491
|
+
items: summary.itemCount,
|
|
29492
|
+
unavailable: summary.unavailableCount,
|
|
29493
|
+
sections: summary.sections.map((section) => `${section.id}:${section.status}`),
|
|
29494
|
+
dropped: summary.dropped,
|
|
29495
|
+
inserted
|
|
29496
|
+
} });
|
|
29497
|
+
await this.stamp(stamp, decision.window.endMs);
|
|
29498
|
+
}
|
|
29499
|
+
/**
|
|
29500
|
+
* The rule's stamp, armed NOW when the rule is new to summary mode or its
|
|
29501
|
+
* schedule changed. `null` when arming could not be written — nothing is
|
|
29502
|
+
* sent on an unarmed rule.
|
|
29503
|
+
*/
|
|
29504
|
+
async armed(rule, schedule, nowMs) {
|
|
29505
|
+
const scheduleKey = canonicalHash(schedule);
|
|
29506
|
+
const held = this.deps.stamps.get(rule.id);
|
|
29507
|
+
if (held !== void 0 && held.scheduleKey === scheduleKey) return held;
|
|
29508
|
+
const armed = {
|
|
29509
|
+
ruleId: rule.id,
|
|
29510
|
+
armedAt: nowMs,
|
|
29511
|
+
lastWindowEnd: held?.lastWindowEnd ?? 0,
|
|
29512
|
+
scheduleKey
|
|
29513
|
+
};
|
|
29514
|
+
try {
|
|
29515
|
+
await this.deps.stamps.put(armed);
|
|
29516
|
+
} catch (err) {
|
|
29517
|
+
this.deps.logger.warn("state summary rule could not be armed — nothing is sent until it is", { meta: {
|
|
29518
|
+
ruleId: rule.id,
|
|
29519
|
+
error: err instanceof Error ? err.message : String(err)
|
|
29520
|
+
} });
|
|
29521
|
+
return null;
|
|
29522
|
+
}
|
|
29523
|
+
this.deps.logger.info("state summary rule armed — windows closing from now on are sent", { meta: {
|
|
29524
|
+
ruleId: rule.id,
|
|
29525
|
+
rule: rule.name,
|
|
29526
|
+
rearmed: held !== void 0
|
|
29527
|
+
} });
|
|
29528
|
+
return armed;
|
|
29529
|
+
}
|
|
29530
|
+
async stamp(held, windowEndMs) {
|
|
29531
|
+
try {
|
|
29532
|
+
await this.deps.stamps.put({
|
|
29533
|
+
...held,
|
|
29534
|
+
lastWindowEnd: Math.max(held.lastWindowEnd, windowEndMs)
|
|
29535
|
+
});
|
|
29536
|
+
} catch (err) {
|
|
29537
|
+
this.deps.logger.warn("state summary stamp did not land — the outbox dedup still holds", { meta: {
|
|
29538
|
+
ruleId: held.ruleId,
|
|
29539
|
+
windowEnd: windowEndMs,
|
|
29540
|
+
error: err instanceof Error ? err.message : String(err)
|
|
29541
|
+
} });
|
|
29542
|
+
}
|
|
29543
|
+
}
|
|
29544
|
+
};
|
|
29545
|
+
var SPEC = {
|
|
29546
|
+
collection: "notification-center:state-summary-stamps",
|
|
29547
|
+
columns: [
|
|
29548
|
+
{
|
|
29549
|
+
name: "ruleId",
|
|
29550
|
+
type: "TEXT",
|
|
29551
|
+
primaryKey: true,
|
|
29552
|
+
notNull: true
|
|
29553
|
+
},
|
|
29554
|
+
{
|
|
29555
|
+
name: "armedAt",
|
|
29556
|
+
type: "INTEGER",
|
|
29557
|
+
notNull: true
|
|
29558
|
+
},
|
|
29559
|
+
{
|
|
29560
|
+
name: "lastWindowEnd",
|
|
29561
|
+
type: "INTEGER",
|
|
29562
|
+
notNull: true
|
|
29563
|
+
},
|
|
29564
|
+
{
|
|
29565
|
+
name: "scheduleKey",
|
|
29566
|
+
type: "TEXT",
|
|
29567
|
+
notNull: true
|
|
29568
|
+
}
|
|
29569
|
+
],
|
|
29570
|
+
writeMode: "write-through",
|
|
29571
|
+
keyOf: (row) => row.ruleId,
|
|
29572
|
+
toValue: (row) => ({
|
|
29573
|
+
armedAt: row.armedAt,
|
|
29574
|
+
lastWindowEnd: row.lastWindowEnd,
|
|
29575
|
+
scheduleKey: row.scheduleKey
|
|
29576
|
+
}),
|
|
29577
|
+
fromRecord: (key, data) => {
|
|
29578
|
+
const armedAt = data["armedAt"];
|
|
29579
|
+
const lastWindowEnd = data["lastWindowEnd"];
|
|
29580
|
+
const scheduleKey = data["scheduleKey"];
|
|
29581
|
+
if (typeof armedAt !== "number" || typeof lastWindowEnd !== "number") return null;
|
|
29582
|
+
if (typeof scheduleKey !== "string") return null;
|
|
29583
|
+
return {
|
|
29584
|
+
ruleId: key,
|
|
29585
|
+
armedAt,
|
|
29586
|
+
lastWindowEnd,
|
|
29587
|
+
scheduleKey
|
|
29588
|
+
};
|
|
29589
|
+
},
|
|
29590
|
+
loadLimit: 1e4
|
|
29591
|
+
};
|
|
29592
|
+
var NcStateSummaryStampStore = class {
|
|
29593
|
+
ledger;
|
|
29594
|
+
constructor(deps) {
|
|
29595
|
+
this.ledger = new DurableLedger({
|
|
29596
|
+
spec: SPEC,
|
|
29597
|
+
store: deps.store,
|
|
29598
|
+
logger: deps.logger
|
|
29599
|
+
});
|
|
29600
|
+
}
|
|
29601
|
+
static declare(store) {
|
|
29602
|
+
return DurableLedger.declare(store, SPEC);
|
|
29603
|
+
}
|
|
29604
|
+
async load() {
|
|
29605
|
+
return (await this.ledger.load()).length;
|
|
29606
|
+
}
|
|
29607
|
+
port() {
|
|
29608
|
+
return {
|
|
29609
|
+
get: (ruleId) => this.ledger.get(ruleId),
|
|
29610
|
+
put: (row) => this.ledger.put(row),
|
|
29611
|
+
answered: () => this.ledger.hasAnswered()
|
|
29612
|
+
};
|
|
29613
|
+
}
|
|
29614
|
+
};
|
|
29615
|
+
//#endregion
|
|
29616
|
+
//#region src/notification-center/state-summary/system-state-ledger.ts
|
|
29617
|
+
/**
|
|
29618
|
+
* @durable class=ledger owner=notification-center
|
|
29619
|
+
* 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"
|
|
29620
|
+
* retention="none — a row lives exactly as long as its state is open. Bounded by the installation: streams, devices, nodes and installed packages."
|
|
29621
|
+
*/
|
|
29622
|
+
var NC_SYSTEM_STATE_COLLECTION = "notification-center:system-state";
|
|
29623
|
+
var NC_SYSTEM_STATE_COLUMNS = [
|
|
29624
|
+
(
|
|
29625
|
+
/** The state's subject (`stream:<id>`, `device-switch:<id>`,
|
|
29626
|
+
* `update:addon:<node>:<package>`, ...). */
|
|
29627
|
+
{
|
|
29628
|
+
name: "key",
|
|
29629
|
+
type: "TEXT",
|
|
29630
|
+
primaryKey: true,
|
|
29631
|
+
notNull: true
|
|
29632
|
+
}),
|
|
29633
|
+
(
|
|
29634
|
+
/** The OPENING kind. */
|
|
29635
|
+
{
|
|
29636
|
+
name: "kind",
|
|
29637
|
+
type: "TEXT",
|
|
29638
|
+
notNull: true
|
|
29639
|
+
}),
|
|
29640
|
+
{
|
|
29641
|
+
name: "since",
|
|
29642
|
+
type: "INTEGER",
|
|
29643
|
+
notNull: true
|
|
29644
|
+
},
|
|
29645
|
+
(
|
|
29646
|
+
/** Indexed: every question about a device is asked per device. */
|
|
29647
|
+
{
|
|
29648
|
+
name: "deviceId",
|
|
29649
|
+
type: "INTEGER"
|
|
29650
|
+
}),
|
|
29651
|
+
{
|
|
29652
|
+
name: "nodeId",
|
|
29653
|
+
type: "TEXT"
|
|
29654
|
+
},
|
|
29655
|
+
{
|
|
29656
|
+
name: "packageName",
|
|
29657
|
+
type: "TEXT"
|
|
29658
|
+
},
|
|
29659
|
+
(
|
|
29660
|
+
/** The words' facts (device name, profile, versions, addons). */
|
|
29661
|
+
{
|
|
29662
|
+
name: "params",
|
|
29663
|
+
type: "JSON",
|
|
29664
|
+
notNull: true
|
|
29665
|
+
})
|
|
29666
|
+
];
|
|
29667
|
+
var NC_SYSTEM_STATE_INDEXES = [{
|
|
29668
|
+
name: "idx_nc_system_state_device",
|
|
29669
|
+
columns: ["deviceId"]
|
|
29670
|
+
}];
|
|
29671
|
+
/** The kinds that OPEN a row. */
|
|
29672
|
+
var NC_SYSTEM_STATE_OPENING_KINDS = [
|
|
29673
|
+
"stream-offline",
|
|
29674
|
+
"device-disabled",
|
|
29675
|
+
"detection-blind",
|
|
29676
|
+
"node-inference-unavailable",
|
|
29677
|
+
"addon-crash-loop",
|
|
29678
|
+
"addon-update-available",
|
|
29679
|
+
"server-update-available",
|
|
29680
|
+
"wrapper-update-available"
|
|
29681
|
+
];
|
|
29682
|
+
function isOpeningKind(value) {
|
|
29683
|
+
return NC_SYSTEM_STATE_OPENING_KINDS.some((kind) => kind === value);
|
|
29684
|
+
}
|
|
29685
|
+
function stringRecord(value) {
|
|
29686
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
29687
|
+
const out = {};
|
|
29688
|
+
for (const [key, entry] of Object.entries(value)) if (typeof entry === "string") out[key] = entry;
|
|
29689
|
+
return out;
|
|
29690
|
+
}
|
|
29691
|
+
var NC_SYSTEM_STATE_SPEC = {
|
|
29692
|
+
collection: NC_SYSTEM_STATE_COLLECTION,
|
|
29693
|
+
columns: NC_SYSTEM_STATE_COLUMNS,
|
|
29694
|
+
indexes: NC_SYSTEM_STATE_INDEXES,
|
|
29695
|
+
writeMode: "write-behind",
|
|
29696
|
+
keyOf: (row) => row.key,
|
|
29697
|
+
toValue: (row) => ({
|
|
29698
|
+
kind: row.kind,
|
|
29699
|
+
since: row.since,
|
|
29700
|
+
...row.deviceId !== void 0 ? { deviceId: row.deviceId } : {},
|
|
29701
|
+
...row.nodeId !== void 0 ? { nodeId: row.nodeId } : {},
|
|
29702
|
+
...row.packageName !== void 0 ? { packageName: row.packageName } : {},
|
|
29703
|
+
params: {
|
|
29704
|
+
...row.params,
|
|
29705
|
+
...row.deviceType !== void 0 ? { deviceType: row.deviceType } : {}
|
|
29706
|
+
}
|
|
29707
|
+
}),
|
|
29708
|
+
fromRecord: (key, data) => {
|
|
29709
|
+
const kind = data["kind"];
|
|
29710
|
+
const since = data["since"];
|
|
29711
|
+
const params = stringRecord(data["params"]);
|
|
29712
|
+
if (!isOpeningKind(kind) || typeof since !== "number" || params === null) return null;
|
|
29713
|
+
const deviceId = data["deviceId"];
|
|
29714
|
+
const nodeId = data["nodeId"];
|
|
29715
|
+
const packageName = data["packageName"];
|
|
29716
|
+
const { deviceType, ...words } = params;
|
|
29717
|
+
return {
|
|
29718
|
+
key,
|
|
29719
|
+
kind,
|
|
29720
|
+
since,
|
|
29721
|
+
...typeof deviceId === "number" ? { deviceId } : {},
|
|
29722
|
+
...deviceType !== void 0 ? { deviceType } : {},
|
|
29723
|
+
...typeof nodeId === "string" ? { nodeId } : {},
|
|
29724
|
+
...typeof packageName === "string" ? { packageName } : {},
|
|
29725
|
+
params: words
|
|
29726
|
+
};
|
|
29727
|
+
},
|
|
29728
|
+
deviceIdOf: (row) => row.deviceId
|
|
29729
|
+
};
|
|
29730
|
+
function isUpdateTarget(value) {
|
|
29731
|
+
return value === "addon" || value === "server" || value === "wrapper";
|
|
29732
|
+
}
|
|
29733
|
+
function updateKind(target) {
|
|
29734
|
+
return target === "addon" ? "addon-update-available" : target === "server" ? "server-update-available" : "wrapper-update-available";
|
|
29735
|
+
}
|
|
29736
|
+
function updateStateKey(target, nodeId, packageName) {
|
|
29737
|
+
return `update:${target}:${nodeId ?? "hub"}:${packageName}`;
|
|
29738
|
+
}
|
|
29739
|
+
var NcSystemStateLedger = class {
|
|
29740
|
+
deps;
|
|
29741
|
+
constructor(deps) {
|
|
29742
|
+
this.deps = deps;
|
|
29743
|
+
}
|
|
29744
|
+
/** The open rows, or `null` while the store has never answered. */
|
|
29745
|
+
openRows() {
|
|
29746
|
+
return this.deps.rows.answered() ? this.deps.rows.all() : null;
|
|
29747
|
+
}
|
|
29748
|
+
/**
|
|
29749
|
+
* One system subject the intake accepted. The update-available kinds are
|
|
29750
|
+
* not read here: {@link observeUpdateList} replaces them from the raw event.
|
|
29751
|
+
*/
|
|
29752
|
+
observe(system, atMs) {
|
|
29753
|
+
switch (system.kind) {
|
|
29754
|
+
case "stream-offline":
|
|
29755
|
+
case "device-disabled":
|
|
29756
|
+
case "detection-blind":
|
|
29757
|
+
case "node-inference-unavailable":
|
|
29758
|
+
case "addon-crash-loop":
|
|
29759
|
+
this.open(system, atMs);
|
|
29760
|
+
return;
|
|
29761
|
+
case "stream-online":
|
|
29762
|
+
case "device-enabled":
|
|
29763
|
+
this.close(system.subject, system.kind);
|
|
29764
|
+
return;
|
|
29765
|
+
case "node-online":
|
|
29766
|
+
case "node-offline":
|
|
29767
|
+
if (system.nodeId !== void 0) this.closeNodeFaults(system.nodeId, system.kind);
|
|
29768
|
+
return;
|
|
29769
|
+
case "server-updated":
|
|
29770
|
+
if (system.nodeId !== void 0) this.closeServerUpdate(system.nodeId);
|
|
29771
|
+
return;
|
|
29772
|
+
default: return;
|
|
29773
|
+
}
|
|
29774
|
+
}
|
|
29775
|
+
/** A detection on a camera: it is not blind. Called for every detection — a Map lookup. */
|
|
29776
|
+
observeDetection(deviceId) {
|
|
29777
|
+
const key = `detection-blind:${String(deviceId)}`;
|
|
29778
|
+
if (this.deps.rows.get(key) === void 0) return;
|
|
29779
|
+
this.close(key, "detection");
|
|
29780
|
+
}
|
|
29781
|
+
/**
|
|
29782
|
+
* Any `stream.online`, whatever its reason (D647 review I1). The intake gate
|
|
29783
|
+
* drops a `first-packet` online — a recreated broker's — because it is not
|
|
29784
|
+
* a recovery worth a notification; it is still proof the stream is up.
|
|
29785
|
+
*/
|
|
29786
|
+
observeStreamOnline(camStreamId) {
|
|
29787
|
+
this.close(`stream:${camStreamId}`, "stream-online");
|
|
29788
|
+
}
|
|
29789
|
+
/** One `update.available` list: REPLACE the rows of every node it names. */
|
|
29790
|
+
observeUpdateList(list, atMs) {
|
|
29791
|
+
const keep = new Set(list.rows.map((pkg) => updateStateKey(list.target, pkg.nodeId, pkg.packageName)));
|
|
29792
|
+
for (const nodeId of list.nodeIds) {
|
|
29793
|
+
const prefix = `update:${list.target}:${nodeId}:`;
|
|
29794
|
+
for (const row of this.deps.rows.all()) if (row.key.startsWith(prefix) && !keep.has(row.key)) this.close(row.key, "update-list");
|
|
29795
|
+
}
|
|
29796
|
+
for (const pkg of list.rows) this.openUpdate(list.target, pkg, atMs);
|
|
29797
|
+
}
|
|
29798
|
+
/**
|
|
29799
|
+
* An addon STARTED on `nodeId` (D647 review I5): the runner hosting it is
|
|
29800
|
+
* up again, so a crash-loop row naming it on THAT node is over — and only
|
|
29801
|
+
* on that node.
|
|
29802
|
+
*/
|
|
29803
|
+
observeAddonStarted(addonId, nodeId) {
|
|
29804
|
+
for (const row of this.deps.rows.all()) {
|
|
29805
|
+
if (row.kind !== "addon-crash-loop") continue;
|
|
29806
|
+
if (row.nodeId !== nodeId) continue;
|
|
29807
|
+
if ((row.params["addons"] ?? "").split(",").map((id) => id.trim()).includes(addonId)) this.close(row.key, "addon-started");
|
|
29808
|
+
}
|
|
29809
|
+
}
|
|
29810
|
+
/**
|
|
29811
|
+
* The balancer placed a camera on a node (not pinned): placement skips a
|
|
29812
|
+
* node that cannot infer, so the node can infer again (D647 review I5).
|
|
29813
|
+
*/
|
|
29814
|
+
observeCameraPlaced(nodeId) {
|
|
29815
|
+
this.close(`node-inference:${nodeId}`, "camera-placed");
|
|
29816
|
+
}
|
|
29817
|
+
/** `addon.updated` — the package is current on that node. */
|
|
29818
|
+
observeAddonUpdated(update) {
|
|
29819
|
+
this.close(updateStateKey("addon", update.nodeId, update.packageName), "addon-updated");
|
|
29820
|
+
}
|
|
29821
|
+
/** Every device that holds an open state — the deleted-device reconcile reads it. */
|
|
29822
|
+
deviceIds() {
|
|
29823
|
+
const ids = /* @__PURE__ */ new Set();
|
|
29824
|
+
for (const row of this.deps.rows.all()) if (row.deviceId !== void 0) ids.add(row.deviceId);
|
|
29825
|
+
return [...ids];
|
|
29826
|
+
}
|
|
29827
|
+
/** A deleted device leaves (D633). Returns how many rows went. */
|
|
29828
|
+
forgetDevice(deviceId) {
|
|
29829
|
+
const keys = this.deps.rows.all().filter((row) => row.deviceId === deviceId).map((row) => row.key);
|
|
29830
|
+
for (const key of keys) this.deps.rows.remove(key);
|
|
29831
|
+
return keys.length;
|
|
29832
|
+
}
|
|
29833
|
+
open(system, atMs) {
|
|
29834
|
+
const held = this.deps.rows.get(system.subject);
|
|
29835
|
+
const since = held !== void 0 && held.kind === system.kind ? held.since : atMs;
|
|
29836
|
+
this.deps.rows.put({
|
|
29837
|
+
key: system.subject,
|
|
29838
|
+
kind: system.kind,
|
|
29839
|
+
since,
|
|
29840
|
+
...system.deviceId !== void 0 ? { deviceId: system.deviceId } : {},
|
|
29841
|
+
...system.deviceType !== void 0 ? { deviceType: system.deviceType } : {},
|
|
29842
|
+
...system.nodeId !== void 0 ? { nodeId: system.nodeId } : {},
|
|
29843
|
+
params: system.textParams ?? {}
|
|
29844
|
+
});
|
|
29845
|
+
}
|
|
29846
|
+
openUpdate(target, pkg, atMs) {
|
|
29847
|
+
const key = updateStateKey(target, pkg.nodeId, pkg.packageName);
|
|
29848
|
+
const held = this.deps.rows.get(key);
|
|
29849
|
+
const kind = updateKind(target);
|
|
29850
|
+
this.deps.rows.put({
|
|
29851
|
+
key,
|
|
29852
|
+
kind,
|
|
29853
|
+
since: held?.since ?? atMs,
|
|
29854
|
+
...pkg.nodeId !== void 0 ? { nodeId: pkg.nodeId } : {},
|
|
29855
|
+
packageName: pkg.packageName,
|
|
29856
|
+
params: {
|
|
29857
|
+
packageName: pkg.packageName,
|
|
29858
|
+
currentVersion: pkg.currentVersion,
|
|
29859
|
+
latestVersion: pkg.latestVersion,
|
|
29860
|
+
...pkg.nodeId !== void 0 ? { nodeId: pkg.nodeId } : {}
|
|
29861
|
+
}
|
|
29862
|
+
});
|
|
29863
|
+
}
|
|
29864
|
+
close(key, by) {
|
|
29865
|
+
const held = this.deps.rows.get(key);
|
|
29866
|
+
if (held === void 0) return;
|
|
29867
|
+
this.deps.rows.remove(key);
|
|
29868
|
+
this.deps.logger?.debug("system state closed", {
|
|
29869
|
+
...held.deviceId !== void 0 ? { tags: { deviceId: held.deviceId } } : {},
|
|
29870
|
+
meta: {
|
|
29871
|
+
key,
|
|
29872
|
+
kind: held.kind,
|
|
29873
|
+
by,
|
|
29874
|
+
since: held.since
|
|
29875
|
+
}
|
|
29876
|
+
});
|
|
29877
|
+
}
|
|
29878
|
+
closeNodeFaults(nodeId, by) {
|
|
29879
|
+
for (const row of this.deps.rows.all()) {
|
|
29880
|
+
if (row.nodeId !== nodeId) continue;
|
|
29881
|
+
if (row.kind === "node-inference-unavailable" || row.kind === "addon-crash-loop") this.close(row.key, by);
|
|
29882
|
+
}
|
|
29883
|
+
}
|
|
29884
|
+
closeServerUpdate(nodeId) {
|
|
29885
|
+
for (const row of this.deps.rows.all()) {
|
|
29886
|
+
if ((row.nodeId ?? "hub") !== nodeId) continue;
|
|
29887
|
+
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");
|
|
29888
|
+
}
|
|
29889
|
+
}
|
|
29890
|
+
};
|
|
29891
|
+
/** The durable home of the rows. */
|
|
29892
|
+
var NcSystemStateStore = class {
|
|
29893
|
+
ledger;
|
|
29894
|
+
constructor(deps) {
|
|
29895
|
+
this.ledger = new DurableLedger({
|
|
29896
|
+
spec: NC_SYSTEM_STATE_SPEC,
|
|
29897
|
+
store: deps.store,
|
|
29898
|
+
logger: deps.logger
|
|
29899
|
+
});
|
|
29900
|
+
}
|
|
29901
|
+
static declare(store) {
|
|
29902
|
+
return DurableLedger.declare(store, NC_SYSTEM_STATE_SPEC);
|
|
29903
|
+
}
|
|
29904
|
+
/** Boot reseed. A failed load keeps the mirror and leaves `answered` false. */
|
|
29905
|
+
async load() {
|
|
29906
|
+
return (await this.ledger.load()).length;
|
|
29907
|
+
}
|
|
29908
|
+
port() {
|
|
29909
|
+
return {
|
|
29910
|
+
get: (key) => this.ledger.get(key),
|
|
29911
|
+
put: (row) => {
|
|
29912
|
+
this.ledger.put(row).catch(() => void 0);
|
|
29913
|
+
},
|
|
29914
|
+
remove: (key) => {
|
|
29915
|
+
this.ledger.forget(key);
|
|
29916
|
+
},
|
|
29917
|
+
all: () => this.ledger.snapshot(),
|
|
29918
|
+
answered: () => this.ledger.hasAnswered()
|
|
29919
|
+
};
|
|
29920
|
+
}
|
|
29921
|
+
};
|
|
29922
|
+
//#endregion
|
|
29923
|
+
//#region src/notification-center/state-summary/state-summary-service.ts
|
|
29924
|
+
function stringOf(data, key) {
|
|
29925
|
+
const value = data[key];
|
|
29926
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
29927
|
+
}
|
|
29928
|
+
/**
|
|
29929
|
+
* The full list an `update.available` event carries, and the nodes it speaks
|
|
29930
|
+
* for: every node a row names, plus the event's own `nodeId`. The emitter
|
|
29931
|
+
* never sends an empty list (it emits only when a `latestVersion` changes), so
|
|
29932
|
+
* the empty case is defensive, not a path that clears anything today — see
|
|
29933
|
+
* {@link NcUpdateList} for the limit that leaves.
|
|
29934
|
+
*/
|
|
29935
|
+
function updateListOf(data) {
|
|
29936
|
+
const target = data["target"];
|
|
29937
|
+
if (!isUpdateTarget(target)) return null;
|
|
29938
|
+
const rows = updatePackages(data);
|
|
29939
|
+
const eventNode = stringOf(data, "nodeId");
|
|
29940
|
+
const nodes = new Set(rows.map((row) => row.nodeId ?? eventNode ?? "hub"));
|
|
29941
|
+
if (eventNode !== void 0) nodes.add(eventNode);
|
|
29942
|
+
const withNode = rows.map((row) => row.nodeId === void 0 && eventNode !== void 0 ? {
|
|
29943
|
+
...row,
|
|
29944
|
+
nodeId: eventNode
|
|
29945
|
+
} : row);
|
|
29946
|
+
return {
|
|
29947
|
+
target,
|
|
29948
|
+
nodeIds: [...nodes],
|
|
29949
|
+
rows: withNode
|
|
29950
|
+
};
|
|
29951
|
+
}
|
|
29952
|
+
var NcStateSummaryService = class {
|
|
29953
|
+
deps;
|
|
29954
|
+
stateStore;
|
|
29955
|
+
stampStore;
|
|
29956
|
+
ledger;
|
|
29957
|
+
scheduler;
|
|
29958
|
+
constructor(deps) {
|
|
29959
|
+
this.deps = deps;
|
|
29960
|
+
this.stateStore = new NcSystemStateStore({
|
|
29961
|
+
store: deps.store,
|
|
29962
|
+
logger: deps.logger
|
|
29963
|
+
});
|
|
29964
|
+
this.stampStore = new NcStateSummaryStampStore({
|
|
29965
|
+
store: deps.store,
|
|
29966
|
+
logger: deps.logger
|
|
29967
|
+
});
|
|
29968
|
+
this.ledger = new NcSystemStateLedger({
|
|
29969
|
+
rows: this.stateStore.port(),
|
|
29970
|
+
logger: deps.logger
|
|
29971
|
+
});
|
|
29972
|
+
this.scheduler = new NcStateSummaryScheduler({
|
|
29973
|
+
rules: deps.rules,
|
|
29974
|
+
stamps: this.stampStore.port(),
|
|
29975
|
+
collect: (rule, nowMs) => this.collect(rule, nowMs),
|
|
29976
|
+
deliver: (rule, summary, window) => this.deliver(rule, summary, window),
|
|
29977
|
+
now: deps.now,
|
|
29978
|
+
logger: deps.logger
|
|
29979
|
+
});
|
|
29980
|
+
}
|
|
29981
|
+
static async declare(store) {
|
|
29982
|
+
await NcSystemStateStore.declare(store);
|
|
29983
|
+
await NcStateSummaryStampStore.declare(store);
|
|
29984
|
+
}
|
|
29985
|
+
/** Boot back-fill. Each load that fails leaves its mirror unanswered — the summary then says so. */
|
|
29986
|
+
async load() {
|
|
29987
|
+
return {
|
|
29988
|
+
states: await this.stateStore.load(),
|
|
29989
|
+
stamps: await this.stampStore.load()
|
|
29990
|
+
};
|
|
29991
|
+
}
|
|
29992
|
+
/** One system subject the intake accepted. */
|
|
29993
|
+
observeSystem(system, atMs) {
|
|
29994
|
+
this.ledger.observe(system, atMs);
|
|
29995
|
+
}
|
|
29996
|
+
/**
|
|
29997
|
+
* The RAW bus event, before the intake's gates (D647 review I1/I3/I5): the
|
|
29998
|
+
* facts that close or replace a state even when no notification is owed —
|
|
29999
|
+
* any `stream.online`, the full `update.available` list of a node, an addon
|
|
30000
|
+
* that started, a camera the balancer placed.
|
|
30001
|
+
*/
|
|
30002
|
+
observeRawEvent(category, data, atMs, sourceNodeId) {
|
|
30003
|
+
switch (category) {
|
|
30004
|
+
case "stream.online": {
|
|
30005
|
+
const camStreamId = stringOf(data, "camStreamId");
|
|
30006
|
+
if (camStreamId !== void 0) this.ledger.observeStreamOnline(camStreamId);
|
|
30007
|
+
return;
|
|
30008
|
+
}
|
|
30009
|
+
case "update.available": {
|
|
30010
|
+
const list = updateListOf(data);
|
|
30011
|
+
if (list !== null) this.ledger.observeUpdateList(list, atMs);
|
|
30012
|
+
return;
|
|
30013
|
+
}
|
|
30014
|
+
case "addon.started":
|
|
30015
|
+
case "addon.restarted": {
|
|
30016
|
+
const addonId = stringOf(data, "addonId");
|
|
30017
|
+
if (addonId !== void 0) this.ledger.observeAddonStarted(addonId, sourceNodeId ?? "hub");
|
|
30018
|
+
return;
|
|
30019
|
+
}
|
|
30020
|
+
case "pipeline.camera-assigned": {
|
|
30021
|
+
const nodeId = stringOf(data, "agentNodeId");
|
|
30022
|
+
const placed = data["pinned"] === false && data["reason"] !== "unassigned";
|
|
30023
|
+
if (nodeId !== void 0 && placed) this.ledger.observeCameraPlaced(nodeId);
|
|
30024
|
+
return;
|
|
30025
|
+
}
|
|
30026
|
+
default: return;
|
|
30027
|
+
}
|
|
30028
|
+
}
|
|
30029
|
+
observeDetection(deviceId) {
|
|
30030
|
+
this.ledger.observeDetection(deviceId);
|
|
30031
|
+
}
|
|
30032
|
+
observeAddonUpdated(update) {
|
|
30033
|
+
this.ledger.observeAddonUpdated(update);
|
|
30034
|
+
}
|
|
30035
|
+
tick() {
|
|
30036
|
+
return this.scheduler.tick();
|
|
30037
|
+
}
|
|
30038
|
+
/** The summary this rule would send NOW (`previewStateSummary`) — sends nothing, stamps nothing. */
|
|
30039
|
+
async preview(rule) {
|
|
30040
|
+
const nowMs = this.deps.now();
|
|
30041
|
+
const summary = await this.collect(rule, nowMs);
|
|
30042
|
+
const text = this.word(summary, nowMs);
|
|
30043
|
+
return {
|
|
30044
|
+
empty: summary.itemCount === 0 && summary.unavailableCount === 0,
|
|
30045
|
+
count: text.count,
|
|
30046
|
+
text: text.text,
|
|
30047
|
+
sections: summary.sections.map((section) => ({
|
|
30048
|
+
id: section.id,
|
|
30049
|
+
status: section.status,
|
|
30050
|
+
items: section.items.length,
|
|
30051
|
+
...section.reason !== void 0 ? { reason: section.reason } : {}
|
|
30052
|
+
})),
|
|
30053
|
+
dropped: { ...summary.dropped },
|
|
30054
|
+
excludedGroups: [...summary.excludedGroups]
|
|
30055
|
+
};
|
|
30056
|
+
}
|
|
30057
|
+
async collect(rule, nowMs) {
|
|
30058
|
+
const nodes = await this.nodeView();
|
|
30059
|
+
return collectStateSummary({
|
|
30060
|
+
rule,
|
|
30061
|
+
nowMs,
|
|
30062
|
+
isMuted: this.deps.isMuted,
|
|
30063
|
+
sources: stateSources({
|
|
30064
|
+
alerts: this.deps.alerts,
|
|
30065
|
+
liveness: this.deps.liveness,
|
|
30066
|
+
state: () => this.ledger.openRows(),
|
|
30067
|
+
lookup: this.deps.lookup,
|
|
30068
|
+
devicesAnswered: this.deps.devicesAnswered,
|
|
30069
|
+
nodes,
|
|
30070
|
+
livenessHoldoffMs: this.deps.livenessHoldoffMs
|
|
30071
|
+
})
|
|
30072
|
+
});
|
|
30073
|
+
}
|
|
30074
|
+
/** The node directory for one summary, or `null` (logged) when it did not answer. */
|
|
30075
|
+
async nodeView() {
|
|
30076
|
+
const read = this.deps.readNodes;
|
|
30077
|
+
if (read === void 0) return null;
|
|
30078
|
+
try {
|
|
30079
|
+
const answer = await read();
|
|
30080
|
+
const online = new Map(answer.nodes.map((node) => [node.id, node.isOnline]));
|
|
30081
|
+
return {
|
|
30082
|
+
complete: answer.complete,
|
|
30083
|
+
online: (nodeId) => online.get(nodeId)
|
|
30084
|
+
};
|
|
30085
|
+
} catch (err) {
|
|
30086
|
+
this.deps.logger.warn("state summary: the node directory did not answer — node sections unavailable", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
30087
|
+
return null;
|
|
30088
|
+
}
|
|
30089
|
+
}
|
|
30090
|
+
word(summary, nowMs) {
|
|
30091
|
+
return stateSummaryText({
|
|
30092
|
+
texts: this.deps.texts(),
|
|
30093
|
+
summary,
|
|
30094
|
+
nowMs
|
|
30095
|
+
});
|
|
30096
|
+
}
|
|
30097
|
+
async deliver(rule, summary, window) {
|
|
30098
|
+
const subject = stateSummarySubject({
|
|
30099
|
+
rule,
|
|
30100
|
+
summary,
|
|
30101
|
+
text: this.word(summary, window.endMs),
|
|
30102
|
+
window
|
|
30103
|
+
});
|
|
30104
|
+
if (subject === null) {
|
|
30105
|
+
this.deps.logger.warn("state summary rule has no system-event condition — not sent", { meta: { ruleId: rule.id } });
|
|
30106
|
+
return 0;
|
|
30107
|
+
}
|
|
30108
|
+
return this.deps.enqueue(rule, subject);
|
|
30109
|
+
}
|
|
30110
|
+
};
|
|
30111
|
+
//#endregion
|
|
28415
30112
|
//#region src/notification-center/zone-owner-cache.ts
|
|
28416
30113
|
var NcZoneOwnershipCache = class {
|
|
28417
30114
|
deps;
|
|
@@ -28969,6 +30666,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
28969
30666
|
alertStore;
|
|
28970
30667
|
/** Battery band → notification, and the reminder sweep. */
|
|
28971
30668
|
deviceAlerts;
|
|
30669
|
+
/** The scheduled STATE SUMMARY of system rules (D647): its ledgers and its tick. */
|
|
30670
|
+
stateSummary;
|
|
28972
30671
|
/** States the `deviceState` rule gate reads. Refreshed on the reload tick;
|
|
28973
30672
|
* read with no I/O on the persist hook. */
|
|
28974
30673
|
deviceStates;
|
|
@@ -29862,6 +31561,21 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29862
31561
|
logger: this.logger.child("device-alerts"),
|
|
29863
31562
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
29864
31563
|
});
|
|
31564
|
+
this.stateSummary = new NcStateSummaryService({
|
|
31565
|
+
store: deps.store,
|
|
31566
|
+
logger: this.logger.child("state-summary"),
|
|
31567
|
+
now: () => this.now(),
|
|
31568
|
+
rules: () => this.rules.listEnabled("system-event"),
|
|
31569
|
+
alerts: () => this.deviceAlerts.openEpisodes(),
|
|
31570
|
+
liveness: () => this.liveness.hasAnswered() ? this.liveness.snapshot() : null,
|
|
31571
|
+
lookup: (deviceId) => this.deviceDirectory.get(deviceId),
|
|
31572
|
+
devicesAnswered: () => this.deviceDirectory.isWarm(),
|
|
31573
|
+
...deps.readNodeDirectory !== void 0 ? { readNodes: deps.readNodeDirectory } : {},
|
|
31574
|
+
livenessHoldoffMs: deps.livenessHoldoffMs ?? 3e5,
|
|
31575
|
+
isMuted: (deviceId) => this.deviceMutes.isMuted(deviceId),
|
|
31576
|
+
texts: () => this.texts,
|
|
31577
|
+
enqueue: (rule, subject) => this.enqueueStateSummary(rule, subject)
|
|
31578
|
+
});
|
|
29865
31579
|
this.confirmGate = deps.generateVision !== void 0 ? new NcConfirmGate({
|
|
29866
31580
|
logger: this.logger.child("confirm"),
|
|
29867
31581
|
generateVision: deps.generateVision,
|
|
@@ -29959,7 +31673,10 @@ var NotificationCenter = class NotificationCenter {
|
|
|
29959
31673
|
summary: this.summaryRules,
|
|
29960
31674
|
mutes: this.deviceMutes,
|
|
29961
31675
|
alarm: () => this.alarmPanel,
|
|
29962
|
-
alerts:
|
|
31676
|
+
alerts: {
|
|
31677
|
+
deviceIds: () => [...new Set([...this.deviceAlerts.deviceIds(), ...this.stateSummary.ledger.deviceIds()])],
|
|
31678
|
+
forget: async (deviceId) => await this.deviceAlerts.forget(deviceId) + this.stateSummary.ledger.forgetDevice(deviceId)
|
|
31679
|
+
},
|
|
29963
31680
|
directory: this.deviceDirectory,
|
|
29964
31681
|
isDeviceDeleted: deps.isDeviceDeleted,
|
|
29965
31682
|
targetCatalog: () => this.dispatcher.catalogSnapshot()
|
|
@@ -30070,6 +31787,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30070
31787
|
await OccupancyStore.declare(store);
|
|
30071
31788
|
await NcLivenessLedger.declare(store);
|
|
30072
31789
|
await NcAlertStore.declare(store);
|
|
31790
|
+
await NcStateSummaryService.declare(store);
|
|
30073
31791
|
await NcSnoozeStore.declare(store);
|
|
30074
31792
|
await NcDeviceMuteStore.declare(store);
|
|
30075
31793
|
await TimelapseStore.declare(store);
|
|
@@ -30263,6 +31981,10 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30263
31981
|
const rows = await this.alertStore.load();
|
|
30264
31982
|
this.logger.info("device alert ledger reseeded", { meta: { rows } });
|
|
30265
31983
|
})) return;
|
|
31984
|
+
if (!await phase("stateSummary", async () => {
|
|
31985
|
+
const loaded = await this.stateSummary.load();
|
|
31986
|
+
this.logger.info("state summary ledgers reseeded", { meta: loaded });
|
|
31987
|
+
})) return;
|
|
30266
31988
|
if (!await phase("occupancy", () => this.hydrateOccupancy())) return;
|
|
30267
31989
|
if (!await phase("outbox", () => this.outbox.load())) return;
|
|
30268
31990
|
if (!await phase("cooldowns", () => this.seedCooldowns())) return;
|
|
@@ -30568,6 +32290,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30568
32290
|
atMs: event.timestamp,
|
|
30569
32291
|
...event.trackId !== void 0 ? { trackId: event.trackId } : {}
|
|
30570
32292
|
});
|
|
32293
|
+
if (this.evaluationActive) this.stateSummary.observeDetection(event.deviceId);
|
|
30571
32294
|
this.consumeEvent(incomingFromObjectEvent(event));
|
|
30572
32295
|
}
|
|
30573
32296
|
/**
|
|
@@ -30951,9 +32674,15 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30951
32674
|
if (!this.evaluationActive) return;
|
|
30952
32675
|
if (event.category === "addon.updated") {
|
|
30953
32676
|
const update = parseAddonUpdateComplete(event.data);
|
|
30954
|
-
if (update !== null)
|
|
32677
|
+
if (update !== null) {
|
|
32678
|
+
this.addonUpdates.observe(update);
|
|
32679
|
+
this.stateSummary.observeAddonUpdated(update);
|
|
32680
|
+
}
|
|
30955
32681
|
return;
|
|
30956
32682
|
}
|
|
32683
|
+
this.stateSummary.observeRawEvent(event.category, event.data, this.now(), event.source.nodeId);
|
|
32684
|
+
if (event.category === "addon.started" || event.category === "addon.restarted") return;
|
|
32685
|
+
if (event.category === "pipeline.camera-assigned") return;
|
|
30957
32686
|
const battery = batterySliceOf(event);
|
|
30958
32687
|
if (battery !== null) {
|
|
30959
32688
|
this.deviceAlerts.observeBattery(battery.deviceId, battery.reading);
|
|
@@ -30983,6 +32712,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
30983
32712
|
return;
|
|
30984
32713
|
}
|
|
30985
32714
|
const system = incoming.subject.systemEvent;
|
|
32715
|
+
if (system !== void 0) this.stateSummary.observeSystem(system, this.now());
|
|
30986
32716
|
const observation = system !== void 0 ? ledgeredLiveness(system) : null;
|
|
30987
32717
|
if (observation !== null) {
|
|
30988
32718
|
const previous = this.liveness.stateOf(observation.subject);
|
|
@@ -31037,6 +32767,10 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31037
32767
|
logger: this.logger,
|
|
31038
32768
|
by: caller.userId
|
|
31039
32769
|
});
|
|
32770
|
+
guardRuleSystemEventDelivery(rule, {
|
|
32771
|
+
logger: this.logger,
|
|
32772
|
+
by: caller.userId
|
|
32773
|
+
});
|
|
31040
32774
|
await this.validateTargetRefs(rule.targets.map((t) => t.targetId));
|
|
31041
32775
|
const created = await this.rules.create(rule, caller.userId);
|
|
31042
32776
|
this.logger.info("notification rule created", { meta: {
|
|
@@ -31065,6 +32799,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31065
32799
|
by: caller.userId,
|
|
31066
32800
|
ruleId
|
|
31067
32801
|
});
|
|
32802
|
+
guardPatchSystemEventDelivery(existing, patch, clear ?? [], {
|
|
32803
|
+
logger: this.logger,
|
|
32804
|
+
by: caller.userId,
|
|
32805
|
+
ruleId
|
|
32806
|
+
});
|
|
31068
32807
|
const { disabledTargetIds: _optOut, ...safePatch } = patch;
|
|
31069
32808
|
const updated = await this.rules.update(ruleId, safePatch, clear ?? []);
|
|
31070
32809
|
this.logger.info("notification rule updated", { meta: {
|
|
@@ -31083,6 +32822,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31083
32822
|
return { success: true };
|
|
31084
32823
|
},
|
|
31085
32824
|
testRule: async ({ rule, lookbackMinutes }) => ({ results: [...await this.dryRun(rule, lookbackMinutes)] }),
|
|
32825
|
+
previewStateSummary: async ({ rule }) => this.stateSummary.preview(rule),
|
|
31086
32826
|
getConditionCatalog: async () => ({
|
|
31087
32827
|
catalog: [...NC_CONDITION_CATALOG],
|
|
31088
32828
|
taxonomy: NC_TAXONOMY
|
|
@@ -31964,6 +33704,29 @@ var NotificationCenter = class NotificationCenter {
|
|
|
31964
33704
|
});
|
|
31965
33705
|
continue;
|
|
31966
33706
|
}
|
|
33707
|
+
if (isSystemEventSummaryRule(rule)) {
|
|
33708
|
+
this.logger.debug("rule matched — held for its scheduled state summary", {
|
|
33709
|
+
tags: { deviceId: subject.deviceId },
|
|
33710
|
+
meta: {
|
|
33711
|
+
ruleId: rule.id,
|
|
33712
|
+
rule: rule.name,
|
|
33713
|
+
kind: subject.systemEvent?.kind,
|
|
33714
|
+
eventId: subject.recordId
|
|
33715
|
+
}
|
|
33716
|
+
});
|
|
33717
|
+
outcomes.push({
|
|
33718
|
+
ruleId: rule.id,
|
|
33719
|
+
ruleName: rule.name,
|
|
33720
|
+
matched: true,
|
|
33721
|
+
...evaluation.matchedOn !== void 0 ? { matchedOn: [...evaluation.matchedOn] } : {},
|
|
33722
|
+
cooldown: "n/a",
|
|
33723
|
+
enqueued: 0,
|
|
33724
|
+
targetIds: [],
|
|
33725
|
+
heldForSummary: true
|
|
33726
|
+
});
|
|
33727
|
+
anyMatched = true;
|
|
33728
|
+
continue;
|
|
33729
|
+
}
|
|
31967
33730
|
this.sceneGateReported.delete(rule.id);
|
|
31968
33731
|
this.runRuleActions(rule, subject, kind, controls);
|
|
31969
33732
|
const admission = this.admitToGroup(rule, subject, now);
|
|
@@ -32155,6 +33918,31 @@ var NotificationCenter = class NotificationCenter {
|
|
|
32155
33918
|
return report({ candidates: candidates.length });
|
|
32156
33919
|
}
|
|
32157
33920
|
/**
|
|
33921
|
+
* Enqueue ONE state summary (D647) through the rule's ordinary rows: its
|
|
33922
|
+
* targets, its users (admins only — the row files under the system device),
|
|
33923
|
+
* its template, and the drain's snooze gate and retries. The record id is
|
|
33924
|
+
* the dedup key, so a window is enqueued once whatever the restarts.
|
|
33925
|
+
*/
|
|
33926
|
+
async enqueueStateSummary(rule, subject) {
|
|
33927
|
+
const appended = withSummaryBody(rule);
|
|
33928
|
+
if (appended !== null) this.logger.warn("state summary rule body names no summary content — {{summaryText}} appended", { meta: {
|
|
33929
|
+
ruleId: rule.id,
|
|
33930
|
+
rule: rule.name
|
|
33931
|
+
} });
|
|
33932
|
+
const sent = appended ?? rule;
|
|
33933
|
+
const userTargets = await this.resolveUserTargets(sent, subject.deviceId);
|
|
33934
|
+
const entries = this.buildEntries(sent, subject, "system-event", ["systemEvent"], userTargets, "system");
|
|
33935
|
+
if (entries.length === 0) {
|
|
33936
|
+
this.logger.warn("state summary reached NO target — its users are not admins, and it has no targets", { meta: {
|
|
33937
|
+
ruleId: rule.id,
|
|
33938
|
+
rule: rule.name,
|
|
33939
|
+
targetUsers: rule.targetUsers ?? []
|
|
33940
|
+
} });
|
|
33941
|
+
return 0;
|
|
33942
|
+
}
|
|
33943
|
+
return this.outbox.enqueue(entries);
|
|
33944
|
+
}
|
|
33945
|
+
/**
|
|
32158
33946
|
* The rules a trigger may fire, which is `listEnabled(delivery)` plus and
|
|
32159
33947
|
* minus exactly one thing: `waitForEnhancement`.
|
|
32160
33948
|
*
|
|
@@ -32897,6 +34685,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
32897
34685
|
async reloadRules() {
|
|
32898
34686
|
await this.rules.load();
|
|
32899
34687
|
if (this.evaluationActive) this.deviceAlerts.tick();
|
|
34688
|
+
if (this.evaluationActive) this.stateSummary.tick().catch((err) => {
|
|
34689
|
+
this.logger.warn("state summary tick failed", { meta: { error: String(err) } });
|
|
34690
|
+
});
|
|
32900
34691
|
await this.timelapseRules.load();
|
|
32901
34692
|
await this.summaryRules.load();
|
|
32902
34693
|
await this.ruleAi.load();
|
|
@@ -47254,6 +49045,16 @@ var FrameProcessor = class {
|
|
|
47254
49045
|
};
|
|
47255
49046
|
}
|
|
47256
49047
|
};
|
|
49048
|
+
//#endregion
|
|
49049
|
+
//#region src/pipeline-analytics/pipeline/track-provenance.ts
|
|
49050
|
+
function trackProvenanceOf(frame) {
|
|
49051
|
+
if (!(frame.width > 0 && frame.height > 0)) return void 0;
|
|
49052
|
+
return {
|
|
49053
|
+
...frame.rootModelId !== void 0 && frame.rootModelId.length > 0 ? { modelId: frame.rootModelId } : {},
|
|
49054
|
+
frameWidth: frame.width,
|
|
49055
|
+
frameHeight: frame.height
|
|
49056
|
+
};
|
|
49057
|
+
}
|
|
47257
49058
|
/** Bounded trackId → pending-upgrade map. Insertion-ordered, so the cap
|
|
47258
49059
|
* evicts the oldest record. */
|
|
47259
49060
|
var PendingPreviewUpgrades = class {
|
|
@@ -50075,51 +51876,6 @@ var PlateGalleryProvider = class {
|
|
|
50075
51876
|
}
|
|
50076
51877
|
};
|
|
50077
51878
|
//#endregion
|
|
50078
|
-
//#region src/pipeline-analytics/replay/frame-processor-pass.ts
|
|
50079
|
-
/**
|
|
50080
|
-
* Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
|
|
50081
|
-
* return the tracks it produced.
|
|
50082
|
-
*
|
|
50083
|
-
* No `TrackStore`, no bus, no media — a track here is a summary built
|
|
50084
|
-
* ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
|
|
50085
|
-
*/
|
|
50086
|
-
function runFrameProcessorPass(input) {
|
|
50087
|
-
const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
|
|
50088
|
-
if (input.zones !== void 0) processor.setZones(input.zones);
|
|
50089
|
-
if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
|
|
50090
|
-
if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
|
|
50091
|
-
const accum = /* @__PURE__ */ new Map();
|
|
50092
|
-
for (const { timestamp, frame } of input.frames) {
|
|
50093
|
-
const result = processor.process({
|
|
50094
|
-
timestamp,
|
|
50095
|
-
frame
|
|
50096
|
-
});
|
|
50097
|
-
for (const td of result.tracked) {
|
|
50098
|
-
const existing = accum.get(td.trackId);
|
|
50099
|
-
const matched = td.matchedThisFrame ? 1 : 0;
|
|
50100
|
-
if (existing === void 0) accum.set(td.trackId, {
|
|
50101
|
-
className: td.className,
|
|
50102
|
-
firstSeenMs: timestamp,
|
|
50103
|
-
lastSeenMs: timestamp,
|
|
50104
|
-
bbox: { ...td.bbox },
|
|
50105
|
-
framesMatched: matched
|
|
50106
|
-
});
|
|
50107
|
-
else {
|
|
50108
|
-
existing.className = td.className;
|
|
50109
|
-
existing.lastSeenMs = timestamp;
|
|
50110
|
-
existing.framesMatched += matched;
|
|
50111
|
-
}
|
|
50112
|
-
}
|
|
50113
|
-
}
|
|
50114
|
-
return { tracks: [...accum.values()].map((t) => ({
|
|
50115
|
-
className: t.className,
|
|
50116
|
-
firstSeenMs: t.firstSeenMs,
|
|
50117
|
-
lastSeenMs: t.lastSeenMs,
|
|
50118
|
-
bbox: t.bbox,
|
|
50119
|
-
framesMatched: t.framesMatched
|
|
50120
|
-
})) };
|
|
50121
|
-
}
|
|
50122
|
-
//#endregion
|
|
50123
51879
|
//#region src/pipeline-analytics/retention/disk-reconcile.ts
|
|
50124
51880
|
/** Is this an event-owned media row — `motion` / `object` / `audio` (D476;
|
|
50125
51881
|
* `event` used to cover all three under one owner kind). */
|
|
@@ -52390,6 +54146,10 @@ var TRACKS_COLUMNS = [
|
|
|
52390
54146
|
name: "frameHeight",
|
|
52391
54147
|
type: "INTEGER"
|
|
52392
54148
|
},
|
|
54149
|
+
{
|
|
54150
|
+
name: "modelId",
|
|
54151
|
+
type: "TEXT"
|
|
54152
|
+
},
|
|
52393
54153
|
{
|
|
52394
54154
|
name: "closedReason",
|
|
52395
54155
|
type: "TEXT"
|
|
@@ -52554,7 +54314,12 @@ function cloneTrack(t) {
|
|
|
52554
54314
|
...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
|
|
52555
54315
|
...t.hasEmbeddedFace !== void 0 ? { hasEmbeddedFace: t.hasEmbeddedFace } : {},
|
|
52556
54316
|
...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {},
|
|
52557
|
-
...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {}
|
|
54317
|
+
...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {},
|
|
54318
|
+
...t.provenance?.modelId !== void 0 ? { modelId: t.provenance.modelId } : {},
|
|
54319
|
+
...t.provenance !== void 0 ? {
|
|
54320
|
+
frameWidth: t.provenance.frameWidth,
|
|
54321
|
+
frameHeight: t.provenance.frameHeight
|
|
54322
|
+
} : {}
|
|
52558
54323
|
};
|
|
52559
54324
|
}
|
|
52560
54325
|
var TrackStore = class {
|
|
@@ -52651,7 +54416,8 @@ var TrackStore = class {
|
|
|
52651
54416
|
totalDistance: 0,
|
|
52652
54417
|
state: params.state,
|
|
52653
54418
|
active: true,
|
|
52654
|
-
lastSnapshotAt: 0
|
|
54419
|
+
lastSnapshotAt: 0,
|
|
54420
|
+
...params.provenance !== void 0 ? { provenance: params.provenance } : {}
|
|
52655
54421
|
};
|
|
52656
54422
|
this.active.set(params.trackId, fresh);
|
|
52657
54423
|
return fresh;
|
|
@@ -54112,7 +55878,10 @@ var TrackStore = class {
|
|
|
54112
55878
|
});
|
|
54113
55879
|
}
|
|
54114
55880
|
async persistCompleted(t) {
|
|
54115
|
-
const dims =
|
|
55881
|
+
const dims = t.frameWidth !== void 0 && t.frameHeight !== void 0 ? {
|
|
55882
|
+
w: t.frameWidth,
|
|
55883
|
+
h: t.frameHeight
|
|
55884
|
+
} : this.frameDims?.(t.deviceId);
|
|
54116
55885
|
const envelope = computeTrackEnvelope(t.positions, dims?.w, dims?.h);
|
|
54117
55886
|
await this.store.set.mutate({
|
|
54118
55887
|
collection: TRACKS_COLLECTION,
|
|
@@ -54148,14 +55917,17 @@ var TrackStore = class {
|
|
|
54148
55917
|
...t.hasEmbeddedFace !== void 0 ? { hasEmbeddedFace: t.hasEmbeddedFace } : {},
|
|
54149
55918
|
...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {},
|
|
54150
55919
|
...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {},
|
|
54151
|
-
...envelope !== null
|
|
55920
|
+
...envelope !== null ? {
|
|
54152
55921
|
envMinX: envelope.minX,
|
|
54153
55922
|
envMinY: envelope.minY,
|
|
54154
55923
|
envMaxX: envelope.maxX,
|
|
54155
|
-
envMaxY: envelope.maxY
|
|
55924
|
+
envMaxY: envelope.maxY
|
|
55925
|
+
} : {},
|
|
55926
|
+
...dims !== void 0 ? {
|
|
54156
55927
|
frameWidth: dims.w,
|
|
54157
55928
|
frameHeight: dims.h
|
|
54158
|
-
} : {}
|
|
55929
|
+
} : {},
|
|
55930
|
+
...t.modelId !== void 0 ? { modelId: t.modelId } : {}
|
|
54159
55931
|
}
|
|
54160
55932
|
});
|
|
54161
55933
|
}
|
|
@@ -54215,6 +55987,9 @@ var TrackStore = class {
|
|
|
54215
55987
|
/** Free-form composed token line (see `composePreviewMissReason`) — a
|
|
54216
55988
|
* string read, absent stays absent. */
|
|
54217
55989
|
const previewMissReason = data["previewMissReason"];
|
|
55990
|
+
const modelId = data["modelId"];
|
|
55991
|
+
const frameWidth = data["frameWidth"];
|
|
55992
|
+
const frameHeight = data["frameHeight"];
|
|
54218
55993
|
const envMinX = data["envMinX"];
|
|
54219
55994
|
const envMinY = data["envMinY"];
|
|
54220
55995
|
const envMaxX = data["envMaxX"];
|
|
@@ -54258,7 +56033,12 @@ var TrackStore = class {
|
|
|
54258
56033
|
...closedReason !== void 0 ? { closedReason } : {},
|
|
54259
56034
|
...typeof previewMissReason === "string" && previewMissReason.length > 0 ? { previewMissReason } : {},
|
|
54260
56035
|
...observations !== void 0 ? { observations } : {},
|
|
54261
|
-
...envelope !== null ? { envelope } : {}
|
|
56036
|
+
...envelope !== null ? { envelope } : {},
|
|
56037
|
+
...typeof modelId === "string" && modelId.length > 0 ? { modelId } : {},
|
|
56038
|
+
...typeof frameWidth === "number" && typeof frameHeight === "number" ? {
|
|
56039
|
+
frameWidth,
|
|
56040
|
+
frameHeight
|
|
56041
|
+
} : {}
|
|
54262
56042
|
});
|
|
54263
56043
|
}
|
|
54264
56044
|
};
|
|
@@ -59660,7 +61440,6 @@ var DEFAULT_EMBEDDING_CONFIG = {
|
|
|
59660
61440
|
enabled: true,
|
|
59661
61441
|
modelId: "clip-vit-b32",
|
|
59662
61442
|
agentId: "local",
|
|
59663
|
-
runtime: "node",
|
|
59664
61443
|
backend: "cpu",
|
|
59665
61444
|
classes: [],
|
|
59666
61445
|
minConfidence: .5,
|
|
@@ -77598,6 +79377,9 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
|
|
|
77598
79377
|
EventCategory.PipelineNodeInferenceUnavailable,
|
|
77599
79378
|
EventCategory.PipelineDetectionBlind,
|
|
77600
79379
|
EventCategory.AddonRunnerFailed,
|
|
79380
|
+
EventCategory.AddonStarted,
|
|
79381
|
+
EventCategory.AddonRestarted,
|
|
79382
|
+
EventCategory.PipelineCameraAssigned,
|
|
77601
79383
|
EventCategory.UpdateAvailable,
|
|
77602
79384
|
EventCategory.AddonUpdated,
|
|
77603
79385
|
EventCategory.SystemRestartCompleted,
|
|
@@ -87220,6 +89002,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
87220
89002
|
};
|
|
87221
89003
|
},
|
|
87222
89004
|
isDeviceDeleted: async (deviceId) => await api.deviceManager.getDevice.query({ deviceId }) === null,
|
|
89005
|
+
readNodeDirectory: () => api.nodes.directory.query(),
|
|
87223
89006
|
getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
|
|
87224
89007
|
...buildLinkedSnapshotReads(api.snapshot),
|
|
87225
89008
|
identityIdsByName: async () => {
|
|
@@ -87946,21 +89729,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
87946
89729
|
...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
|
|
87947
89730
|
});
|
|
87948
89731
|
}
|
|
87949
|
-
/**
|
|
87950
|
-
* The FrameProcessor pass of a replay run — see the cap doc on
|
|
87951
|
-
* `runReplayFrameProcessor` for why this is not the `processFrame` method
|
|
87952
|
-
* this addon deliberately does not have.
|
|
87953
|
-
*
|
|
87954
|
-
* `runFrameProcessorPass` is pure w.r.t. this addon's own state: it
|
|
87955
|
-
* constructs a FRESH `FrameProcessor` and does not touch `this.trackStore`,
|
|
87956
|
-
* `this.stationaryRegistry` or any other instance field — unlike
|
|
87957
|
-
* `getOrCreateProcessor` (the production path), which wires both.
|
|
87958
|
-
* `scripts/check-replay-isolation.ts` holds that boundary on the
|
|
87959
|
-
* `replay/` directory this delegates into.
|
|
87960
|
-
*/
|
|
87961
|
-
async runReplayFrameProcessor(input) {
|
|
87962
|
-
return runFrameProcessorPass(input);
|
|
87963
|
-
}
|
|
87964
89732
|
async listRetrainAnnotations(input) {
|
|
87965
89733
|
return this.requireRetrain().listAnnotations(input.trackId);
|
|
87966
89734
|
}
|
|
@@ -89066,6 +90834,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
89066
90834
|
});
|
|
89067
90835
|
const currentTrackIds = /* @__PURE__ */ new Set();
|
|
89068
90836
|
const positionsCountById = /* @__PURE__ */ new Map();
|
|
90837
|
+
const provenance = trackProvenanceOf(frame);
|
|
89069
90838
|
for (const t of result.tracked) {
|
|
89070
90839
|
currentTrackIds.add(t.trackId);
|
|
89071
90840
|
if (this.suppressedBirths.isRejected(key, t.trackId)) continue;
|
|
@@ -89089,7 +90858,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
89089
90858
|
score: t.confidence
|
|
89090
90859
|
},
|
|
89091
90860
|
zones: t.zones,
|
|
89092
|
-
state: t.state
|
|
90861
|
+
state: t.state,
|
|
90862
|
+
...provenance !== void 0 ? { provenance } : {}
|
|
89093
90863
|
});
|
|
89094
90864
|
positionsCountById.set(t.trackId, upserted.positions.length);
|
|
89095
90865
|
}
|