@camstack/addon-post-analysis 1.2.307 → 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-C67UM_q_.js → clip-model-registry-B3M4ZI17.js} +392 -264
- package/dist/{clip-model-registry-C9Aw4qkh.mjs → clip-model-registry-CrBn6Qvt.mjs} +387 -265
- 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-QE0Juzwb.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-DzNrV8y8.mjs → hostInit-Dj0aaMhv.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +1350 -389
- package/dist/pipeline-analytics/index.mjs +1350 -389
- 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-MwQBD526.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);
|
|
@@ -42578,6 +43097,280 @@ function parseDebugDeviceIds(url) {
|
|
|
42578
43097
|
return out;
|
|
42579
43098
|
}
|
|
42580
43099
|
//#endregion
|
|
43100
|
+
//#region src/pipeline-analytics/store/tracking-tape-index-store.ts
|
|
43101
|
+
/**
|
|
43102
|
+
* @durable class=ledger owner=pipeline-analytics
|
|
43103
|
+
* write="one row per PROMOTED tracking-tape window (`debug.tape.promote`);
|
|
43104
|
+
* updated when the operator marks it reviewed or releases its pins;
|
|
43105
|
+
* deleted together with its file by `debug.tape.delete`."
|
|
43106
|
+
* retention="NOT an age clock and NOT track retention — a promoted window is
|
|
43107
|
+
* history being labelled (D681). Bounded by the promotion budget
|
|
43108
|
+
* (MAX_PROMOTED_TAPES = 60, 1 GiB): a promotion beyond it is REFUSED,
|
|
43109
|
+
* never satisfied by evicting another row."
|
|
43110
|
+
*/
|
|
43111
|
+
var TRACKING_TAPES_COLLECTION = "pipeline-analytics:tracking-tapes";
|
|
43112
|
+
var TRACKING_TAPES_COLUMNS = [
|
|
43113
|
+
{
|
|
43114
|
+
name: "id",
|
|
43115
|
+
type: "TEXT",
|
|
43116
|
+
primaryKey: true,
|
|
43117
|
+
notNull: true
|
|
43118
|
+
},
|
|
43119
|
+
{
|
|
43120
|
+
name: "deviceId",
|
|
43121
|
+
type: "INTEGER",
|
|
43122
|
+
notNull: true
|
|
43123
|
+
},
|
|
43124
|
+
{
|
|
43125
|
+
name: "scenario",
|
|
43126
|
+
type: "TEXT",
|
|
43127
|
+
notNull: true
|
|
43128
|
+
},
|
|
43129
|
+
{
|
|
43130
|
+
name: "note",
|
|
43131
|
+
type: "TEXT"
|
|
43132
|
+
},
|
|
43133
|
+
{
|
|
43134
|
+
name: "lighting",
|
|
43135
|
+
type: "TEXT",
|
|
43136
|
+
notNull: true
|
|
43137
|
+
},
|
|
43138
|
+
{
|
|
43139
|
+
name: "fromMs",
|
|
43140
|
+
type: "INTEGER",
|
|
43141
|
+
notNull: true
|
|
43142
|
+
},
|
|
43143
|
+
{
|
|
43144
|
+
name: "toMs",
|
|
43145
|
+
type: "INTEGER",
|
|
43146
|
+
notNull: true
|
|
43147
|
+
},
|
|
43148
|
+
{
|
|
43149
|
+
name: "state",
|
|
43150
|
+
type: "TEXT",
|
|
43151
|
+
notNull: true
|
|
43152
|
+
},
|
|
43153
|
+
{
|
|
43154
|
+
name: "frames",
|
|
43155
|
+
type: "INTEGER",
|
|
43156
|
+
notNull: true
|
|
43157
|
+
},
|
|
43158
|
+
{
|
|
43159
|
+
name: "skips",
|
|
43160
|
+
type: "INTEGER",
|
|
43161
|
+
notNull: true
|
|
43162
|
+
},
|
|
43163
|
+
{
|
|
43164
|
+
name: "gaps",
|
|
43165
|
+
type: "INTEGER",
|
|
43166
|
+
notNull: true
|
|
43167
|
+
},
|
|
43168
|
+
{
|
|
43169
|
+
name: "bytes",
|
|
43170
|
+
type: "INTEGER",
|
|
43171
|
+
notNull: true
|
|
43172
|
+
},
|
|
43173
|
+
{
|
|
43174
|
+
name: "reviewed",
|
|
43175
|
+
type: "BOOLEAN",
|
|
43176
|
+
notNull: true
|
|
43177
|
+
},
|
|
43178
|
+
{
|
|
43179
|
+
name: "trackIds",
|
|
43180
|
+
type: "TEXT",
|
|
43181
|
+
notNull: true
|
|
43182
|
+
},
|
|
43183
|
+
{
|
|
43184
|
+
name: "createdAt",
|
|
43185
|
+
type: "INTEGER",
|
|
43186
|
+
notNull: true
|
|
43187
|
+
},
|
|
43188
|
+
{
|
|
43189
|
+
name: "releasedAt",
|
|
43190
|
+
type: "INTEGER"
|
|
43191
|
+
},
|
|
43192
|
+
{
|
|
43193
|
+
name: "pinsSettled",
|
|
43194
|
+
type: "BOOLEAN",
|
|
43195
|
+
notNull: true
|
|
43196
|
+
}
|
|
43197
|
+
];
|
|
43198
|
+
var TrackingTapeRowSchema = object({
|
|
43199
|
+
id: string(),
|
|
43200
|
+
deviceId: number().int(),
|
|
43201
|
+
scenario: string(),
|
|
43202
|
+
note: string().nullable(),
|
|
43203
|
+
lighting: TapeLightingSchema,
|
|
43204
|
+
fromMs: number().int(),
|
|
43205
|
+
toMs: number().int(),
|
|
43206
|
+
state: _enum(["complete", "incomplete"]),
|
|
43207
|
+
frames: number().int(),
|
|
43208
|
+
skips: number().int(),
|
|
43209
|
+
gaps: number().int(),
|
|
43210
|
+
bytes: number().int(),
|
|
43211
|
+
reviewed: boolean(),
|
|
43212
|
+
trackIds: array(string()),
|
|
43213
|
+
createdAt: number().int(),
|
|
43214
|
+
releasedAt: number().int().nullable(),
|
|
43215
|
+
pinsSettled: boolean()
|
|
43216
|
+
});
|
|
43217
|
+
var TrackIdsSchema = array(string());
|
|
43218
|
+
/**
|
|
43219
|
+
* True when the SQLite settings backend would hand `text` back DECODED: it
|
|
43220
|
+
* parses any TEXT that starts with `[` or `{` and is valid JSON
|
|
43221
|
+
* (`decodeColumnValue` decodes by content, not by declared type). Free text
|
|
43222
|
+
* like that cannot round-trip through a TEXT column (follow-up 617, review M2).
|
|
43223
|
+
*/
|
|
43224
|
+
function decodesAsJson(text) {
|
|
43225
|
+
if (!(text.startsWith("[") || text.startsWith("{"))) return false;
|
|
43226
|
+
try {
|
|
43227
|
+
JSON.parse(text);
|
|
43228
|
+
return true;
|
|
43229
|
+
} catch {
|
|
43230
|
+
return false;
|
|
43231
|
+
}
|
|
43232
|
+
}
|
|
43233
|
+
/**
|
|
43234
|
+
* Free text (`scenario`, `note`) as the column hands it back: a value the
|
|
43235
|
+
* backend decoded is re-encoded, so a row written before `debug.tape.promote`
|
|
43236
|
+
* refused such text never vanishes (its whitespace may differ). Anything else
|
|
43237
|
+
* goes to the schema as it came.
|
|
43238
|
+
*/
|
|
43239
|
+
function freeText(raw) {
|
|
43240
|
+
return raw !== null && typeof raw === "object" ? JSON.stringify(raw) : raw;
|
|
43241
|
+
}
|
|
43242
|
+
/**
|
|
43243
|
+
* `trackIds` as the column hands it back. The SQLite backend decodes a TEXT
|
|
43244
|
+
* that starts with `[` on read (`decodeColumnValue`), so the store sees the
|
|
43245
|
+
* ARRAY it wrote; a string is parsed here only for a backend (or an old row)
|
|
43246
|
+
* that hands the text back as written. `JSON.parse(String(array))` threw on the
|
|
43247
|
+
* decoded array and every promoted row was skipped as malformed (follow-up 617).
|
|
43248
|
+
* `null` = unreadable, absent included (the column is NOT NULL, so absence is
|
|
43249
|
+
* corruption) — the row is reported, never repaired.
|
|
43250
|
+
*/
|
|
43251
|
+
function decodeTrackIds(raw) {
|
|
43252
|
+
if (raw === void 0 || raw === null) return null;
|
|
43253
|
+
if (typeof raw !== "string") return raw;
|
|
43254
|
+
try {
|
|
43255
|
+
return JSON.parse(raw);
|
|
43256
|
+
} catch {
|
|
43257
|
+
return null;
|
|
43258
|
+
}
|
|
43259
|
+
}
|
|
43260
|
+
var TrackingTapeIndexStore = class {
|
|
43261
|
+
store;
|
|
43262
|
+
logger;
|
|
43263
|
+
constructor(deps) {
|
|
43264
|
+
this.store = deps.store;
|
|
43265
|
+
this.logger = deps.logger;
|
|
43266
|
+
}
|
|
43267
|
+
static async declare(store) {
|
|
43268
|
+
await store.declareCollection.mutate({
|
|
43269
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43270
|
+
columns: [...TRACKING_TAPES_COLUMNS],
|
|
43271
|
+
indexes: [{
|
|
43272
|
+
name: "idx_trackingtapes_device_created",
|
|
43273
|
+
columns: ["deviceId", "createdAt"]
|
|
43274
|
+
}]
|
|
43275
|
+
});
|
|
43276
|
+
}
|
|
43277
|
+
async insert(row) {
|
|
43278
|
+
const { id, trackIds, ...rest } = TrackingTapeRowSchema.parse(row);
|
|
43279
|
+
await this.store.insert.mutate({
|
|
43280
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43281
|
+
record: {
|
|
43282
|
+
id,
|
|
43283
|
+
data: {
|
|
43284
|
+
...rest,
|
|
43285
|
+
trackIds: JSON.stringify(trackIds)
|
|
43286
|
+
}
|
|
43287
|
+
}
|
|
43288
|
+
});
|
|
43289
|
+
}
|
|
43290
|
+
async get(id) {
|
|
43291
|
+
const first = (await this.store.query.query({
|
|
43292
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43293
|
+
filter: {
|
|
43294
|
+
where: { id },
|
|
43295
|
+
limit: 1
|
|
43296
|
+
}
|
|
43297
|
+
}))[0];
|
|
43298
|
+
return first === void 0 ? null : this.parse(first.id, first.data);
|
|
43299
|
+
}
|
|
43300
|
+
async list(deviceId) {
|
|
43301
|
+
const rows = await this.store.query.query({
|
|
43302
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43303
|
+
filter: {
|
|
43304
|
+
...deviceId !== void 0 ? { where: { deviceId } } : {},
|
|
43305
|
+
orderBy: {
|
|
43306
|
+
field: "createdAt",
|
|
43307
|
+
direction: "desc"
|
|
43308
|
+
},
|
|
43309
|
+
limit: 1e3
|
|
43310
|
+
}
|
|
43311
|
+
});
|
|
43312
|
+
const out = [];
|
|
43313
|
+
for (const r of rows) {
|
|
43314
|
+
const parsed = this.parse(r.id, r.data);
|
|
43315
|
+
if (parsed !== null) out.push(parsed);
|
|
43316
|
+
}
|
|
43317
|
+
return out;
|
|
43318
|
+
}
|
|
43319
|
+
async setReviewed(id, reviewed) {
|
|
43320
|
+
await this.store.update.mutate({
|
|
43321
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43322
|
+
id,
|
|
43323
|
+
data: { reviewed }
|
|
43324
|
+
});
|
|
43325
|
+
}
|
|
43326
|
+
/** Release AND unsettle in one row write: the unpins the release implies
|
|
43327
|
+
* are owed from this moment, whether or not the caller lives to make them. */
|
|
43328
|
+
async setReleased(id, atMs) {
|
|
43329
|
+
await this.store.update.mutate({
|
|
43330
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43331
|
+
id,
|
|
43332
|
+
data: {
|
|
43333
|
+
releasedAt: atMs,
|
|
43334
|
+
pinsSettled: false
|
|
43335
|
+
}
|
|
43336
|
+
});
|
|
43337
|
+
}
|
|
43338
|
+
async setPinsSettled(id, settled) {
|
|
43339
|
+
await this.store.update.mutate({
|
|
43340
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43341
|
+
id,
|
|
43342
|
+
data: { pinsSettled: settled }
|
|
43343
|
+
});
|
|
43344
|
+
}
|
|
43345
|
+
async remove(id) {
|
|
43346
|
+
await this.store.delete.mutate({
|
|
43347
|
+
collection: TRACKING_TAPES_COLLECTION,
|
|
43348
|
+
key: id
|
|
43349
|
+
});
|
|
43350
|
+
}
|
|
43351
|
+
parse(id, data) {
|
|
43352
|
+
const ids = TrackIdsSchema.safeParse(decodeTrackIds(data["trackIds"]));
|
|
43353
|
+
const parsed = TrackingTapeRowSchema.safeParse({
|
|
43354
|
+
id,
|
|
43355
|
+
...data,
|
|
43356
|
+
scenario: freeText(data["scenario"]),
|
|
43357
|
+
note: freeText(data["note"] ?? null),
|
|
43358
|
+
releasedAt: data["releasedAt"] ?? null,
|
|
43359
|
+
trackIds: ids.success ? ids.data : null
|
|
43360
|
+
});
|
|
43361
|
+
if (parsed.success) return parsed.data;
|
|
43362
|
+
this.logger.warn("tracking tape index: skipped a malformed row", {
|
|
43363
|
+
meta: {
|
|
43364
|
+
id,
|
|
43365
|
+
issues: parsed.error.issues.map((i) => i.path.join(".")),
|
|
43366
|
+
trackIdsType: typeof data["trackIds"]
|
|
43367
|
+
},
|
|
43368
|
+
...typeof data["deviceId"] === "number" ? { tags: { deviceId: data["deviceId"] } } : {}
|
|
43369
|
+
});
|
|
43370
|
+
return null;
|
|
43371
|
+
}
|
|
43372
|
+
};
|
|
43373
|
+
//#endregion
|
|
42581
43374
|
//#region src/pipeline-analytics/debug/tape-actions.ts
|
|
42582
43375
|
/**
|
|
42583
43376
|
* `debug.tape.*` — the tracking-tape surface (D681, spec §3.3). Admin,
|
|
@@ -42608,12 +43401,19 @@ var TapeStatusRowSchema = object({
|
|
|
42608
43401
|
var TapeStatusInputSchema = object({ deviceIds: array(number().int()).optional() });
|
|
42609
43402
|
var TapeStatusOutputSchema = object({ cameras: array(TapeStatusRowSchema).readonly() });
|
|
42610
43403
|
var TapeLightingInputSchema = _enum(["day", "ir"]);
|
|
43404
|
+
/**
|
|
43405
|
+
* Free text the settings store would hand back decoded is refused by name
|
|
43406
|
+
* (follow-up 617, review M2): whole JSON array/object text in a TEXT column
|
|
43407
|
+
* comes back as an array or object, and the index could not read the row.
|
|
43408
|
+
*/
|
|
43409
|
+
var NOT_JSON_TEXT = "must not be JSON array/object text — the settings store would hand it back decoded";
|
|
43410
|
+
var notJsonText = (s) => !decodesAsJson(s);
|
|
42611
43411
|
var PromoteInputSchema = object({
|
|
42612
43412
|
deviceId: number().int().positive(),
|
|
42613
43413
|
fromMs: number().int(),
|
|
42614
43414
|
toMs: number().int(),
|
|
42615
|
-
scenario: string(),
|
|
42616
|
-
note: string().max(500).optional(),
|
|
43415
|
+
scenario: string().refine(notJsonText, { message: NOT_JSON_TEXT }),
|
|
43416
|
+
note: string().max(500).refine(notJsonText, { message: NOT_JSON_TEXT }).optional(),
|
|
42617
43417
|
lighting: TapeLightingInputSchema.optional()
|
|
42618
43418
|
});
|
|
42619
43419
|
var PromoteOutputSchema = discriminatedUnion("ok", [object({
|
|
@@ -51074,6 +51874,17 @@ function pairRiders(detections) {
|
|
|
51074
51874
|
* (D438) no frame did, and a newcomer on the stale prediction took the id.
|
|
51075
51875
|
*/
|
|
51076
51876
|
var MAX_PREDICTION_MS = 1e3;
|
|
51877
|
+
/**
|
|
51878
|
+
* A frame more than `maxMissedMs` behind the latest is a CLOCK STEP only as
|
|
51879
|
+
* the K-th such frame in a row, each in order with the one before (follow-up
|
|
51880
|
+
* 617, fix round 3; D49: a read that would destroy work needs another to
|
|
51881
|
+
* agree). A step ends every track on the camera; one stray timestamp — a
|
|
51882
|
+
* backlog spike 4.5 s late — or two runner clocks delivering interleaved
|
|
51883
|
+
* (a camera migrating between runners) must not. Three is the least that a
|
|
51884
|
+
* single late frame plus an interleaved partner cannot reach by chance; the
|
|
51885
|
+
* cost of a real step is K−1 dropped frames (~260 ms at 7.7 fps).
|
|
51886
|
+
*/
|
|
51887
|
+
var CLOCK_STEP_CONFIRM_FRAMES = 3;
|
|
51077
51888
|
/** Confident boxes kept on a tracklet's path (the old tracker's bound). */
|
|
51078
51889
|
var MAX_PATH_LENGTH = 300;
|
|
51079
51890
|
/** Swap test: crossed pairs cheaper than this over the chosen pairs are doubt. */
|
|
@@ -51879,6 +52690,35 @@ function linkReach(from, gapMs) {
|
|
|
51879
52690
|
return diag * Math.min(LINK_REACH_MAX_DIAGONALS, Math.max(LINK_REACH_MIN_DIAGONALS, LINK_REACH_DIAGONALS_PER_SEC * gapMs / 1e3));
|
|
51880
52691
|
}
|
|
51881
52692
|
var arcKey = (from, to) => `${from}\u0000${to}`;
|
|
52693
|
+
/**
|
|
52694
|
+
* The rejections that mean "geometry or appearance said no to a plausible
|
|
52695
|
+
* continuation" of a confirmed predecessor — a track the operator sees break
|
|
52696
|
+
* (follow-up 617, F5). Every one of them is past `hardRejection`, so the
|
|
52697
|
+
* predecessor is confirmed. NOT here (fix round 1): `unconfirmed`;
|
|
52698
|
+
* `has-successor` (continued elsewhere); `concurrent` and `identity-conflict`
|
|
52699
|
+
* (two subjects — the normal case, and the most frequent); `dropped` (an
|
|
52700
|
+
* operator delete or a refused chain).
|
|
52701
|
+
*/
|
|
52702
|
+
var BREAK_REJECTIONS = new Set([
|
|
52703
|
+
"reach",
|
|
52704
|
+
"area",
|
|
52705
|
+
"appearance",
|
|
52706
|
+
"contested",
|
|
52707
|
+
"cost"
|
|
52708
|
+
]);
|
|
52709
|
+
/** The confirmed predecessors whose link a decision rejected as a break (F5). */
|
|
52710
|
+
function confirmedPredecessorRejections(candidates) {
|
|
52711
|
+
const out = [];
|
|
52712
|
+
for (const c of candidates ?? []) {
|
|
52713
|
+
if (c.rejected === void 0 || !BREAK_REJECTIONS.has(c.rejected)) continue;
|
|
52714
|
+
out.push({
|
|
52715
|
+
from: c.from,
|
|
52716
|
+
rejected: c.rejected,
|
|
52717
|
+
gapMs: c.gapMs
|
|
52718
|
+
});
|
|
52719
|
+
}
|
|
52720
|
+
return out;
|
|
52721
|
+
}
|
|
51882
52722
|
function arcFacts(a) {
|
|
51883
52723
|
return {
|
|
51884
52724
|
from: a.from.id,
|
|
@@ -51956,6 +52796,26 @@ var DeferredLinker = class {
|
|
|
51956
52796
|
if (seen === void 0) this.names.set(p.id, new Set([p.identity]));
|
|
51957
52797
|
else seen.add(p.identity);
|
|
51958
52798
|
}
|
|
52799
|
+
/**
|
|
52800
|
+
* A clock step (follow-up 617, fix round 2): every piece still undecided is
|
|
52801
|
+
* decided `new`, so nothing that existed before the step can be absorbed
|
|
52802
|
+
* across it — whatever was ready was committed just before. The facade
|
|
52803
|
+
* rebases its clock past `LINK_MAX_GAP_MS`, so no later piece can continue
|
|
52804
|
+
* one of these either. An extra id, never a mix-up.
|
|
52805
|
+
*/
|
|
52806
|
+
closeUndecided(now, pieces) {
|
|
52807
|
+
for (const p of pieces) this.accept(p, now);
|
|
52808
|
+
for (const p of this.pieces.values()) {
|
|
52809
|
+
if (this.outcomes.has(p.id)) continue;
|
|
52810
|
+
this.decide(p.id, "new");
|
|
52811
|
+
this.observer?.({
|
|
52812
|
+
t: now,
|
|
52813
|
+
kind: "new",
|
|
52814
|
+
to: p.id,
|
|
52815
|
+
reason: "clock-step"
|
|
52816
|
+
});
|
|
52817
|
+
}
|
|
52818
|
+
}
|
|
51959
52819
|
isTailFinal(id, now, isLive) {
|
|
51960
52820
|
const root = this.rootOf(id);
|
|
51961
52821
|
if (this.pieces.has(root) && !this.outcomes.has(root)) return false;
|
|
@@ -52706,7 +53566,15 @@ var Tracklet = class {
|
|
|
52706
53566
|
h: this.assocBox.h
|
|
52707
53567
|
};
|
|
52708
53568
|
}
|
|
53569
|
+
/**
|
|
53570
|
+
* A confident sighting. One OLDER than the last (`t < lastSeenAt` — ~1 % of
|
|
53571
|
+
* frames reach the tracker out of capture order, up to 1.2 s back) is
|
|
53572
|
+
* `stale`: it changes nothing. A rewound tracklet would be measured from a
|
|
53573
|
+
* box its subject left a second ago, and L2 refused a link by reach on
|
|
53574
|
+
* exactly that (follow-up 617, F1). The caller reports it.
|
|
53575
|
+
*/
|
|
52709
53576
|
observeConfident(det, t, faceBearing) {
|
|
53577
|
+
if (t < this.lastSeenAt) return "stale";
|
|
52710
53578
|
const dt = t - this.lastSeenAt;
|
|
52711
53579
|
if (dt > 0) {
|
|
52712
53580
|
const from = centreOf(this.lastBox);
|
|
@@ -52718,6 +53586,7 @@ var Tracklet = class {
|
|
|
52718
53586
|
this.emitIntervalMs = dt;
|
|
52719
53587
|
}
|
|
52720
53588
|
this.recordConfident(det, t, faceBearing);
|
|
53589
|
+
return "recorded";
|
|
52721
53590
|
}
|
|
52722
53591
|
observeLowScore(box, t) {
|
|
52723
53592
|
this.assocBox = box;
|
|
@@ -52819,6 +53688,15 @@ var TrackletTracker = class {
|
|
|
52819
53688
|
demoteSilent(t) {
|
|
52820
53689
|
for (const tr of this.live.values()) if (tr.mode === "active" && t - tr.lastSeenAt > MAX_PREDICTION_MS) tr.mode = "coasting";
|
|
52821
53690
|
}
|
|
53691
|
+
/** Ends every live tracklet at its own last sighting (a clock step); the ids it ended. */
|
|
53692
|
+
endAll(reason) {
|
|
53693
|
+
const ended = [];
|
|
53694
|
+
for (const tr of [...this.live.values()]) {
|
|
53695
|
+
ended.push(tr.id);
|
|
53696
|
+
this.finish(tr, tr.lastSeenAt, reason);
|
|
53697
|
+
}
|
|
53698
|
+
return ended;
|
|
53699
|
+
}
|
|
52822
53700
|
/** Silence: end what the coast budget no longer covers. */
|
|
52823
53701
|
expire(now) {
|
|
52824
53702
|
for (const tr of [...this.live.values()]) if (now - tr.lastSeenAt > this.config.maxMissedMs) this.finish(tr, tr.lastSeenAt, "missed");
|
|
@@ -52909,12 +53787,12 @@ var TrackletTracker = class {
|
|
|
52909
53787
|
const d = frame.detections[pair.detection];
|
|
52910
53788
|
if (tr === void 0 || d === void 0) return;
|
|
52911
53789
|
frame.used.add(pair.detection);
|
|
53790
|
+
if (tr.observeConfident(d, frame.t, frame.ctx.faceBearing?.has(d.bbox) === true) === "stale") return;
|
|
52912
53791
|
tr.mode = "active";
|
|
52913
53792
|
if (d.class !== tr.klass) this.refuse({
|
|
52914
53793
|
reason: "cross-class-absorbed",
|
|
52915
53794
|
against: tr
|
|
52916
53795
|
}, d);
|
|
52917
|
-
tr.observeConfident(d, frame.t, frame.ctx.faceBearing?.has(d.bbox) === true);
|
|
52918
53796
|
if (pair.contested) {
|
|
52919
53797
|
tr.contestedHits += 1;
|
|
52920
53798
|
tr.contestedSince ??= frame.t;
|
|
@@ -53103,27 +53981,151 @@ var TrackerV2 = class {
|
|
|
53103
53981
|
l1;
|
|
53104
53982
|
l2;
|
|
53105
53983
|
pending = [];
|
|
53984
|
+
/**
|
|
53985
|
+
* Internal time = the caller's frame time + `offset`. Zero until a clock
|
|
53986
|
+
* step; each step moves it so the internal clock never goes back and
|
|
53987
|
+
* resumes past `LINK_MAX_GAP_MS` of silence (fix round 2, C1). L1, L2, their
|
|
53988
|
+
* facts and prunes run on internal time; fusions and `frameClock()` are
|
|
53989
|
+
* handed back in the caller's clock.
|
|
53990
|
+
*/
|
|
53991
|
+
offset = 0;
|
|
53106
53992
|
lastT = 0;
|
|
53993
|
+
/** The latest FRAME time taken (internal) — never the sweep's extrapolated `now`. */
|
|
53994
|
+
lastFrameT = Number.NEGATIVE_INFINITY;
|
|
53995
|
+
observers;
|
|
53996
|
+
/** Caller times of the consecutive frames past the stale bound so far — the
|
|
53997
|
+
* run a clock step needs `CLOCK_STEP_CONFIRM_FRAMES` of (fix round 3). */
|
|
53998
|
+
stepRun = [];
|
|
53107
53999
|
constructor(config, deps) {
|
|
53108
54000
|
this.config = config;
|
|
53109
|
-
this.
|
|
54001
|
+
this.observers = deps.observers ?? {};
|
|
54002
|
+
const out = this.observers;
|
|
54003
|
+
const internal = {
|
|
54004
|
+
...out.refusal !== void 0 ? { refusal: out.refusal } : {},
|
|
54005
|
+
ended: (f) => out.ended?.({
|
|
54006
|
+
...f,
|
|
54007
|
+
at: f.at - this.offset
|
|
54008
|
+
}),
|
|
54009
|
+
split: (f) => out.split?.({
|
|
54010
|
+
...f,
|
|
54011
|
+
t: f.t - this.offset
|
|
54012
|
+
})
|
|
54013
|
+
};
|
|
54014
|
+
this.l1 = new TrackletTracker(config, deps.mintTrackId, internal);
|
|
53110
54015
|
this.l2 = new DeferredLinker({
|
|
53111
54016
|
maxMissedMs: config.maxMissedMs,
|
|
53112
54017
|
appearanceResolvesAmbiguity: config.appearanceResolvesAmbiguity
|
|
53113
|
-
},
|
|
54018
|
+
}, (f) => out.link?.({
|
|
54019
|
+
...f,
|
|
54020
|
+
t: f.t - this.offset
|
|
54021
|
+
}));
|
|
53114
54022
|
}
|
|
53115
|
-
update(detections,
|
|
54023
|
+
update(detections, frameT, ctx) {
|
|
54024
|
+
const order = this.classifyFrame(frameT);
|
|
54025
|
+
if (order === "out-of-order") return this.drop(frameT, detections.length);
|
|
54026
|
+
let t = frameT + this.offset;
|
|
54027
|
+
if (order === "clock-step") t = this.clockStep(frameT, this.lastFrameT - t);
|
|
54028
|
+
this.stepRun = [];
|
|
54029
|
+
this.lastFrameT = t;
|
|
53116
54030
|
this.lastT = Math.max(this.lastT, t);
|
|
53117
54031
|
const step = this.l1.step(detections, t, ctx);
|
|
53118
54032
|
this.link(this.lastT);
|
|
53119
54033
|
return step.emissions.map((e) => this.emit(e, t));
|
|
53120
54034
|
}
|
|
53121
54035
|
/**
|
|
54036
|
+
* A frame stamped before the latest one already taken (follow-up 617, F1;
|
|
54037
|
+
* ~1 % of frames, up to 1.2 s back). It is decided ONCE, for the whole
|
|
54038
|
+
* frame (fix round 1, H1): matched against the tracklets' NEWEST boxes, a
|
|
54039
|
+
* stale box can land on another subject — a walker's old box on the walker
|
|
54040
|
+
* behind — and be emitted under that id. So nothing binds, spawns, splits or
|
|
54041
|
+
* is demoted on it; the confirmed tracklets are emitted as on a frame that
|
|
54042
|
+
* matched nothing (no sighting), and one fact says the frame was dropped.
|
|
54043
|
+
*/
|
|
54044
|
+
/**
|
|
54045
|
+
* How `update` will take a frame at `frameT` — without taking it. The
|
|
54046
|
+
* processor asks first (fix round 3, MEDIUM-2), so a dropped frame never
|
|
54047
|
+
* reaches the stationary gate, the zones or the event path.
|
|
54048
|
+
* - `in-order`: not behind the latest frame taken;
|
|
54049
|
+
* - `out-of-order`: behind it — within `maxMissedMs` (disorder, ≤ 1.2 s
|
|
54050
|
+
* measured), or further back but not yet the `CLOCK_STEP_CONFIRM_FRAMES`-th
|
|
54051
|
+
* such frame in a row;
|
|
54052
|
+
* - `clock-step`: the K-th consecutive frame past the bound, each in order
|
|
54053
|
+
* with the one before (within `maxMissedMs`).
|
|
54054
|
+
*/
|
|
54055
|
+
classifyFrame(frameT) {
|
|
54056
|
+
const behindMs = this.lastFrameT - (frameT + this.offset);
|
|
54057
|
+
if (!(behindMs > 0)) return "in-order";
|
|
54058
|
+
if (behindMs <= this.config.maxMissedMs) return "out-of-order";
|
|
54059
|
+
return this.runLength(frameT) >= CLOCK_STEP_CONFIRM_FRAMES ? "clock-step" : "out-of-order";
|
|
54060
|
+
}
|
|
54061
|
+
/** The past-bound run this frame would make, itself included. */
|
|
54062
|
+
runLength(frameT) {
|
|
54063
|
+
const prev = this.stepRun.at(-1);
|
|
54064
|
+
return (prev !== void 0 && frameT >= prev && frameT - prev <= this.config.maxMissedMs ? this.stepRun.length : 0) + 1;
|
|
54065
|
+
}
|
|
54066
|
+
/**
|
|
54067
|
+
* Drops a frame `classifyFrame` called `out-of-order`. A frame past the
|
|
54068
|
+
* bound extends (or restarts) the run a clock step needs; any other breaks
|
|
54069
|
+
* it. One fact per frame.
|
|
54070
|
+
*/
|
|
54071
|
+
drop(frameT, detections) {
|
|
54072
|
+
const behindMs = this.lastFrameT - (frameT + this.offset);
|
|
54073
|
+
let pendingClockStep = 0;
|
|
54074
|
+
if (behindMs > this.config.maxMissedMs) {
|
|
54075
|
+
pendingClockStep = this.runLength(frameT);
|
|
54076
|
+
this.stepRun = pendingClockStep === 1 ? [frameT] : [...this.stepRun, frameT];
|
|
54077
|
+
} else this.stepRun = [];
|
|
54078
|
+
this.observers.outOfOrder?.({
|
|
54079
|
+
t: frameT,
|
|
54080
|
+
lastFrameT: this.frameClock(),
|
|
54081
|
+
behindMs,
|
|
54082
|
+
detections,
|
|
54083
|
+
pendingClockStep
|
|
54084
|
+
});
|
|
54085
|
+
return this.l1.liveTracklets().filter((tr) => tr.confirmed).map((tracklet) => this.emit({
|
|
54086
|
+
tracklet,
|
|
54087
|
+
detection: void 0,
|
|
54088
|
+
matched: false
|
|
54089
|
+
}, this.lastFrameT));
|
|
54090
|
+
}
|
|
54091
|
+
/**
|
|
54092
|
+
* The frame clock stepped back further than `maxMissedMs` (fix round 2, C1):
|
|
54093
|
+
* not disorder — an NTP correction, or a camera moved to a runner whose clock
|
|
54094
|
+
* is behind. An unbounded gate dropped every frame until the new clock passed
|
|
54095
|
+
* the old high mark (60 s back: 461 frames, a new subject never tracked, the
|
|
54096
|
+
* departed one coasting on). Instead: commit what L2 has decided, end every
|
|
54097
|
+
* live tracklet at its own last sighting, decide every piece still undecided
|
|
54098
|
+
* `new` (no fusion crosses the step), and resume the internal clock past
|
|
54099
|
+
* `LINK_MAX_GAP_MS` of silence, so nothing later can continue anything
|
|
54100
|
+
* earlier. Returns the frame's internal time.
|
|
54101
|
+
*/
|
|
54102
|
+
clockStep(frameT, behindMs) {
|
|
54103
|
+
const lastFrameT = this.frameClock();
|
|
54104
|
+
this.link(this.lastT);
|
|
54105
|
+
const ended = this.l1.endAll("clock-step");
|
|
54106
|
+
this.l2.closeUndecided(this.lastT, this.pieces());
|
|
54107
|
+
const resumeAt = Math.max(this.lastT, this.lastFrameT) + LINK_MAX_GAP_MS + 1;
|
|
54108
|
+
this.offset = resumeAt - frameT;
|
|
54109
|
+
this.lastT = resumeAt;
|
|
54110
|
+
this.observers.clockStep?.({
|
|
54111
|
+
t: frameT,
|
|
54112
|
+
lastFrameT,
|
|
54113
|
+
behindMs,
|
|
54114
|
+
ended
|
|
54115
|
+
});
|
|
54116
|
+
return resumeAt;
|
|
54117
|
+
}
|
|
54118
|
+
/** The latest frame time taken, in the caller's clock (rebased by a clock step). */
|
|
54119
|
+
frameClock() {
|
|
54120
|
+
return this.lastFrameT - this.offset;
|
|
54121
|
+
}
|
|
54122
|
+
/**
|
|
53122
54123
|
* The close sweep's entry on a camera whose frames stopped (D438): end the
|
|
53123
54124
|
* tracklets the coast budget no longer covers, THEN run L2 (review C2).
|
|
53124
54125
|
* `now` is FRAME time (the processor converts wall clock, Task 11).
|
|
53125
54126
|
*/
|
|
53126
|
-
advance(
|
|
54127
|
+
advance(frameNow) {
|
|
54128
|
+
const now = frameNow + this.offset;
|
|
53127
54129
|
this.lastT = Math.max(this.lastT, now);
|
|
53128
54130
|
this.l1.expire(now);
|
|
53129
54131
|
this.link(now);
|
|
@@ -53139,6 +54141,7 @@ var TrackerV2 = class {
|
|
|
53139
54141
|
markSuppressed(id) {
|
|
53140
54142
|
this.l1.markSuppressed(id);
|
|
53141
54143
|
}
|
|
54144
|
+
/** In the caller's clock (translated when each was committed, LOW-5). */
|
|
53142
54145
|
drainFusions() {
|
|
53143
54146
|
const out = this.pending;
|
|
53144
54147
|
this.pending = [];
|
|
@@ -53151,8 +54154,8 @@ var TrackerV2 = class {
|
|
|
53151
54154
|
isLive(id) {
|
|
53152
54155
|
return this.l2.membersOf(this.l2.rootOf(id)).some((m) => this.isLiveTracklet(m));
|
|
53153
54156
|
}
|
|
53154
|
-
isTailFinal(id,
|
|
53155
|
-
return this.l2.isTailFinal(id,
|
|
54157
|
+
isTailFinal(id, frameNow) {
|
|
54158
|
+
return this.l2.isTailFinal(id, frameNow + this.offset, (m) => this.isLiveTracklet(m));
|
|
53156
54159
|
}
|
|
53157
54160
|
/**
|
|
53158
54161
|
* Ends every live member of the chain. `stationary` (round 4): the
|
|
@@ -53168,9 +54171,15 @@ var TrackerV2 = class {
|
|
|
53168
54171
|
const tr = this.l1.get(id);
|
|
53169
54172
|
return tr !== void 0 && tr.endedAt === null;
|
|
53170
54173
|
}
|
|
54174
|
+
pieces() {
|
|
54175
|
+
return [...this.l1.liveTracklets(), ...this.l1.endedTracklets()].map(toPiece);
|
|
54176
|
+
}
|
|
53171
54177
|
link(now) {
|
|
53172
|
-
const
|
|
53173
|
-
|
|
54178
|
+
const offset = this.offset;
|
|
54179
|
+
for (const f of this.l2.step(now, this.pieces())) this.pending.push({
|
|
54180
|
+
...f,
|
|
54181
|
+
t: f.t - offset
|
|
54182
|
+
});
|
|
53174
54183
|
this.l1.prune(now);
|
|
53175
54184
|
this.l2.prune(now);
|
|
53176
54185
|
}
|
|
@@ -53233,7 +54242,8 @@ var NO_ENDS = {
|
|
|
53233
54242
|
missed: 0,
|
|
53234
54243
|
ambiguous: 0,
|
|
53235
54244
|
dropped: 0,
|
|
53236
|
-
stationary: 0
|
|
54245
|
+
stationary: 0,
|
|
54246
|
+
"clock-step": 0
|
|
53237
54247
|
};
|
|
53238
54248
|
function emptyStats() {
|
|
53239
54249
|
return {
|
|
@@ -53246,16 +54256,28 @@ function emptyStats() {
|
|
|
53246
54256
|
linksRefused: 0,
|
|
53247
54257
|
linksNew: 0,
|
|
53248
54258
|
linksDisagreed: 0,
|
|
53249
|
-
lowScoreHolds: 0
|
|
54259
|
+
lowScoreHolds: 0,
|
|
54260
|
+
outOfOrderFrames: 0,
|
|
54261
|
+
clockSteps: 0
|
|
53250
54262
|
};
|
|
53251
54263
|
}
|
|
53252
54264
|
/** Nothing happened in the window: no tracklet ended, no split, no L2
|
|
53253
54265
|
* decision, no low-score hold (review LOW-N1 — what the sweep stays quiet about). */
|
|
53254
54266
|
function isIdleTrackerStats(stats) {
|
|
53255
|
-
return [stats.tracklets.confirmed, stats.tracklets.unconfirmed].flatMap((c) => Object.values(c)).every((n) => n === 0) && stats.splits === 0 && stats.linksCommitted === 0 && stats.linksRefused === 0 && stats.linksNew === 0 && stats.linksDisagreed === 0 && stats.lowScoreHolds === 0;
|
|
54267
|
+
return [stats.tracklets.confirmed, stats.tracklets.unconfirmed].flatMap((c) => Object.values(c)).every((n) => n === 0) && stats.splits === 0 && stats.linksCommitted === 0 && stats.linksRefused === 0 && stats.linksNew === 0 && stats.linksDisagreed === 0 && stats.lowScoreHolds === 0 && stats.outOfOrderFrames === 0 && stats.clockSteps === 0;
|
|
53256
54268
|
}
|
|
53257
54269
|
/** Shared empty — a fold is rare and every other frame would allocate. */
|
|
53258
54270
|
var EMPTY_RIDER_FOLDS = [];
|
|
54271
|
+
/** The tape observation of a dropped frame: no gate verdict, nothing bound. */
|
|
54272
|
+
var DROPPED_FRAME_OBSERVATION = {
|
|
54273
|
+
gate: {
|
|
54274
|
+
strong: [],
|
|
54275
|
+
weak: [],
|
|
54276
|
+
confirmed: [],
|
|
54277
|
+
woken: []
|
|
54278
|
+
},
|
|
54279
|
+
live: []
|
|
54280
|
+
};
|
|
53259
54281
|
/** Mapping from StateAnalyzer's `ObjectState.state` values to the
|
|
53260
54282
|
* canonical TrackState enum used on tracks + events. */
|
|
53261
54283
|
function mapObjectStateToTrackState(s) {
|
|
@@ -53434,6 +54456,14 @@ var FrameProcessor = class {
|
|
|
53434
54456
|
else if (f.kind === "new") this.stats.linksNew += 1;
|
|
53435
54457
|
else if (f.kind === "disagreed") this.stats.linksDisagreed += 1;
|
|
53436
54458
|
this.trackerObservers.link?.(f);
|
|
54459
|
+
},
|
|
54460
|
+
outOfOrder: (f) => {
|
|
54461
|
+
this.stats.outOfOrderFrames += 1;
|
|
54462
|
+
this.trackerObservers.outOfOrder?.(f);
|
|
54463
|
+
},
|
|
54464
|
+
clockStep: (f) => {
|
|
54465
|
+
this.stats.clockSteps += 1;
|
|
54466
|
+
this.trackerObservers.clockStep?.(f);
|
|
53437
54467
|
}
|
|
53438
54468
|
}
|
|
53439
54469
|
});
|
|
@@ -53717,6 +54747,37 @@ var FrameProcessor = class {
|
|
|
53717
54747
|
* Zero is the expected reading; a rising one means two people stand close
|
|
53718
54748
|
* together on this camera, which is worth knowing per-camera rather than
|
|
53719
54749
|
* per-fleet. */
|
|
54750
|
+
/**
|
|
54751
|
+
* A frame `classifyFrame` called out of order: the tracker records the drop
|
|
54752
|
+
* (and any clock-step run it extends) and nothing else reads the frame. The
|
|
54753
|
+
* per-frame side tables a reader asks for afterwards are emptied, so it never
|
|
54754
|
+
* re-reads the previous frame's.
|
|
54755
|
+
*/
|
|
54756
|
+
droppedFrame(input) {
|
|
54757
|
+
this.tracker.drop(input.timestamp, input.frame.detections.length);
|
|
54758
|
+
this.lastZoneOverlaps = /* @__PURE__ */ new Map();
|
|
54759
|
+
this.lastZoneRejections = /* @__PURE__ */ new Map();
|
|
54760
|
+
this.lastLabelWriteRefusals = [];
|
|
54761
|
+
const fusions = this.tracker.drainFusions();
|
|
54762
|
+
this.applyFusions(fusions);
|
|
54763
|
+
return {
|
|
54764
|
+
deviceId: this.deviceId,
|
|
54765
|
+
timestamp: input.timestamp,
|
|
54766
|
+
frameWidth: input.frame.width,
|
|
54767
|
+
frameHeight: input.frame.height,
|
|
54768
|
+
tracked: [],
|
|
54769
|
+
objectEvents: [],
|
|
54770
|
+
appearanceEvents: [],
|
|
54771
|
+
rawTrackedDetections: [],
|
|
54772
|
+
stationaryConfirmed: [],
|
|
54773
|
+
stationaryWoken: [],
|
|
54774
|
+
stationaryContinued: 0,
|
|
54775
|
+
riderFolds: [],
|
|
54776
|
+
...input.observe === true ? { observation: DROPPED_FRAME_OBSERVATION } : {},
|
|
54777
|
+
fusions,
|
|
54778
|
+
droppedOutOfOrder: true
|
|
54779
|
+
};
|
|
54780
|
+
}
|
|
53720
54781
|
facesRefusedOutsideParent() {
|
|
53721
54782
|
return this.faceOutsideParentCount;
|
|
53722
54783
|
}
|
|
@@ -53739,6 +54800,7 @@ var FrameProcessor = class {
|
|
|
53739
54800
|
}
|
|
53740
54801
|
process(input) {
|
|
53741
54802
|
const { timestamp, frame } = input;
|
|
54803
|
+
if (this.tracker.classifyFrame(timestamp) === "out-of-order") return this.droppedFrame(input);
|
|
53742
54804
|
this.lastFrameT = timestamp;
|
|
53743
54805
|
this.lastFrameWallAt = this.wallClock();
|
|
53744
54806
|
const frameWidth = frame.width;
|
|
@@ -56490,239 +57552,6 @@ async function promoteTapeWindow(deps, input, signal) {
|
|
|
56490
57552
|
};
|
|
56491
57553
|
}
|
|
56492
57554
|
//#endregion
|
|
56493
|
-
//#region src/pipeline-analytics/store/tracking-tape-index-store.ts
|
|
56494
|
-
/**
|
|
56495
|
-
* @durable class=ledger owner=pipeline-analytics
|
|
56496
|
-
* write="one row per PROMOTED tracking-tape window (`debug.tape.promote`);
|
|
56497
|
-
* updated when the operator marks it reviewed or releases its pins;
|
|
56498
|
-
* deleted together with its file by `debug.tape.delete`."
|
|
56499
|
-
* retention="NOT an age clock and NOT track retention — a promoted window is
|
|
56500
|
-
* history being labelled (D681). Bounded by the promotion budget
|
|
56501
|
-
* (MAX_PROMOTED_TAPES = 60, 1 GiB): a promotion beyond it is REFUSED,
|
|
56502
|
-
* never satisfied by evicting another row."
|
|
56503
|
-
*/
|
|
56504
|
-
var TRACKING_TAPES_COLLECTION = "pipeline-analytics:tracking-tapes";
|
|
56505
|
-
var TRACKING_TAPES_COLUMNS = [
|
|
56506
|
-
{
|
|
56507
|
-
name: "id",
|
|
56508
|
-
type: "TEXT",
|
|
56509
|
-
primaryKey: true,
|
|
56510
|
-
notNull: true
|
|
56511
|
-
},
|
|
56512
|
-
{
|
|
56513
|
-
name: "deviceId",
|
|
56514
|
-
type: "INTEGER",
|
|
56515
|
-
notNull: true
|
|
56516
|
-
},
|
|
56517
|
-
{
|
|
56518
|
-
name: "scenario",
|
|
56519
|
-
type: "TEXT",
|
|
56520
|
-
notNull: true
|
|
56521
|
-
},
|
|
56522
|
-
{
|
|
56523
|
-
name: "note",
|
|
56524
|
-
type: "TEXT"
|
|
56525
|
-
},
|
|
56526
|
-
{
|
|
56527
|
-
name: "lighting",
|
|
56528
|
-
type: "TEXT",
|
|
56529
|
-
notNull: true
|
|
56530
|
-
},
|
|
56531
|
-
{
|
|
56532
|
-
name: "fromMs",
|
|
56533
|
-
type: "INTEGER",
|
|
56534
|
-
notNull: true
|
|
56535
|
-
},
|
|
56536
|
-
{
|
|
56537
|
-
name: "toMs",
|
|
56538
|
-
type: "INTEGER",
|
|
56539
|
-
notNull: true
|
|
56540
|
-
},
|
|
56541
|
-
{
|
|
56542
|
-
name: "state",
|
|
56543
|
-
type: "TEXT",
|
|
56544
|
-
notNull: true
|
|
56545
|
-
},
|
|
56546
|
-
{
|
|
56547
|
-
name: "frames",
|
|
56548
|
-
type: "INTEGER",
|
|
56549
|
-
notNull: true
|
|
56550
|
-
},
|
|
56551
|
-
{
|
|
56552
|
-
name: "skips",
|
|
56553
|
-
type: "INTEGER",
|
|
56554
|
-
notNull: true
|
|
56555
|
-
},
|
|
56556
|
-
{
|
|
56557
|
-
name: "gaps",
|
|
56558
|
-
type: "INTEGER",
|
|
56559
|
-
notNull: true
|
|
56560
|
-
},
|
|
56561
|
-
{
|
|
56562
|
-
name: "bytes",
|
|
56563
|
-
type: "INTEGER",
|
|
56564
|
-
notNull: true
|
|
56565
|
-
},
|
|
56566
|
-
{
|
|
56567
|
-
name: "reviewed",
|
|
56568
|
-
type: "BOOLEAN",
|
|
56569
|
-
notNull: true
|
|
56570
|
-
},
|
|
56571
|
-
{
|
|
56572
|
-
name: "trackIds",
|
|
56573
|
-
type: "TEXT",
|
|
56574
|
-
notNull: true
|
|
56575
|
-
},
|
|
56576
|
-
{
|
|
56577
|
-
name: "createdAt",
|
|
56578
|
-
type: "INTEGER",
|
|
56579
|
-
notNull: true
|
|
56580
|
-
},
|
|
56581
|
-
{
|
|
56582
|
-
name: "releasedAt",
|
|
56583
|
-
type: "INTEGER"
|
|
56584
|
-
},
|
|
56585
|
-
{
|
|
56586
|
-
name: "pinsSettled",
|
|
56587
|
-
type: "BOOLEAN",
|
|
56588
|
-
notNull: true
|
|
56589
|
-
}
|
|
56590
|
-
];
|
|
56591
|
-
var TrackingTapeRowSchema = object({
|
|
56592
|
-
id: string(),
|
|
56593
|
-
deviceId: number().int(),
|
|
56594
|
-
scenario: string(),
|
|
56595
|
-
note: string().nullable(),
|
|
56596
|
-
lighting: TapeLightingSchema,
|
|
56597
|
-
fromMs: number().int(),
|
|
56598
|
-
toMs: number().int(),
|
|
56599
|
-
state: _enum(["complete", "incomplete"]),
|
|
56600
|
-
frames: number().int(),
|
|
56601
|
-
skips: number().int(),
|
|
56602
|
-
gaps: number().int(),
|
|
56603
|
-
bytes: number().int(),
|
|
56604
|
-
reviewed: boolean(),
|
|
56605
|
-
trackIds: array(string()),
|
|
56606
|
-
createdAt: number().int(),
|
|
56607
|
-
releasedAt: number().int().nullable(),
|
|
56608
|
-
pinsSettled: boolean()
|
|
56609
|
-
});
|
|
56610
|
-
var TrackIdsSchema = array(string());
|
|
56611
|
-
var TrackingTapeIndexStore = class {
|
|
56612
|
-
store;
|
|
56613
|
-
logger;
|
|
56614
|
-
constructor(deps) {
|
|
56615
|
-
this.store = deps.store;
|
|
56616
|
-
this.logger = deps.logger;
|
|
56617
|
-
}
|
|
56618
|
-
static async declare(store) {
|
|
56619
|
-
await store.declareCollection.mutate({
|
|
56620
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56621
|
-
columns: [...TRACKING_TAPES_COLUMNS],
|
|
56622
|
-
indexes: [{
|
|
56623
|
-
name: "idx_trackingtapes_device_created",
|
|
56624
|
-
columns: ["deviceId", "createdAt"]
|
|
56625
|
-
}]
|
|
56626
|
-
});
|
|
56627
|
-
}
|
|
56628
|
-
async insert(row) {
|
|
56629
|
-
const { id, trackIds, ...rest } = TrackingTapeRowSchema.parse(row);
|
|
56630
|
-
await this.store.insert.mutate({
|
|
56631
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56632
|
-
record: {
|
|
56633
|
-
id,
|
|
56634
|
-
data: {
|
|
56635
|
-
...rest,
|
|
56636
|
-
trackIds: JSON.stringify(trackIds)
|
|
56637
|
-
}
|
|
56638
|
-
}
|
|
56639
|
-
});
|
|
56640
|
-
}
|
|
56641
|
-
async get(id) {
|
|
56642
|
-
const first = (await this.store.query.query({
|
|
56643
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56644
|
-
filter: {
|
|
56645
|
-
where: { id },
|
|
56646
|
-
limit: 1
|
|
56647
|
-
}
|
|
56648
|
-
}))[0];
|
|
56649
|
-
return first === void 0 ? null : this.parse(first.id, first.data);
|
|
56650
|
-
}
|
|
56651
|
-
async list(deviceId) {
|
|
56652
|
-
const rows = await this.store.query.query({
|
|
56653
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56654
|
-
filter: {
|
|
56655
|
-
...deviceId !== void 0 ? { where: { deviceId } } : {},
|
|
56656
|
-
orderBy: {
|
|
56657
|
-
field: "createdAt",
|
|
56658
|
-
direction: "desc"
|
|
56659
|
-
},
|
|
56660
|
-
limit: 1e3
|
|
56661
|
-
}
|
|
56662
|
-
});
|
|
56663
|
-
const out = [];
|
|
56664
|
-
for (const r of rows) {
|
|
56665
|
-
const parsed = this.parse(r.id, r.data);
|
|
56666
|
-
if (parsed !== null) out.push(parsed);
|
|
56667
|
-
}
|
|
56668
|
-
return out;
|
|
56669
|
-
}
|
|
56670
|
-
async setReviewed(id, reviewed) {
|
|
56671
|
-
await this.store.update.mutate({
|
|
56672
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56673
|
-
id,
|
|
56674
|
-
data: { reviewed }
|
|
56675
|
-
});
|
|
56676
|
-
}
|
|
56677
|
-
/** Release AND unsettle in one row write: the unpins the release implies
|
|
56678
|
-
* are owed from this moment, whether or not the caller lives to make them. */
|
|
56679
|
-
async setReleased(id, atMs) {
|
|
56680
|
-
await this.store.update.mutate({
|
|
56681
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56682
|
-
id,
|
|
56683
|
-
data: {
|
|
56684
|
-
releasedAt: atMs,
|
|
56685
|
-
pinsSettled: false
|
|
56686
|
-
}
|
|
56687
|
-
});
|
|
56688
|
-
}
|
|
56689
|
-
async setPinsSettled(id, settled) {
|
|
56690
|
-
await this.store.update.mutate({
|
|
56691
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56692
|
-
id,
|
|
56693
|
-
data: { pinsSettled: settled }
|
|
56694
|
-
});
|
|
56695
|
-
}
|
|
56696
|
-
async remove(id) {
|
|
56697
|
-
await this.store.delete.mutate({
|
|
56698
|
-
collection: TRACKING_TAPES_COLLECTION,
|
|
56699
|
-
key: id
|
|
56700
|
-
});
|
|
56701
|
-
}
|
|
56702
|
-
parse(id, data) {
|
|
56703
|
-
let trackIds = [];
|
|
56704
|
-
try {
|
|
56705
|
-
trackIds = JSON.parse(String(data["trackIds"] ?? "[]"));
|
|
56706
|
-
} catch {
|
|
56707
|
-
trackIds = null;
|
|
56708
|
-
}
|
|
56709
|
-
const ids = TrackIdsSchema.safeParse(trackIds);
|
|
56710
|
-
const parsed = TrackingTapeRowSchema.safeParse({
|
|
56711
|
-
id,
|
|
56712
|
-
...data,
|
|
56713
|
-
note: data["note"] ?? null,
|
|
56714
|
-
releasedAt: data["releasedAt"] ?? null,
|
|
56715
|
-
trackIds: ids.success ? ids.data : null
|
|
56716
|
-
});
|
|
56717
|
-
if (parsed.success) return parsed.data;
|
|
56718
|
-
this.logger.warn("tracking tape index: skipped a malformed row", {
|
|
56719
|
-
meta: { id },
|
|
56720
|
-
...typeof data["deviceId"] === "number" ? { tags: { deviceId: data["deviceId"] } } : {}
|
|
56721
|
-
});
|
|
56722
|
-
return null;
|
|
56723
|
-
}
|
|
56724
|
-
};
|
|
56725
|
-
//#endregion
|
|
56726
57555
|
//#region src/pipeline-analytics/tracking-bench/recorder/tape-status.ts
|
|
56727
57556
|
/** The one node that may hold tapes: sqlite and bulk state are hub-only, and
|
|
56728
57557
|
* an agent persists nothing (spec §3.1). */
|
|
@@ -87823,6 +88652,18 @@ function isRecord(x) {
|
|
|
87823
88652
|
}
|
|
87824
88653
|
//#endregion
|
|
87825
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
|
+
*/
|
|
87826
88667
|
var TRACK_ALIASES_COLLECTION = "pipeline-analytics:track-aliases";
|
|
87827
88668
|
var TRACK_ALIAS_COLUMNS = [
|
|
87828
88669
|
{
|
|
@@ -88005,6 +88846,20 @@ var TrackAliasStore = class {
|
|
|
88005
88846
|
dropped: 0
|
|
88006
88847
|
}];
|
|
88007
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
|
+
}
|
|
88008
88863
|
async deleteByTracks(trackIds) {
|
|
88009
88864
|
const { deleted } = await this.store.deleteWhere.mutate({
|
|
88010
88865
|
collection: TRACK_ALIASES_COLLECTION,
|
|
@@ -88931,7 +89786,8 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
88931
89786
|
stoppedBy,
|
|
88932
89787
|
nextCursor: offset
|
|
88933
89788
|
} });
|
|
88934
|
-
|
|
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: {
|
|
88935
89791
|
collection: scope.collection,
|
|
88936
89792
|
shard: scope.shard?.value ?? null,
|
|
88937
89793
|
orphans: tally.orphans,
|
|
@@ -89045,6 +89901,40 @@ var ORPHAN_AUDIT_DEFAULTS = {
|
|
|
89045
89901
|
pageSize: OrphanAuditPageSizeSchema.parse(void 0)
|
|
89046
89902
|
};
|
|
89047
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
|
+
}
|
|
89048
89938
|
function buildOrphanReclaimer(ports) {
|
|
89049
89939
|
return async (collection, key) => {
|
|
89050
89940
|
if (collection === MEDIA_COLLECTION) {
|
|
@@ -89068,6 +89958,10 @@ function buildOrphanReclaimer(ports) {
|
|
|
89068
89958
|
bytes: null
|
|
89069
89959
|
};
|
|
89070
89960
|
}
|
|
89961
|
+
if (collection === TRACK_ALIASES_COLLECTION) return {
|
|
89962
|
+
rows: await ports.deleteTrackAlias(key) ? 1 : 0,
|
|
89963
|
+
bytes: null
|
|
89964
|
+
};
|
|
89071
89965
|
if (RAW_DELETE_COLLECTIONS.has(collection)) return {
|
|
89072
89966
|
rows: await ports.deleteRow(collection, key) ? 1 : 0,
|
|
89073
89967
|
bytes: null
|
|
@@ -97602,6 +98496,12 @@ var TRACKER_FACT_LOG_SAMPLE_EVERY = 10;
|
|
|
97602
98496
|
* positive spawns (review M1: ~1 750 `new` decisions per camera per hour) —
|
|
97603
98497
|
* are sampled harder; the per-minute summary carries their exact counts. */
|
|
97604
98498
|
var TRACKER_PIECE_LOG_SAMPLE_EVERY = 50;
|
|
98499
|
+
/** Out-of-order frames (follow-up 617, F1: ~1 % of frames) log the first N,
|
|
98500
|
+
* then one in this many — every line carries the count; the per-minute
|
|
98501
|
+
* summary carries the exact `outOfOrderFrames`. */
|
|
98502
|
+
var TRACKER_OUT_OF_ORDER_LOG_SAMPLE_EVERY = 50;
|
|
98503
|
+
/** Clock-step WARNs past the first ten (fix round 3, MEDIUM-1). */
|
|
98504
|
+
var TRACKER_CLOCK_STEP_LOG_SAMPLE_EVERY = 50;
|
|
97605
98505
|
/** First N, then one in `every` — the refusal stream's cadence (D391). */
|
|
97606
98506
|
function sampledTrackerFact(count, every = TRACKER_FACT_LOG_SAMPLE_EVERY) {
|
|
97607
98507
|
return count <= TRACKER_FACT_LOG_ALWAYS_FIRST || count % every === 0;
|
|
@@ -99431,6 +100331,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
99431
100331
|
},
|
|
99432
100332
|
listTrackGroups: (query) => center.listTrackGroups(query),
|
|
99433
100333
|
injectTestEvent: (input) => center.injectTestEvent(input, { getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }) }),
|
|
100334
|
+
testStateSummary: (rule) => center.testStateSummary(rule),
|
|
99434
100335
|
timelapse: {
|
|
99435
100336
|
store: center.timelapseStore,
|
|
99436
100337
|
getRecordingConfig: (deviceId) => api.recordingArchive.getDeviceConfig.query({ deviceId }).catch(() => null),
|
|
@@ -102249,9 +103150,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
102249
103150
|
let splitCount = 0;
|
|
102250
103151
|
let ignoredCount = 0;
|
|
102251
103152
|
let newCount = 0;
|
|
103153
|
+
let outOfOrderCount = 0;
|
|
103154
|
+
let clockStepCount = 0;
|
|
102252
103155
|
const endedCounts = /* @__PURE__ */ new Map();
|
|
102253
103156
|
const logLink = (f) => {
|
|
102254
|
-
const
|
|
103157
|
+
const decision = {
|
|
102255
103158
|
source,
|
|
102256
103159
|
t: f.t,
|
|
102257
103160
|
kind: f.kind,
|
|
@@ -102260,7 +103163,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
102260
103163
|
cost: f.cost,
|
|
102261
103164
|
gapMs: f.gapMs,
|
|
102262
103165
|
reason: f.reason,
|
|
102263
|
-
recognition: f.recognition
|
|
103166
|
+
recognition: f.recognition
|
|
103167
|
+
};
|
|
103168
|
+
const meta = {
|
|
103169
|
+
...decision,
|
|
102264
103170
|
candidates: f.candidates
|
|
102265
103171
|
};
|
|
102266
103172
|
switch (f.kind) {
|
|
@@ -102298,7 +103204,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
102298
103204
|
meta
|
|
102299
103205
|
});
|
|
102300
103206
|
return;
|
|
102301
|
-
case "new":
|
|
103207
|
+
case "new": {
|
|
103208
|
+
if (f.reason === "clock-step") {
|
|
103209
|
+
trackerLog.debug("tracker: piece decided new — closed by a frame clock step", {
|
|
103210
|
+
tags: { deviceId },
|
|
103211
|
+
meta
|
|
103212
|
+
});
|
|
103213
|
+
return;
|
|
103214
|
+
}
|
|
102302
103215
|
if (f.reason === "unconfirmed") {
|
|
102303
103216
|
trackerLog.debug("tracker: piece decided new — never confirmed", {
|
|
102304
103217
|
tags: { deviceId },
|
|
@@ -102306,6 +103219,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
102306
103219
|
});
|
|
102307
103220
|
return;
|
|
102308
103221
|
}
|
|
103222
|
+
const rejectedPredecessors = confirmedPredecessorRejections(f.candidates);
|
|
103223
|
+
if (rejectedPredecessors.length > 0) {
|
|
103224
|
+
trackerLog.info("tracker: piece decided new — a confirmed predecessor was rejected", {
|
|
103225
|
+
tags: { deviceId },
|
|
103226
|
+
meta: {
|
|
103227
|
+
...decision,
|
|
103228
|
+
rejectedPredecessors
|
|
103229
|
+
}
|
|
103230
|
+
});
|
|
103231
|
+
return;
|
|
103232
|
+
}
|
|
102309
103233
|
newCount += 1;
|
|
102310
103234
|
if (!sampledTrackerFact(newCount, TRACKER_PIECE_LOG_SAMPLE_EVERY)) return;
|
|
102311
103235
|
trackerLog.info("tracker: piece decided new — no predecessor", {
|
|
@@ -102316,6 +103240,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
102316
103240
|
}
|
|
102317
103241
|
});
|
|
102318
103242
|
return;
|
|
103243
|
+
}
|
|
102319
103244
|
}
|
|
102320
103245
|
};
|
|
102321
103246
|
p.setTrackerObservers({
|
|
@@ -102336,6 +103261,39 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
102336
103261
|
});
|
|
102337
103262
|
},
|
|
102338
103263
|
link: logLink,
|
|
103264
|
+
clockStep: (f) => {
|
|
103265
|
+
clockStepCount += 1;
|
|
103266
|
+
if (!sampledTrackerFact(clockStepCount, TRACKER_CLOCK_STEP_LOG_SAMPLE_EVERY)) return;
|
|
103267
|
+
trackerLog.warn("tracker: frame clock stepped back — tracklets ended, clock rebased", {
|
|
103268
|
+
tags: { deviceId },
|
|
103269
|
+
meta: {
|
|
103270
|
+
source,
|
|
103271
|
+
t: f.t,
|
|
103272
|
+
lastFrameT: f.lastFrameT,
|
|
103273
|
+
behindMs: f.behindMs,
|
|
103274
|
+
ended: f.ended,
|
|
103275
|
+
clockStepCount,
|
|
103276
|
+
sampledEvery: TRACKER_CLOCK_STEP_LOG_SAMPLE_EVERY
|
|
103277
|
+
}
|
|
103278
|
+
});
|
|
103279
|
+
},
|
|
103280
|
+
outOfOrder: (f) => {
|
|
103281
|
+
outOfOrderCount += 1;
|
|
103282
|
+
if (!sampledTrackerFact(outOfOrderCount, TRACKER_OUT_OF_ORDER_LOG_SAMPLE_EVERY)) return;
|
|
103283
|
+
trackerLog.info("tracker: out-of-order frame dropped — older than the latest frame", {
|
|
103284
|
+
tags: { deviceId },
|
|
103285
|
+
meta: {
|
|
103286
|
+
source,
|
|
103287
|
+
t: f.t,
|
|
103288
|
+
lastFrameT: f.lastFrameT,
|
|
103289
|
+
behindMs: f.behindMs,
|
|
103290
|
+
detections: f.detections,
|
|
103291
|
+
pendingClockStep: f.pendingClockStep,
|
|
103292
|
+
outOfOrderCount,
|
|
103293
|
+
sampledEvery: TRACKER_OUT_OF_ORDER_LOG_SAMPLE_EVERY
|
|
103294
|
+
}
|
|
103295
|
+
});
|
|
103296
|
+
},
|
|
102339
103297
|
ended: (f) => {
|
|
102340
103298
|
const k = `${f.reason}:${String(f.confirmed)}`;
|
|
102341
103299
|
const n = (endedCounts.get(k) ?? 0) + 1;
|
|
@@ -102477,6 +103435,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
102477
103435
|
});
|
|
102478
103436
|
});
|
|
102479
103437
|
this.reportTrackerSummary(deviceId, key, processor, "frame");
|
|
103438
|
+
if (result.droppedOutOfOrder === true) return;
|
|
102480
103439
|
for (const r of processor.getLastLabelWriteRefusals()) {
|
|
102481
103440
|
const message = r.reason.kind === "macro-class" ? "label tier collapse refused" : "label write refused";
|
|
102482
103441
|
this.ctx.logger.info(message, {
|
|
@@ -107131,10 +108090,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
107131
108090
|
bytes: count.bytes
|
|
107132
108091
|
};
|
|
107133
108092
|
},
|
|
107134
|
-
|
|
107135
|
-
|
|
107136
|
-
|
|
107137
|
-
|
|
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),
|
|
107138
108099
|
logger: this.ctx.logger
|
|
107139
108100
|
}),
|
|
107140
108101
|
now: () => Date.now(),
|