@camstack/addon-post-analysis 1.2.607 → 1.2.608
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-VgOXFRFr.js → clip-model-registry-B11yS32V.js} +472 -47
- package/dist/{clip-model-registry-CTfe5v5_.mjs → clip-model-registry-BItfTZEx.mjs} +431 -48
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DUNQoBDI.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-LGLlz5Sk.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-iXmJsQ9Y.mjs +26 -0
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Cm7pZeMH.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-B8mC_3oV.mjs → hostInit-Cko0y33G.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +457 -51
- package/dist/pipeline-analytics/index.mjs +457 -51
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-1AlitJg2.mjs +0 -26
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DalpVM1q.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as MediaFileKindEnum, $i as
|
|
1
|
+
import { $ as MediaFileKindEnum, $i as boolean, $n as coreBlockIdFromAddonId, $r as readTimelapseGeneratedAt, $t as NcTemplateVarDescriptorSchema, A as DeclaredDevices, Ai as viewerLinkBaseOf, An as TrackSideSchema, Ar as isEventOwnerType, At as NcAlarmSensorsInputSchema, B as FULL_IMAGE_BBOX, Bi as RECORD_MAX_DATA_BYTES, Bn as applyModeDelaysEdits, Br as kebabToCamel, Bt as NcRuleSchema, C as ClearTrackRelationResultSchema, Ci as systemEventFamilyProblems, Cn as TRACK_RELATIONS_DEFAULT_LIMIT, Cr as isAlertEpisodeOpen, Ct as NC_SNOOZE_MAX_MINUTES, D as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Di as templateCarriesSummary, Dn as TrackPuritySchema, Dr as isDefaultAlarmProfileId, Dt as NC_TEMPLATE_VARS, E as DEFAULT_EVENT_COLOR, Ei as systemEventSummarySplitOf, En as TimelapseRuleSchema, Er as isAudioClassName, Et as NC_TAXONOMY, F as EVENT_DENSITY_BATCH_MAX, Fi as isSourceCap, Fn as alarmPanelCapability, Fr as isRootSuppliableStepId, Ft as NcLegacyRuleActionSequenceSchema, G as ListTrackRelationsResultSchema, Gi as hydrateSchema, Gn as audioMetricsCapability, Gr as occupancyScope, Gt as NcSnoozeInputSchema, H as LABEL_SUPPLY_MACRO, Hi as customAction, Hn as applySecureOnArmEdit, Hr as mayWriteToLocation, Ht as NcRuleUpdateInputSchema, I as EVENT_KIND_BY_CAP, Ii as DeviceType, In as alertEpisodeId, Ir as isScheduleActive, It as NcRuleActionSchema, J as MAX_BIRTH_DECISION_RECORDS, Jn as buildViewerLink, Jr as pickFaceRescoreIntervalMinutes, Jt as NcStateSummarySectionIdSchema, K as LockStateSchema, Ki as isDeviceScopedCap, Kn as audioModeOf, Kr as parseAlertEpisodePayload, Kt as NcSnoozeSchema, L as EVENT_OWNER_TYPES, Li as errMsg, Ln as alertEpisodeKind, Lr as isSystemDelivery, Lt as NcRuleActionsSchema, M as EMBEDDING_MODEL_FAILED, Mi as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Mn as activateAlarmProfile, Mr as isLabelSupplyStepId, Mt as NcAlarmSettingsPatchSchema, N as EMBEDDING_REQUIREMENTS_FAILED, Ni as canonicalHash, Nn as addonWidgetsSourceCapability, Nr as isMediaOwnerType, Nt as NcConditionDescriptorSchema, O as DETECTION_MACRO_CLASSES, Oi as templateVarsFor, On as TrackRelationKindSchema, Or as isDetectionMacroClass, Ot as NcAlarmConfigSchema, P as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Pi as evaluateSensorEdge, Pn as alarmDeviceSecureOnArm, Pr as isRetiredClearKey, Pt as NcConditionsSchema, Q as MOTION_CLOSE_AFTER_MS, Qi as array, Qn as consumableLowPercentOf, Qr as readMotionTriggeredBy, Qt as NcTemplatePreviewSchema, R as EXPORT_DENSE_MAX_RANGES, Ri as BaseAddon, Rn as alertEpisodePayload, Rr as isSystemEventSummaryRule, Rt as NcRuleInputSchema, S as ClearTrackRelationInputSchema, Si as systemEventDeliveryProblems, Sn as TIMELAPSE_DENSE_FLOOR_SEC, Sr as isAlarmControllableCap, St as NC_SENSOR_TRANSITION_KINDS, T as DEFAULT_DETAIL_CROP_CONVENTION, Ti as systemEventSummaryScheduleOf, Tn as TimelapseRulePatchSchema, Tr as isAppLinkBase, Tt as NC_TANK_SYSTEM_EVENT_KINDS, U as LabelAttributionSchema, Ui as defineCustomActions, Un as armRemovalReconcile, Ur as memoizePinnedFfmpeg, Ut as NcScheduleSchema, V as FailureCounters, Vi as createEvent, Vn as applyNonBlockingEdit, Vr as linkedStepOf, Vt as NcRuleTargetSchema, W as ListTrackRelationsInputSchema, Wi as defineRecordKinds, Wn as assertTimelapseCadences, Wr as notificationRulesCapability, Wt as NcSharedSequenceSchema, X as MAX_TRACK_PURITIES, Xi as ZodObject, Xn as classifyRetrainReview, Xr as plateGalleryCapability, Xt as NcTaxonomySchema, Y as MAX_BIRTH_LATENCY_PROXY_MS, Yi as sleep$1, Yn as calendarEventLocalSpan, Yr as pipelineAnalyticsCapability, Yt as NcSystemEventKindSchema, Z as MAX_TRACK_RELATIONS, Zi as _enum, Zn as cloneAlarmSettings, Zr as readDeviceStateFrom, Zt as NcTemplatePreviewInputSchema, _ as BIRTH_DECISION_DEFAULT_LIMIT, _i as startMoverUnderClearance, _n as SCENE_RESET_RECAPTURES, _r as evictionPolicyOfLocation, _t as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, aa as partialRecord, an as RebuildObjectEmbeddingsRefusalSchema, ar as deriveRecordingMode, at as NC_ALARM_NOTIFY_FIELDS, b as BucketAccumulator, bi as summarizeReview, bn as SetTrackPurityResultSchema, br as gifDeriveSpeed, bt as NC_GROUPING_MAX_SEC, c as readClusterModelPinStrict, ca as string, ci as ruleAutoTriggersAlarm, cn as RetrainStatusSchema, cr as deviceRemovalHookCapability, ct as NC_AUDIO_CONFIRM_HITS_DEFAULT, d as APPEARANCE_BLOCKS, da as union, di as ruleGroupingApplies, dn as SCENE_DEFAULT_ANCHOR_THRESHOLD, dr as embeddingRefusalOf, dt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, ea as discriminatedUnion, ei as renameAlarmProfile, en as OPS_LOG_DEFAULT_LIMIT, er as cosineSimilarity$1, et as MotionTriggeredBySchema, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fa as unknown, fi as ruleKindOf, fn as SCENE_DEFAULT_CHECK_INTERVAL_SEC, ft as NC_CLIP_RETENTION_MAX_SEC, g as ArchivedDebugNoteSchema, gi as servedTemplateVars, gn as SCENE_DIVERGED, gr as eventKindLabel, gt as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, h as AlarmArmModeSchema, hi as sensorRaisedOf, hn as SCENE_DEFAULT_UNCOVERED_POLICY, hr as ensureAlarmProfiles, ht as NC_CONFIRM_DEFAULT_TIMEOUT_MS, ia as object, ii as resolveModeNotifyPolicy, in as RECORDING_SOURCE_CAMSTACK_ADDON, ir as derivePostTriggeredAction, it as NC_ALARM_DELAY_MAX_SEC, j as DetectionSourceSchema, ji as zoneAnalyticsCapability, jn as TrackSourceSchema, jr as isKnownTemplateVar, jt as NcAlarmSensorsSchema, k as DETECTION_PIPELINE_CAP_NAME, ki as vectorDimFromBase64, kn as TrackRelationSchema, kr as isEventKindLabelKey, kt as NcAlarmPostTriggeredActionSchema, l as resolveClusterModelPin, la as templateLiteral, li as ruleBelongsToAlarm$1, ln as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, lt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, m as AUDIO_MACRO_LABELS, mi as secureActuationFor, mn as SCENE_DEFAULT_QUIET_SECONDS, mr as encodeVectorBase64, mt as NC_CONFIRM_DEFAULT_MAX_IMAGE_PX, na as looseObject, ni as resolveLocationMode, nr as deleteAlarmProfile, nt as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, o as CLUSTER_MODEL_OWNER_ADDON_ID, oa as record, oi as retrainRevisionConflictMessage, on as RetrainDismissedSchema, or as describeSystemEventDeliveryProblem, ot as NC_ALARM_SYSTEM_EVENT_KINDS, p as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, pa as EventCategory, pi as sceneMonitorCapability, pn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, pt as NC_CONDITION_CATALOG, q as MAX_ARCHIVED_DEBUG_NOTES, qi as nodePin, qn as buildEventKindDescriptor, qt as NcSnoozeSuppressedSchema, ra as number, ri as resolveModeDelays, rn as RECORDING_EXPORT_MAX_READ_BYTES, rr as deriveDetailCropRect, rt as NC_ALARM_DELAY_FIELDS, s as FACE_EMBEDDING_STEP_ID, sa as strictObject, si as rootSupplyServes, sn as RetrainOriginSchema, sr as describeSystemEventFamilyProblem, st as NC_ALARM_TRIGGERED_MAX_SEC, t as BUILTIN_CLIP_MODELS, ta as literal, ti as resolveClipSearchMinScore, tn as OpsLogEntrySchema, tr as createAlarmProfile, tt as NC_ALARM_DEFAULT_PROFILE_ID, u as APPEARANCE_BIN_COUNT, ua as tuple, ui as ruleDeviceCompatCapQueries, un as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, ut as NC_CLIP_POST_ROLL_DEFAULT_SEC, v as BaseDevice, vi as storageOccupancyCapability, vn as SceneMonitorSchema, vr as faceGalleryCapability, vt as NC_DEFAULT_SNOOZE_MINUTES, w as CoverStateSchema, wi as systemEventFilterApplies, wn as TimelapseRuleInputSchema, wr as isAlertEpisodeSeverity, wt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, x as COCO_TO_MACRO, xi as syncActiveAlarmProfileSettings, xn as SetTrackRelationInputSchema, xr as gifFromMp4Args, xt as NC_GROUPING_MIN_SEC, y as BirthDecisionRecordSchema, yi as subKindsOf, yn as SetTrackPurityInputSchema, yr as failureContributionCapability, yt as NC_GROUPING_DEFAULT_SEC, z as FIRST_LEVEL_MACRO_CLASSES, zi as CamProfileSchema, zn as applyBlockingEdit, zr as isVectorIndexDimConflict, zt as NcRulePatchSchema } from "../clip-model-registry-BItfTZEx.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";
|
|
@@ -10273,6 +10273,7 @@ var NC_SYSTEM_STATE_LEGACY = "notification-center:system-state";
|
|
|
10273
10273
|
var NC_OCCUPANCY_LEGACY = "notification-center:occupancy";
|
|
10274
10274
|
var NC_DEVICE_PRUNES_LEGACY = "notification-center:device-prunes";
|
|
10275
10275
|
var NC_STATE_SUMMARY_STAMPS_LEGACY = "notification-center:state-summary-stamps";
|
|
10276
|
+
var NC_ALERT_EPISODES_LEGACY = "notification-center:alert-episodes";
|
|
10276
10277
|
var NC_META_LEGACY = "notification-center:meta";
|
|
10277
10278
|
var NC_SNOOZES_LEGACY = "notification-center:snoozes";
|
|
10278
10279
|
var NC_SNOOZE_SUPPRESSED_LEGACY = "notification-center:snooze-suppressed";
|
|
@@ -10293,6 +10294,7 @@ var ncDurableRecordKinds = defineRecordKinds({
|
|
|
10293
10294
|
liveness: ledger(NC_LIVENESS_LEGACY, "a liveness state TRANSITION the centre accepted (seed or flip); a no-flip writes nothing"),
|
|
10294
10295
|
"device-alerts": ledger(NC_DEVICE_ALERTS_LEGACY, "a sustained device alert episode the centre confirmed or cleared"),
|
|
10295
10296
|
"system-state": ledger(NC_SYSTEM_STATE_LEGACY, "a system-state subject flip the state summary accepted"),
|
|
10297
|
+
"alert-episodes": ledger(NC_ALERT_EPISODES_LEGACY, "an alert episode the centre notified (critical or error); its resolution deletes the row"),
|
|
10296
10298
|
occupancy: ledger(NC_OCCUPANCY_LEGACY, "a confirmed occupancy level write-through after the watcher decided an edge"),
|
|
10297
10299
|
"device-prunes": ledger(NC_DEVICE_PRUNES_LEGACY, "one row recording what a deleted-device prune removed"),
|
|
10298
10300
|
"state-summary-stamps": ledger(NC_STATE_SUMMARY_STAMPS_LEGACY, "digest delivery stamps per rule kind"),
|
|
@@ -10312,6 +10314,7 @@ var NC_PHASE3_RECORD_BACKED_LEGACY_COLLECTIONS = [
|
|
|
10312
10314
|
NC_LIVENESS_LEGACY,
|
|
10313
10315
|
NC_DEVICE_ALERTS_LEGACY,
|
|
10314
10316
|
NC_SYSTEM_STATE_LEGACY,
|
|
10317
|
+
NC_ALERT_EPISODES_LEGACY,
|
|
10315
10318
|
NC_OCCUPANCY_LEGACY,
|
|
10316
10319
|
NC_DEVICE_PRUNES_LEGACY,
|
|
10317
10320
|
NC_STATE_SUMMARY_STAMPS_LEGACY,
|
|
@@ -10733,6 +10736,120 @@ var NcAlertStore = class {
|
|
|
10733
10736
|
}
|
|
10734
10737
|
};
|
|
10735
10738
|
//#endregion
|
|
10739
|
+
//#region src/notification-center/alert-episode-ledger.ts
|
|
10740
|
+
/**
|
|
10741
|
+
* @durable class=ledger owner=notification-center
|
|
10742
|
+
* write="an alert episode the centre notified (alert-critical / alert-error); its resolution deletes the row"
|
|
10743
|
+
* retention="a row lives while its episode is open. A row whose alert was pruned by the alert centre's own retention is dropped after NC_ALERT_EPISODE_MAX_AGE_MS by the reconcile, never on absence alone (D49)."
|
|
10744
|
+
*/
|
|
10745
|
+
var NC_ALERT_EPISODES_COLLECTION = "notification-center:alert-episodes";
|
|
10746
|
+
var NC_ALERT_EPISODES_COLUMNS = [
|
|
10747
|
+
{
|
|
10748
|
+
name: "episodeId",
|
|
10749
|
+
type: "TEXT",
|
|
10750
|
+
primaryKey: true,
|
|
10751
|
+
notNull: true
|
|
10752
|
+
},
|
|
10753
|
+
(
|
|
10754
|
+
/** The kind that was notified: `alert-critical` or `alert-error`. */
|
|
10755
|
+
{
|
|
10756
|
+
name: "kind",
|
|
10757
|
+
type: "TEXT",
|
|
10758
|
+
notNull: true
|
|
10759
|
+
}),
|
|
10760
|
+
{
|
|
10761
|
+
name: "alertId",
|
|
10762
|
+
type: "TEXT",
|
|
10763
|
+
notNull: true
|
|
10764
|
+
},
|
|
10765
|
+
{
|
|
10766
|
+
name: "since",
|
|
10767
|
+
type: "INTEGER",
|
|
10768
|
+
notNull: true
|
|
10769
|
+
}
|
|
10770
|
+
];
|
|
10771
|
+
/** Longer than the alert centre's maximum retention (365 days). */
|
|
10772
|
+
var NC_ALERT_EPISODE_MAX_AGE_MS = 400 * 24 * 60 * 60 * 1e3;
|
|
10773
|
+
function isEpisodeKind(value) {
|
|
10774
|
+
return value === "alert-critical" || value === "alert-error";
|
|
10775
|
+
}
|
|
10776
|
+
var NC_ALERT_EPISODES_SPEC = {
|
|
10777
|
+
collection: NC_ALERT_EPISODES_COLLECTION,
|
|
10778
|
+
columns: NC_ALERT_EPISODES_COLUMNS,
|
|
10779
|
+
writeMode: "write-behind",
|
|
10780
|
+
keyOf: (row) => row.episodeId,
|
|
10781
|
+
toValue: (row) => ({
|
|
10782
|
+
kind: row.kind,
|
|
10783
|
+
alertId: row.alertId,
|
|
10784
|
+
since: row.since
|
|
10785
|
+
}),
|
|
10786
|
+
fromRecord: (key, data) => {
|
|
10787
|
+
const kind = data["kind"];
|
|
10788
|
+
const alertId = data["alertId"];
|
|
10789
|
+
const since = data["since"];
|
|
10790
|
+
if (!isEpisodeKind(kind) || typeof alertId !== "string" || typeof since !== "number") return null;
|
|
10791
|
+
return {
|
|
10792
|
+
episodeId: key,
|
|
10793
|
+
kind,
|
|
10794
|
+
alertId,
|
|
10795
|
+
since
|
|
10796
|
+
};
|
|
10797
|
+
}
|
|
10798
|
+
};
|
|
10799
|
+
NC_ALERT_EPISODES_SPEC.fromRecord, NC_ALERT_EPISODES_SPEC.toValue;
|
|
10800
|
+
var NcAlertEpisodeLedger = class {
|
|
10801
|
+
ledger;
|
|
10802
|
+
constructor(deps) {
|
|
10803
|
+
this.ledger = new DurableLedger({
|
|
10804
|
+
spec: NC_ALERT_EPISODES_SPEC,
|
|
10805
|
+
kind: resolveDurableKind(NC_ALERT_EPISODES_SPEC, ncDurableRecordKinds["alert-episodes"], deps),
|
|
10806
|
+
logger: deps.logger
|
|
10807
|
+
});
|
|
10808
|
+
}
|
|
10809
|
+
static declare(store) {
|
|
10810
|
+
return declareDurableSpecLegacy(store, NC_ALERT_EPISODES_SPEC);
|
|
10811
|
+
}
|
|
10812
|
+
async load() {
|
|
10813
|
+
return (await this.ledger.load()).length;
|
|
10814
|
+
}
|
|
10815
|
+
/** Has a load READ the store once? Until then the mirror is not the truth. */
|
|
10816
|
+
hasAnswered() {
|
|
10817
|
+
return this.ledger.hasAnswered();
|
|
10818
|
+
}
|
|
10819
|
+
has(episodeId) {
|
|
10820
|
+
return this.ledger.has(episodeId);
|
|
10821
|
+
}
|
|
10822
|
+
snapshot() {
|
|
10823
|
+
return this.ledger.snapshot();
|
|
10824
|
+
}
|
|
10825
|
+
/**
|
|
10826
|
+
* Judge one announced edge and advance the ledger. Synchronous: the verdict
|
|
10827
|
+
* is a function of the mirror (D49); the durable write trails it.
|
|
10828
|
+
*/
|
|
10829
|
+
admit(edge, now) {
|
|
10830
|
+
const held = this.ledger.get(edge.episodeId);
|
|
10831
|
+
if (edge.phase === "raised") {
|
|
10832
|
+
if (edge.kind !== "alert-critical" && edge.kind !== "alert-error") return "duplicate";
|
|
10833
|
+
if (held !== void 0 && (held.kind === "alert-critical" || edge.kind === "alert-error")) return "duplicate";
|
|
10834
|
+
this.ledger.put({
|
|
10835
|
+
episodeId: edge.episodeId,
|
|
10836
|
+
kind: edge.kind,
|
|
10837
|
+
alertId: edge.alertId,
|
|
10838
|
+
since: held?.since ?? now
|
|
10839
|
+
});
|
|
10840
|
+
return "notify";
|
|
10841
|
+
}
|
|
10842
|
+
if (held === void 0) return "unknown-episode";
|
|
10843
|
+
this.ledger.forget(edge.episodeId);
|
|
10844
|
+
return held.kind === "alert-critical" && edge.kind === "alert-critical-resolved" ? "notify" : "closed-silently";
|
|
10845
|
+
}
|
|
10846
|
+
/** Drop rows older than the alert centre could still hold the alert. */
|
|
10847
|
+
async pruneOlderThan(cutoffMs) {
|
|
10848
|
+
const keep = new Set(this.ledger.snapshot().filter((row) => row.since >= cutoffMs).map((row) => row.episodeId));
|
|
10849
|
+
return this.ledger.pruneExcept(keep);
|
|
10850
|
+
}
|
|
10851
|
+
};
|
|
10852
|
+
//#endregion
|
|
10736
10853
|
//#region src/notification-center/device-prune-ledger.ts
|
|
10737
10854
|
/**
|
|
10738
10855
|
* @durable class=ledger owner=notification-center
|
|
@@ -16018,6 +16135,34 @@ function bestMatchingKindPreference(signal, ownerKind) {
|
|
|
16018
16135
|
}
|
|
16019
16136
|
//#endregion
|
|
16020
16137
|
//#region src/notification-center/event-intake.ts
|
|
16138
|
+
/**
|
|
16139
|
+
* The notification path's INTAKE SEAM — one event in, no knowledge of who
|
|
16140
|
+
* produced it.
|
|
16141
|
+
*
|
|
16142
|
+
* Why this file exists at all: until now every producer reached the centre
|
|
16143
|
+
* through its own record-shaped door (`onObjectEventPersisted(ObjectEvent)`,
|
|
16144
|
+
* `onSensorEventPersisted(SensorEvent)`, …). The doors did nothing but build a
|
|
16145
|
+
* subject and schedule an evaluation, yet their SIGNATURES made the notification
|
|
16146
|
+
* path look like a function of the pipeline's row types — so "notify about this"
|
|
16147
|
+
* was not expressible without first having a persisted row to hand.
|
|
16148
|
+
*
|
|
16149
|
+
* The split this file draws is between PRODUCING an event and CONSUMING one:
|
|
16150
|
+
*
|
|
16151
|
+
* - The producer owns persistence. The tracker persists its tracks, the sensor
|
|
16152
|
+
* ingest persists its rows, and a synthetic producer (the rule tester)
|
|
16153
|
+
* persists NOTHING. Whether a row exists is the producer's business and
|
|
16154
|
+
* never the consumer's.
|
|
16155
|
+
* - The consumer owns match → media → delivery. It receives an
|
|
16156
|
+
* {@link NcIncomingEvent} and cannot tell which producer built it, because
|
|
16157
|
+
* the only thing the matcher can read is `subject`, and `subject` carries no
|
|
16158
|
+
* provenance beyond the detection `source` a real record would carry anyway.
|
|
16159
|
+
*
|
|
16160
|
+
* {@link NcIncomingEvent.origin} exists for LOGS, and for the outbox row's
|
|
16161
|
+
* marker. It is deliberately NOT part of `NcRuleSubject`: a condition cannot
|
|
16162
|
+
* reach it, so no rule can be authored that behaves differently for a test. That
|
|
16163
|
+
* is the property the tester is worth nothing without — a test that takes a
|
|
16164
|
+
* different branch is a test of the branch, not of the rule.
|
|
16165
|
+
*/
|
|
16021
16166
|
/** Object-event insert (movement-state transitions + zone crossings). */
|
|
16022
16167
|
function incomingFromObjectEvent(event) {
|
|
16023
16168
|
return {
|
|
@@ -16861,9 +17006,45 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
16861
17006
|
case "system.restarting": return incomingFromNodeRestarting(facts, data);
|
|
16862
17007
|
case "recording.export.completed": return incomingFromExportCompleted(facts, data);
|
|
16863
17008
|
case "backup.run-completed": return incomingFromBackupCompleted(facts, data);
|
|
17009
|
+
case "alert.episode": return incomingFromAlertEpisode(facts);
|
|
16864
17010
|
default: return null;
|
|
16865
17011
|
}
|
|
16866
17012
|
}
|
|
17013
|
+
/**
|
|
17014
|
+
* An alert episode edge (`alert-critical`, `alert-error`, `alert-critical-resolved`).
|
|
17015
|
+
*
|
|
17016
|
+
* The WORDS are the alert's own: the alert centre composed the title and the
|
|
17017
|
+
* message when it opened the alert, and a rule's template may reword them with
|
|
17018
|
+
* `{{title}}`, `{{message}}`, `{{severity}}`, `{{category}}`, `{{source}}`,
|
|
17019
|
+
* `{{node}}` and `{{actionUrl}}`. The subject is the EPISODE, so the dedup slot
|
|
17020
|
+
* of a new episode is never the one a previous, resolved, episode held.
|
|
17021
|
+
*
|
|
17022
|
+
* Whether an edge is notified at all (once per episode, reconciled from the
|
|
17023
|
+
* alerts store) is decided before this point, by `NcAlertEpisodeLedger`.
|
|
17024
|
+
*/
|
|
17025
|
+
function incomingFromAlertEpisode(facts) {
|
|
17026
|
+
const payload = parseAlertEpisodePayload(facts.data);
|
|
17027
|
+
if (payload === null) return null;
|
|
17028
|
+
const kind = alertEpisodeKind(payload.severity, payload.phase);
|
|
17029
|
+
if (kind === null) return null;
|
|
17030
|
+
return systemSubject({
|
|
17031
|
+
facts,
|
|
17032
|
+
kind,
|
|
17033
|
+
subject: `alert:${payload.episodeId}`,
|
|
17034
|
+
titleKey: `system.${kind}.title`,
|
|
17035
|
+
bodyKey: `system.${kind}.body`,
|
|
17036
|
+
textParams: {
|
|
17037
|
+
title: payload.title,
|
|
17038
|
+
message: payload.message,
|
|
17039
|
+
severity: payload.severity,
|
|
17040
|
+
category: payload.category,
|
|
17041
|
+
source: payload.sourceId ?? "",
|
|
17042
|
+
nodeId: payload.nodeId ?? "",
|
|
17043
|
+
actionUrl: payload.actionUrl ?? ""
|
|
17044
|
+
},
|
|
17045
|
+
...payload.nodeId !== void 0 ? { nodeId: payload.nodeId } : {}
|
|
17046
|
+
});
|
|
17047
|
+
}
|
|
16867
17048
|
/** The shells whose update has its own sentence. Anything else gets the plain
|
|
16868
17049
|
* one: a body that names a shell nobody observed is worse than a vague body. */
|
|
16869
17050
|
var WRAPPER_BODY_KINDS = new Set([
|
|
@@ -19346,6 +19527,7 @@ async function declareNcPhase3SettingsStoreLegacyCollections(store) {
|
|
|
19346
19527
|
async function declareNcPhase3RecordBackedLegacyCollections(store) {
|
|
19347
19528
|
await OccupancyStore.declare(store);
|
|
19348
19529
|
await NcLivenessLedger.declare(store);
|
|
19530
|
+
await NcAlertEpisodeLedger.declare(store);
|
|
19349
19531
|
await NcAlertStore.declare(store);
|
|
19350
19532
|
await NcStateSummaryService.declare(store);
|
|
19351
19533
|
await NcSnoozeStore.declare(store);
|
|
@@ -32855,6 +33037,12 @@ var en_default = {
|
|
|
32855
33037
|
"system.backup-completed.title": "Backup completed",
|
|
32856
33038
|
"system.backup-completed.body": "Backup saved to {{destinationsOk}} of {{destinationsTotal}} destinations ({{sizeMB}} MB)",
|
|
32857
33039
|
"system.backup-completed.body.partial": "Backup saved to {{destinationsOk}} of {{destinationsTotal}} destinations — {{destinationsFailed}} failed",
|
|
33040
|
+
"system.alert-critical.title": "{{title}}",
|
|
33041
|
+
"system.alert-critical.body": "{{message}}",
|
|
33042
|
+
"system.alert-error.title": "{{title}}",
|
|
33043
|
+
"system.alert-error.body": "{{message}}",
|
|
33044
|
+
"system.alert-critical-resolved.title": "Resolved: {{title}}",
|
|
33045
|
+
"system.alert-critical-resolved.body": "{{message}}",
|
|
32858
33046
|
"system.device-consumable-low.title": "Consumable low — {{deviceName}}",
|
|
32859
33047
|
"system.device-consumable-low.body": "{{item}} on {{deviceName}} is at {{percentage}}%",
|
|
32860
33048
|
"system.device-consumable-low.body.unmeasured": "{{item}} on {{deviceName}} needs replacing",
|
|
@@ -33235,6 +33423,12 @@ var it_default = {
|
|
|
33235
33423
|
"system.backup-completed.title": "Backup completato",
|
|
33236
33424
|
"system.backup-completed.body": "Backup salvato su {{destinationsOk}} destinazioni su {{destinationsTotal}} ({{sizeMB}} MB)",
|
|
33237
33425
|
"system.backup-completed.body.partial": "Backup salvato su {{destinationsOk}} destinazioni su {{destinationsTotal}} — {{destinationsFailed}} fallite",
|
|
33426
|
+
"system.alert-critical.title": "{{title}}",
|
|
33427
|
+
"system.alert-critical.body": "{{message}}",
|
|
33428
|
+
"system.alert-error.title": "{{title}}",
|
|
33429
|
+
"system.alert-error.body": "{{message}}",
|
|
33430
|
+
"system.alert-critical-resolved.title": "Risolto: {{title}}",
|
|
33431
|
+
"system.alert-critical-resolved.body": "{{message}}",
|
|
33238
33432
|
"system.device-consumable-low.title": "Consumabile basso — {{deviceName}}",
|
|
33239
33433
|
"system.device-consumable-low.body": "{{item}} su {{deviceName}} è al {{percentage}}%",
|
|
33240
33434
|
"system.device-consumable-low.body.unmeasured": "{{item}} su {{deviceName}} va sostituito",
|
|
@@ -35210,6 +35404,7 @@ function systemEventSubject(kind, scope) {
|
|
|
35210
35404
|
if (kind === "node-updating") return `node-updating:${scope.nodeId ?? "hub"}:1.0.1`;
|
|
35211
35405
|
if (kind === "export-completed") return `export:${scope.deviceId ?? 0}`;
|
|
35212
35406
|
if (kind === "backup-completed") return "backup:test";
|
|
35407
|
+
if (kind.startsWith("alert-")) return "alert:test";
|
|
35213
35408
|
return `alarm:${scope.deviceId ?? 0}`;
|
|
35214
35409
|
}
|
|
35215
35410
|
/**
|
|
@@ -35350,7 +35545,12 @@ function planRuleTest(input) {
|
|
|
35350
35545
|
nodes: "hub",
|
|
35351
35546
|
fromVersion: "1.0.0",
|
|
35352
35547
|
toVersion: "1.0.1",
|
|
35353
|
-
title: "Export",
|
|
35548
|
+
title: systemEventKind.startsWith("alert-") ? "Recording location full" : "Export",
|
|
35549
|
+
message: "No free space left on the recording location",
|
|
35550
|
+
severity: systemEventKind === "alert-error" ? "error" : "critical",
|
|
35551
|
+
category: "recorder.placement",
|
|
35552
|
+
source: "recorder",
|
|
35553
|
+
actionUrl: "/system/addons",
|
|
35354
35554
|
count: "1"
|
|
35355
35555
|
},
|
|
35356
35556
|
...systemScope.deviceId !== void 0 ? { deviceId: systemScope.deviceId } : {},
|
|
@@ -51921,6 +52121,10 @@ function groupedAlarmPhaseKey(ruleId, trackId) {
|
|
|
51921
52121
|
var NO_MATCH_REPORT_INTERVAL_MS = 6e4;
|
|
51922
52122
|
var DEFAULT_DRAIN_INTERVAL_MS = 2e3;
|
|
51923
52123
|
var DEFAULT_RULE_RELOAD_INTERVAL_MS = 3e4;
|
|
52124
|
+
/** How often open critical/error alerts are reconciled against the notified episodes. */
|
|
52125
|
+
var NC_ALERT_RECONCILE_INTERVAL_MS = 5 * 6e4;
|
|
52126
|
+
/** The most alerts one reconcile reads (the alert centre's own ceiling). */
|
|
52127
|
+
var NC_ALERT_RECONCILE_LIMIT = 5e3;
|
|
51924
52128
|
var DEFAULT_RECONCILE_WINDOW_MS = 15 * 6e4;
|
|
51925
52129
|
/** Overlap subtracted from the watermark on boot (idempotent, cheap). */
|
|
51926
52130
|
var RECONCILE_OVERLAP_MS = 6e4;
|
|
@@ -52493,6 +52697,15 @@ var NotificationCenter = class {
|
|
|
52493
52697
|
* seeds, only a real flip notifies.
|
|
52494
52698
|
*/
|
|
52495
52699
|
liveness;
|
|
52700
|
+
/**
|
|
52701
|
+
* Which alert episodes were already notified (`alert-episode-ledger.ts`): the
|
|
52702
|
+
* alert centre sends nothing of its own, so this is what makes its bus
|
|
52703
|
+
* announcements and the reconcile from the alerts store safe to run together.
|
|
52704
|
+
*/
|
|
52705
|
+
alertEpisodes;
|
|
52706
|
+
/** The boot load of {@link alertEpisodes} has been ATTEMPTED (bus edges before it are the reconcile's). */
|
|
52707
|
+
alertEpisodesLoaded = false;
|
|
52708
|
+
alertReconcileTimer = null;
|
|
52496
52709
|
/** Batches `addon.updated` (version-bearing) into one notification per 30 s. */
|
|
52497
52710
|
addonUpdates;
|
|
52498
52711
|
/** Trailing 5 min hold-off for camera liveness flaps (plugin restart). */
|
|
@@ -53194,6 +53407,10 @@ var NotificationCenter = class {
|
|
|
53194
53407
|
...durableStore,
|
|
53195
53408
|
logger: this.logger.child("liveness")
|
|
53196
53409
|
});
|
|
53410
|
+
this.alertEpisodes = new NcAlertEpisodeLedger({
|
|
53411
|
+
...durableStore,
|
|
53412
|
+
logger: this.logger.child("alert-episodes")
|
|
53413
|
+
});
|
|
53197
53414
|
this.addonUpdates = new AddonUpdateCoalescer((updates) => {
|
|
53198
53415
|
const incoming = incomingFromAddonUpdates({
|
|
53199
53416
|
eventId: `addon-updated:${updates.map((u) => u.packageName).join(",")}`,
|
|
@@ -53590,6 +53807,10 @@ var NotificationCenter = class {
|
|
|
53590
53807
|
this.reloadTimer = setInterval(() => {
|
|
53591
53808
|
this.reloadRules();
|
|
53592
53809
|
}, this.deps.ruleReloadIntervalMs ?? DEFAULT_RULE_RELOAD_INTERVAL_MS);
|
|
53810
|
+
this.alertReconcileTimer = setInterval(() => {
|
|
53811
|
+
this.reconcileAlertEpisodes();
|
|
53812
|
+
}, this.deps.alertReconcileIntervalMs ?? NC_ALERT_RECONCILE_INTERVAL_MS);
|
|
53813
|
+
this.alertReconcileTimer.unref?.();
|
|
53593
53814
|
const timelapseScheduler = this.timelapseScheduler;
|
|
53594
53815
|
if (timelapseScheduler !== null) await timed("timelapseProducer", () => timelapseScheduler.start()).catch((err) => {
|
|
53595
53816
|
this.logger.warn("timelapse producer did not start", { meta: { error: String(err) } });
|
|
@@ -53705,6 +53926,7 @@ var NotificationCenter = class {
|
|
|
53705
53926
|
await this.deviceDirectory.refresh();
|
|
53706
53927
|
})) return;
|
|
53707
53928
|
if (!await phase("liveness", () => this.hydrateLiveness())) return;
|
|
53929
|
+
if (!await phase("alertEpisodes", () => this.hydrateAlertEpisodes())) return;
|
|
53708
53930
|
if (!await phase("deviceAlerts", async () => {
|
|
53709
53931
|
const rows = await this.alertStore.load();
|
|
53710
53932
|
this.logger.info("device alert ledger reseeded", { meta: { rows } });
|
|
@@ -53747,6 +53969,10 @@ var NotificationCenter = class {
|
|
|
53747
53969
|
clearInterval(this.reloadTimer);
|
|
53748
53970
|
this.reloadTimer = null;
|
|
53749
53971
|
}
|
|
53972
|
+
if (this.alertReconcileTimer !== null) {
|
|
53973
|
+
clearInterval(this.alertReconcileTimer);
|
|
53974
|
+
this.alertReconcileTimer = null;
|
|
53975
|
+
}
|
|
53750
53976
|
this.timelapseScheduler?.stop();
|
|
53751
53977
|
this.summaryProducer?.stop();
|
|
53752
53978
|
await this.closeGroupedWindowsOnStop();
|
|
@@ -54758,6 +54984,10 @@ var NotificationCenter = class {
|
|
|
54758
54984
|
return;
|
|
54759
54985
|
}
|
|
54760
54986
|
this.stateSummary.observeRawEvent(event.category, event.data, this.now(), event.source.nodeId);
|
|
54987
|
+
if (event.category === "alert.episode") {
|
|
54988
|
+
if (this.alertEpisodesLoaded) this.admitAlertEpisode(event, "bus");
|
|
54989
|
+
return;
|
|
54990
|
+
}
|
|
54761
54991
|
if (event.category === "addon.started" || event.category === "addon.restarted") return;
|
|
54762
54992
|
if (event.category === "pipeline.camera-assigned") return;
|
|
54763
54993
|
const battery = batterySliceOf(event);
|
|
@@ -54791,10 +55021,6 @@ var NotificationCenter = class {
|
|
|
54791
55021
|
const system = incoming.subject.systemEvent;
|
|
54792
55022
|
if (system !== void 0) this.stateSummary.observeSystem(system, this.now());
|
|
54793
55023
|
if (system !== void 0) this.updateLandings.observe(system, this.now());
|
|
54794
|
-
if (system !== void 0 && system.kind === "addon-crash-loop") {
|
|
54795
|
-
this.logger.debug("addon-crash-loop is pushed by the hub alert centre, not by a rule", { meta: { subject: system.subject } });
|
|
54796
|
-
return;
|
|
54797
|
-
}
|
|
54798
55024
|
const observation = system !== void 0 ? ledgeredLiveness(system) : null;
|
|
54799
55025
|
if (observation !== null) {
|
|
54800
55026
|
const previous = this.liveness.stateOf(observation.subject);
|
|
@@ -57446,6 +57672,89 @@ var NotificationCenter = class {
|
|
|
57446
57672
|
await this.occupancyStore.pruneExcept(this.activeOccupancyKeys());
|
|
57447
57673
|
}
|
|
57448
57674
|
/**
|
|
57675
|
+
* One edge of an alert episode, from the bus or from the reconcile. The
|
|
57676
|
+
* ledger decides whether it is news; only news reaches the rules.
|
|
57677
|
+
*/
|
|
57678
|
+
admitAlertEpisode(event, via) {
|
|
57679
|
+
const payload = parseAlertEpisodePayload(event.data);
|
|
57680
|
+
if (payload === null) {
|
|
57681
|
+
this.logger.warn("alert episode event with an unreadable payload ignored", { meta: {
|
|
57682
|
+
via,
|
|
57683
|
+
eventId: event.id
|
|
57684
|
+
} });
|
|
57685
|
+
return false;
|
|
57686
|
+
}
|
|
57687
|
+
const verdict = this.alertEpisodes.admit({
|
|
57688
|
+
kind: alertEpisodeKind(payload.severity, payload.phase),
|
|
57689
|
+
phase: payload.phase,
|
|
57690
|
+
episodeId: payload.episodeId,
|
|
57691
|
+
alertId: payload.alertId
|
|
57692
|
+
}, this.now());
|
|
57693
|
+
const meta = {
|
|
57694
|
+
via,
|
|
57695
|
+
alertId: payload.alertId,
|
|
57696
|
+
episodeId: payload.episodeId,
|
|
57697
|
+
phase: payload.phase,
|
|
57698
|
+
severity: payload.severity,
|
|
57699
|
+
verdict
|
|
57700
|
+
};
|
|
57701
|
+
if (verdict !== "notify") {
|
|
57702
|
+
this.logger.debug("alert episode edge not notified", { meta });
|
|
57703
|
+
return false;
|
|
57704
|
+
}
|
|
57705
|
+
const incoming = incomingFromSystemEvent(event, this.deviceDirectory.lookup());
|
|
57706
|
+
if (incoming === null) return false;
|
|
57707
|
+
this.logger.info("alert episode notified to the rules", { meta });
|
|
57708
|
+
this.consumeEvent(incoming);
|
|
57709
|
+
return true;
|
|
57710
|
+
}
|
|
57711
|
+
async hydrateAlertEpisodes() {
|
|
57712
|
+
try {
|
|
57713
|
+
const rows = await this.alertEpisodes.load();
|
|
57714
|
+
this.logger.info("alert episode ledger reseeded", { meta: { rows } });
|
|
57715
|
+
} finally {
|
|
57716
|
+
this.alertEpisodesLoaded = true;
|
|
57717
|
+
}
|
|
57718
|
+
await this.reconcileAlertEpisodes();
|
|
57719
|
+
}
|
|
57720
|
+
/**
|
|
57721
|
+
* The alerts store against the notified episodes (D11, D49): every OPEN
|
|
57722
|
+
* critical/error alert not yet notified is notified, and every notified
|
|
57723
|
+
* episode whose alert is CLOSED gets its resolution. Never runs on a ledger
|
|
57724
|
+
* the store has not answered — an empty, unread mirror would re-notify every
|
|
57725
|
+
* open alert — and never reads absence as closure: an alert that is not in
|
|
57726
|
+
* the answer is left alone (rows age out by {@link NC_ALERT_EPISODE_MAX_AGE_MS}).
|
|
57727
|
+
*/
|
|
57728
|
+
async reconcileAlertEpisodes() {
|
|
57729
|
+
const read = this.deps.readAlerts;
|
|
57730
|
+
if (read === void 0 || !this.evaluationActive || !this.alertEpisodes.hasAnswered()) return;
|
|
57731
|
+
let alerts;
|
|
57732
|
+
try {
|
|
57733
|
+
alerts = await read();
|
|
57734
|
+
} catch (err) {
|
|
57735
|
+
this.logger.warn("alert episode reconcile could not read the alerts", { meta: { error: errMsg(err) } });
|
|
57736
|
+
return;
|
|
57737
|
+
}
|
|
57738
|
+
let notified = 0;
|
|
57739
|
+
for (const alert of alerts) {
|
|
57740
|
+
const episodeId = alertEpisodeId(alert);
|
|
57741
|
+
const open = isAlertEpisodeOpen(alert.status);
|
|
57742
|
+
if (open && !isAlertEpisodeSeverity(alert.severity)) continue;
|
|
57743
|
+
if (!open && !this.alertEpisodes.has(episodeId)) continue;
|
|
57744
|
+
const phase = open ? "raised" : "resolved";
|
|
57745
|
+
if (this.admitAlertEpisode(createEvent(EventCategory.AlertEpisode, {
|
|
57746
|
+
type: "addon",
|
|
57747
|
+
id: "notification-center"
|
|
57748
|
+
}, alertEpisodePayload(alert, phase, this.now())), "reconcile")) notified += 1;
|
|
57749
|
+
}
|
|
57750
|
+
const pruned = alerts.length < NC_ALERT_RECONCILE_LIMIT ? await this.alertEpisodes.pruneOlderThan(this.now() - NC_ALERT_EPISODE_MAX_AGE_MS) : 0;
|
|
57751
|
+
if (notified > 0 || pruned > 0) this.logger.info("alert episodes reconciled", { meta: {
|
|
57752
|
+
alerts: alerts.length,
|
|
57753
|
+
notified,
|
|
57754
|
+
pruned
|
|
57755
|
+
} });
|
|
57756
|
+
}
|
|
57757
|
+
/**
|
|
57449
57758
|
* Boot reseed of the durable liveness ledger.
|
|
57450
57759
|
*
|
|
57451
57760
|
* The count is said OUT LOUD at info, because it is the one line that
|
|
@@ -94546,6 +94855,62 @@ function pickWritableMediaLocation(locations, stickyId) {
|
|
|
94546
94855
|
return writable.toSorted((a, b) => b.headroomBytes - a.headroomBytes || a.id.localeCompare(b.id))[0]?.id ?? null;
|
|
94547
94856
|
}
|
|
94548
94857
|
//#endregion
|
|
94858
|
+
//#region src/pipeline-analytics/event-media-write-location.ts
|
|
94859
|
+
var EventMediaWriteLocator = class {
|
|
94860
|
+
deps;
|
|
94861
|
+
cached = null;
|
|
94862
|
+
/**
|
|
94863
|
+
* The last location actually picked. Survives {@link invalidate}, so a ring
|
|
94864
|
+
* keeps writing where it was writing while that stays writable — the
|
|
94865
|
+
* stickiness the class default used to provide (D383).
|
|
94866
|
+
*/
|
|
94867
|
+
lastPick = null;
|
|
94868
|
+
constructor(deps) {
|
|
94869
|
+
this.deps = deps;
|
|
94870
|
+
}
|
|
94871
|
+
/** Drop the cached pick: the next write re-reads the location set. */
|
|
94872
|
+
invalidate() {
|
|
94873
|
+
this.cached = null;
|
|
94874
|
+
}
|
|
94875
|
+
async resolve(logger, deviceId) {
|
|
94876
|
+
if (this.cached !== null) return this.cached;
|
|
94877
|
+
let locations;
|
|
94878
|
+
try {
|
|
94879
|
+
locations = await this.deps.list();
|
|
94880
|
+
} catch (err) {
|
|
94881
|
+
logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: errMsg(err) } });
|
|
94882
|
+
this.cached = DEFAULT_MEDIA_LOCATION;
|
|
94883
|
+
return DEFAULT_MEDIA_LOCATION;
|
|
94884
|
+
}
|
|
94885
|
+
const picked = pickWritableMediaLocation(locations.map((l) => ({
|
|
94886
|
+
id: l.id,
|
|
94887
|
+
mode: resolveLocationMode(l),
|
|
94888
|
+
headroomBytes: l.capacity?.availableBytes ?? 0
|
|
94889
|
+
})), this.lastPick ?? void 0);
|
|
94890
|
+
if (picked === null) {
|
|
94891
|
+
const seen = locations.length > 0 ? locations.map((l) => l.id).join(", ") : "(none registered)";
|
|
94892
|
+
logger.error("pipeline-analytics: no writable eventMedia storage location — refusing the write instead of failing far downstream as a filesystem error", {
|
|
94893
|
+
tags: { deviceId },
|
|
94894
|
+
meta: { locationsSeen: seen }
|
|
94895
|
+
});
|
|
94896
|
+
throw new Error(`no writable eventMedia storage location (seen: ${seen})`);
|
|
94897
|
+
}
|
|
94898
|
+
this.cached = picked;
|
|
94899
|
+
this.lastPick = picked;
|
|
94900
|
+
return picked;
|
|
94901
|
+
}
|
|
94902
|
+
};
|
|
94903
|
+
/**
|
|
94904
|
+
* Ring the locator from the location-set doorbell. `onRing` lets the caller drop
|
|
94905
|
+
* its other location-derived caches in the same breath.
|
|
94906
|
+
*/
|
|
94907
|
+
function subscribeEventMediaWriteLocator(eventBus, locator, onRing) {
|
|
94908
|
+
return eventBus.subscribe({ category: EventCategory.StorageLocationsChanged }, () => {
|
|
94909
|
+
locator.invalidate();
|
|
94910
|
+
onRing();
|
|
94911
|
+
});
|
|
94912
|
+
}
|
|
94913
|
+
//#endregion
|
|
94549
94914
|
//#region src/pipeline-analytics/media-relocate-engine.ts
|
|
94550
94915
|
/**
|
|
94551
94916
|
* Event-media relocation engine (entity-routing spec, Phase 4) — the
|
|
@@ -104539,7 +104904,8 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
|
|
|
104539
104904
|
EventCategory.SystemRestartCompleted,
|
|
104540
104905
|
EventCategory.SystemRestarting,
|
|
104541
104906
|
EventCategory.RecordingExportCompleted,
|
|
104542
|
-
EventCategory.BackupRunCompleted
|
|
104907
|
+
EventCategory.BackupRunCompleted,
|
|
104908
|
+
EventCategory.AlertEpisode
|
|
104543
104909
|
];
|
|
104544
104910
|
/**
|
|
104545
104911
|
* Install the addon's bus subscriptions. Identical set + relative order to the
|
|
@@ -107427,6 +107793,15 @@ Object.freeze({
|
|
|
107427
107793
|
"valueType": "boolean"
|
|
107428
107794
|
}],
|
|
107429
107795
|
"dishwasher": [
|
|
107796
|
+
{
|
|
107797
|
+
"path": "availability",
|
|
107798
|
+
"valueType": "enum",
|
|
107799
|
+
"enumValues": [
|
|
107800
|
+
"available",
|
|
107801
|
+
"offline",
|
|
107802
|
+
"description-missing"
|
|
107803
|
+
]
|
|
107804
|
+
},
|
|
107430
107805
|
{
|
|
107431
107806
|
"path": "state",
|
|
107432
107807
|
"valueType": "enum",
|
|
@@ -107439,12 +107814,35 @@ Object.freeze({
|
|
|
107439
107814
|
"unavailable"
|
|
107440
107815
|
]
|
|
107441
107816
|
},
|
|
107817
|
+
{
|
|
107818
|
+
"path": "operationState",
|
|
107819
|
+
"valueType": "enum",
|
|
107820
|
+
"enumValues": [
|
|
107821
|
+
"Inactive",
|
|
107822
|
+
"Ready",
|
|
107823
|
+
"DelayedStart",
|
|
107824
|
+
"Run",
|
|
107825
|
+
"Pause",
|
|
107826
|
+
"ActionRequired",
|
|
107827
|
+
"Finished",
|
|
107828
|
+
"Error",
|
|
107829
|
+
"Aborting"
|
|
107830
|
+
]
|
|
107831
|
+
},
|
|
107442
107832
|
{
|
|
107443
107833
|
"path": "program",
|
|
107444
107834
|
"valueType": "string"
|
|
107445
107835
|
},
|
|
107446
107836
|
{
|
|
107447
|
-
"path": "
|
|
107837
|
+
"path": "selectedProgram",
|
|
107838
|
+
"valueType": "string"
|
|
107839
|
+
},
|
|
107840
|
+
{
|
|
107841
|
+
"path": "remainingSeconds",
|
|
107842
|
+
"valueType": "number"
|
|
107843
|
+
},
|
|
107844
|
+
{
|
|
107845
|
+
"path": "delayedStartSeconds",
|
|
107448
107846
|
"valueType": "number"
|
|
107449
107847
|
},
|
|
107450
107848
|
{
|
|
@@ -107455,14 +107853,30 @@ Object.freeze({
|
|
|
107455
107853
|
"path": "doorOpen",
|
|
107456
107854
|
"valueType": "boolean"
|
|
107457
107855
|
},
|
|
107856
|
+
{
|
|
107857
|
+
"path": "doorLocked",
|
|
107858
|
+
"valueType": "boolean"
|
|
107859
|
+
},
|
|
107458
107860
|
{
|
|
107459
107861
|
"path": "remoteStartAllowed",
|
|
107460
107862
|
"valueType": "boolean"
|
|
107461
107863
|
},
|
|
107864
|
+
{
|
|
107865
|
+
"path": "remoteControlActive",
|
|
107866
|
+
"valueType": "boolean"
|
|
107867
|
+
},
|
|
107868
|
+
{
|
|
107869
|
+
"path": "localControlActive",
|
|
107870
|
+
"valueType": "boolean"
|
|
107871
|
+
},
|
|
107462
107872
|
{
|
|
107463
107873
|
"path": "powerOn",
|
|
107464
107874
|
"valueType": "boolean"
|
|
107465
107875
|
},
|
|
107876
|
+
{
|
|
107877
|
+
"path": "powerState",
|
|
107878
|
+
"valueType": "string"
|
|
107879
|
+
},
|
|
107466
107880
|
{
|
|
107467
107881
|
"path": "saltLevel",
|
|
107468
107882
|
"valueType": "enum",
|
|
@@ -113784,20 +114198,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
113784
114198
|
mediaRelocate = null;
|
|
113785
114199
|
mediaLocationStorage = null;
|
|
113786
114200
|
/**
|
|
113787
|
-
*
|
|
113788
|
-
*
|
|
113789
|
-
* `
|
|
113790
|
-
*
|
|
113791
|
-
* `enabled` flags only change via operator reconfig.
|
|
113792
|
-
*/
|
|
113793
|
-
eventMediaWriteLocationCache = null;
|
|
113794
|
-
/**
|
|
113795
|
-
* The last location this process actually picked for an `eventMedia` write.
|
|
113796
|
-
* Survives the cache invalidation a migration performs, so a refresh keeps
|
|
113797
|
-
* writing where it was writing while that stays writable — the stickiness the
|
|
113798
|
-
* class default used to provide before it was deleted (D383).
|
|
114201
|
+
* Where NEW `eventMedia` writes land. Its pick is cached per location-set
|
|
114202
|
+
* generation and dropped by the `StorageLocationsChanged` doorbell (D387) and
|
|
114203
|
+
* by `refreshStorageLocationsForMigration`, so a mode change or a repoint takes
|
|
114204
|
+
* effect without a restart.
|
|
113799
114205
|
*/
|
|
113800
|
-
|
|
114206
|
+
eventMediaWriteLocator = null;
|
|
114207
|
+
eventMediaLocationDoorbellUnsub = null;
|
|
113801
114208
|
mediaWriteGate = new MediaWriteGate();
|
|
113802
114209
|
storageMigrationLeaseId = null;
|
|
113803
114210
|
/** Per-camera history of LINKED-device sensor state changes (Part B). */
|
|
@@ -115837,33 +116244,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
115837
116244
|
}
|
|
115838
116245
|
/** One line per process: this repeats on every occupancy refresh otherwise. */
|
|
115839
116246
|
mediaOccupancyUnattributedLogged = false;
|
|
115840
|
-
|
|
115841
|
-
|
|
115842
|
-
|
|
115843
|
-
let locations;
|
|
115844
|
-
try {
|
|
115845
|
-
locations = await api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION });
|
|
115846
|
-
} catch (err) {
|
|
115847
|
-
logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: errMsg(err) } });
|
|
115848
|
-
this.eventMediaWriteLocationCache = DEFAULT_MEDIA_LOCATION;
|
|
115849
|
-
return DEFAULT_MEDIA_LOCATION;
|
|
115850
|
-
}
|
|
115851
|
-
const picked = pickWritableMediaLocation(locations.map((l) => ({
|
|
115852
|
-
id: l.id,
|
|
115853
|
-
mode: resolveLocationMode(l),
|
|
115854
|
-
headroomBytes: l.capacity?.availableBytes ?? 0
|
|
115855
|
-
})), this.lastEventMediaWritePick ?? void 0);
|
|
115856
|
-
if (picked === null) {
|
|
115857
|
-
const seen = locations.length > 0 ? locations.map((l) => l.id).join(", ") : "(none registered)";
|
|
115858
|
-
logger.error("pipeline-analytics: no writable eventMedia storage location — refusing the write instead of failing far downstream as a filesystem error", {
|
|
115859
|
-
tags: { deviceId },
|
|
115860
|
-
meta: { locationsSeen: seen }
|
|
115861
|
-
});
|
|
115862
|
-
throw new Error(`no writable eventMedia storage location (seen: ${seen})`);
|
|
115863
|
-
}
|
|
115864
|
-
this.eventMediaWriteLocationCache = picked;
|
|
115865
|
-
this.lastEventMediaWritePick = picked;
|
|
115866
|
-
return picked;
|
|
116247
|
+
resolveEventMediaWriteLocation(api, logger, deviceId) {
|
|
116248
|
+
this.eventMediaWriteLocator ??= new EventMediaWriteLocator({ list: () => api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION }) });
|
|
116249
|
+
return this.eventMediaWriteLocator.resolve(logger, deviceId);
|
|
115867
116250
|
}
|
|
115868
116251
|
/** Constructs every SQLite-backed store plus the stationary/package-drop/
|
|
115869
116252
|
* sensor plumbing, in the exact pre-S7 order. Returns the non-null bundle
|
|
@@ -115964,6 +116347,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
115964
116347
|
resolveEventMediaLocation: (deviceId) => this.resolveEventMediaWriteLocation(api, logger, deviceId)
|
|
115965
116348
|
});
|
|
115966
116349
|
this.mediaStore = mediaStore;
|
|
116350
|
+
this.eventMediaWriteLocator ??= new EventMediaWriteLocator({ list: () => api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION }) });
|
|
116351
|
+
this.eventMediaLocationDoorbellUnsub?.();
|
|
116352
|
+
this.eventMediaLocationDoorbellUnsub = subscribeEventMediaWriteLocator(this.ctx.eventBus, this.eventMediaWriteLocator, () => this.mediaLocationStorage?.invalidateRoots());
|
|
115967
116353
|
const eventLedgerWiring = this.eventLedgerWiring(api.settingsStore);
|
|
115968
116354
|
const retrainFrames = new RetrainFrameStore({
|
|
115969
116355
|
store: api.settingsStore,
|
|
@@ -116546,6 +116932,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
116546
116932
|
coreBlocks: async () => (await api.coreBlocks.list.query({})).blocks,
|
|
116547
116933
|
nodeAddons: (nodeId) => api.nodes.getNodeAddons.query({ nodeId })
|
|
116548
116934
|
},
|
|
116935
|
+
readAlerts: () => api.alerts.list.query({ limit: 5e3 }),
|
|
116549
116936
|
getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
|
|
116550
116937
|
recognitionOutlook: ({ deviceId, trackId, className, labelKind, now }) => recognitionOutlook({
|
|
116551
116938
|
className,
|
|
@@ -118559,6 +118946,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
118559
118946
|
this.alarmPanelConvergence?.stop();
|
|
118560
118947
|
this.alarmPanelConvergence = null;
|
|
118561
118948
|
for (const unsub of this.subscriptionUnsubs) unsub();
|
|
118949
|
+
this.eventMediaLocationDoorbellUnsub?.();
|
|
118950
|
+
this.eventMediaLocationDoorbellUnsub = null;
|
|
118562
118951
|
this.subscriptionUnsubs = [];
|
|
118563
118952
|
await Promise.all([this.drainSensorHandlers(), this.motionEpisodes?.closeAll("addon-shutdown")]);
|
|
118564
118953
|
await this.settleTrackFusions();
|
|
@@ -125813,7 +126202,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
125813
126202
|
async refreshStorageLocationsForMigration(input) {
|
|
125814
126203
|
if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
|
|
125815
126204
|
this.mediaLocationStorage?.invalidateRoots();
|
|
125816
|
-
this.
|
|
126205
|
+
this.eventMediaWriteLocator?.invalidate();
|
|
125817
126206
|
return { refreshed: true };
|
|
125818
126207
|
}
|
|
125819
126208
|
/**
|
|
@@ -126176,7 +126565,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
126176
126565
|
async startStorageMigrationMove(input) {
|
|
126177
126566
|
if (!this.mediaRelocate) throw new Error("media relocation unavailable");
|
|
126178
126567
|
if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
|
|
126179
|
-
return { jobId: this.
|
|
126568
|
+
return { jobId: await this.startMediaMover(input) };
|
|
126569
|
+
}
|
|
126570
|
+
/**
|
|
126571
|
+
* Start a media move only while no single-location repoint holds an overlapping
|
|
126572
|
+
* physical root (the storage orchestrator owns that claim). A shared recordings
|
|
126573
|
+
* root is the case: a repoint of another class there must not run beside us.
|
|
126574
|
+
*/
|
|
126575
|
+
startMediaMover(input) {
|
|
126576
|
+
const engine = this.mediaRelocate;
|
|
126577
|
+
if (!engine) throw new Error("media relocation unavailable");
|
|
126578
|
+
return startMoverUnderClearance({
|
|
126579
|
+
clearance: (locationIds) => this.ctx.api.storageMigration.moverClearance.query({ locationIds }),
|
|
126580
|
+
start: () => engine.start(input),
|
|
126581
|
+
cancel: (jobId) => {
|
|
126582
|
+
engine.cancel(jobId);
|
|
126583
|
+
},
|
|
126584
|
+
locationIds: [input.fromLocationId ?? "*", input.toLocationId]
|
|
126585
|
+
});
|
|
126180
126586
|
}
|
|
126181
126587
|
async getStorageMigrationMoveStatus(input) {
|
|
126182
126588
|
return this.mediaRelocate?.list().find((job) => job.jobId === input.jobId) ?? null;
|
|
@@ -126194,7 +126600,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
126194
126600
|
*/
|
|
126195
126601
|
async relocateMedia(input) {
|
|
126196
126602
|
if (!this.mediaRelocate) throw new Error("media relocation unavailable");
|
|
126197
|
-
const jobId = this.
|
|
126603
|
+
const jobId = await this.startMediaMover(input);
|
|
126198
126604
|
this.ctx.logger.info("pipeline-analytics: media relocate job enqueued", { meta: {
|
|
126199
126605
|
jobId,
|
|
126200
126606
|
mode: input.mode ?? "move",
|