@camstack/addon-post-analysis 1.2.308 → 1.2.309
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{clip-model-registry-BqrTKyUe.js → clip-model-registry-B3M4ZI17.js} +53 -4
- package/dist/{clip-model-registry-FGOIz3PZ.mjs → clip-model-registry-CrBn6Qvt.mjs} +48 -5
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-61DKRi3y.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Bkf0-pki.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DWg2WU0a.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-BeNsqz7j.mjs → hostInit-Dj0aaMhv.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +726 -139
- package/dist/pipeline-analytics/index.mjs +726 -139
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C-9GoNIB.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, $n as ruleDeviceCompatCapQueries, $t as TimelapseRuleInputSchema, A as EMBEDDING_REQUIREMENTS_FAILED, An as isDetectionMacroClass, Ar as
|
|
1
|
+
import { $ as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, $n as ruleDeviceCompatCapQueries, $t as TimelapseRuleInputSchema, A as EMBEDDING_REQUIREMENTS_FAILED, An as isDetectionMacroClass, Ar as sleep$1, At as NcTemplateVarDescriptorSchema, B as ListTrackRelationsInputSchema, Bn as memoizePinnedFfmpeg, Br as partialRecord, Bt as SCENE_DEFAULT_ANCHOR_THRESHOLD, C as DEFAULT_EVENT_COLOR, Cn as encodeVectorBase64, Cr as createEvent, Ct as NcSnoozeInputSchema, D as DeclaredDevices, Dn as gifDeriveSpeed, Dr as isDeviceScopedCap, Dt as NcTaxonomySchema, E as DETECTION_PIPELINE_CAP_NAME, En as failureContributionCapability, Er as hydrateSchema, Et as NcSystemEventKindSchema, F as EXPORT_DENSE_MAX_RANGES, Fn as isSystemDelivery, Fr as discriminatedUnion, Ft as RECORDING_SOURCE_CAMSTACK_ADDON, G as MAX_BIRTH_LATENCY_PROXY_MS, Gn as plateGalleryCapability, Gr as union, Gt as SCENE_DIVERGED, H as LockStateSchema, Hn as occupancyScope, Hr as strictObject, Ht as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, I as FIRST_LEVEL_MACRO_CLASSES, In as isSystemEventSummaryRule, Ir as literal, It as RebuildObjectEmbeddingsRefusalSchema, J as MOTION_CLOSE_AFTER_MS, Jn as readTimelapseGeneratedAt, Jt as SetTrackPurityInputSchema, K as MAX_TRACK_PURITIES, Kn as readDeviceStateFrom, Kr as unknown, Kt as SCENE_RESET_RECAPTURES, L as FULL_IMAGE_BBOX, Ln as isVectorIndexDimConflict, Lr as looseObject, Lt as RetrainStatusSchema, M as EVENT_DENSITY_BATCH_MAX, Mn as isKnownTemplateVar, Mr as _enum, Mt as OpsLogEntrySchema, N as EVENT_KIND_BY_CAP, Nn as isMediaOwnerType, Nr as array, O as DetectionSourceSchema, On as gifFromMp4Args, Or as nodePin, Ot as NcTemplatePreviewInputSchema, P as EVENT_OWNER_TYPES, Pn as isScheduleActive, Pr as boolean, Pt as RECORDING_EXPORT_MAX_READ_BYTES, Q as NC_AUDIO_CONFIRM_HITS_DEFAULT, Qn as ruleBelongsToAlarm$1, Qt as TRACK_RELATIONS_DEFAULT_LIMIT, R as FailureCounters, Rn as kebabToCamel, Rr as number, Rt as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, S as CoverStateSchema, Sr as CamProfileSchema, St as NcScheduleSchema, T as DETECTION_MACRO_CLASSES, Tn as faceGalleryCapability, Tr as defineCustomActions, Tt as NcSnoozeSuppressedSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Ur as string, Ut as SCENE_DEFAULT_QUIET_SECONDS, V as ListTrackRelationsResultSchema, Vn as notificationRulesCapability, Vr as record, Vt as SCENE_DEFAULT_CHECK_INTERVAL_SEC, W as MAX_BIRTH_DECISION_RECORDS, Wn as pipelineAnalyticsCapability, Wr as tuple, Wt as SCENE_DEFAULT_UNCOVERED_POLICY, X as MotionTriggeredBySchema, Xn as resolveLocationMode, Xt as SetTrackRelationInputSchema, Y as MediaFileKindEnum, Yn as resolveClipSearchMinScore, Yt as SetTrackPurityResultSchema, Z as NC_ALARM_SYSTEM_EVENT_KINDS, Zt as TIMELAPSE_DENSE_FLOOR_SEC, _ as BaseDevice, _n as describeSystemEventFamilyProblem, _r as evaluateSensorEdge, _t as NcRuleInputSchema, a as CLIP_EMBEDDING_STEP_ID, an as TrackSideSchema, ar as subKindsOf, at as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, b as ClearTrackRelationInputSchema, bn as embeddingRefusalOf, br as errMsg, bt as NcRuleTargetSchema, c as resolveClusterModelPin, cn as alarmPanelCapability, cr as systemEventFilterApplies, ct as NC_SNOOZE_MAX_MINUTES, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, dn as audioModeOf, dr as templateCarriesSummary, dt as NC_TAXONOMY, en as TimelapseRulePatchSchema, er as ruleKindOf, et as NC_CLIP_POST_ROLL_DEFAULT_SEC, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fn as buildEventKindDescriptor, fr as templateVarsFor, ft as NC_TEMPLATE_VARS, g as BIRTH_DECISION_DEFAULT_LIMIT, gn as describeSystemEventDeliveryProblem, gr as canonicalHash, gt as NcConditionsSchema, h as ArchivedDebugNoteSchema, hn as deriveRecordingMode, hr as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, ht as NcConditionDescriptorSchema, in as TrackRelationSchema, ir as storageOccupancyCapability, it as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as isEventOwnerType, jr as ZodObject, jt as OPS_LOG_DEFAULT_LIMIT, k as EMBEDDING_MODEL_FAILED, kn as isAudioClassName, kt as NcTemplatePreviewSchema, l as APPEARANCE_BIN_COUNT, ln as assertTimelapseCadences, lr as systemEventSummaryScheduleOf, lt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, m as AlarmArmModeSchema, mn as cosineSimilarity$1, mr as zoneAnalyticsCapability, mt as NcAlarmSettingsPatchSchema, nn as TrackPuritySchema, nr as sensorRaisedOf, nt as NC_CLIP_RETENTION_MAX_SEC, o as FACE_EMBEDDING_STEP_ID, on as TrackSourceSchema, or as systemEventDeliveryProblems, ot as NC_DEFAULT_SNOOZE_MINUTES, p as AUDIO_MACRO_LABELS, pn as consumableLowPercentOf, pr as vectorDimFromBase64, pt as NcAlarmConfigSchema, q as MAX_TRACK_RELATIONS, qn as readMotionTriggeredBy, qr as EventCategory, qt as SceneMonitorSchema, rn as TrackRelationKindSchema, rr as servedTemplateVars, rt as NC_CONDITION_CATALOG, s as readClusterModelPinStrict, sn as addonWidgetsSourceCapability, sr as systemEventFamilyProblems, st as NC_SENSOR_TRANSITION_KINDS, t as BUILTIN_CLIP_MODELS, tn as TimelapseRuleSchema, tr as sceneMonitorCapability, tt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, u as APPEARANCE_BLOCKS, un as audioMetricsCapability, ur as systemEventSummarySplitOf, ut as NC_TANK_SYSTEM_EVENT_KINDS, v as BirthDecisionRecordSchema, vr as isSourceCap, vt as NcRulePatchSchema, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wn as evictionPolicyOfLocation, wr as customAction, wt as NcSnoozeSchema, x as ClearTrackRelationResultSchema, xr as BaseAddon, xt as NcRuleUpdateInputSchema, y as COCO_TO_MACRO, yr as DeviceType, yt as NcRuleSchema, z as LabelAttributionSchema, zn as mayWriteToLocation, zr as object, zt as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS } from "../clip-model-registry-CrBn6Qvt.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs$1 from "node:fs";
|
|
4
4
|
import { createWriteStream, promises } from "node:fs";
|
|
@@ -13484,6 +13484,8 @@ var en_default = {
|
|
|
13484
13484
|
"summary.body.empty": "nothing was seen in this window",
|
|
13485
13485
|
"system.state-summary.title": "{{rule}}",
|
|
13486
13486
|
"system.state-summary.body": "{{summaryText}}",
|
|
13487
|
+
"system.state-summary.section.title": "{{rule}} — {{summarySection}}",
|
|
13488
|
+
"system.state-summary.section.body": "{{summaryText}}",
|
|
13487
13489
|
"summary.section.device-state": "Devices offline or switched off",
|
|
13488
13490
|
"summary.section.streams": "Streams offline",
|
|
13489
13491
|
"summary.section.battery": "Low batteries",
|
|
@@ -13812,6 +13814,8 @@ var it_default = {
|
|
|
13812
13814
|
"summary.body.empty": "nessun evento in questa finestra",
|
|
13813
13815
|
"system.state-summary.title": "{{rule}}",
|
|
13814
13816
|
"system.state-summary.body": "{{summaryText}}",
|
|
13817
|
+
"system.state-summary.section.title": "{{rule}} — {{summarySection}}",
|
|
13818
|
+
"system.state-summary.section.body": "{{summaryText}}",
|
|
13815
13819
|
"summary.section.device-state": "Dispositivi offline o spenti",
|
|
13816
13820
|
"summary.section.streams": "Stream offline",
|
|
13817
13821
|
"summary.section.battery": "Batterie scariche",
|
|
@@ -15757,6 +15761,38 @@ function planRuleTest(input) {
|
|
|
15757
15761
|
};
|
|
15758
15762
|
}
|
|
15759
15763
|
//#endregion
|
|
15764
|
+
//#region src/notification-center/state-summary/state-summary-test.ts
|
|
15765
|
+
/**
|
|
15766
|
+
* What TESTING a state-summary rule answers (D698, `nc.testRule`).
|
|
15767
|
+
*
|
|
15768
|
+
* A summary rule sends nothing per event (D647), so the per-event test the
|
|
15769
|
+
* other rules get — one synthetic event, injected — can never deliver for it.
|
|
15770
|
+
* Its test builds the summary NOW and enqueues it; this is the report: the
|
|
15771
|
+
* rows it enqueued (one, or one per section) and every section's read, so
|
|
15772
|
+
* "nothing to report" is an answer with its reason per section, never a
|
|
15773
|
+
* silence.
|
|
15774
|
+
*/
|
|
15775
|
+
var NcStateSummaryTestSchema = object({
|
|
15776
|
+
/** The test's record id — a per-section row appends `:<sectionId>`. */
|
|
15777
|
+
recordId: string(),
|
|
15778
|
+
split: _enum(["rule", "section"]),
|
|
15779
|
+
/** Nothing open and every section read: nothing was enqueued. */
|
|
15780
|
+
nothingToReport: boolean(),
|
|
15781
|
+
sections: array(object({
|
|
15782
|
+
id: string(),
|
|
15783
|
+
status: _enum(["read", "unavailable"]),
|
|
15784
|
+
items: number().int().nonnegative(),
|
|
15785
|
+
reason: string().optional()
|
|
15786
|
+
})),
|
|
15787
|
+
rows: array(object({
|
|
15788
|
+
recordId: string(),
|
|
15789
|
+
/** The section of a per-section row. */
|
|
15790
|
+
section: string().optional(),
|
|
15791
|
+
enqueued: number().int().nonnegative(),
|
|
15792
|
+
targetIds: array(string())
|
|
15793
|
+
}))
|
|
15794
|
+
});
|
|
15795
|
+
//#endregion
|
|
15760
15796
|
//#region src/notification-center/clock-text.ts
|
|
15761
15797
|
/**
|
|
15762
15798
|
* `{{time}}` — hours and minutes in the NOTIFICATION language.
|
|
@@ -17109,13 +17145,18 @@ var ncActions = defineCustomActions({
|
|
|
17109
17145
|
deviceId: number().int().optional()
|
|
17110
17146
|
}), NcTestEventResultSchema.extend({
|
|
17111
17147
|
/** The subject kind the rule was tested with — the answer to "why was my
|
|
17112
|
-
* rule not a candidate". */
|
|
17148
|
+
* rule not a candidate". `state-summary` for a summary rule (D698). */
|
|
17113
17149
|
trigger: string(),
|
|
17114
17150
|
/** What the plan could not reproduce (a device-state gate, a polygon). */
|
|
17115
17151
|
planNotes: array(object({
|
|
17116
17152
|
conditionId: string(),
|
|
17117
17153
|
note: string()
|
|
17118
|
-
}))
|
|
17154
|
+
})),
|
|
17155
|
+
/**
|
|
17156
|
+
* A STATE-SUMMARY rule's test (D698): the summary built now and the rows
|
|
17157
|
+
* it enqueued, or why nothing was — absent on every other rule.
|
|
17158
|
+
*/
|
|
17159
|
+
stateSummary: NcStateSummaryTestSchema.optional()
|
|
17119
17160
|
}), {
|
|
17120
17161
|
kind: "mutation",
|
|
17121
17162
|
auth: "admin",
|
|
@@ -17474,6 +17515,48 @@ function makeNcActionHandlers(deps) {
|
|
|
17474
17515
|
if (rule.ownerUserId !== void 0 && rule.ownerUserId !== caller.userId) throw new Error(`forbidden: rule not owned: ${ruleId}`);
|
|
17475
17516
|
return rule;
|
|
17476
17517
|
};
|
|
17518
|
+
/**
|
|
17519
|
+
* TEST a STATE-SUMMARY rule (D698). The per-event injection cannot deliver
|
|
17520
|
+
* for it — a summary rule sends nothing per event (D647), and that test used
|
|
17521
|
+
* to be dropped without a word — so the summary is built NOW and enqueued.
|
|
17522
|
+
* The answer is shaped like every rule test's, plus the summary's own report.
|
|
17523
|
+
*/
|
|
17524
|
+
const testSummaryRule = async (rule, by) => {
|
|
17525
|
+
const test = deps.testStateSummary;
|
|
17526
|
+
if (test === void 0) throw new Error("this node does not host the notification centre — test on the hub");
|
|
17527
|
+
deps.logger.info("rule test requested", { meta: {
|
|
17528
|
+
by,
|
|
17529
|
+
ruleId: rule.id,
|
|
17530
|
+
ruleName: rule.name,
|
|
17531
|
+
trigger: "state-summary",
|
|
17532
|
+
bypassMute: true
|
|
17533
|
+
} });
|
|
17534
|
+
const report = await test(rule);
|
|
17535
|
+
const enqueued = report.rows.reduce((sum, row) => sum + row.enqueued, 0);
|
|
17536
|
+
const notes = ["per-camera notification mute bypassed"];
|
|
17537
|
+
if (report.nothingToReport) notes.push(`nothing to report — ${report.sections.map((section) => `${section.id}: ${section.status === "read" ? `${String(section.items)} open` : "unavailable"}`).join(", ")} — nothing was sent, as the scheduled summary would send nothing`);
|
|
17538
|
+
else if (enqueued === 0) notes.push("the summary was built but no row was enqueued — the rule reaches no target");
|
|
17539
|
+
return {
|
|
17540
|
+
recordId: report.recordId,
|
|
17541
|
+
origin: "synthetic",
|
|
17542
|
+
delivery: rule.delivery,
|
|
17543
|
+
candidates: 1,
|
|
17544
|
+
outcomes: [{
|
|
17545
|
+
ruleId: rule.id,
|
|
17546
|
+
ruleName: rule.name,
|
|
17547
|
+
matched: !report.nothingToReport,
|
|
17548
|
+
cooldown: "bypassed",
|
|
17549
|
+
enqueued,
|
|
17550
|
+
targetIds: [...new Set(report.rows.flatMap((row) => row.targetIds))]
|
|
17551
|
+
}],
|
|
17552
|
+
still: "unattachable",
|
|
17553
|
+
notes,
|
|
17554
|
+
...report.nothingToReport ? { droppedBy: "nothing-to-report" } : {},
|
|
17555
|
+
trigger: "state-summary",
|
|
17556
|
+
planNotes: [],
|
|
17557
|
+
stateSummary: report
|
|
17558
|
+
};
|
|
17559
|
+
};
|
|
17477
17560
|
/** The producer half, or an honest refusal. */
|
|
17478
17561
|
const requireTimelapse = () => {
|
|
17479
17562
|
const tl = deps.timelapse;
|
|
@@ -17808,6 +17891,7 @@ function makeNcActionHandlers(deps) {
|
|
|
17808
17891
|
const inject = deps.injectTestEvent;
|
|
17809
17892
|
if (inject === void 0) throw new Error("this node does not host the notification centre — test on the hub");
|
|
17810
17893
|
const rule = assertRuleVisible(input.ruleId, c);
|
|
17894
|
+
if (isSystemEventSummaryRule(rule)) return testSummaryRule(rule, c.userId);
|
|
17811
17895
|
const plan = planRuleTest({
|
|
17812
17896
|
rule,
|
|
17813
17897
|
now: Date.now(),
|
|
@@ -19642,6 +19726,77 @@ var NcDeviceStateCache = class {
|
|
|
19642
19726
|
}
|
|
19643
19727
|
};
|
|
19644
19728
|
//#endregion
|
|
19729
|
+
//#region src/notification-center/state-summary/state-model.ts
|
|
19730
|
+
/**
|
|
19731
|
+
* The STATE SUMMARY's vocabulary (D647): its sections, and what one reported
|
|
19732
|
+
* item is.
|
|
19733
|
+
*
|
|
19734
|
+
* A section is one of the D637 kind GROUPS (`NC_SYSTEM_EVENT_KIND_GROUP_SPECS`)
|
|
19735
|
+
* — the same grouping both editors list kinds under — so "the rule selected a
|
|
19736
|
+
* battery kind" and "the summary has a battery section" are the same fact.
|
|
19737
|
+
* `backups-exports` is the one group with no section: a backup or an export is
|
|
19738
|
+
* a job that COMPLETED, and the state it leaves behind is not a condition that
|
|
19739
|
+
* is still true. The ADR records why it is excluded rather than approximated.
|
|
19740
|
+
*/
|
|
19741
|
+
var NC_STATE_SUMMARY_SECTION_IDS = [
|
|
19742
|
+
"device-state",
|
|
19743
|
+
"streams",
|
|
19744
|
+
"battery",
|
|
19745
|
+
"consumables",
|
|
19746
|
+
"tanks",
|
|
19747
|
+
"detection-blind",
|
|
19748
|
+
"nodes",
|
|
19749
|
+
"addons",
|
|
19750
|
+
"updates"
|
|
19751
|
+
];
|
|
19752
|
+
/** The kind groups that have no section, and why — said on the preview. */
|
|
19753
|
+
var NC_STATE_SUMMARY_EXCLUDED_GROUPS = { "backups-exports": "a backup or an export is a completed job, not a state that is still true" };
|
|
19754
|
+
function isStateSectionId(value) {
|
|
19755
|
+
return NC_STATE_SUMMARY_SECTION_IDS.some((id) => id === value);
|
|
19756
|
+
}
|
|
19757
|
+
/** The template variable that carries one section's lines (`template-vars.ts`). */
|
|
19758
|
+
var NC_STATE_SECTION_VAR = {
|
|
19759
|
+
"device-state": "summaryDevices",
|
|
19760
|
+
streams: "summaryStreams",
|
|
19761
|
+
battery: "summaryBattery",
|
|
19762
|
+
consumables: "summaryConsumables",
|
|
19763
|
+
tanks: "summaryTanks",
|
|
19764
|
+
"detection-blind": "summaryDetection",
|
|
19765
|
+
nodes: "summaryNodes",
|
|
19766
|
+
addons: "summaryAddons",
|
|
19767
|
+
updates: "summaryUpdates"
|
|
19768
|
+
};
|
|
19769
|
+
/** The kinds of a section — its kind group. */
|
|
19770
|
+
function sectionKinds(section) {
|
|
19771
|
+
return NC_SYSTEM_EVENT_KIND_GROUP_SPECS.find((group) => group.id === section)?.kinds ?? [];
|
|
19772
|
+
}
|
|
19773
|
+
/**
|
|
19774
|
+
* The sections a rule reports, in section order: every section whose kind
|
|
19775
|
+
* group shares a kind with the rule. A rule that selected only
|
|
19776
|
+
* `device-battery-low` still reports the battery STATE — the state has no
|
|
19777
|
+
* direction, only the event did.
|
|
19778
|
+
*/
|
|
19779
|
+
function sectionsOfRule(condition) {
|
|
19780
|
+
return NC_STATE_SUMMARY_SECTION_IDS.filter((section) => sectionKinds(section).some((kind) => condition.kinds.includes(kind)));
|
|
19781
|
+
}
|
|
19782
|
+
/** The kind groups the rule selected that have no section. */
|
|
19783
|
+
function excludedGroupsOfRule(condition) {
|
|
19784
|
+
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);
|
|
19785
|
+
}
|
|
19786
|
+
/**
|
|
19787
|
+
* The condition the summary judges its items with: the rule's own, with its
|
|
19788
|
+
* kinds widened to every kind of every section it reports — so a rule that
|
|
19789
|
+
* named the raise is not told "not my kind" about the state the raise left
|
|
19790
|
+
* behind. Every FILTER is the rule's, untouched.
|
|
19791
|
+
*/
|
|
19792
|
+
function summaryCondition(condition) {
|
|
19793
|
+
const kinds = sectionsOfRule(condition).flatMap((section) => sectionKinds(section));
|
|
19794
|
+
return {
|
|
19795
|
+
...condition,
|
|
19796
|
+
kinds: [...new Set([...condition.kinds, ...kinds])]
|
|
19797
|
+
};
|
|
19798
|
+
}
|
|
19799
|
+
//#endregion
|
|
19645
19800
|
//#region src/notification-center/state-summary/state-summary-delivery.ts
|
|
19646
19801
|
/**
|
|
19647
19802
|
* A state summary as an ORDINARY system-event subject (D647), so it reaches the
|
|
@@ -19650,7 +19805,9 @@ var NcDeviceStateCache = class {
|
|
|
19650
19805
|
* which renders the rule's own template with the summary's variables.
|
|
19651
19806
|
*
|
|
19652
19807
|
* - `recordId` is `state-summary:<ruleId>:<windowEndMs>` — the outbox dedup
|
|
19653
|
-
* key, so a restart can never enqueue one window twice.
|
|
19808
|
+
* key, so a restart can never enqueue one window twice. Split per SECTION
|
|
19809
|
+
* (D698), each row is `state-summary:<ruleId>:<windowEndMs>:<sectionId>`:
|
|
19810
|
+
* the same guarantee, per section.
|
|
19654
19811
|
* - The row files under the system device (0): a summary spans devices.
|
|
19655
19812
|
* `targetUsers` therefore reach admins only — a user whose grant is
|
|
19656
19813
|
* narrower than the whole installation must not be sent the names of
|
|
@@ -19664,20 +19821,37 @@ var NcDeviceStateCache = class {
|
|
|
19664
19821
|
*/
|
|
19665
19822
|
var NC_STATE_SUMMARY_TITLE_KEY = "system.state-summary.title";
|
|
19666
19823
|
var NC_STATE_SUMMARY_BODY_KEY = "system.state-summary.body";
|
|
19824
|
+
/** A per-section row's title and body (D698): the rule's name and the section's. */
|
|
19825
|
+
var NC_STATE_SUMMARY_SECTION_TITLE_KEY = "system.state-summary.section.title";
|
|
19826
|
+
var NC_STATE_SUMMARY_SECTION_BODY_KEY = "system.state-summary.section.body";
|
|
19667
19827
|
function stateSummaryRecordId(ruleId, windowEndMs) {
|
|
19668
19828
|
return `state-summary:${ruleId}:${String(windowEndMs)}`;
|
|
19669
19829
|
}
|
|
19830
|
+
/**
|
|
19831
|
+
* The record id of a TEST of the rule (D698, `nc.testRule`): its own prefix,
|
|
19832
|
+
* so a test can never take — or be swallowed by — a real window's dedup key.
|
|
19833
|
+
*/
|
|
19834
|
+
function stateSummaryTestRecordId(ruleId, nowMs) {
|
|
19835
|
+
return `state-summary-test:${ruleId}:${String(nowMs)}`;
|
|
19836
|
+
}
|
|
19837
|
+
/** One section's row: the record id of the whole, plus `:<sectionId>` — the per-section dedup key. */
|
|
19838
|
+
function sectionRecordId(base, sectionId) {
|
|
19839
|
+
return `${base}:${sectionId}`;
|
|
19840
|
+
}
|
|
19670
19841
|
/** Is this system-event data the data of a state-summary row? */
|
|
19671
19842
|
function isStateSummaryData(data) {
|
|
19672
19843
|
return data["stateSummary"] === true;
|
|
19673
19844
|
}
|
|
19845
|
+
function recordBaseOf(input) {
|
|
19846
|
+
return input.recordBase ?? stateSummaryRecordId(input.rule.id, input.window.endMs);
|
|
19847
|
+
}
|
|
19674
19848
|
/** `null` when the rule has no system-event condition (the guard refuses that). */
|
|
19675
19849
|
function stateSummarySubject(input) {
|
|
19676
19850
|
const kind = input.rule.conditions.systemEvent?.kinds[0];
|
|
19677
19851
|
if (kind === void 0) return null;
|
|
19678
19852
|
return {
|
|
19679
19853
|
kind: "system-event",
|
|
19680
|
-
recordId:
|
|
19854
|
+
recordId: recordBaseOf(input),
|
|
19681
19855
|
deviceId: NC_SYSTEM_DEVICE_ID$1,
|
|
19682
19856
|
timestamp: input.window.endMs,
|
|
19683
19857
|
classNames: [],
|
|
@@ -19690,6 +19864,7 @@ function stateSummarySubject(input) {
|
|
|
19690
19864
|
textParams: {
|
|
19691
19865
|
summaryText: input.text.text,
|
|
19692
19866
|
summaryCount: String(input.text.count),
|
|
19867
|
+
summarySection: input.text.sections.map((section) => section.title).join(", "),
|
|
19693
19868
|
...input.text.sectionVars
|
|
19694
19869
|
},
|
|
19695
19870
|
textCount: input.text.count,
|
|
@@ -19707,13 +19882,82 @@ function stateSummarySubject(input) {
|
|
|
19707
19882
|
};
|
|
19708
19883
|
}
|
|
19709
19884
|
/**
|
|
19885
|
+
* The rows one summary window sends. `rule`: the one D647 row. `section`: one
|
|
19886
|
+
* row per section with something to say — its items, or the sentence that it
|
|
19887
|
+
* is UNAVAILABLE, exactly as the combined text words it. A section with
|
|
19888
|
+
* nothing open sends nothing; neither does a summary with nothing at all.
|
|
19889
|
+
*/
|
|
19890
|
+
function stateSummarySubjects(input) {
|
|
19891
|
+
if (input.split === "rule") {
|
|
19892
|
+
const subject = stateSummarySubject(input);
|
|
19893
|
+
return subject === null ? [] : [subject];
|
|
19894
|
+
}
|
|
19895
|
+
const kind = input.rule.conditions.systemEvent?.kinds[0];
|
|
19896
|
+
if (kind === void 0) return [];
|
|
19897
|
+
return input.text.sections.map((section) => sectionSubject(input, kind, section));
|
|
19898
|
+
}
|
|
19899
|
+
function sectionSubject(input, kind, section) {
|
|
19900
|
+
const sectionVars = {};
|
|
19901
|
+
for (const name of Object.keys(input.text.sectionVars)) sectionVars[name] = "";
|
|
19902
|
+
sectionVars[NC_STATE_SECTION_VAR[section.id]] = section.text;
|
|
19903
|
+
return {
|
|
19904
|
+
kind: "system-event",
|
|
19905
|
+
recordId: sectionRecordId(recordBaseOf(input), section.id),
|
|
19906
|
+
deviceId: NC_SYSTEM_DEVICE_ID$1,
|
|
19907
|
+
timestamp: input.window.endMs,
|
|
19908
|
+
classNames: [],
|
|
19909
|
+
zones: [],
|
|
19910
|
+
systemEvent: {
|
|
19911
|
+
kind,
|
|
19912
|
+
subject: `state-summary:${input.rule.id}:${section.id}`,
|
|
19913
|
+
titleKey: NC_STATE_SUMMARY_SECTION_TITLE_KEY,
|
|
19914
|
+
bodyKey: NC_STATE_SUMMARY_SECTION_BODY_KEY,
|
|
19915
|
+
textParams: {
|
|
19916
|
+
summaryText: section.body,
|
|
19917
|
+
summaryCount: String(section.count),
|
|
19918
|
+
summarySection: section.title,
|
|
19919
|
+
...sectionVars
|
|
19920
|
+
},
|
|
19921
|
+
textCount: section.count,
|
|
19922
|
+
data: {
|
|
19923
|
+
stateSummary: true,
|
|
19924
|
+
section: section.id,
|
|
19925
|
+
windowStartMs: input.window.startMs,
|
|
19926
|
+
windowEndMs: input.window.endMs,
|
|
19927
|
+
sections: [{
|
|
19928
|
+
id: section.id,
|
|
19929
|
+
status: section.status,
|
|
19930
|
+
items: section.count
|
|
19931
|
+
}]
|
|
19932
|
+
}
|
|
19933
|
+
}
|
|
19934
|
+
};
|
|
19935
|
+
}
|
|
19936
|
+
/** The section a per-section row is about (`data.section`), or `undefined` on a combined row. */
|
|
19937
|
+
function stateSummarySectionOf(subject) {
|
|
19938
|
+
const section = subject.systemEvent?.data["section"];
|
|
19939
|
+
return typeof section === "string" && isStateSectionId(section) ? section : void 0;
|
|
19940
|
+
}
|
|
19941
|
+
/** Does a body name `{{summaryText}}` or this section's own variable? */
|
|
19942
|
+
function bodyCarriesSection(body, section) {
|
|
19943
|
+
return new RegExp(`\\{\\{\\s*(?:summaryText|${NC_STATE_SECTION_VAR[section]})\\s*\\}\\}`).test(body);
|
|
19944
|
+
}
|
|
19945
|
+
/**
|
|
19710
19946
|
* The rule as its summary is ENQUEUED: a custom body that carries none of the
|
|
19711
19947
|
* summary's content gets `{{summaryText}}` appended on its own line, so an
|
|
19712
19948
|
* operator's body never sends an empty summary (review M3). `null` = unchanged.
|
|
19949
|
+
*
|
|
19950
|
+
* On a per-SECTION row (D698 review M3) the content is THIS section's: a body
|
|
19951
|
+
* naming only `{{summaryBattery}}` carries the battery row and nothing on the
|
|
19952
|
+
* streams row, so every other section's row gets `{{summaryText}}` — its own
|
|
19953
|
+
* lines — appended.
|
|
19713
19954
|
*/
|
|
19714
|
-
function withSummaryBody(rule) {
|
|
19955
|
+
function withSummaryBody(rule, section) {
|
|
19715
19956
|
const body = rule.template?.body;
|
|
19716
|
-
if (templateCarriesSummary(body))
|
|
19957
|
+
if (templateCarriesSummary(body)) {
|
|
19958
|
+
if (section === void 0 || body === void 0 || body.trim().length === 0) return null;
|
|
19959
|
+
if (bodyCarriesSection(body, section)) return null;
|
|
19960
|
+
}
|
|
19717
19961
|
return {
|
|
19718
19962
|
...rule,
|
|
19719
19963
|
template: {
|
|
@@ -23762,6 +24006,120 @@ function withTimeout$3(promise, ms, what) {
|
|
|
23762
24006
|
});
|
|
23763
24007
|
}
|
|
23764
24008
|
//#endregion
|
|
24009
|
+
//#region src/notification-center/boot-window-rows.ts
|
|
24010
|
+
var BootWindowRows = class {
|
|
24011
|
+
deps;
|
|
24012
|
+
window = /* @__PURE__ */ new Map();
|
|
24013
|
+
/** Deferred closes, keyed so a repeat (one per detection) replaces rather than grows. */
|
|
24014
|
+
deferred = /* @__PURE__ */ new Map();
|
|
24015
|
+
settled = false;
|
|
24016
|
+
constructor(deps) {
|
|
24017
|
+
this.deps = deps;
|
|
24018
|
+
}
|
|
24019
|
+
/** Is the ledger still inside its boot window (not settled after an answered load)? */
|
|
24020
|
+
open() {
|
|
24021
|
+
return !this.settled;
|
|
24022
|
+
}
|
|
24023
|
+
/** Stage a row that would be CREATED inside the window — mirror only, never persisted. */
|
|
24024
|
+
stageProvisional(row) {
|
|
24025
|
+
this.deps.ledger.stage(row);
|
|
24026
|
+
this.window.set(this.deps.keyOf(row), {
|
|
24027
|
+
kind: "provisional",
|
|
24028
|
+
row
|
|
24029
|
+
});
|
|
24030
|
+
}
|
|
24031
|
+
/** A row the ledger persisted inside the window: the load must not regress it. */
|
|
24032
|
+
noteWritten(row) {
|
|
24033
|
+
this.window.set(this.deps.keyOf(row), {
|
|
24034
|
+
kind: "written",
|
|
24035
|
+
row
|
|
24036
|
+
});
|
|
24037
|
+
}
|
|
24038
|
+
/** A key the ledger removed inside the window: the load must not resurrect it. */
|
|
24039
|
+
noteRemoved(key) {
|
|
24040
|
+
this.window.set(key, { kind: "removed" });
|
|
24041
|
+
}
|
|
24042
|
+
/**
|
|
24043
|
+
* While the window is open, keep `replay` to run against the LOADED rows at
|
|
24044
|
+
* the settle; once settled, nothing — the caller's own pass already saw every
|
|
24045
|
+
* row (review LOW-5: no allocation per detection after boot). `key` names the
|
|
24046
|
+
* close: the same key deferred twice runs once, at the place it was first
|
|
24047
|
+
* deferred. `replay` answers how many rows it closed.
|
|
24048
|
+
*/
|
|
24049
|
+
deferWhileOpen(key, replay) {
|
|
24050
|
+
if (this.open()) this.deferred.set(key, replay);
|
|
24051
|
+
}
|
|
24052
|
+
/**
|
|
24053
|
+
* Close the window — call right after the ledger's `load`. A load that did
|
|
24054
|
+
* not answer leaves the window open and returns `null`.
|
|
24055
|
+
*/
|
|
24056
|
+
settle() {
|
|
24057
|
+
if (this.settled) return null;
|
|
24058
|
+
if (!this.deps.ledger.hasAnswered()) {
|
|
24059
|
+
this.deps.logger.warn("boot window NOT settled — the load did not answer", { meta: {
|
|
24060
|
+
collection: this.deps.ledger.collection,
|
|
24061
|
+
pendingCloses: this.deferred.size,
|
|
24062
|
+
provisionalRows: this.window.size
|
|
24063
|
+
} });
|
|
24064
|
+
return null;
|
|
24065
|
+
}
|
|
24066
|
+
this.settled = true;
|
|
24067
|
+
const ledger = this.deps.ledger;
|
|
24068
|
+
let replayed = 0;
|
|
24069
|
+
for (const replay of this.deferred.values()) replayed += replay();
|
|
24070
|
+
this.deferred.clear();
|
|
24071
|
+
let keptStored = 0;
|
|
24072
|
+
let written = 0;
|
|
24073
|
+
for (const [key, entry] of this.window) {
|
|
24074
|
+
if (entry.kind === "removed") {
|
|
24075
|
+
ledger.forget(key);
|
|
24076
|
+
continue;
|
|
24077
|
+
}
|
|
24078
|
+
if (entry.kind === "written") {
|
|
24079
|
+
ledger.stage(entry.row);
|
|
24080
|
+
continue;
|
|
24081
|
+
}
|
|
24082
|
+
const mirrored = ledger.get(key);
|
|
24083
|
+
const stored = mirrored === entry.row ? void 0 : mirrored;
|
|
24084
|
+
const held = this.deps.merge(stored, entry.row);
|
|
24085
|
+
const deviceId = this.deps.deviceIdOf(entry.row);
|
|
24086
|
+
const tags = deviceId !== void 0 ? { tags: { deviceId } } : {};
|
|
24087
|
+
if (stored !== void 0 && held === stored) {
|
|
24088
|
+
keptStored += 1;
|
|
24089
|
+
this.deps.logger.debug("boot-window row kept as stored — the restart learned nothing new", {
|
|
24090
|
+
...tags,
|
|
24091
|
+
meta: {
|
|
24092
|
+
collection: ledger.collection,
|
|
24093
|
+
key
|
|
24094
|
+
}
|
|
24095
|
+
});
|
|
24096
|
+
continue;
|
|
24097
|
+
}
|
|
24098
|
+
ledger.put(held);
|
|
24099
|
+
written += 1;
|
|
24100
|
+
this.deps.logger.debug("boot-window row written at load", {
|
|
24101
|
+
...tags,
|
|
24102
|
+
meta: {
|
|
24103
|
+
collection: ledger.collection,
|
|
24104
|
+
key,
|
|
24105
|
+
stored: stored !== void 0
|
|
24106
|
+
}
|
|
24107
|
+
});
|
|
24108
|
+
}
|
|
24109
|
+
this.window.clear();
|
|
24110
|
+
const merge = {
|
|
24111
|
+
keptStored,
|
|
24112
|
+
written,
|
|
24113
|
+
replayed
|
|
24114
|
+
};
|
|
24115
|
+
this.deps.logger.info("boot window settled", { meta: {
|
|
24116
|
+
collection: ledger.collection,
|
|
24117
|
+
...merge
|
|
24118
|
+
} });
|
|
24119
|
+
return merge;
|
|
24120
|
+
}
|
|
24121
|
+
};
|
|
24122
|
+
//#endregion
|
|
23765
24123
|
//#region src/notification-center/liveness-ledger.ts
|
|
23766
24124
|
/**
|
|
23767
24125
|
* @durable class=ledger owner=notification-center
|
|
@@ -23872,12 +24230,26 @@ var NC_LIVENESS_SPEC = {
|
|
|
23872
24230
|
};
|
|
23873
24231
|
var NcLivenessLedger = class {
|
|
23874
24232
|
ledger;
|
|
24233
|
+
/**
|
|
24234
|
+
* What was observed before the boot load answered (D698) — see
|
|
24235
|
+
* `boot-window-rows.ts`. A stored row holding the same STATE keeps its own
|
|
24236
|
+
* `updatedAt`: a restart that re-hears "offline" learned nothing about WHEN
|
|
24237
|
+
* the device went offline, and `updatedAt` is the state summary's "since".
|
|
24238
|
+
*/
|
|
24239
|
+
bootWindow;
|
|
23875
24240
|
constructor(deps) {
|
|
23876
24241
|
this.ledger = new DurableLedger({
|
|
23877
24242
|
spec: NC_LIVENESS_SPEC,
|
|
23878
24243
|
store: deps.store,
|
|
23879
24244
|
logger: deps.logger
|
|
23880
24245
|
});
|
|
24246
|
+
this.bootWindow = new BootWindowRows({
|
|
24247
|
+
ledger: this.ledger,
|
|
24248
|
+
keyOf: (row) => row.subject,
|
|
24249
|
+
deviceIdOf: (row) => row.deviceId,
|
|
24250
|
+
logger: deps.logger,
|
|
24251
|
+
merge: (stored, provisional) => stored !== void 0 && stored.state === provisional.state ? stored : provisional
|
|
24252
|
+
});
|
|
23881
24253
|
}
|
|
23882
24254
|
static declare(store) {
|
|
23883
24255
|
return DurableLedger.declare(store, NC_LIVENESS_SPEC);
|
|
@@ -23892,7 +24264,9 @@ var NcLivenessLedger = class {
|
|
|
23892
24264
|
* flood, so no error path may do it on purpose.
|
|
23893
24265
|
*/
|
|
23894
24266
|
async load() {
|
|
23895
|
-
|
|
24267
|
+
const loaded = (await this.ledger.load()).length;
|
|
24268
|
+
this.bootWindow.settle();
|
|
24269
|
+
return loaded;
|
|
23896
24270
|
}
|
|
23897
24271
|
/** Has a load READ the store once? Until then the mirror is not the truth (D647 reads it). */
|
|
23898
24272
|
hasAnswered() {
|
|
@@ -23985,13 +24359,21 @@ var NcLivenessLedger = class {
|
|
|
23985
24359
|
* spoke". That is the timestamp anyone reading the table wants.
|
|
23986
24360
|
*/
|
|
23987
24361
|
observe(observation, now) {
|
|
23988
|
-
|
|
24362
|
+
const row = {
|
|
23989
24363
|
subject: observation.subject,
|
|
23990
24364
|
state: observation.state,
|
|
23991
24365
|
kind: observation.kind,
|
|
23992
24366
|
...observation.deviceId !== void 0 ? { deviceId: observation.deviceId } : {},
|
|
23993
24367
|
updatedAt: now
|
|
23994
|
-
}
|
|
24368
|
+
};
|
|
24369
|
+
if (!this.bootWindow.open()) return this.ledger.observe(row);
|
|
24370
|
+
if (this.ledger.get(row.subject) === void 0) {
|
|
24371
|
+
this.bootWindow.stageProvisional(row);
|
|
24372
|
+
return "seeded";
|
|
24373
|
+
}
|
|
24374
|
+
const verdict = this.ledger.observe(row);
|
|
24375
|
+
if (verdict === "flip") this.bootWindow.noteWritten(row);
|
|
24376
|
+
return verdict;
|
|
23995
24377
|
}
|
|
23996
24378
|
};
|
|
23997
24379
|
/** The persisted column map (the `subject` PK is passed separately). */
|
|
@@ -30526,74 +30908,6 @@ function stateSources(ports) {
|
|
|
30526
30908
|
return { read: (section, nowMs) => readSection(section, ports, nowMs) };
|
|
30527
30909
|
}
|
|
30528
30910
|
//#endregion
|
|
30529
|
-
//#region src/notification-center/state-summary/state-model.ts
|
|
30530
|
-
/**
|
|
30531
|
-
* The STATE SUMMARY's vocabulary (D647): its sections, and what one reported
|
|
30532
|
-
* item is.
|
|
30533
|
-
*
|
|
30534
|
-
* A section is one of the D637 kind GROUPS (`NC_SYSTEM_EVENT_KIND_GROUP_SPECS`)
|
|
30535
|
-
* — the same grouping both editors list kinds under — so "the rule selected a
|
|
30536
|
-
* battery kind" and "the summary has a battery section" are the same fact.
|
|
30537
|
-
* `backups-exports` is the one group with no section: a backup or an export is
|
|
30538
|
-
* a job that COMPLETED, and the state it leaves behind is not a condition that
|
|
30539
|
-
* is still true. The ADR records why it is excluded rather than approximated.
|
|
30540
|
-
*/
|
|
30541
|
-
var NC_STATE_SUMMARY_SECTION_IDS = [
|
|
30542
|
-
"device-state",
|
|
30543
|
-
"streams",
|
|
30544
|
-
"battery",
|
|
30545
|
-
"consumables",
|
|
30546
|
-
"tanks",
|
|
30547
|
-
"detection-blind",
|
|
30548
|
-
"nodes",
|
|
30549
|
-
"addons",
|
|
30550
|
-
"updates"
|
|
30551
|
-
];
|
|
30552
|
-
/** The kind groups that have no section, and why — said on the preview. */
|
|
30553
|
-
var NC_STATE_SUMMARY_EXCLUDED_GROUPS = { "backups-exports": "a backup or an export is a completed job, not a state that is still true" };
|
|
30554
|
-
/** The template variable that carries one section's lines (`template-vars.ts`). */
|
|
30555
|
-
var NC_STATE_SECTION_VAR = {
|
|
30556
|
-
"device-state": "summaryDevices",
|
|
30557
|
-
streams: "summaryStreams",
|
|
30558
|
-
battery: "summaryBattery",
|
|
30559
|
-
consumables: "summaryConsumables",
|
|
30560
|
-
tanks: "summaryTanks",
|
|
30561
|
-
"detection-blind": "summaryDetection",
|
|
30562
|
-
nodes: "summaryNodes",
|
|
30563
|
-
addons: "summaryAddons",
|
|
30564
|
-
updates: "summaryUpdates"
|
|
30565
|
-
};
|
|
30566
|
-
/** The kinds of a section — its kind group. */
|
|
30567
|
-
function sectionKinds(section) {
|
|
30568
|
-
return NC_SYSTEM_EVENT_KIND_GROUP_SPECS.find((group) => group.id === section)?.kinds ?? [];
|
|
30569
|
-
}
|
|
30570
|
-
/**
|
|
30571
|
-
* The sections a rule reports, in section order: every section whose kind
|
|
30572
|
-
* group shares a kind with the rule. A rule that selected only
|
|
30573
|
-
* `device-battery-low` still reports the battery STATE — the state has no
|
|
30574
|
-
* direction, only the event did.
|
|
30575
|
-
*/
|
|
30576
|
-
function sectionsOfRule(condition) {
|
|
30577
|
-
return NC_STATE_SUMMARY_SECTION_IDS.filter((section) => sectionKinds(section).some((kind) => condition.kinds.includes(kind)));
|
|
30578
|
-
}
|
|
30579
|
-
/** The kind groups the rule selected that have no section. */
|
|
30580
|
-
function excludedGroupsOfRule(condition) {
|
|
30581
|
-
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);
|
|
30582
|
-
}
|
|
30583
|
-
/**
|
|
30584
|
-
* The condition the summary judges its items with: the rule's own, with its
|
|
30585
|
-
* kinds widened to every kind of every section it reports — so a rule that
|
|
30586
|
-
* named the raise is not told "not my kind" about the state the raise left
|
|
30587
|
-
* behind. Every FILTER is the rule's, untouched.
|
|
30588
|
-
*/
|
|
30589
|
-
function summaryCondition(condition) {
|
|
30590
|
-
const kinds = sectionsOfRule(condition).flatMap((section) => sectionKinds(section));
|
|
30591
|
-
return {
|
|
30592
|
-
...condition,
|
|
30593
|
-
kinds: [...new Set([...condition.kinds, ...kinds])]
|
|
30594
|
-
};
|
|
30595
|
-
}
|
|
30596
|
-
//#endregion
|
|
30597
30911
|
//#region src/notification-center/state-summary/state-summary.ts
|
|
30598
30912
|
/** Nothing to say: no item, and every section READ. Such a summary is not sent. */
|
|
30599
30913
|
function isEmptyStateSummary(summary) {
|
|
@@ -30694,28 +31008,50 @@ function lineOf(input, item) {
|
|
|
30694
31008
|
}
|
|
30695
31009
|
});
|
|
30696
31010
|
}
|
|
30697
|
-
function
|
|
30698
|
-
|
|
30699
|
-
|
|
30700
|
-
|
|
30701
|
-
|
|
30702
|
-
|
|
31011
|
+
function sectionTitle(input, section) {
|
|
31012
|
+
return input.texts.text({ key: `summary.section.${section.id}` });
|
|
31013
|
+
}
|
|
31014
|
+
/** A section's words: its block, and the body a per-section row sends. `null` = nothing to say. */
|
|
31015
|
+
function wordsOf(input, section) {
|
|
31016
|
+
const title = sectionTitle(input, section);
|
|
31017
|
+
if (section.status === "unavailable") {
|
|
31018
|
+
const sentence = input.texts.text({
|
|
31019
|
+
key: "summary.section.unavailable",
|
|
31020
|
+
vars: { section: title }
|
|
31021
|
+
});
|
|
31022
|
+
return {
|
|
31023
|
+
block: sentence,
|
|
31024
|
+
body: sentence
|
|
31025
|
+
};
|
|
31026
|
+
}
|
|
30703
31027
|
if (section.items.length === 0) return null;
|
|
30704
|
-
const lines = section.items.map((item) => `• ${lineOf(input, item)}`);
|
|
30705
|
-
return
|
|
31028
|
+
const lines = section.items.map((item) => `• ${lineOf(input, item)}`).join("\n");
|
|
31029
|
+
return {
|
|
31030
|
+
block: `${title}:\n${lines}`,
|
|
31031
|
+
body: lines
|
|
31032
|
+
};
|
|
30706
31033
|
}
|
|
30707
31034
|
function stateSummaryText(input) {
|
|
30708
31035
|
const sectionVars = {};
|
|
30709
|
-
const
|
|
31036
|
+
const sections = [];
|
|
30710
31037
|
for (const section of input.summary.sections) {
|
|
30711
|
-
const
|
|
30712
|
-
sectionVars[NC_STATE_SECTION_VAR[section.id]] = block ?? "";
|
|
30713
|
-
if (
|
|
31038
|
+
const words = wordsOf(input, section);
|
|
31039
|
+
sectionVars[NC_STATE_SECTION_VAR[section.id]] = words?.block ?? "";
|
|
31040
|
+
if (words === null) continue;
|
|
31041
|
+
sections.push({
|
|
31042
|
+
id: section.id,
|
|
31043
|
+
title: sectionTitle(input, section),
|
|
31044
|
+
text: words.block,
|
|
31045
|
+
body: words.body,
|
|
31046
|
+
count: section.items.length,
|
|
31047
|
+
status: section.status
|
|
31048
|
+
});
|
|
30714
31049
|
}
|
|
30715
31050
|
return {
|
|
30716
|
-
text:
|
|
31051
|
+
text: sections.map((section) => section.text).join("\n"),
|
|
30717
31052
|
count: input.summary.itemCount,
|
|
30718
|
-
sectionVars
|
|
31053
|
+
sectionVars,
|
|
31054
|
+
sections
|
|
30719
31055
|
};
|
|
30720
31056
|
}
|
|
30721
31057
|
//#endregion
|
|
@@ -31152,6 +31488,40 @@ var NcStateSummaryStampStore = class {
|
|
|
31152
31488
|
//#endregion
|
|
31153
31489
|
//#region src/notification-center/state-summary/system-state-ledger.ts
|
|
31154
31490
|
/**
|
|
31491
|
+
* NcSystemStateLedger — what is STILL TRUE among the event-driven system facts
|
|
31492
|
+
* no other ledger holds (D647).
|
|
31493
|
+
*
|
|
31494
|
+
* Battery, consumable and tank episodes already have a durable row
|
|
31495
|
+
* (`alert-store.ts`), and device/node liveness has its own
|
|
31496
|
+
* (`liveness-ledger.ts`). The rest of the summarisable kinds are EVENTS the
|
|
31497
|
+
* centre hears once — a stream going offline, a device switched off, a camera
|
|
31498
|
+
* detecting nothing, an update becoming available — and nothing remembered
|
|
31499
|
+
* that they were still true. A state summary is a claim about now, so this
|
|
31500
|
+
* ledger keeps one row per OPEN state, fed by the same intake the instant
|
|
31501
|
+
* rules read, and removes it when the closing event arrives:
|
|
31502
|
+
*
|
|
31503
|
+
* | opens | closes |
|
|
31504
|
+
* | --- | --- |
|
|
31505
|
+
* | `stream-offline` | `stream-online` (same subject) |
|
|
31506
|
+
* | `device-disabled` | `device-enabled` (same subject) |
|
|
31507
|
+
* | `detection-blind` | a DETECTION on that camera — the only evidence of recovery there is |
|
|
31508
|
+
* | `node-inference-unavailable`, `addon-crash-loop` | the node's next liveness edge, or a server update on it |
|
|
31509
|
+
* | `addon-update-available` (per package, per node) | `addon.updated` for that package on that node |
|
|
31510
|
+
* | `server-` / `wrapper-update-available` | `server-updated` on that node |
|
|
31511
|
+
*
|
|
31512
|
+
* Two kinds have no recovery event at all (`node-inference-unavailable`,
|
|
31513
|
+
* `addon-crash-loop`): the orchestrator drops such a node until it comes back,
|
|
31514
|
+
* and a crash-looped runner stays down until something respawns it — a node
|
|
31515
|
+
* restart or an update. The closing edges above are those respawns. A newer
|
|
31516
|
+
* `update-available` for the same package REPLACES the row.
|
|
31517
|
+
*
|
|
31518
|
+
* Durable (write-behind), so a restart does not forget an outage it heard
|
|
31519
|
+
* about. It is fed only on the evaluating node, like the liveness ledger. The
|
|
31520
|
+
* summary reads the RAM mirror only — never SQLite on its schedule (D447) —
|
|
31521
|
+
* and a mirror that has not loaded answers `null`: the sections it feeds say
|
|
31522
|
+
* "unavailable", never "none" (D49).
|
|
31523
|
+
*/
|
|
31524
|
+
/**
|
|
31155
31525
|
* @durable class=ledger owner=notification-center
|
|
31156
31526
|
* 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"
|
|
31157
31527
|
* retention="none — a row lives exactly as long as its state is open. Bounded by the installation: streams, devices, nodes and installed packages."
|
|
@@ -31313,9 +31683,7 @@ var NcSystemStateLedger = class {
|
|
|
31313
31683
|
}
|
|
31314
31684
|
/** A detection on a camera: it is not blind. Called for every detection — a Map lookup. */
|
|
31315
31685
|
observeDetection(deviceId) {
|
|
31316
|
-
|
|
31317
|
-
if (this.deps.rows.get(key) === void 0) return;
|
|
31318
|
-
this.close(key, "detection");
|
|
31686
|
+
this.close(`detection-blind:${String(deviceId)}`, "detection");
|
|
31319
31687
|
}
|
|
31320
31688
|
/**
|
|
31321
31689
|
* Any `stream.online`, whatever its reason (D647 review I1). The intake gate
|
|
@@ -31330,7 +31698,7 @@ var NcSystemStateLedger = class {
|
|
|
31330
31698
|
const keep = new Set(list.rows.map((pkg) => updateStateKey(list.target, pkg.nodeId, pkg.packageName)));
|
|
31331
31699
|
for (const nodeId of list.nodeIds) {
|
|
31332
31700
|
const prefix = `update:${list.target}:${nodeId}:`;
|
|
31333
|
-
|
|
31701
|
+
this.closeWhere(`update-list:${list.target}:${nodeId}`, (row) => row.key.startsWith(prefix) && !keep.has(row.key), "update-list");
|
|
31334
31702
|
}
|
|
31335
31703
|
for (const pkg of list.rows) this.openUpdate(list.target, pkg, atMs);
|
|
31336
31704
|
}
|
|
@@ -31340,11 +31708,7 @@ var NcSystemStateLedger = class {
|
|
|
31340
31708
|
* on that node.
|
|
31341
31709
|
*/
|
|
31342
31710
|
observeAddonStarted(addonId, nodeId) {
|
|
31343
|
-
|
|
31344
|
-
if (row.kind !== "addon-crash-loop") continue;
|
|
31345
|
-
if (row.nodeId !== nodeId) continue;
|
|
31346
|
-
if ((row.params["addons"] ?? "").split(",").map((id) => id.trim()).includes(addonId)) this.close(row.key, "addon-started");
|
|
31347
|
-
}
|
|
31711
|
+
this.closeWhere(`addon-started:${nodeId}:${addonId}`, (row) => row.kind === "addon-crash-loop" && row.nodeId === nodeId && (row.params["addons"] ?? "").split(",").map((id) => id.trim()).includes(addonId), "addon-started");
|
|
31348
31712
|
}
|
|
31349
31713
|
/**
|
|
31350
31714
|
* The balancer placed a camera on a node (not pinned): placement skips a
|
|
@@ -31400,11 +31764,31 @@ var NcSystemStateLedger = class {
|
|
|
31400
31764
|
}
|
|
31401
31765
|
});
|
|
31402
31766
|
}
|
|
31767
|
+
/**
|
|
31768
|
+
* Close one state: the row held now (a loaded one, or one opened in the boot
|
|
31769
|
+
* window), and — deferred while the boot window is open — the STORED row
|
|
31770
|
+
* the window cannot see yet (D698 review H1).
|
|
31771
|
+
*/
|
|
31403
31772
|
close(key, by) {
|
|
31773
|
+
this.closeIfHeld(key, by, false);
|
|
31774
|
+
this.deps.rows.deferWhileOpen(`close:${key}`, () => this.closeIfHeld(key, by, true) ? 1 : 0);
|
|
31775
|
+
}
|
|
31776
|
+
/** {@link close} for every row a predicate names — now, and against the loaded rows. */
|
|
31777
|
+
closeWhere(key, names, by) {
|
|
31778
|
+
const run = (replayed) => {
|
|
31779
|
+
let closed = 0;
|
|
31780
|
+
for (const row of this.deps.rows.all()) if (names(row) && this.closeIfHeld(row.key, by, replayed)) closed += 1;
|
|
31781
|
+
return closed;
|
|
31782
|
+
};
|
|
31783
|
+
run(false);
|
|
31784
|
+
this.deps.rows.deferWhileOpen(`where:${key}`, () => run(true));
|
|
31785
|
+
}
|
|
31786
|
+
/** Close the row if one is held; `true` when it was. */
|
|
31787
|
+
closeIfHeld(key, by, replayed) {
|
|
31404
31788
|
const held = this.deps.rows.get(key);
|
|
31405
|
-
if (held === void 0) return;
|
|
31789
|
+
if (held === void 0) return false;
|
|
31406
31790
|
this.deps.rows.remove(key);
|
|
31407
|
-
|
|
31791
|
+
const line = {
|
|
31408
31792
|
...held.deviceId !== void 0 ? { tags: { deviceId: held.deviceId } } : {},
|
|
31409
31793
|
meta: {
|
|
31410
31794
|
key,
|
|
@@ -31412,49 +31796,78 @@ var NcSystemStateLedger = class {
|
|
|
31412
31796
|
by,
|
|
31413
31797
|
since: held.since
|
|
31414
31798
|
}
|
|
31415
|
-
}
|
|
31799
|
+
};
|
|
31800
|
+
if (replayed) {
|
|
31801
|
+
this.deps.logger?.info("system state closed by an event heard before the load", line);
|
|
31802
|
+
return true;
|
|
31803
|
+
}
|
|
31804
|
+
this.deps.logger?.debug("system state closed", line);
|
|
31805
|
+
return true;
|
|
31416
31806
|
}
|
|
31417
31807
|
closeNodeFaults(nodeId, by) {
|
|
31418
|
-
|
|
31419
|
-
if (row.nodeId !== nodeId) continue;
|
|
31420
|
-
if (row.kind === "node-inference-unavailable" || row.kind === "addon-crash-loop") this.close(row.key, by);
|
|
31421
|
-
}
|
|
31808
|
+
this.closeWhere(`node-faults:${nodeId}`, (row) => row.nodeId === nodeId && (row.kind === "node-inference-unavailable" || row.kind === "addon-crash-loop"), by);
|
|
31422
31809
|
}
|
|
31423
31810
|
closeServerUpdate(nodeId) {
|
|
31424
|
-
|
|
31425
|
-
if ((row.nodeId ?? HUB_NODE) !== nodeId) continue;
|
|
31426
|
-
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");
|
|
31427
|
-
}
|
|
31811
|
+
this.closeWhere(`server-update:${nodeId}`, (row) => (row.nodeId ?? HUB_NODE) === nodeId && (row.kind === "server-update-available" || row.kind === "wrapper-update-available" || row.kind === "addon-crash-loop" || row.kind === "node-inference-unavailable"), "server-updated");
|
|
31428
31812
|
}
|
|
31429
31813
|
};
|
|
31430
31814
|
/** The durable home of the rows. */
|
|
31431
31815
|
var NcSystemStateStore = class {
|
|
31432
31816
|
ledger;
|
|
31817
|
+
/**
|
|
31818
|
+
* What the intake opened or closed before the boot load answered (D698,
|
|
31819
|
+
* `boot-window-rows.ts`). A stored row of the same KIND keeps its own
|
|
31820
|
+
* `since`: a stream re-announced offline at boot was not offline "since the
|
|
31821
|
+
* restart".
|
|
31822
|
+
*/
|
|
31823
|
+
bootWindow;
|
|
31433
31824
|
constructor(deps) {
|
|
31434
31825
|
this.ledger = new DurableLedger({
|
|
31435
31826
|
spec: NC_SYSTEM_STATE_SPEC,
|
|
31436
31827
|
store: deps.store,
|
|
31437
31828
|
logger: deps.logger
|
|
31438
31829
|
});
|
|
31830
|
+
this.bootWindow = new BootWindowRows({
|
|
31831
|
+
ledger: this.ledger,
|
|
31832
|
+
keyOf: (row) => row.key,
|
|
31833
|
+
deviceIdOf: (row) => row.deviceId,
|
|
31834
|
+
logger: deps.logger,
|
|
31835
|
+
merge: (stored, provisional) => {
|
|
31836
|
+
if (stored === void 0 || stored.kind !== provisional.kind) return provisional;
|
|
31837
|
+
const merged = {
|
|
31838
|
+
...provisional,
|
|
31839
|
+
since: stored.since
|
|
31840
|
+
};
|
|
31841
|
+
return isDeepStrictEqual(merged, stored) ? stored : merged;
|
|
31842
|
+
}
|
|
31843
|
+
});
|
|
31439
31844
|
}
|
|
31440
31845
|
static declare(store) {
|
|
31441
31846
|
return DurableLedger.declare(store, NC_SYSTEM_STATE_SPEC);
|
|
31442
31847
|
}
|
|
31443
31848
|
/** Boot reseed. A failed load keeps the mirror and leaves `answered` false. */
|
|
31444
31849
|
async load() {
|
|
31445
|
-
|
|
31850
|
+
const loaded = (await this.ledger.load()).length;
|
|
31851
|
+
this.bootWindow.settle();
|
|
31852
|
+
return loaded;
|
|
31446
31853
|
}
|
|
31447
31854
|
port() {
|
|
31448
31855
|
return {
|
|
31449
31856
|
get: (key) => this.ledger.get(key),
|
|
31450
31857
|
put: (row) => {
|
|
31858
|
+
if (this.bootWindow.open()) {
|
|
31859
|
+
this.bootWindow.stageProvisional(row);
|
|
31860
|
+
return;
|
|
31861
|
+
}
|
|
31451
31862
|
this.ledger.put(row).catch(() => void 0);
|
|
31452
31863
|
},
|
|
31453
31864
|
remove: (key) => {
|
|
31865
|
+
if (this.bootWindow.open()) this.bootWindow.noteRemoved(key);
|
|
31454
31866
|
this.ledger.forget(key);
|
|
31455
31867
|
},
|
|
31456
31868
|
all: () => this.ledger.snapshot(),
|
|
31457
|
-
answered: () => this.ledger.hasAnswered()
|
|
31869
|
+
answered: () => this.ledger.hasAnswered(),
|
|
31870
|
+
deferWhileOpen: (key, replay) => this.bootWindow.deferWhileOpen(key, replay)
|
|
31458
31871
|
};
|
|
31459
31872
|
}
|
|
31460
31873
|
};
|
|
@@ -31579,8 +31992,21 @@ var NcStateSummaryService = class {
|
|
|
31579
31992
|
const nowMs = this.deps.now();
|
|
31580
31993
|
const summary = await this.collect(rule, nowMs);
|
|
31581
31994
|
const text = this.word(summary, nowMs);
|
|
31995
|
+
const split = systemEventSummarySplitOf(rule);
|
|
31996
|
+
const empty = isEmptyStateSummary(summary);
|
|
31582
31997
|
return {
|
|
31583
|
-
empty
|
|
31998
|
+
empty,
|
|
31999
|
+
split,
|
|
32000
|
+
rows: empty ? [] : split === "section" ? text.sections.map((section) => ({
|
|
32001
|
+
section: section.id,
|
|
32002
|
+
title: section.title,
|
|
32003
|
+
text: section.body,
|
|
32004
|
+
count: section.count
|
|
32005
|
+
})) : [{
|
|
32006
|
+
title: text.sections.map((section) => section.title).join(", "),
|
|
32007
|
+
text: text.text,
|
|
32008
|
+
count: text.count
|
|
32009
|
+
}],
|
|
31584
32010
|
count: text.count,
|
|
31585
32011
|
text: text.text,
|
|
31586
32012
|
sections: summary.sections.map((section) => ({
|
|
@@ -31593,12 +32019,84 @@ var NcStateSummaryService = class {
|
|
|
31593
32019
|
excludedGroups: [...summary.excludedGroups]
|
|
31594
32020
|
};
|
|
31595
32021
|
}
|
|
31596
|
-
|
|
32022
|
+
/**
|
|
32023
|
+
* TEST the rule (D698, `nc.testRule`): the summary it would send NOW,
|
|
32024
|
+
* ENQUEUED through the ordinary outbox under a test record id — split as the
|
|
32025
|
+
* rule is. The per-camera mute is bypassed, as every rule test bypasses it,
|
|
32026
|
+
* and a summary has no cooldown to bypass. Nothing to report sends nothing
|
|
32027
|
+
* and SAYS so, per section.
|
|
32028
|
+
*/
|
|
32029
|
+
async test(rule) {
|
|
32030
|
+
const nowMs = this.deps.now();
|
|
32031
|
+
const summary = await this.collect(rule, nowMs, () => false);
|
|
32032
|
+
const split = systemEventSummarySplitOf(rule);
|
|
32033
|
+
const recordId = stateSummaryTestRecordId(rule.id, nowMs);
|
|
32034
|
+
const sections = summary.sections.map((section) => ({
|
|
32035
|
+
id: section.id,
|
|
32036
|
+
status: section.status,
|
|
32037
|
+
items: section.items.length,
|
|
32038
|
+
...section.reason !== void 0 ? { reason: section.reason } : {}
|
|
32039
|
+
}));
|
|
32040
|
+
const meta = {
|
|
32041
|
+
ruleId: rule.id,
|
|
32042
|
+
rule: rule.name,
|
|
32043
|
+
split,
|
|
32044
|
+
bypassMute: true
|
|
32045
|
+
};
|
|
32046
|
+
if (isEmptyStateSummary(summary)) {
|
|
32047
|
+
this.deps.logger.info("state summary TEST has nothing to report — not sent", { meta: {
|
|
32048
|
+
...meta,
|
|
32049
|
+
dropped: summary.dropped
|
|
32050
|
+
} });
|
|
32051
|
+
return {
|
|
32052
|
+
recordId,
|
|
32053
|
+
split,
|
|
32054
|
+
nothingToReport: true,
|
|
32055
|
+
sections,
|
|
32056
|
+
rows: []
|
|
32057
|
+
};
|
|
32058
|
+
}
|
|
32059
|
+
const subjects = stateSummarySubjects({
|
|
32060
|
+
rule,
|
|
32061
|
+
summary,
|
|
32062
|
+
text: this.word(summary, nowMs),
|
|
32063
|
+
window: {
|
|
32064
|
+
startMs: nowMs,
|
|
32065
|
+
endMs: nowMs
|
|
32066
|
+
},
|
|
32067
|
+
split,
|
|
32068
|
+
recordBase: recordId
|
|
32069
|
+
});
|
|
32070
|
+
const rows = [];
|
|
32071
|
+
for (const subject of subjects) {
|
|
32072
|
+
const enqueued = await this.deps.enqueue(rule, subject, "synthetic");
|
|
32073
|
+
const section = subject.systemEvent?.data["section"];
|
|
32074
|
+
rows.push({
|
|
32075
|
+
recordId: subject.recordId,
|
|
32076
|
+
...typeof section === "string" ? { section } : {},
|
|
32077
|
+
enqueued: enqueued.inserted,
|
|
32078
|
+
targetIds: [...enqueued.targetIds]
|
|
32079
|
+
});
|
|
32080
|
+
}
|
|
32081
|
+
this.deps.logger.info("state summary TEST enqueued", { meta: {
|
|
32082
|
+
...meta,
|
|
32083
|
+
items: summary.itemCount,
|
|
32084
|
+
rows: rows.map((row) => `${row.recordId}:${String(row.enqueued)}`)
|
|
32085
|
+
} });
|
|
32086
|
+
return {
|
|
32087
|
+
recordId,
|
|
32088
|
+
split,
|
|
32089
|
+
nothingToReport: false,
|
|
32090
|
+
sections,
|
|
32091
|
+
rows
|
|
32092
|
+
};
|
|
32093
|
+
}
|
|
32094
|
+
async collect(rule, nowMs, isMuted = this.deps.isMuted) {
|
|
31597
32095
|
const nodes = await this.nodeView();
|
|
31598
32096
|
return collectStateSummary({
|
|
31599
32097
|
rule,
|
|
31600
32098
|
nowMs,
|
|
31601
|
-
isMuted
|
|
32099
|
+
isMuted,
|
|
31602
32100
|
sources: stateSources({
|
|
31603
32101
|
alerts: this.deps.alerts,
|
|
31604
32102
|
liveness: this.deps.liveness,
|
|
@@ -31634,17 +32132,24 @@ var NcStateSummaryService = class {
|
|
|
31634
32132
|
});
|
|
31635
32133
|
}
|
|
31636
32134
|
async deliver(rule, summary, window) {
|
|
31637
|
-
const
|
|
32135
|
+
const text = this.word(summary, window.endMs);
|
|
32136
|
+
const subjects = stateSummarySubjects({
|
|
31638
32137
|
rule,
|
|
31639
32138
|
summary,
|
|
31640
|
-
text
|
|
31641
|
-
window
|
|
32139
|
+
text,
|
|
32140
|
+
window,
|
|
32141
|
+
split: systemEventSummarySplitOf(rule)
|
|
31642
32142
|
});
|
|
31643
|
-
if (
|
|
31644
|
-
this.deps.logger.warn("state summary
|
|
32143
|
+
if (subjects.length === 0) {
|
|
32144
|
+
this.deps.logger.warn("state summary produced no row — not sent", { meta: {
|
|
32145
|
+
ruleId: rule.id,
|
|
32146
|
+
sections: text.sections.length
|
|
32147
|
+
} });
|
|
31645
32148
|
return 0;
|
|
31646
32149
|
}
|
|
31647
|
-
|
|
32150
|
+
let inserted = 0;
|
|
32151
|
+
for (const subject of subjects) inserted += (await this.deps.enqueue(rule, subject, "system")).inserted;
|
|
32152
|
+
return inserted;
|
|
31648
32153
|
}
|
|
31649
32154
|
};
|
|
31650
32155
|
//#endregion
|
|
@@ -33370,7 +33875,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
33370
33875
|
livenessHoldoffMs: deps.livenessHoldoffMs ?? DEVICE_LIVENESS_HOLDOFF_MS,
|
|
33371
33876
|
isMuted: (deviceId) => this.deviceMutes.isMuted(deviceId),
|
|
33372
33877
|
texts: () => this.texts,
|
|
33373
|
-
enqueue: (rule, subject) => this.enqueueStateSummary(rule, subject)
|
|
33878
|
+
enqueue: (rule, subject, origin) => this.enqueueStateSummary(rule, subject, origin)
|
|
33374
33879
|
});
|
|
33375
33880
|
this.confirmGate = deps.generateVision !== void 0 ? new NcConfirmGate({
|
|
33376
33881
|
logger: this.logger.child("confirm"),
|
|
@@ -35911,30 +36416,42 @@ var NotificationCenter = class NotificationCenter {
|
|
|
35911
36416
|
await this.enqueuePickerTargets(event, reached, outcomes);
|
|
35912
36417
|
return report({ candidates: candidates.length });
|
|
35913
36418
|
}
|
|
36419
|
+
/** TEST a state-summary rule (D698, `nc.testRule`) — see `NcStateSummaryService.test`. */
|
|
36420
|
+
testStateSummary(rule) {
|
|
36421
|
+
return this.stateSummary.test(rule);
|
|
36422
|
+
}
|
|
35914
36423
|
/**
|
|
35915
|
-
* Enqueue ONE state summary (D647) through the rule's ordinary rows: its
|
|
36424
|
+
* Enqueue ONE state summary row (D647) through the rule's ordinary rows: its
|
|
35916
36425
|
* targets, its users (admins only — the row files under the system device),
|
|
35917
36426
|
* its template, and the drain's snooze gate and retries. The record id is
|
|
35918
|
-
* the dedup key, so a window
|
|
36427
|
+
* the dedup key, so a window — or each section of it (D698) — is enqueued
|
|
36428
|
+
* once whatever the restarts. A TEST's rows carry the `synthetic` origin, so
|
|
36429
|
+
* they are marked as a test exactly like every other rule test's.
|
|
35919
36430
|
*/
|
|
35920
|
-
async enqueueStateSummary(rule, subject) {
|
|
35921
|
-
const appended = withSummaryBody(rule);
|
|
36431
|
+
async enqueueStateSummary(rule, subject, origin) {
|
|
36432
|
+
const appended = withSummaryBody(rule, stateSummarySectionOf(subject));
|
|
35922
36433
|
if (appended !== null) this.logger.warn("state summary rule body names no summary content — {{summaryText}} appended", { meta: {
|
|
35923
36434
|
ruleId: rule.id,
|
|
35924
36435
|
rule: rule.name
|
|
35925
36436
|
} });
|
|
35926
36437
|
const sent = appended ?? rule;
|
|
35927
36438
|
const userTargets = await this.resolveUserTargets(sent, subject.deviceId);
|
|
35928
|
-
const entries = this.buildEntries(sent, subject, "system-event", ["systemEvent"], userTargets,
|
|
36439
|
+
const entries = this.buildEntries(sent, subject, "system-event", ["systemEvent"], userTargets, origin);
|
|
35929
36440
|
if (entries.length === 0) {
|
|
35930
36441
|
this.logger.warn("state summary reached NO target — its users are not admins, and it has no targets", { meta: {
|
|
35931
36442
|
ruleId: rule.id,
|
|
35932
36443
|
rule: rule.name,
|
|
35933
36444
|
targetUsers: rule.targetUsers ?? []
|
|
35934
36445
|
} });
|
|
35935
|
-
return
|
|
36446
|
+
return {
|
|
36447
|
+
inserted: 0,
|
|
36448
|
+
targetIds: []
|
|
36449
|
+
};
|
|
35936
36450
|
}
|
|
35937
|
-
return
|
|
36451
|
+
return {
|
|
36452
|
+
inserted: await this.outbox.enqueue(entries),
|
|
36453
|
+
targetIds: entries.map((entry) => entry.targetId)
|
|
36454
|
+
};
|
|
35938
36455
|
}
|
|
35939
36456
|
/**
|
|
35940
36457
|
* The rules a trigger may fire, which is `listEnabled(delivery)` plus and
|
|
@@ -36648,9 +37165,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
36648
37165
|
*
|
|
36649
37166
|
* A seed is INFO and a repeat is DEBUG on purpose: seeding happens once per
|
|
36650
37167
|
* subject per lifetime of the ledger, and it is exactly what an operator
|
|
36651
|
-
* reading the logs after a restart needs to see ("it
|
|
36652
|
-
*
|
|
36653
|
-
*
|
|
37168
|
+
* reading the logs after a restart needs to see ("it did not announce
|
|
37169
|
+
* them"). A seed heard BEFORE the boot load is provisional (D698): the load
|
|
37170
|
+
* keeps the stored row when it holds the same state, and its `boot window
|
|
37171
|
+
* settled` line says how many it kept and wrote. A no-flip is ordinary bus
|
|
37172
|
+
* chatter — two categories describing one edge — and would be noise at info.
|
|
36654
37173
|
*/
|
|
36655
37174
|
reportLivenessDrop(incoming, verdict) {
|
|
36656
37175
|
const dropped = discardedLivenessEvent(incoming, verdict);
|
|
@@ -88133,6 +88652,18 @@ function isRecord(x) {
|
|
|
88133
88652
|
}
|
|
88134
88653
|
//#endregion
|
|
88135
88654
|
//#region src/pipeline-analytics/store/track-alias-store.ts
|
|
88655
|
+
/**
|
|
88656
|
+
* The alias collection.
|
|
88657
|
+
*
|
|
88658
|
+
* @durable class=ledger owner=pipeline-analytics
|
|
88659
|
+
* write="one row per fused piece (D683): aliasId = the absorbed provisional
|
|
88660
|
+
* id, trackId = its survivor; rewritten when the survivor is itself
|
|
88661
|
+
* absorbed by a later fusion"
|
|
88662
|
+
* retention="TRACK-OWNED by its survivor through the declared `trackId`
|
|
88663
|
+
* column: aliases leave with the survivor in the retention cascade, and
|
|
88664
|
+
* widen that cascade (`expandLeafIds`) to rows still keyed by an
|
|
88665
|
+
* absorbed id"
|
|
88666
|
+
*/
|
|
88136
88667
|
var TRACK_ALIASES_COLLECTION = "pipeline-analytics:track-aliases";
|
|
88137
88668
|
var TRACK_ALIAS_COLUMNS = [
|
|
88138
88669
|
{
|
|
@@ -88315,6 +88846,20 @@ var TrackAliasStore = class {
|
|
|
88315
88846
|
dropped: 0
|
|
88316
88847
|
}];
|
|
88317
88848
|
}
|
|
88849
|
+
/**
|
|
88850
|
+
* The orphan audit's reclaim of ONE alias row: the row AND the `recent`
|
|
88851
|
+
* entry that would otherwise keep resolving it for up to an hour. `true`
|
|
88852
|
+
* only when a row was actually deleted. (`misses` needs nothing: a deleted
|
|
88853
|
+
* alias IS a non-alias, which is what a miss records.)
|
|
88854
|
+
*/
|
|
88855
|
+
async deleteAlias(aliasId) {
|
|
88856
|
+
const { deleted } = await this.store.deleteWhere.mutate({
|
|
88857
|
+
collection: TRACK_ALIASES_COLLECTION,
|
|
88858
|
+
filter: { where: { id: aliasId } }
|
|
88859
|
+
});
|
|
88860
|
+
this.recent.delete(aliasId);
|
|
88861
|
+
return deleted > 0;
|
|
88862
|
+
}
|
|
88318
88863
|
async deleteByTracks(trackIds) {
|
|
88319
88864
|
const { deleted } = await this.store.deleteWhere.mutate({
|
|
88320
88865
|
collection: TRACK_ALIASES_COLLECTION,
|
|
@@ -89241,7 +89786,8 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
89241
89786
|
stoppedBy,
|
|
89242
89787
|
nextCursor: offset
|
|
89243
89788
|
} });
|
|
89244
|
-
|
|
89789
|
+
const freedNothing = scope.sizeField === null ? tally.reclaimed === 0 : tally.bytesReclaimed === 0;
|
|
89790
|
+
if (limits.mode === "reclaim" && tally.orphans > 0 && freedNothing) deps.logger.warn("orphan audit: reclaim pass freed no bytes against a standing backlog", { meta: {
|
|
89245
89791
|
collection: scope.collection,
|
|
89246
89792
|
shard: scope.shard?.value ?? null,
|
|
89247
89793
|
orphans: tally.orphans,
|
|
@@ -89355,6 +89901,40 @@ var ORPHAN_AUDIT_DEFAULTS = {
|
|
|
89355
89901
|
pageSize: OrphanAuditPageSizeSchema.parse(void 0)
|
|
89356
89902
|
};
|
|
89357
89903
|
var RAW_DELETE_COLLECTIONS = new Set(["pipeline-analytics:object-events", "pipeline-analytics:plates"]);
|
|
89904
|
+
/**
|
|
89905
|
+
* The `deleteRow` port: ONE row by its primary key `id` (what every
|
|
89906
|
+
* raw-delete collection above declares), answering whether a row went.
|
|
89907
|
+
*
|
|
89908
|
+
* Through `deleteWhere`, because it is the delete that SAYS: `delete`'s output
|
|
89909
|
+
* is `z.void()`, and reading it as `!== undefined` answered `false` for every
|
|
89910
|
+
* row it really deleted — the audit counted every plates/object-events reclaim
|
|
89911
|
+
* as "found nothing", and the per-scope reclaim ceiling never tripped.
|
|
89912
|
+
*/
|
|
89913
|
+
function settingsRowDeleter(store) {
|
|
89914
|
+
return async (collection, key) => {
|
|
89915
|
+
const { deleted } = await store.deleteWhere.mutate({
|
|
89916
|
+
collection,
|
|
89917
|
+
filter: { where: { id: key } }
|
|
89918
|
+
});
|
|
89919
|
+
return deleted > 0;
|
|
89920
|
+
};
|
|
89921
|
+
}
|
|
89922
|
+
/**
|
|
89923
|
+
* The `deleteRow` port AS PRODUCTION WIRES IT: {@link settingsRowDeleter} over
|
|
89924
|
+
* a store that may not be bound yet. Unbound THROWS — the engine counts that as
|
|
89925
|
+
* `reclaimFailed` and keeps the row — rather than answering "nothing deleted",
|
|
89926
|
+
* which is indistinguishable from a row already gone. A named factory so the
|
|
89927
|
+
* wiring itself is under test (D695 T7 review M1: the inline wiring could be
|
|
89928
|
+
* reverted to the original bug with every spec green);
|
|
89929
|
+
* `scripts/check-orphan-reclaimers.ts` checks `index.ts` uses it.
|
|
89930
|
+
*/
|
|
89931
|
+
function settingsRowDeleterOf(getStore) {
|
|
89932
|
+
return async (collection, key) => {
|
|
89933
|
+
const store = getStore();
|
|
89934
|
+
if (store === null) throw new Error("orphan audit: settings store not bound");
|
|
89935
|
+
return await settingsRowDeleter(store)(collection, key);
|
|
89936
|
+
};
|
|
89937
|
+
}
|
|
89358
89938
|
function buildOrphanReclaimer(ports) {
|
|
89359
89939
|
return async (collection, key) => {
|
|
89360
89940
|
if (collection === MEDIA_COLLECTION) {
|
|
@@ -89378,6 +89958,10 @@ function buildOrphanReclaimer(ports) {
|
|
|
89378
89958
|
bytes: null
|
|
89379
89959
|
};
|
|
89380
89960
|
}
|
|
89961
|
+
if (collection === TRACK_ALIASES_COLLECTION) return {
|
|
89962
|
+
rows: await ports.deleteTrackAlias(key) ? 1 : 0,
|
|
89963
|
+
bytes: null
|
|
89964
|
+
};
|
|
89381
89965
|
if (RAW_DELETE_COLLECTIONS.has(collection)) return {
|
|
89382
89966
|
rows: await ports.deleteRow(collection, key) ? 1 : 0,
|
|
89383
89967
|
bytes: null
|
|
@@ -99747,6 +100331,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
99747
100331
|
},
|
|
99748
100332
|
listTrackGroups: (query) => center.listTrackGroups(query),
|
|
99749
100333
|
injectTestEvent: (input) => center.injectTestEvent(input, { getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }) }),
|
|
100334
|
+
testStateSummary: (rule) => center.testStateSummary(rule),
|
|
99750
100335
|
timelapse: {
|
|
99751
100336
|
store: center.timelapseStore,
|
|
99752
100337
|
getRecordingConfig: (deviceId) => api.recordingArchive.getDeviceConfig.query({ deviceId }).catch(() => null),
|
|
@@ -107505,10 +108090,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
107505
108090
|
bytes: count.bytes
|
|
107506
108091
|
};
|
|
107507
108092
|
},
|
|
107508
|
-
|
|
107509
|
-
|
|
107510
|
-
|
|
107511
|
-
|
|
108093
|
+
deleteTrackAlias: async (aliasId) => {
|
|
108094
|
+
const aliasStore = this.trackAliasStore;
|
|
108095
|
+
if (aliasStore === null) throw new Error("track alias store not bound");
|
|
108096
|
+
return await aliasStore.deleteAlias(aliasId);
|
|
108097
|
+
},
|
|
108098
|
+
deleteRow: settingsRowDeleterOf(() => this.settingsStore),
|
|
107512
108099
|
logger: this.ctx.logger
|
|
107513
108100
|
}),
|
|
107514
108101
|
now: () => Date.now(),
|