@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
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_clip_model_registry = require("../clip-model-registry-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-B3M4ZI17.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_clip_model_registry.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -13492,6 +13492,8 @@ var en_default = {
|
|
|
13492
13492
|
"summary.body.empty": "nothing was seen in this window",
|
|
13493
13493
|
"system.state-summary.title": "{{rule}}",
|
|
13494
13494
|
"system.state-summary.body": "{{summaryText}}",
|
|
13495
|
+
"system.state-summary.section.title": "{{rule}} — {{summarySection}}",
|
|
13496
|
+
"system.state-summary.section.body": "{{summaryText}}",
|
|
13495
13497
|
"summary.section.device-state": "Devices offline or switched off",
|
|
13496
13498
|
"summary.section.streams": "Streams offline",
|
|
13497
13499
|
"summary.section.battery": "Low batteries",
|
|
@@ -13820,6 +13822,8 @@ var it_default = {
|
|
|
13820
13822
|
"summary.body.empty": "nessun evento in questa finestra",
|
|
13821
13823
|
"system.state-summary.title": "{{rule}}",
|
|
13822
13824
|
"system.state-summary.body": "{{summaryText}}",
|
|
13825
|
+
"system.state-summary.section.title": "{{rule}} — {{summarySection}}",
|
|
13826
|
+
"system.state-summary.section.body": "{{summaryText}}",
|
|
13823
13827
|
"summary.section.device-state": "Dispositivi offline o spenti",
|
|
13824
13828
|
"summary.section.streams": "Stream offline",
|
|
13825
13829
|
"summary.section.battery": "Batterie scariche",
|
|
@@ -15765,6 +15769,38 @@ function planRuleTest(input) {
|
|
|
15765
15769
|
};
|
|
15766
15770
|
}
|
|
15767
15771
|
//#endregion
|
|
15772
|
+
//#region src/notification-center/state-summary/state-summary-test.ts
|
|
15773
|
+
/**
|
|
15774
|
+
* What TESTING a state-summary rule answers (D698, `nc.testRule`).
|
|
15775
|
+
*
|
|
15776
|
+
* A summary rule sends nothing per event (D647), so the per-event test the
|
|
15777
|
+
* other rules get — one synthetic event, injected — can never deliver for it.
|
|
15778
|
+
* Its test builds the summary NOW and enqueues it; this is the report: the
|
|
15779
|
+
* rows it enqueued (one, or one per section) and every section's read, so
|
|
15780
|
+
* "nothing to report" is an answer with its reason per section, never a
|
|
15781
|
+
* silence.
|
|
15782
|
+
*/
|
|
15783
|
+
var NcStateSummaryTestSchema = require_clip_model_registry.object({
|
|
15784
|
+
/** The test's record id — a per-section row appends `:<sectionId>`. */
|
|
15785
|
+
recordId: require_clip_model_registry.string(),
|
|
15786
|
+
split: require_clip_model_registry._enum(["rule", "section"]),
|
|
15787
|
+
/** Nothing open and every section read: nothing was enqueued. */
|
|
15788
|
+
nothingToReport: require_clip_model_registry.boolean(),
|
|
15789
|
+
sections: require_clip_model_registry.array(require_clip_model_registry.object({
|
|
15790
|
+
id: require_clip_model_registry.string(),
|
|
15791
|
+
status: require_clip_model_registry._enum(["read", "unavailable"]),
|
|
15792
|
+
items: require_clip_model_registry.number().int().nonnegative(),
|
|
15793
|
+
reason: require_clip_model_registry.string().optional()
|
|
15794
|
+
})),
|
|
15795
|
+
rows: require_clip_model_registry.array(require_clip_model_registry.object({
|
|
15796
|
+
recordId: require_clip_model_registry.string(),
|
|
15797
|
+
/** The section of a per-section row. */
|
|
15798
|
+
section: require_clip_model_registry.string().optional(),
|
|
15799
|
+
enqueued: require_clip_model_registry.number().int().nonnegative(),
|
|
15800
|
+
targetIds: require_clip_model_registry.array(require_clip_model_registry.string())
|
|
15801
|
+
}))
|
|
15802
|
+
});
|
|
15803
|
+
//#endregion
|
|
15768
15804
|
//#region src/notification-center/clock-text.ts
|
|
15769
15805
|
/**
|
|
15770
15806
|
* `{{time}}` — hours and minutes in the NOTIFICATION language.
|
|
@@ -17132,13 +17168,18 @@ var ncActions = require_clip_model_registry.defineCustomActions({
|
|
|
17132
17168
|
deviceId: require_clip_model_registry.number().int().optional()
|
|
17133
17169
|
}), NcTestEventResultSchema.extend({
|
|
17134
17170
|
/** The subject kind the rule was tested with — the answer to "why was my
|
|
17135
|
-
* rule not a candidate". */
|
|
17171
|
+
* rule not a candidate". `state-summary` for a summary rule (D698). */
|
|
17136
17172
|
trigger: require_clip_model_registry.string(),
|
|
17137
17173
|
/** What the plan could not reproduce (a device-state gate, a polygon). */
|
|
17138
17174
|
planNotes: require_clip_model_registry.array(require_clip_model_registry.object({
|
|
17139
17175
|
conditionId: require_clip_model_registry.string(),
|
|
17140
17176
|
note: require_clip_model_registry.string()
|
|
17141
|
-
}))
|
|
17177
|
+
})),
|
|
17178
|
+
/**
|
|
17179
|
+
* A STATE-SUMMARY rule's test (D698): the summary built now and the rows
|
|
17180
|
+
* it enqueued, or why nothing was — absent on every other rule.
|
|
17181
|
+
*/
|
|
17182
|
+
stateSummary: NcStateSummaryTestSchema.optional()
|
|
17142
17183
|
}), {
|
|
17143
17184
|
kind: "mutation",
|
|
17144
17185
|
auth: "admin",
|
|
@@ -17497,6 +17538,48 @@ function makeNcActionHandlers(deps) {
|
|
|
17497
17538
|
if (rule.ownerUserId !== void 0 && rule.ownerUserId !== caller.userId) throw new Error(`forbidden: rule not owned: ${ruleId}`);
|
|
17498
17539
|
return rule;
|
|
17499
17540
|
};
|
|
17541
|
+
/**
|
|
17542
|
+
* TEST a STATE-SUMMARY rule (D698). The per-event injection cannot deliver
|
|
17543
|
+
* for it — a summary rule sends nothing per event (D647), and that test used
|
|
17544
|
+
* to be dropped without a word — so the summary is built NOW and enqueued.
|
|
17545
|
+
* The answer is shaped like every rule test's, plus the summary's own report.
|
|
17546
|
+
*/
|
|
17547
|
+
const testSummaryRule = async (rule, by) => {
|
|
17548
|
+
const test = deps.testStateSummary;
|
|
17549
|
+
if (test === void 0) throw new Error("this node does not host the notification centre — test on the hub");
|
|
17550
|
+
deps.logger.info("rule test requested", { meta: {
|
|
17551
|
+
by,
|
|
17552
|
+
ruleId: rule.id,
|
|
17553
|
+
ruleName: rule.name,
|
|
17554
|
+
trigger: "state-summary",
|
|
17555
|
+
bypassMute: true
|
|
17556
|
+
} });
|
|
17557
|
+
const report = await test(rule);
|
|
17558
|
+
const enqueued = report.rows.reduce((sum, row) => sum + row.enqueued, 0);
|
|
17559
|
+
const notes = ["per-camera notification mute bypassed"];
|
|
17560
|
+
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`);
|
|
17561
|
+
else if (enqueued === 0) notes.push("the summary was built but no row was enqueued — the rule reaches no target");
|
|
17562
|
+
return {
|
|
17563
|
+
recordId: report.recordId,
|
|
17564
|
+
origin: "synthetic",
|
|
17565
|
+
delivery: rule.delivery,
|
|
17566
|
+
candidates: 1,
|
|
17567
|
+
outcomes: [{
|
|
17568
|
+
ruleId: rule.id,
|
|
17569
|
+
ruleName: rule.name,
|
|
17570
|
+
matched: !report.nothingToReport,
|
|
17571
|
+
cooldown: "bypassed",
|
|
17572
|
+
enqueued,
|
|
17573
|
+
targetIds: [...new Set(report.rows.flatMap((row) => row.targetIds))]
|
|
17574
|
+
}],
|
|
17575
|
+
still: "unattachable",
|
|
17576
|
+
notes,
|
|
17577
|
+
...report.nothingToReport ? { droppedBy: "nothing-to-report" } : {},
|
|
17578
|
+
trigger: "state-summary",
|
|
17579
|
+
planNotes: [],
|
|
17580
|
+
stateSummary: report
|
|
17581
|
+
};
|
|
17582
|
+
};
|
|
17500
17583
|
/** The producer half, or an honest refusal. */
|
|
17501
17584
|
const requireTimelapse = () => {
|
|
17502
17585
|
const tl = deps.timelapse;
|
|
@@ -17831,6 +17914,7 @@ function makeNcActionHandlers(deps) {
|
|
|
17831
17914
|
const inject = deps.injectTestEvent;
|
|
17832
17915
|
if (inject === void 0) throw new Error("this node does not host the notification centre — test on the hub");
|
|
17833
17916
|
const rule = assertRuleVisible(input.ruleId, c);
|
|
17917
|
+
if (require_clip_model_registry.isSystemEventSummaryRule(rule)) return testSummaryRule(rule, c.userId);
|
|
17834
17918
|
const plan = planRuleTest({
|
|
17835
17919
|
rule,
|
|
17836
17920
|
now: Date.now(),
|
|
@@ -19665,6 +19749,77 @@ var NcDeviceStateCache = class {
|
|
|
19665
19749
|
}
|
|
19666
19750
|
};
|
|
19667
19751
|
//#endregion
|
|
19752
|
+
//#region src/notification-center/state-summary/state-model.ts
|
|
19753
|
+
/**
|
|
19754
|
+
* The STATE SUMMARY's vocabulary (D647): its sections, and what one reported
|
|
19755
|
+
* item is.
|
|
19756
|
+
*
|
|
19757
|
+
* A section is one of the D637 kind GROUPS (`NC_SYSTEM_EVENT_KIND_GROUP_SPECS`)
|
|
19758
|
+
* — the same grouping both editors list kinds under — so "the rule selected a
|
|
19759
|
+
* battery kind" and "the summary has a battery section" are the same fact.
|
|
19760
|
+
* `backups-exports` is the one group with no section: a backup or an export is
|
|
19761
|
+
* a job that COMPLETED, and the state it leaves behind is not a condition that
|
|
19762
|
+
* is still true. The ADR records why it is excluded rather than approximated.
|
|
19763
|
+
*/
|
|
19764
|
+
var NC_STATE_SUMMARY_SECTION_IDS = [
|
|
19765
|
+
"device-state",
|
|
19766
|
+
"streams",
|
|
19767
|
+
"battery",
|
|
19768
|
+
"consumables",
|
|
19769
|
+
"tanks",
|
|
19770
|
+
"detection-blind",
|
|
19771
|
+
"nodes",
|
|
19772
|
+
"addons",
|
|
19773
|
+
"updates"
|
|
19774
|
+
];
|
|
19775
|
+
/** The kind groups that have no section, and why — said on the preview. */
|
|
19776
|
+
var NC_STATE_SUMMARY_EXCLUDED_GROUPS = { "backups-exports": "a backup or an export is a completed job, not a state that is still true" };
|
|
19777
|
+
function isStateSectionId(value) {
|
|
19778
|
+
return NC_STATE_SUMMARY_SECTION_IDS.some((id) => id === value);
|
|
19779
|
+
}
|
|
19780
|
+
/** The template variable that carries one section's lines (`template-vars.ts`). */
|
|
19781
|
+
var NC_STATE_SECTION_VAR = {
|
|
19782
|
+
"device-state": "summaryDevices",
|
|
19783
|
+
streams: "summaryStreams",
|
|
19784
|
+
battery: "summaryBattery",
|
|
19785
|
+
consumables: "summaryConsumables",
|
|
19786
|
+
tanks: "summaryTanks",
|
|
19787
|
+
"detection-blind": "summaryDetection",
|
|
19788
|
+
nodes: "summaryNodes",
|
|
19789
|
+
addons: "summaryAddons",
|
|
19790
|
+
updates: "summaryUpdates"
|
|
19791
|
+
};
|
|
19792
|
+
/** The kinds of a section — its kind group. */
|
|
19793
|
+
function sectionKinds(section) {
|
|
19794
|
+
return require_clip_model_registry.NC_SYSTEM_EVENT_KIND_GROUP_SPECS.find((group) => group.id === section)?.kinds ?? [];
|
|
19795
|
+
}
|
|
19796
|
+
/**
|
|
19797
|
+
* The sections a rule reports, in section order: every section whose kind
|
|
19798
|
+
* group shares a kind with the rule. A rule that selected only
|
|
19799
|
+
* `device-battery-low` still reports the battery STATE — the state has no
|
|
19800
|
+
* direction, only the event did.
|
|
19801
|
+
*/
|
|
19802
|
+
function sectionsOfRule(condition) {
|
|
19803
|
+
return NC_STATE_SUMMARY_SECTION_IDS.filter((section) => sectionKinds(section).some((kind) => condition.kinds.includes(kind)));
|
|
19804
|
+
}
|
|
19805
|
+
/** The kind groups the rule selected that have no section. */
|
|
19806
|
+
function excludedGroupsOfRule(condition) {
|
|
19807
|
+
return require_clip_model_registry.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);
|
|
19808
|
+
}
|
|
19809
|
+
/**
|
|
19810
|
+
* The condition the summary judges its items with: the rule's own, with its
|
|
19811
|
+
* kinds widened to every kind of every section it reports — so a rule that
|
|
19812
|
+
* named the raise is not told "not my kind" about the state the raise left
|
|
19813
|
+
* behind. Every FILTER is the rule's, untouched.
|
|
19814
|
+
*/
|
|
19815
|
+
function summaryCondition(condition) {
|
|
19816
|
+
const kinds = sectionsOfRule(condition).flatMap((section) => sectionKinds(section));
|
|
19817
|
+
return {
|
|
19818
|
+
...condition,
|
|
19819
|
+
kinds: [...new Set([...condition.kinds, ...kinds])]
|
|
19820
|
+
};
|
|
19821
|
+
}
|
|
19822
|
+
//#endregion
|
|
19668
19823
|
//#region src/notification-center/state-summary/state-summary-delivery.ts
|
|
19669
19824
|
/**
|
|
19670
19825
|
* A state summary as an ORDINARY system-event subject (D647), so it reaches the
|
|
@@ -19673,7 +19828,9 @@ var NcDeviceStateCache = class {
|
|
|
19673
19828
|
* which renders the rule's own template with the summary's variables.
|
|
19674
19829
|
*
|
|
19675
19830
|
* - `recordId` is `state-summary:<ruleId>:<windowEndMs>` — the outbox dedup
|
|
19676
|
-
* key, so a restart can never enqueue one window twice.
|
|
19831
|
+
* key, so a restart can never enqueue one window twice. Split per SECTION
|
|
19832
|
+
* (D698), each row is `state-summary:<ruleId>:<windowEndMs>:<sectionId>`:
|
|
19833
|
+
* the same guarantee, per section.
|
|
19677
19834
|
* - The row files under the system device (0): a summary spans devices.
|
|
19678
19835
|
* `targetUsers` therefore reach admins only — a user whose grant is
|
|
19679
19836
|
* narrower than the whole installation must not be sent the names of
|
|
@@ -19687,20 +19844,37 @@ var NcDeviceStateCache = class {
|
|
|
19687
19844
|
*/
|
|
19688
19845
|
var NC_STATE_SUMMARY_TITLE_KEY = "system.state-summary.title";
|
|
19689
19846
|
var NC_STATE_SUMMARY_BODY_KEY = "system.state-summary.body";
|
|
19847
|
+
/** A per-section row's title and body (D698): the rule's name and the section's. */
|
|
19848
|
+
var NC_STATE_SUMMARY_SECTION_TITLE_KEY = "system.state-summary.section.title";
|
|
19849
|
+
var NC_STATE_SUMMARY_SECTION_BODY_KEY = "system.state-summary.section.body";
|
|
19690
19850
|
function stateSummaryRecordId(ruleId, windowEndMs) {
|
|
19691
19851
|
return `state-summary:${ruleId}:${String(windowEndMs)}`;
|
|
19692
19852
|
}
|
|
19853
|
+
/**
|
|
19854
|
+
* The record id of a TEST of the rule (D698, `nc.testRule`): its own prefix,
|
|
19855
|
+
* so a test can never take — or be swallowed by — a real window's dedup key.
|
|
19856
|
+
*/
|
|
19857
|
+
function stateSummaryTestRecordId(ruleId, nowMs) {
|
|
19858
|
+
return `state-summary-test:${ruleId}:${String(nowMs)}`;
|
|
19859
|
+
}
|
|
19860
|
+
/** One section's row: the record id of the whole, plus `:<sectionId>` — the per-section dedup key. */
|
|
19861
|
+
function sectionRecordId(base, sectionId) {
|
|
19862
|
+
return `${base}:${sectionId}`;
|
|
19863
|
+
}
|
|
19693
19864
|
/** Is this system-event data the data of a state-summary row? */
|
|
19694
19865
|
function isStateSummaryData(data) {
|
|
19695
19866
|
return data["stateSummary"] === true;
|
|
19696
19867
|
}
|
|
19868
|
+
function recordBaseOf(input) {
|
|
19869
|
+
return input.recordBase ?? stateSummaryRecordId(input.rule.id, input.window.endMs);
|
|
19870
|
+
}
|
|
19697
19871
|
/** `null` when the rule has no system-event condition (the guard refuses that). */
|
|
19698
19872
|
function stateSummarySubject(input) {
|
|
19699
19873
|
const kind = input.rule.conditions.systemEvent?.kinds[0];
|
|
19700
19874
|
if (kind === void 0) return null;
|
|
19701
19875
|
return {
|
|
19702
19876
|
kind: "system-event",
|
|
19703
|
-
recordId:
|
|
19877
|
+
recordId: recordBaseOf(input),
|
|
19704
19878
|
deviceId: NC_SYSTEM_DEVICE_ID$1,
|
|
19705
19879
|
timestamp: input.window.endMs,
|
|
19706
19880
|
classNames: [],
|
|
@@ -19713,6 +19887,7 @@ function stateSummarySubject(input) {
|
|
|
19713
19887
|
textParams: {
|
|
19714
19888
|
summaryText: input.text.text,
|
|
19715
19889
|
summaryCount: String(input.text.count),
|
|
19890
|
+
summarySection: input.text.sections.map((section) => section.title).join(", "),
|
|
19716
19891
|
...input.text.sectionVars
|
|
19717
19892
|
},
|
|
19718
19893
|
textCount: input.text.count,
|
|
@@ -19730,13 +19905,82 @@ function stateSummarySubject(input) {
|
|
|
19730
19905
|
};
|
|
19731
19906
|
}
|
|
19732
19907
|
/**
|
|
19908
|
+
* The rows one summary window sends. `rule`: the one D647 row. `section`: one
|
|
19909
|
+
* row per section with something to say — its items, or the sentence that it
|
|
19910
|
+
* is UNAVAILABLE, exactly as the combined text words it. A section with
|
|
19911
|
+
* nothing open sends nothing; neither does a summary with nothing at all.
|
|
19912
|
+
*/
|
|
19913
|
+
function stateSummarySubjects(input) {
|
|
19914
|
+
if (input.split === "rule") {
|
|
19915
|
+
const subject = stateSummarySubject(input);
|
|
19916
|
+
return subject === null ? [] : [subject];
|
|
19917
|
+
}
|
|
19918
|
+
const kind = input.rule.conditions.systemEvent?.kinds[0];
|
|
19919
|
+
if (kind === void 0) return [];
|
|
19920
|
+
return input.text.sections.map((section) => sectionSubject(input, kind, section));
|
|
19921
|
+
}
|
|
19922
|
+
function sectionSubject(input, kind, section) {
|
|
19923
|
+
const sectionVars = {};
|
|
19924
|
+
for (const name of Object.keys(input.text.sectionVars)) sectionVars[name] = "";
|
|
19925
|
+
sectionVars[NC_STATE_SECTION_VAR[section.id]] = section.text;
|
|
19926
|
+
return {
|
|
19927
|
+
kind: "system-event",
|
|
19928
|
+
recordId: sectionRecordId(recordBaseOf(input), section.id),
|
|
19929
|
+
deviceId: NC_SYSTEM_DEVICE_ID$1,
|
|
19930
|
+
timestamp: input.window.endMs,
|
|
19931
|
+
classNames: [],
|
|
19932
|
+
zones: [],
|
|
19933
|
+
systemEvent: {
|
|
19934
|
+
kind,
|
|
19935
|
+
subject: `state-summary:${input.rule.id}:${section.id}`,
|
|
19936
|
+
titleKey: NC_STATE_SUMMARY_SECTION_TITLE_KEY,
|
|
19937
|
+
bodyKey: NC_STATE_SUMMARY_SECTION_BODY_KEY,
|
|
19938
|
+
textParams: {
|
|
19939
|
+
summaryText: section.body,
|
|
19940
|
+
summaryCount: String(section.count),
|
|
19941
|
+
summarySection: section.title,
|
|
19942
|
+
...sectionVars
|
|
19943
|
+
},
|
|
19944
|
+
textCount: section.count,
|
|
19945
|
+
data: {
|
|
19946
|
+
stateSummary: true,
|
|
19947
|
+
section: section.id,
|
|
19948
|
+
windowStartMs: input.window.startMs,
|
|
19949
|
+
windowEndMs: input.window.endMs,
|
|
19950
|
+
sections: [{
|
|
19951
|
+
id: section.id,
|
|
19952
|
+
status: section.status,
|
|
19953
|
+
items: section.count
|
|
19954
|
+
}]
|
|
19955
|
+
}
|
|
19956
|
+
}
|
|
19957
|
+
};
|
|
19958
|
+
}
|
|
19959
|
+
/** The section a per-section row is about (`data.section`), or `undefined` on a combined row. */
|
|
19960
|
+
function stateSummarySectionOf(subject) {
|
|
19961
|
+
const section = subject.systemEvent?.data["section"];
|
|
19962
|
+
return typeof section === "string" && isStateSectionId(section) ? section : void 0;
|
|
19963
|
+
}
|
|
19964
|
+
/** Does a body name `{{summaryText}}` or this section's own variable? */
|
|
19965
|
+
function bodyCarriesSection(body, section) {
|
|
19966
|
+
return new RegExp(`\\{\\{\\s*(?:summaryText|${NC_STATE_SECTION_VAR[section]})\\s*\\}\\}`).test(body);
|
|
19967
|
+
}
|
|
19968
|
+
/**
|
|
19733
19969
|
* The rule as its summary is ENQUEUED: a custom body that carries none of the
|
|
19734
19970
|
* summary's content gets `{{summaryText}}` appended on its own line, so an
|
|
19735
19971
|
* operator's body never sends an empty summary (review M3). `null` = unchanged.
|
|
19972
|
+
*
|
|
19973
|
+
* On a per-SECTION row (D698 review M3) the content is THIS section's: a body
|
|
19974
|
+
* naming only `{{summaryBattery}}` carries the battery row and nothing on the
|
|
19975
|
+
* streams row, so every other section's row gets `{{summaryText}}` — its own
|
|
19976
|
+
* lines — appended.
|
|
19736
19977
|
*/
|
|
19737
|
-
function withSummaryBody(rule) {
|
|
19978
|
+
function withSummaryBody(rule, section) {
|
|
19738
19979
|
const body = rule.template?.body;
|
|
19739
|
-
if (require_clip_model_registry.templateCarriesSummary(body))
|
|
19980
|
+
if (require_clip_model_registry.templateCarriesSummary(body)) {
|
|
19981
|
+
if (section === void 0 || body === void 0 || body.trim().length === 0) return null;
|
|
19982
|
+
if (bodyCarriesSection(body, section)) return null;
|
|
19983
|
+
}
|
|
19740
19984
|
return {
|
|
19741
19985
|
...rule,
|
|
19742
19986
|
template: {
|
|
@@ -23785,6 +24029,120 @@ function withTimeout$3(promise, ms, what) {
|
|
|
23785
24029
|
});
|
|
23786
24030
|
}
|
|
23787
24031
|
//#endregion
|
|
24032
|
+
//#region src/notification-center/boot-window-rows.ts
|
|
24033
|
+
var BootWindowRows = class {
|
|
24034
|
+
deps;
|
|
24035
|
+
window = /* @__PURE__ */ new Map();
|
|
24036
|
+
/** Deferred closes, keyed so a repeat (one per detection) replaces rather than grows. */
|
|
24037
|
+
deferred = /* @__PURE__ */ new Map();
|
|
24038
|
+
settled = false;
|
|
24039
|
+
constructor(deps) {
|
|
24040
|
+
this.deps = deps;
|
|
24041
|
+
}
|
|
24042
|
+
/** Is the ledger still inside its boot window (not settled after an answered load)? */
|
|
24043
|
+
open() {
|
|
24044
|
+
return !this.settled;
|
|
24045
|
+
}
|
|
24046
|
+
/** Stage a row that would be CREATED inside the window — mirror only, never persisted. */
|
|
24047
|
+
stageProvisional(row) {
|
|
24048
|
+
this.deps.ledger.stage(row);
|
|
24049
|
+
this.window.set(this.deps.keyOf(row), {
|
|
24050
|
+
kind: "provisional",
|
|
24051
|
+
row
|
|
24052
|
+
});
|
|
24053
|
+
}
|
|
24054
|
+
/** A row the ledger persisted inside the window: the load must not regress it. */
|
|
24055
|
+
noteWritten(row) {
|
|
24056
|
+
this.window.set(this.deps.keyOf(row), {
|
|
24057
|
+
kind: "written",
|
|
24058
|
+
row
|
|
24059
|
+
});
|
|
24060
|
+
}
|
|
24061
|
+
/** A key the ledger removed inside the window: the load must not resurrect it. */
|
|
24062
|
+
noteRemoved(key) {
|
|
24063
|
+
this.window.set(key, { kind: "removed" });
|
|
24064
|
+
}
|
|
24065
|
+
/**
|
|
24066
|
+
* While the window is open, keep `replay` to run against the LOADED rows at
|
|
24067
|
+
* the settle; once settled, nothing — the caller's own pass already saw every
|
|
24068
|
+
* row (review LOW-5: no allocation per detection after boot). `key` names the
|
|
24069
|
+
* close: the same key deferred twice runs once, at the place it was first
|
|
24070
|
+
* deferred. `replay` answers how many rows it closed.
|
|
24071
|
+
*/
|
|
24072
|
+
deferWhileOpen(key, replay) {
|
|
24073
|
+
if (this.open()) this.deferred.set(key, replay);
|
|
24074
|
+
}
|
|
24075
|
+
/**
|
|
24076
|
+
* Close the window — call right after the ledger's `load`. A load that did
|
|
24077
|
+
* not answer leaves the window open and returns `null`.
|
|
24078
|
+
*/
|
|
24079
|
+
settle() {
|
|
24080
|
+
if (this.settled) return null;
|
|
24081
|
+
if (!this.deps.ledger.hasAnswered()) {
|
|
24082
|
+
this.deps.logger.warn("boot window NOT settled — the load did not answer", { meta: {
|
|
24083
|
+
collection: this.deps.ledger.collection,
|
|
24084
|
+
pendingCloses: this.deferred.size,
|
|
24085
|
+
provisionalRows: this.window.size
|
|
24086
|
+
} });
|
|
24087
|
+
return null;
|
|
24088
|
+
}
|
|
24089
|
+
this.settled = true;
|
|
24090
|
+
const ledger = this.deps.ledger;
|
|
24091
|
+
let replayed = 0;
|
|
24092
|
+
for (const replay of this.deferred.values()) replayed += replay();
|
|
24093
|
+
this.deferred.clear();
|
|
24094
|
+
let keptStored = 0;
|
|
24095
|
+
let written = 0;
|
|
24096
|
+
for (const [key, entry] of this.window) {
|
|
24097
|
+
if (entry.kind === "removed") {
|
|
24098
|
+
ledger.forget(key);
|
|
24099
|
+
continue;
|
|
24100
|
+
}
|
|
24101
|
+
if (entry.kind === "written") {
|
|
24102
|
+
ledger.stage(entry.row);
|
|
24103
|
+
continue;
|
|
24104
|
+
}
|
|
24105
|
+
const mirrored = ledger.get(key);
|
|
24106
|
+
const stored = mirrored === entry.row ? void 0 : mirrored;
|
|
24107
|
+
const held = this.deps.merge(stored, entry.row);
|
|
24108
|
+
const deviceId = this.deps.deviceIdOf(entry.row);
|
|
24109
|
+
const tags = deviceId !== void 0 ? { tags: { deviceId } } : {};
|
|
24110
|
+
if (stored !== void 0 && held === stored) {
|
|
24111
|
+
keptStored += 1;
|
|
24112
|
+
this.deps.logger.debug("boot-window row kept as stored — the restart learned nothing new", {
|
|
24113
|
+
...tags,
|
|
24114
|
+
meta: {
|
|
24115
|
+
collection: ledger.collection,
|
|
24116
|
+
key
|
|
24117
|
+
}
|
|
24118
|
+
});
|
|
24119
|
+
continue;
|
|
24120
|
+
}
|
|
24121
|
+
ledger.put(held);
|
|
24122
|
+
written += 1;
|
|
24123
|
+
this.deps.logger.debug("boot-window row written at load", {
|
|
24124
|
+
...tags,
|
|
24125
|
+
meta: {
|
|
24126
|
+
collection: ledger.collection,
|
|
24127
|
+
key,
|
|
24128
|
+
stored: stored !== void 0
|
|
24129
|
+
}
|
|
24130
|
+
});
|
|
24131
|
+
}
|
|
24132
|
+
this.window.clear();
|
|
24133
|
+
const merge = {
|
|
24134
|
+
keptStored,
|
|
24135
|
+
written,
|
|
24136
|
+
replayed
|
|
24137
|
+
};
|
|
24138
|
+
this.deps.logger.info("boot window settled", { meta: {
|
|
24139
|
+
collection: ledger.collection,
|
|
24140
|
+
...merge
|
|
24141
|
+
} });
|
|
24142
|
+
return merge;
|
|
24143
|
+
}
|
|
24144
|
+
};
|
|
24145
|
+
//#endregion
|
|
23788
24146
|
//#region src/notification-center/liveness-ledger.ts
|
|
23789
24147
|
/**
|
|
23790
24148
|
* @durable class=ledger owner=notification-center
|
|
@@ -23895,12 +24253,26 @@ var NC_LIVENESS_SPEC = {
|
|
|
23895
24253
|
};
|
|
23896
24254
|
var NcLivenessLedger = class {
|
|
23897
24255
|
ledger;
|
|
24256
|
+
/**
|
|
24257
|
+
* What was observed before the boot load answered (D698) — see
|
|
24258
|
+
* `boot-window-rows.ts`. A stored row holding the same STATE keeps its own
|
|
24259
|
+
* `updatedAt`: a restart that re-hears "offline" learned nothing about WHEN
|
|
24260
|
+
* the device went offline, and `updatedAt` is the state summary's "since".
|
|
24261
|
+
*/
|
|
24262
|
+
bootWindow;
|
|
23898
24263
|
constructor(deps) {
|
|
23899
24264
|
this.ledger = new DurableLedger({
|
|
23900
24265
|
spec: NC_LIVENESS_SPEC,
|
|
23901
24266
|
store: deps.store,
|
|
23902
24267
|
logger: deps.logger
|
|
23903
24268
|
});
|
|
24269
|
+
this.bootWindow = new BootWindowRows({
|
|
24270
|
+
ledger: this.ledger,
|
|
24271
|
+
keyOf: (row) => row.subject,
|
|
24272
|
+
deviceIdOf: (row) => row.deviceId,
|
|
24273
|
+
logger: deps.logger,
|
|
24274
|
+
merge: (stored, provisional) => stored !== void 0 && stored.state === provisional.state ? stored : provisional
|
|
24275
|
+
});
|
|
23904
24276
|
}
|
|
23905
24277
|
static declare(store) {
|
|
23906
24278
|
return DurableLedger.declare(store, NC_LIVENESS_SPEC);
|
|
@@ -23915,7 +24287,9 @@ var NcLivenessLedger = class {
|
|
|
23915
24287
|
* flood, so no error path may do it on purpose.
|
|
23916
24288
|
*/
|
|
23917
24289
|
async load() {
|
|
23918
|
-
|
|
24290
|
+
const loaded = (await this.ledger.load()).length;
|
|
24291
|
+
this.bootWindow.settle();
|
|
24292
|
+
return loaded;
|
|
23919
24293
|
}
|
|
23920
24294
|
/** Has a load READ the store once? Until then the mirror is not the truth (D647 reads it). */
|
|
23921
24295
|
hasAnswered() {
|
|
@@ -24008,13 +24382,21 @@ var NcLivenessLedger = class {
|
|
|
24008
24382
|
* spoke". That is the timestamp anyone reading the table wants.
|
|
24009
24383
|
*/
|
|
24010
24384
|
observe(observation, now) {
|
|
24011
|
-
|
|
24385
|
+
const row = {
|
|
24012
24386
|
subject: observation.subject,
|
|
24013
24387
|
state: observation.state,
|
|
24014
24388
|
kind: observation.kind,
|
|
24015
24389
|
...observation.deviceId !== void 0 ? { deviceId: observation.deviceId } : {},
|
|
24016
24390
|
updatedAt: now
|
|
24017
|
-
}
|
|
24391
|
+
};
|
|
24392
|
+
if (!this.bootWindow.open()) return this.ledger.observe(row);
|
|
24393
|
+
if (this.ledger.get(row.subject) === void 0) {
|
|
24394
|
+
this.bootWindow.stageProvisional(row);
|
|
24395
|
+
return "seeded";
|
|
24396
|
+
}
|
|
24397
|
+
const verdict = this.ledger.observe(row);
|
|
24398
|
+
if (verdict === "flip") this.bootWindow.noteWritten(row);
|
|
24399
|
+
return verdict;
|
|
24018
24400
|
}
|
|
24019
24401
|
};
|
|
24020
24402
|
/** The persisted column map (the `subject` PK is passed separately). */
|
|
@@ -30549,74 +30931,6 @@ function stateSources(ports) {
|
|
|
30549
30931
|
return { read: (section, nowMs) => readSection(section, ports, nowMs) };
|
|
30550
30932
|
}
|
|
30551
30933
|
//#endregion
|
|
30552
|
-
//#region src/notification-center/state-summary/state-model.ts
|
|
30553
|
-
/**
|
|
30554
|
-
* The STATE SUMMARY's vocabulary (D647): its sections, and what one reported
|
|
30555
|
-
* item is.
|
|
30556
|
-
*
|
|
30557
|
-
* A section is one of the D637 kind GROUPS (`NC_SYSTEM_EVENT_KIND_GROUP_SPECS`)
|
|
30558
|
-
* — the same grouping both editors list kinds under — so "the rule selected a
|
|
30559
|
-
* battery kind" and "the summary has a battery section" are the same fact.
|
|
30560
|
-
* `backups-exports` is the one group with no section: a backup or an export is
|
|
30561
|
-
* a job that COMPLETED, and the state it leaves behind is not a condition that
|
|
30562
|
-
* is still true. The ADR records why it is excluded rather than approximated.
|
|
30563
|
-
*/
|
|
30564
|
-
var NC_STATE_SUMMARY_SECTION_IDS = [
|
|
30565
|
-
"device-state",
|
|
30566
|
-
"streams",
|
|
30567
|
-
"battery",
|
|
30568
|
-
"consumables",
|
|
30569
|
-
"tanks",
|
|
30570
|
-
"detection-blind",
|
|
30571
|
-
"nodes",
|
|
30572
|
-
"addons",
|
|
30573
|
-
"updates"
|
|
30574
|
-
];
|
|
30575
|
-
/** The kind groups that have no section, and why — said on the preview. */
|
|
30576
|
-
var NC_STATE_SUMMARY_EXCLUDED_GROUPS = { "backups-exports": "a backup or an export is a completed job, not a state that is still true" };
|
|
30577
|
-
/** The template variable that carries one section's lines (`template-vars.ts`). */
|
|
30578
|
-
var NC_STATE_SECTION_VAR = {
|
|
30579
|
-
"device-state": "summaryDevices",
|
|
30580
|
-
streams: "summaryStreams",
|
|
30581
|
-
battery: "summaryBattery",
|
|
30582
|
-
consumables: "summaryConsumables",
|
|
30583
|
-
tanks: "summaryTanks",
|
|
30584
|
-
"detection-blind": "summaryDetection",
|
|
30585
|
-
nodes: "summaryNodes",
|
|
30586
|
-
addons: "summaryAddons",
|
|
30587
|
-
updates: "summaryUpdates"
|
|
30588
|
-
};
|
|
30589
|
-
/** The kinds of a section — its kind group. */
|
|
30590
|
-
function sectionKinds(section) {
|
|
30591
|
-
return require_clip_model_registry.NC_SYSTEM_EVENT_KIND_GROUP_SPECS.find((group) => group.id === section)?.kinds ?? [];
|
|
30592
|
-
}
|
|
30593
|
-
/**
|
|
30594
|
-
* The sections a rule reports, in section order: every section whose kind
|
|
30595
|
-
* group shares a kind with the rule. A rule that selected only
|
|
30596
|
-
* `device-battery-low` still reports the battery STATE — the state has no
|
|
30597
|
-
* direction, only the event did.
|
|
30598
|
-
*/
|
|
30599
|
-
function sectionsOfRule(condition) {
|
|
30600
|
-
return NC_STATE_SUMMARY_SECTION_IDS.filter((section) => sectionKinds(section).some((kind) => condition.kinds.includes(kind)));
|
|
30601
|
-
}
|
|
30602
|
-
/** The kind groups the rule selected that have no section. */
|
|
30603
|
-
function excludedGroupsOfRule(condition) {
|
|
30604
|
-
return require_clip_model_registry.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);
|
|
30605
|
-
}
|
|
30606
|
-
/**
|
|
30607
|
-
* The condition the summary judges its items with: the rule's own, with its
|
|
30608
|
-
* kinds widened to every kind of every section it reports — so a rule that
|
|
30609
|
-
* named the raise is not told "not my kind" about the state the raise left
|
|
30610
|
-
* behind. Every FILTER is the rule's, untouched.
|
|
30611
|
-
*/
|
|
30612
|
-
function summaryCondition(condition) {
|
|
30613
|
-
const kinds = sectionsOfRule(condition).flatMap((section) => sectionKinds(section));
|
|
30614
|
-
return {
|
|
30615
|
-
...condition,
|
|
30616
|
-
kinds: [...new Set([...condition.kinds, ...kinds])]
|
|
30617
|
-
};
|
|
30618
|
-
}
|
|
30619
|
-
//#endregion
|
|
30620
30934
|
//#region src/notification-center/state-summary/state-summary.ts
|
|
30621
30935
|
/** Nothing to say: no item, and every section READ. Such a summary is not sent. */
|
|
30622
30936
|
function isEmptyStateSummary(summary) {
|
|
@@ -30717,28 +31031,50 @@ function lineOf(input, item) {
|
|
|
30717
31031
|
}
|
|
30718
31032
|
});
|
|
30719
31033
|
}
|
|
30720
|
-
function
|
|
30721
|
-
|
|
30722
|
-
|
|
30723
|
-
|
|
30724
|
-
|
|
30725
|
-
|
|
31034
|
+
function sectionTitle(input, section) {
|
|
31035
|
+
return input.texts.text({ key: `summary.section.${section.id}` });
|
|
31036
|
+
}
|
|
31037
|
+
/** A section's words: its block, and the body a per-section row sends. `null` = nothing to say. */
|
|
31038
|
+
function wordsOf(input, section) {
|
|
31039
|
+
const title = sectionTitle(input, section);
|
|
31040
|
+
if (section.status === "unavailable") {
|
|
31041
|
+
const sentence = input.texts.text({
|
|
31042
|
+
key: "summary.section.unavailable",
|
|
31043
|
+
vars: { section: title }
|
|
31044
|
+
});
|
|
31045
|
+
return {
|
|
31046
|
+
block: sentence,
|
|
31047
|
+
body: sentence
|
|
31048
|
+
};
|
|
31049
|
+
}
|
|
30726
31050
|
if (section.items.length === 0) return null;
|
|
30727
|
-
const lines = section.items.map((item) => `• ${lineOf(input, item)}`);
|
|
30728
|
-
return
|
|
31051
|
+
const lines = section.items.map((item) => `• ${lineOf(input, item)}`).join("\n");
|
|
31052
|
+
return {
|
|
31053
|
+
block: `${title}:\n${lines}`,
|
|
31054
|
+
body: lines
|
|
31055
|
+
};
|
|
30729
31056
|
}
|
|
30730
31057
|
function stateSummaryText(input) {
|
|
30731
31058
|
const sectionVars = {};
|
|
30732
|
-
const
|
|
31059
|
+
const sections = [];
|
|
30733
31060
|
for (const section of input.summary.sections) {
|
|
30734
|
-
const
|
|
30735
|
-
sectionVars[NC_STATE_SECTION_VAR[section.id]] = block ?? "";
|
|
30736
|
-
if (
|
|
31061
|
+
const words = wordsOf(input, section);
|
|
31062
|
+
sectionVars[NC_STATE_SECTION_VAR[section.id]] = words?.block ?? "";
|
|
31063
|
+
if (words === null) continue;
|
|
31064
|
+
sections.push({
|
|
31065
|
+
id: section.id,
|
|
31066
|
+
title: sectionTitle(input, section),
|
|
31067
|
+
text: words.block,
|
|
31068
|
+
body: words.body,
|
|
31069
|
+
count: section.items.length,
|
|
31070
|
+
status: section.status
|
|
31071
|
+
});
|
|
30737
31072
|
}
|
|
30738
31073
|
return {
|
|
30739
|
-
text:
|
|
31074
|
+
text: sections.map((section) => section.text).join("\n"),
|
|
30740
31075
|
count: input.summary.itemCount,
|
|
30741
|
-
sectionVars
|
|
31076
|
+
sectionVars,
|
|
31077
|
+
sections
|
|
30742
31078
|
};
|
|
30743
31079
|
}
|
|
30744
31080
|
//#endregion
|
|
@@ -31175,6 +31511,40 @@ var NcStateSummaryStampStore = class {
|
|
|
31175
31511
|
//#endregion
|
|
31176
31512
|
//#region src/notification-center/state-summary/system-state-ledger.ts
|
|
31177
31513
|
/**
|
|
31514
|
+
* NcSystemStateLedger — what is STILL TRUE among the event-driven system facts
|
|
31515
|
+
* no other ledger holds (D647).
|
|
31516
|
+
*
|
|
31517
|
+
* Battery, consumable and tank episodes already have a durable row
|
|
31518
|
+
* (`alert-store.ts`), and device/node liveness has its own
|
|
31519
|
+
* (`liveness-ledger.ts`). The rest of the summarisable kinds are EVENTS the
|
|
31520
|
+
* centre hears once — a stream going offline, a device switched off, a camera
|
|
31521
|
+
* detecting nothing, an update becoming available — and nothing remembered
|
|
31522
|
+
* that they were still true. A state summary is a claim about now, so this
|
|
31523
|
+
* ledger keeps one row per OPEN state, fed by the same intake the instant
|
|
31524
|
+
* rules read, and removes it when the closing event arrives:
|
|
31525
|
+
*
|
|
31526
|
+
* | opens | closes |
|
|
31527
|
+
* | --- | --- |
|
|
31528
|
+
* | `stream-offline` | `stream-online` (same subject) |
|
|
31529
|
+
* | `device-disabled` | `device-enabled` (same subject) |
|
|
31530
|
+
* | `detection-blind` | a DETECTION on that camera — the only evidence of recovery there is |
|
|
31531
|
+
* | `node-inference-unavailable`, `addon-crash-loop` | the node's next liveness edge, or a server update on it |
|
|
31532
|
+
* | `addon-update-available` (per package, per node) | `addon.updated` for that package on that node |
|
|
31533
|
+
* | `server-` / `wrapper-update-available` | `server-updated` on that node |
|
|
31534
|
+
*
|
|
31535
|
+
* Two kinds have no recovery event at all (`node-inference-unavailable`,
|
|
31536
|
+
* `addon-crash-loop`): the orchestrator drops such a node until it comes back,
|
|
31537
|
+
* and a crash-looped runner stays down until something respawns it — a node
|
|
31538
|
+
* restart or an update. The closing edges above are those respawns. A newer
|
|
31539
|
+
* `update-available` for the same package REPLACES the row.
|
|
31540
|
+
*
|
|
31541
|
+
* Durable (write-behind), so a restart does not forget an outage it heard
|
|
31542
|
+
* about. It is fed only on the evaluating node, like the liveness ledger. The
|
|
31543
|
+
* summary reads the RAM mirror only — never SQLite on its schedule (D447) —
|
|
31544
|
+
* and a mirror that has not loaded answers `null`: the sections it feeds say
|
|
31545
|
+
* "unavailable", never "none" (D49).
|
|
31546
|
+
*/
|
|
31547
|
+
/**
|
|
31178
31548
|
* @durable class=ledger owner=notification-center
|
|
31179
31549
|
* 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"
|
|
31180
31550
|
* retention="none — a row lives exactly as long as its state is open. Bounded by the installation: streams, devices, nodes and installed packages."
|
|
@@ -31336,9 +31706,7 @@ var NcSystemStateLedger = class {
|
|
|
31336
31706
|
}
|
|
31337
31707
|
/** A detection on a camera: it is not blind. Called for every detection — a Map lookup. */
|
|
31338
31708
|
observeDetection(deviceId) {
|
|
31339
|
-
|
|
31340
|
-
if (this.deps.rows.get(key) === void 0) return;
|
|
31341
|
-
this.close(key, "detection");
|
|
31709
|
+
this.close(`detection-blind:${String(deviceId)}`, "detection");
|
|
31342
31710
|
}
|
|
31343
31711
|
/**
|
|
31344
31712
|
* Any `stream.online`, whatever its reason (D647 review I1). The intake gate
|
|
@@ -31353,7 +31721,7 @@ var NcSystemStateLedger = class {
|
|
|
31353
31721
|
const keep = new Set(list.rows.map((pkg) => updateStateKey(list.target, pkg.nodeId, pkg.packageName)));
|
|
31354
31722
|
for (const nodeId of list.nodeIds) {
|
|
31355
31723
|
const prefix = `update:${list.target}:${nodeId}:`;
|
|
31356
|
-
|
|
31724
|
+
this.closeWhere(`update-list:${list.target}:${nodeId}`, (row) => row.key.startsWith(prefix) && !keep.has(row.key), "update-list");
|
|
31357
31725
|
}
|
|
31358
31726
|
for (const pkg of list.rows) this.openUpdate(list.target, pkg, atMs);
|
|
31359
31727
|
}
|
|
@@ -31363,11 +31731,7 @@ var NcSystemStateLedger = class {
|
|
|
31363
31731
|
* on that node.
|
|
31364
31732
|
*/
|
|
31365
31733
|
observeAddonStarted(addonId, nodeId) {
|
|
31366
|
-
|
|
31367
|
-
if (row.kind !== "addon-crash-loop") continue;
|
|
31368
|
-
if (row.nodeId !== nodeId) continue;
|
|
31369
|
-
if ((row.params["addons"] ?? "").split(",").map((id) => id.trim()).includes(addonId)) this.close(row.key, "addon-started");
|
|
31370
|
-
}
|
|
31734
|
+
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");
|
|
31371
31735
|
}
|
|
31372
31736
|
/**
|
|
31373
31737
|
* The balancer placed a camera on a node (not pinned): placement skips a
|
|
@@ -31423,11 +31787,31 @@ var NcSystemStateLedger = class {
|
|
|
31423
31787
|
}
|
|
31424
31788
|
});
|
|
31425
31789
|
}
|
|
31790
|
+
/**
|
|
31791
|
+
* Close one state: the row held now (a loaded one, or one opened in the boot
|
|
31792
|
+
* window), and — deferred while the boot window is open — the STORED row
|
|
31793
|
+
* the window cannot see yet (D698 review H1).
|
|
31794
|
+
*/
|
|
31426
31795
|
close(key, by) {
|
|
31796
|
+
this.closeIfHeld(key, by, false);
|
|
31797
|
+
this.deps.rows.deferWhileOpen(`close:${key}`, () => this.closeIfHeld(key, by, true) ? 1 : 0);
|
|
31798
|
+
}
|
|
31799
|
+
/** {@link close} for every row a predicate names — now, and against the loaded rows. */
|
|
31800
|
+
closeWhere(key, names, by) {
|
|
31801
|
+
const run = (replayed) => {
|
|
31802
|
+
let closed = 0;
|
|
31803
|
+
for (const row of this.deps.rows.all()) if (names(row) && this.closeIfHeld(row.key, by, replayed)) closed += 1;
|
|
31804
|
+
return closed;
|
|
31805
|
+
};
|
|
31806
|
+
run(false);
|
|
31807
|
+
this.deps.rows.deferWhileOpen(`where:${key}`, () => run(true));
|
|
31808
|
+
}
|
|
31809
|
+
/** Close the row if one is held; `true` when it was. */
|
|
31810
|
+
closeIfHeld(key, by, replayed) {
|
|
31427
31811
|
const held = this.deps.rows.get(key);
|
|
31428
|
-
if (held === void 0) return;
|
|
31812
|
+
if (held === void 0) return false;
|
|
31429
31813
|
this.deps.rows.remove(key);
|
|
31430
|
-
|
|
31814
|
+
const line = {
|
|
31431
31815
|
...held.deviceId !== void 0 ? { tags: { deviceId: held.deviceId } } : {},
|
|
31432
31816
|
meta: {
|
|
31433
31817
|
key,
|
|
@@ -31435,49 +31819,78 @@ var NcSystemStateLedger = class {
|
|
|
31435
31819
|
by,
|
|
31436
31820
|
since: held.since
|
|
31437
31821
|
}
|
|
31438
|
-
}
|
|
31822
|
+
};
|
|
31823
|
+
if (replayed) {
|
|
31824
|
+
this.deps.logger?.info("system state closed by an event heard before the load", line);
|
|
31825
|
+
return true;
|
|
31826
|
+
}
|
|
31827
|
+
this.deps.logger?.debug("system state closed", line);
|
|
31828
|
+
return true;
|
|
31439
31829
|
}
|
|
31440
31830
|
closeNodeFaults(nodeId, by) {
|
|
31441
|
-
|
|
31442
|
-
if (row.nodeId !== nodeId) continue;
|
|
31443
|
-
if (row.kind === "node-inference-unavailable" || row.kind === "addon-crash-loop") this.close(row.key, by);
|
|
31444
|
-
}
|
|
31831
|
+
this.closeWhere(`node-faults:${nodeId}`, (row) => row.nodeId === nodeId && (row.kind === "node-inference-unavailable" || row.kind === "addon-crash-loop"), by);
|
|
31445
31832
|
}
|
|
31446
31833
|
closeServerUpdate(nodeId) {
|
|
31447
|
-
|
|
31448
|
-
if ((row.nodeId ?? HUB_NODE) !== nodeId) continue;
|
|
31449
|
-
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");
|
|
31450
|
-
}
|
|
31834
|
+
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");
|
|
31451
31835
|
}
|
|
31452
31836
|
};
|
|
31453
31837
|
/** The durable home of the rows. */
|
|
31454
31838
|
var NcSystemStateStore = class {
|
|
31455
31839
|
ledger;
|
|
31840
|
+
/**
|
|
31841
|
+
* What the intake opened or closed before the boot load answered (D698,
|
|
31842
|
+
* `boot-window-rows.ts`). A stored row of the same KIND keeps its own
|
|
31843
|
+
* `since`: a stream re-announced offline at boot was not offline "since the
|
|
31844
|
+
* restart".
|
|
31845
|
+
*/
|
|
31846
|
+
bootWindow;
|
|
31456
31847
|
constructor(deps) {
|
|
31457
31848
|
this.ledger = new DurableLedger({
|
|
31458
31849
|
spec: NC_SYSTEM_STATE_SPEC,
|
|
31459
31850
|
store: deps.store,
|
|
31460
31851
|
logger: deps.logger
|
|
31461
31852
|
});
|
|
31853
|
+
this.bootWindow = new BootWindowRows({
|
|
31854
|
+
ledger: this.ledger,
|
|
31855
|
+
keyOf: (row) => row.key,
|
|
31856
|
+
deviceIdOf: (row) => row.deviceId,
|
|
31857
|
+
logger: deps.logger,
|
|
31858
|
+
merge: (stored, provisional) => {
|
|
31859
|
+
if (stored === void 0 || stored.kind !== provisional.kind) return provisional;
|
|
31860
|
+
const merged = {
|
|
31861
|
+
...provisional,
|
|
31862
|
+
since: stored.since
|
|
31863
|
+
};
|
|
31864
|
+
return (0, node_util.isDeepStrictEqual)(merged, stored) ? stored : merged;
|
|
31865
|
+
}
|
|
31866
|
+
});
|
|
31462
31867
|
}
|
|
31463
31868
|
static declare(store) {
|
|
31464
31869
|
return DurableLedger.declare(store, NC_SYSTEM_STATE_SPEC);
|
|
31465
31870
|
}
|
|
31466
31871
|
/** Boot reseed. A failed load keeps the mirror and leaves `answered` false. */
|
|
31467
31872
|
async load() {
|
|
31468
|
-
|
|
31873
|
+
const loaded = (await this.ledger.load()).length;
|
|
31874
|
+
this.bootWindow.settle();
|
|
31875
|
+
return loaded;
|
|
31469
31876
|
}
|
|
31470
31877
|
port() {
|
|
31471
31878
|
return {
|
|
31472
31879
|
get: (key) => this.ledger.get(key),
|
|
31473
31880
|
put: (row) => {
|
|
31881
|
+
if (this.bootWindow.open()) {
|
|
31882
|
+
this.bootWindow.stageProvisional(row);
|
|
31883
|
+
return;
|
|
31884
|
+
}
|
|
31474
31885
|
this.ledger.put(row).catch(() => void 0);
|
|
31475
31886
|
},
|
|
31476
31887
|
remove: (key) => {
|
|
31888
|
+
if (this.bootWindow.open()) this.bootWindow.noteRemoved(key);
|
|
31477
31889
|
this.ledger.forget(key);
|
|
31478
31890
|
},
|
|
31479
31891
|
all: () => this.ledger.snapshot(),
|
|
31480
|
-
answered: () => this.ledger.hasAnswered()
|
|
31892
|
+
answered: () => this.ledger.hasAnswered(),
|
|
31893
|
+
deferWhileOpen: (key, replay) => this.bootWindow.deferWhileOpen(key, replay)
|
|
31481
31894
|
};
|
|
31482
31895
|
}
|
|
31483
31896
|
};
|
|
@@ -31602,8 +32015,21 @@ var NcStateSummaryService = class {
|
|
|
31602
32015
|
const nowMs = this.deps.now();
|
|
31603
32016
|
const summary = await this.collect(rule, nowMs);
|
|
31604
32017
|
const text = this.word(summary, nowMs);
|
|
32018
|
+
const split = require_clip_model_registry.systemEventSummarySplitOf(rule);
|
|
32019
|
+
const empty = isEmptyStateSummary(summary);
|
|
31605
32020
|
return {
|
|
31606
|
-
empty
|
|
32021
|
+
empty,
|
|
32022
|
+
split,
|
|
32023
|
+
rows: empty ? [] : split === "section" ? text.sections.map((section) => ({
|
|
32024
|
+
section: section.id,
|
|
32025
|
+
title: section.title,
|
|
32026
|
+
text: section.body,
|
|
32027
|
+
count: section.count
|
|
32028
|
+
})) : [{
|
|
32029
|
+
title: text.sections.map((section) => section.title).join(", "),
|
|
32030
|
+
text: text.text,
|
|
32031
|
+
count: text.count
|
|
32032
|
+
}],
|
|
31607
32033
|
count: text.count,
|
|
31608
32034
|
text: text.text,
|
|
31609
32035
|
sections: summary.sections.map((section) => ({
|
|
@@ -31616,12 +32042,84 @@ var NcStateSummaryService = class {
|
|
|
31616
32042
|
excludedGroups: [...summary.excludedGroups]
|
|
31617
32043
|
};
|
|
31618
32044
|
}
|
|
31619
|
-
|
|
32045
|
+
/**
|
|
32046
|
+
* TEST the rule (D698, `nc.testRule`): the summary it would send NOW,
|
|
32047
|
+
* ENQUEUED through the ordinary outbox under a test record id — split as the
|
|
32048
|
+
* rule is. The per-camera mute is bypassed, as every rule test bypasses it,
|
|
32049
|
+
* and a summary has no cooldown to bypass. Nothing to report sends nothing
|
|
32050
|
+
* and SAYS so, per section.
|
|
32051
|
+
*/
|
|
32052
|
+
async test(rule) {
|
|
32053
|
+
const nowMs = this.deps.now();
|
|
32054
|
+
const summary = await this.collect(rule, nowMs, () => false);
|
|
32055
|
+
const split = require_clip_model_registry.systemEventSummarySplitOf(rule);
|
|
32056
|
+
const recordId = stateSummaryTestRecordId(rule.id, nowMs);
|
|
32057
|
+
const sections = summary.sections.map((section) => ({
|
|
32058
|
+
id: section.id,
|
|
32059
|
+
status: section.status,
|
|
32060
|
+
items: section.items.length,
|
|
32061
|
+
...section.reason !== void 0 ? { reason: section.reason } : {}
|
|
32062
|
+
}));
|
|
32063
|
+
const meta = {
|
|
32064
|
+
ruleId: rule.id,
|
|
32065
|
+
rule: rule.name,
|
|
32066
|
+
split,
|
|
32067
|
+
bypassMute: true
|
|
32068
|
+
};
|
|
32069
|
+
if (isEmptyStateSummary(summary)) {
|
|
32070
|
+
this.deps.logger.info("state summary TEST has nothing to report — not sent", { meta: {
|
|
32071
|
+
...meta,
|
|
32072
|
+
dropped: summary.dropped
|
|
32073
|
+
} });
|
|
32074
|
+
return {
|
|
32075
|
+
recordId,
|
|
32076
|
+
split,
|
|
32077
|
+
nothingToReport: true,
|
|
32078
|
+
sections,
|
|
32079
|
+
rows: []
|
|
32080
|
+
};
|
|
32081
|
+
}
|
|
32082
|
+
const subjects = stateSummarySubjects({
|
|
32083
|
+
rule,
|
|
32084
|
+
summary,
|
|
32085
|
+
text: this.word(summary, nowMs),
|
|
32086
|
+
window: {
|
|
32087
|
+
startMs: nowMs,
|
|
32088
|
+
endMs: nowMs
|
|
32089
|
+
},
|
|
32090
|
+
split,
|
|
32091
|
+
recordBase: recordId
|
|
32092
|
+
});
|
|
32093
|
+
const rows = [];
|
|
32094
|
+
for (const subject of subjects) {
|
|
32095
|
+
const enqueued = await this.deps.enqueue(rule, subject, "synthetic");
|
|
32096
|
+
const section = subject.systemEvent?.data["section"];
|
|
32097
|
+
rows.push({
|
|
32098
|
+
recordId: subject.recordId,
|
|
32099
|
+
...typeof section === "string" ? { section } : {},
|
|
32100
|
+
enqueued: enqueued.inserted,
|
|
32101
|
+
targetIds: [...enqueued.targetIds]
|
|
32102
|
+
});
|
|
32103
|
+
}
|
|
32104
|
+
this.deps.logger.info("state summary TEST enqueued", { meta: {
|
|
32105
|
+
...meta,
|
|
32106
|
+
items: summary.itemCount,
|
|
32107
|
+
rows: rows.map((row) => `${row.recordId}:${String(row.enqueued)}`)
|
|
32108
|
+
} });
|
|
32109
|
+
return {
|
|
32110
|
+
recordId,
|
|
32111
|
+
split,
|
|
32112
|
+
nothingToReport: false,
|
|
32113
|
+
sections,
|
|
32114
|
+
rows
|
|
32115
|
+
};
|
|
32116
|
+
}
|
|
32117
|
+
async collect(rule, nowMs, isMuted = this.deps.isMuted) {
|
|
31620
32118
|
const nodes = await this.nodeView();
|
|
31621
32119
|
return collectStateSummary({
|
|
31622
32120
|
rule,
|
|
31623
32121
|
nowMs,
|
|
31624
|
-
isMuted
|
|
32122
|
+
isMuted,
|
|
31625
32123
|
sources: stateSources({
|
|
31626
32124
|
alerts: this.deps.alerts,
|
|
31627
32125
|
liveness: this.deps.liveness,
|
|
@@ -31657,17 +32155,24 @@ var NcStateSummaryService = class {
|
|
|
31657
32155
|
});
|
|
31658
32156
|
}
|
|
31659
32157
|
async deliver(rule, summary, window) {
|
|
31660
|
-
const
|
|
32158
|
+
const text = this.word(summary, window.endMs);
|
|
32159
|
+
const subjects = stateSummarySubjects({
|
|
31661
32160
|
rule,
|
|
31662
32161
|
summary,
|
|
31663
|
-
text
|
|
31664
|
-
window
|
|
32162
|
+
text,
|
|
32163
|
+
window,
|
|
32164
|
+
split: require_clip_model_registry.systemEventSummarySplitOf(rule)
|
|
31665
32165
|
});
|
|
31666
|
-
if (
|
|
31667
|
-
this.deps.logger.warn("state summary
|
|
32166
|
+
if (subjects.length === 0) {
|
|
32167
|
+
this.deps.logger.warn("state summary produced no row — not sent", { meta: {
|
|
32168
|
+
ruleId: rule.id,
|
|
32169
|
+
sections: text.sections.length
|
|
32170
|
+
} });
|
|
31668
32171
|
return 0;
|
|
31669
32172
|
}
|
|
31670
|
-
|
|
32173
|
+
let inserted = 0;
|
|
32174
|
+
for (const subject of subjects) inserted += (await this.deps.enqueue(rule, subject, "system")).inserted;
|
|
32175
|
+
return inserted;
|
|
31671
32176
|
}
|
|
31672
32177
|
};
|
|
31673
32178
|
//#endregion
|
|
@@ -33393,7 +33898,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
33393
33898
|
livenessHoldoffMs: deps.livenessHoldoffMs ?? DEVICE_LIVENESS_HOLDOFF_MS,
|
|
33394
33899
|
isMuted: (deviceId) => this.deviceMutes.isMuted(deviceId),
|
|
33395
33900
|
texts: () => this.texts,
|
|
33396
|
-
enqueue: (rule, subject) => this.enqueueStateSummary(rule, subject)
|
|
33901
|
+
enqueue: (rule, subject, origin) => this.enqueueStateSummary(rule, subject, origin)
|
|
33397
33902
|
});
|
|
33398
33903
|
this.confirmGate = deps.generateVision !== void 0 ? new NcConfirmGate({
|
|
33399
33904
|
logger: this.logger.child("confirm"),
|
|
@@ -35934,30 +36439,42 @@ var NotificationCenter = class NotificationCenter {
|
|
|
35934
36439
|
await this.enqueuePickerTargets(event, reached, outcomes);
|
|
35935
36440
|
return report({ candidates: candidates.length });
|
|
35936
36441
|
}
|
|
36442
|
+
/** TEST a state-summary rule (D698, `nc.testRule`) — see `NcStateSummaryService.test`. */
|
|
36443
|
+
testStateSummary(rule) {
|
|
36444
|
+
return this.stateSummary.test(rule);
|
|
36445
|
+
}
|
|
35937
36446
|
/**
|
|
35938
|
-
* Enqueue ONE state summary (D647) through the rule's ordinary rows: its
|
|
36447
|
+
* Enqueue ONE state summary row (D647) through the rule's ordinary rows: its
|
|
35939
36448
|
* targets, its users (admins only — the row files under the system device),
|
|
35940
36449
|
* its template, and the drain's snooze gate and retries. The record id is
|
|
35941
|
-
* the dedup key, so a window
|
|
36450
|
+
* the dedup key, so a window — or each section of it (D698) — is enqueued
|
|
36451
|
+
* once whatever the restarts. A TEST's rows carry the `synthetic` origin, so
|
|
36452
|
+
* they are marked as a test exactly like every other rule test's.
|
|
35942
36453
|
*/
|
|
35943
|
-
async enqueueStateSummary(rule, subject) {
|
|
35944
|
-
const appended = withSummaryBody(rule);
|
|
36454
|
+
async enqueueStateSummary(rule, subject, origin) {
|
|
36455
|
+
const appended = withSummaryBody(rule, stateSummarySectionOf(subject));
|
|
35945
36456
|
if (appended !== null) this.logger.warn("state summary rule body names no summary content — {{summaryText}} appended", { meta: {
|
|
35946
36457
|
ruleId: rule.id,
|
|
35947
36458
|
rule: rule.name
|
|
35948
36459
|
} });
|
|
35949
36460
|
const sent = appended ?? rule;
|
|
35950
36461
|
const userTargets = await this.resolveUserTargets(sent, subject.deviceId);
|
|
35951
|
-
const entries = this.buildEntries(sent, subject, "system-event", ["systemEvent"], userTargets,
|
|
36462
|
+
const entries = this.buildEntries(sent, subject, "system-event", ["systemEvent"], userTargets, origin);
|
|
35952
36463
|
if (entries.length === 0) {
|
|
35953
36464
|
this.logger.warn("state summary reached NO target — its users are not admins, and it has no targets", { meta: {
|
|
35954
36465
|
ruleId: rule.id,
|
|
35955
36466
|
rule: rule.name,
|
|
35956
36467
|
targetUsers: rule.targetUsers ?? []
|
|
35957
36468
|
} });
|
|
35958
|
-
return
|
|
36469
|
+
return {
|
|
36470
|
+
inserted: 0,
|
|
36471
|
+
targetIds: []
|
|
36472
|
+
};
|
|
35959
36473
|
}
|
|
35960
|
-
return
|
|
36474
|
+
return {
|
|
36475
|
+
inserted: await this.outbox.enqueue(entries),
|
|
36476
|
+
targetIds: entries.map((entry) => entry.targetId)
|
|
36477
|
+
};
|
|
35961
36478
|
}
|
|
35962
36479
|
/**
|
|
35963
36480
|
* The rules a trigger may fire, which is `listEnabled(delivery)` plus and
|
|
@@ -36671,9 +37188,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
36671
37188
|
*
|
|
36672
37189
|
* A seed is INFO and a repeat is DEBUG on purpose: seeding happens once per
|
|
36673
37190
|
* subject per lifetime of the ledger, and it is exactly what an operator
|
|
36674
|
-
* reading the logs after a restart needs to see ("it
|
|
36675
|
-
*
|
|
36676
|
-
*
|
|
37191
|
+
* reading the logs after a restart needs to see ("it did not announce
|
|
37192
|
+
* them"). A seed heard BEFORE the boot load is provisional (D698): the load
|
|
37193
|
+
* keeps the stored row when it holds the same state, and its `boot window
|
|
37194
|
+
* settled` line says how many it kept and wrote. A no-flip is ordinary bus
|
|
37195
|
+
* chatter — two categories describing one edge — and would be noise at info.
|
|
36677
37196
|
*/
|
|
36678
37197
|
reportLivenessDrop(incoming, verdict) {
|
|
36679
37198
|
const dropped = discardedLivenessEvent(incoming, verdict);
|
|
@@ -88228,6 +88747,18 @@ function isRecord(x) {
|
|
|
88228
88747
|
}
|
|
88229
88748
|
//#endregion
|
|
88230
88749
|
//#region src/pipeline-analytics/store/track-alias-store.ts
|
|
88750
|
+
/**
|
|
88751
|
+
* The alias collection.
|
|
88752
|
+
*
|
|
88753
|
+
* @durable class=ledger owner=pipeline-analytics
|
|
88754
|
+
* write="one row per fused piece (D683): aliasId = the absorbed provisional
|
|
88755
|
+
* id, trackId = its survivor; rewritten when the survivor is itself
|
|
88756
|
+
* absorbed by a later fusion"
|
|
88757
|
+
* retention="TRACK-OWNED by its survivor through the declared `trackId`
|
|
88758
|
+
* column: aliases leave with the survivor in the retention cascade, and
|
|
88759
|
+
* widen that cascade (`expandLeafIds`) to rows still keyed by an
|
|
88760
|
+
* absorbed id"
|
|
88761
|
+
*/
|
|
88231
88762
|
var TRACK_ALIASES_COLLECTION = "pipeline-analytics:track-aliases";
|
|
88232
88763
|
var TRACK_ALIAS_COLUMNS = [
|
|
88233
88764
|
{
|
|
@@ -88410,6 +88941,20 @@ var TrackAliasStore = class {
|
|
|
88410
88941
|
dropped: 0
|
|
88411
88942
|
}];
|
|
88412
88943
|
}
|
|
88944
|
+
/**
|
|
88945
|
+
* The orphan audit's reclaim of ONE alias row: the row AND the `recent`
|
|
88946
|
+
* entry that would otherwise keep resolving it for up to an hour. `true`
|
|
88947
|
+
* only when a row was actually deleted. (`misses` needs nothing: a deleted
|
|
88948
|
+
* alias IS a non-alias, which is what a miss records.)
|
|
88949
|
+
*/
|
|
88950
|
+
async deleteAlias(aliasId) {
|
|
88951
|
+
const { deleted } = await this.store.deleteWhere.mutate({
|
|
88952
|
+
collection: TRACK_ALIASES_COLLECTION,
|
|
88953
|
+
filter: { where: { id: aliasId } }
|
|
88954
|
+
});
|
|
88955
|
+
this.recent.delete(aliasId);
|
|
88956
|
+
return deleted > 0;
|
|
88957
|
+
}
|
|
88413
88958
|
async deleteByTracks(trackIds) {
|
|
88414
88959
|
const { deleted } = await this.store.deleteWhere.mutate({
|
|
88415
88960
|
collection: TRACK_ALIASES_COLLECTION,
|
|
@@ -89336,7 +89881,8 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
89336
89881
|
stoppedBy,
|
|
89337
89882
|
nextCursor: offset
|
|
89338
89883
|
} });
|
|
89339
|
-
|
|
89884
|
+
const freedNothing = scope.sizeField === null ? tally.reclaimed === 0 : tally.bytesReclaimed === 0;
|
|
89885
|
+
if (limits.mode === "reclaim" && tally.orphans > 0 && freedNothing) deps.logger.warn("orphan audit: reclaim pass freed no bytes against a standing backlog", { meta: {
|
|
89340
89886
|
collection: scope.collection,
|
|
89341
89887
|
shard: scope.shard?.value ?? null,
|
|
89342
89888
|
orphans: tally.orphans,
|
|
@@ -89450,6 +89996,40 @@ var ORPHAN_AUDIT_DEFAULTS = {
|
|
|
89450
89996
|
pageSize: OrphanAuditPageSizeSchema.parse(void 0)
|
|
89451
89997
|
};
|
|
89452
89998
|
var RAW_DELETE_COLLECTIONS = new Set(["pipeline-analytics:object-events", "pipeline-analytics:plates"]);
|
|
89999
|
+
/**
|
|
90000
|
+
* The `deleteRow` port: ONE row by its primary key `id` (what every
|
|
90001
|
+
* raw-delete collection above declares), answering whether a row went.
|
|
90002
|
+
*
|
|
90003
|
+
* Through `deleteWhere`, because it is the delete that SAYS: `delete`'s output
|
|
90004
|
+
* is `z.void()`, and reading it as `!== undefined` answered `false` for every
|
|
90005
|
+
* row it really deleted — the audit counted every plates/object-events reclaim
|
|
90006
|
+
* as "found nothing", and the per-scope reclaim ceiling never tripped.
|
|
90007
|
+
*/
|
|
90008
|
+
function settingsRowDeleter(store) {
|
|
90009
|
+
return async (collection, key) => {
|
|
90010
|
+
const { deleted } = await store.deleteWhere.mutate({
|
|
90011
|
+
collection,
|
|
90012
|
+
filter: { where: { id: key } }
|
|
90013
|
+
});
|
|
90014
|
+
return deleted > 0;
|
|
90015
|
+
};
|
|
90016
|
+
}
|
|
90017
|
+
/**
|
|
90018
|
+
* The `deleteRow` port AS PRODUCTION WIRES IT: {@link settingsRowDeleter} over
|
|
90019
|
+
* a store that may not be bound yet. Unbound THROWS — the engine counts that as
|
|
90020
|
+
* `reclaimFailed` and keeps the row — rather than answering "nothing deleted",
|
|
90021
|
+
* which is indistinguishable from a row already gone. A named factory so the
|
|
90022
|
+
* wiring itself is under test (D695 T7 review M1: the inline wiring could be
|
|
90023
|
+
* reverted to the original bug with every spec green);
|
|
90024
|
+
* `scripts/check-orphan-reclaimers.ts` checks `index.ts` uses it.
|
|
90025
|
+
*/
|
|
90026
|
+
function settingsRowDeleterOf(getStore) {
|
|
90027
|
+
return async (collection, key) => {
|
|
90028
|
+
const store = getStore();
|
|
90029
|
+
if (store === null) throw new Error("orphan audit: settings store not bound");
|
|
90030
|
+
return await settingsRowDeleter(store)(collection, key);
|
|
90031
|
+
};
|
|
90032
|
+
}
|
|
89453
90033
|
function buildOrphanReclaimer(ports) {
|
|
89454
90034
|
return async (collection, key) => {
|
|
89455
90035
|
if (collection === MEDIA_COLLECTION) {
|
|
@@ -89473,6 +90053,10 @@ function buildOrphanReclaimer(ports) {
|
|
|
89473
90053
|
bytes: null
|
|
89474
90054
|
};
|
|
89475
90055
|
}
|
|
90056
|
+
if (collection === TRACK_ALIASES_COLLECTION) return {
|
|
90057
|
+
rows: await ports.deleteTrackAlias(key) ? 1 : 0,
|
|
90058
|
+
bytes: null
|
|
90059
|
+
};
|
|
89476
90060
|
if (RAW_DELETE_COLLECTIONS.has(collection)) return {
|
|
89477
90061
|
rows: await ports.deleteRow(collection, key) ? 1 : 0,
|
|
89478
90062
|
bytes: null
|
|
@@ -99842,6 +100426,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
99842
100426
|
},
|
|
99843
100427
|
listTrackGroups: (query) => center.listTrackGroups(query),
|
|
99844
100428
|
injectTestEvent: (input) => center.injectTestEvent(input, { getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }) }),
|
|
100429
|
+
testStateSummary: (rule) => center.testStateSummary(rule),
|
|
99845
100430
|
timelapse: {
|
|
99846
100431
|
store: center.timelapseStore,
|
|
99847
100432
|
getRecordingConfig: (deviceId) => api.recordingArchive.getDeviceConfig.query({ deviceId }).catch(() => null),
|
|
@@ -107600,10 +108185,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
107600
108185
|
bytes: count.bytes
|
|
107601
108186
|
};
|
|
107602
108187
|
},
|
|
107603
|
-
|
|
107604
|
-
|
|
107605
|
-
|
|
107606
|
-
|
|
108188
|
+
deleteTrackAlias: async (aliasId) => {
|
|
108189
|
+
const aliasStore = this.trackAliasStore;
|
|
108190
|
+
if (aliasStore === null) throw new Error("track alias store not bound");
|
|
108191
|
+
return await aliasStore.deleteAlias(aliasId);
|
|
108192
|
+
},
|
|
108193
|
+
deleteRow: settingsRowDeleterOf(() => this.settingsStore),
|
|
107607
108194
|
logger: this.ctx.logger
|
|
107608
108195
|
}),
|
|
107609
108196
|
now: () => Date.now(),
|