@camstack/addon-post-analysis 1.2.607 → 1.2.609
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 +583 -68
- package/dist/pipeline-analytics/index.mjs +583 -68
- 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
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_clip_model_registry = require("../clip-model-registry-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-B11yS32V.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_clip_model_registry.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -10281,6 +10281,7 @@ var NC_SYSTEM_STATE_LEGACY = "notification-center:system-state";
|
|
|
10281
10281
|
var NC_OCCUPANCY_LEGACY = "notification-center:occupancy";
|
|
10282
10282
|
var NC_DEVICE_PRUNES_LEGACY = "notification-center:device-prunes";
|
|
10283
10283
|
var NC_STATE_SUMMARY_STAMPS_LEGACY = "notification-center:state-summary-stamps";
|
|
10284
|
+
var NC_ALERT_EPISODES_LEGACY = "notification-center:alert-episodes";
|
|
10284
10285
|
var NC_META_LEGACY = "notification-center:meta";
|
|
10285
10286
|
var NC_SNOOZES_LEGACY = "notification-center:snoozes";
|
|
10286
10287
|
var NC_SNOOZE_SUPPRESSED_LEGACY = "notification-center:snooze-suppressed";
|
|
@@ -10301,6 +10302,7 @@ var ncDurableRecordKinds = require_clip_model_registry.defineRecordKinds({
|
|
|
10301
10302
|
liveness: ledger(NC_LIVENESS_LEGACY, "a liveness state TRANSITION the centre accepted (seed or flip); a no-flip writes nothing"),
|
|
10302
10303
|
"device-alerts": ledger(NC_DEVICE_ALERTS_LEGACY, "a sustained device alert episode the centre confirmed or cleared"),
|
|
10303
10304
|
"system-state": ledger(NC_SYSTEM_STATE_LEGACY, "a system-state subject flip the state summary accepted"),
|
|
10305
|
+
"alert-episodes": ledger(NC_ALERT_EPISODES_LEGACY, "an alert episode the centre notified (critical or error); its resolution deletes the row"),
|
|
10304
10306
|
occupancy: ledger(NC_OCCUPANCY_LEGACY, "a confirmed occupancy level write-through after the watcher decided an edge"),
|
|
10305
10307
|
"device-prunes": ledger(NC_DEVICE_PRUNES_LEGACY, "one row recording what a deleted-device prune removed"),
|
|
10306
10308
|
"state-summary-stamps": ledger(NC_STATE_SUMMARY_STAMPS_LEGACY, "digest delivery stamps per rule kind"),
|
|
@@ -10320,6 +10322,7 @@ var NC_PHASE3_RECORD_BACKED_LEGACY_COLLECTIONS = [
|
|
|
10320
10322
|
NC_LIVENESS_LEGACY,
|
|
10321
10323
|
NC_DEVICE_ALERTS_LEGACY,
|
|
10322
10324
|
NC_SYSTEM_STATE_LEGACY,
|
|
10325
|
+
NC_ALERT_EPISODES_LEGACY,
|
|
10323
10326
|
NC_OCCUPANCY_LEGACY,
|
|
10324
10327
|
NC_DEVICE_PRUNES_LEGACY,
|
|
10325
10328
|
NC_STATE_SUMMARY_STAMPS_LEGACY,
|
|
@@ -10741,6 +10744,120 @@ var NcAlertStore = class {
|
|
|
10741
10744
|
}
|
|
10742
10745
|
};
|
|
10743
10746
|
//#endregion
|
|
10747
|
+
//#region src/notification-center/alert-episode-ledger.ts
|
|
10748
|
+
/**
|
|
10749
|
+
* @durable class=ledger owner=notification-center
|
|
10750
|
+
* write="an alert episode the centre notified (alert-critical / alert-error); its resolution deletes the row"
|
|
10751
|
+
* 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)."
|
|
10752
|
+
*/
|
|
10753
|
+
var NC_ALERT_EPISODES_COLLECTION = "notification-center:alert-episodes";
|
|
10754
|
+
var NC_ALERT_EPISODES_COLUMNS = [
|
|
10755
|
+
{
|
|
10756
|
+
name: "episodeId",
|
|
10757
|
+
type: "TEXT",
|
|
10758
|
+
primaryKey: true,
|
|
10759
|
+
notNull: true
|
|
10760
|
+
},
|
|
10761
|
+
(
|
|
10762
|
+
/** The kind that was notified: `alert-critical` or `alert-error`. */
|
|
10763
|
+
{
|
|
10764
|
+
name: "kind",
|
|
10765
|
+
type: "TEXT",
|
|
10766
|
+
notNull: true
|
|
10767
|
+
}),
|
|
10768
|
+
{
|
|
10769
|
+
name: "alertId",
|
|
10770
|
+
type: "TEXT",
|
|
10771
|
+
notNull: true
|
|
10772
|
+
},
|
|
10773
|
+
{
|
|
10774
|
+
name: "since",
|
|
10775
|
+
type: "INTEGER",
|
|
10776
|
+
notNull: true
|
|
10777
|
+
}
|
|
10778
|
+
];
|
|
10779
|
+
/** Longer than the alert centre's maximum retention (365 days). */
|
|
10780
|
+
var NC_ALERT_EPISODE_MAX_AGE_MS = 400 * 24 * 60 * 60 * 1e3;
|
|
10781
|
+
function isEpisodeKind(value) {
|
|
10782
|
+
return value === "alert-critical" || value === "alert-error";
|
|
10783
|
+
}
|
|
10784
|
+
var NC_ALERT_EPISODES_SPEC = {
|
|
10785
|
+
collection: NC_ALERT_EPISODES_COLLECTION,
|
|
10786
|
+
columns: NC_ALERT_EPISODES_COLUMNS,
|
|
10787
|
+
writeMode: "write-behind",
|
|
10788
|
+
keyOf: (row) => row.episodeId,
|
|
10789
|
+
toValue: (row) => ({
|
|
10790
|
+
kind: row.kind,
|
|
10791
|
+
alertId: row.alertId,
|
|
10792
|
+
since: row.since
|
|
10793
|
+
}),
|
|
10794
|
+
fromRecord: (key, data) => {
|
|
10795
|
+
const kind = data["kind"];
|
|
10796
|
+
const alertId = data["alertId"];
|
|
10797
|
+
const since = data["since"];
|
|
10798
|
+
if (!isEpisodeKind(kind) || typeof alertId !== "string" || typeof since !== "number") return null;
|
|
10799
|
+
return {
|
|
10800
|
+
episodeId: key,
|
|
10801
|
+
kind,
|
|
10802
|
+
alertId,
|
|
10803
|
+
since
|
|
10804
|
+
};
|
|
10805
|
+
}
|
|
10806
|
+
};
|
|
10807
|
+
NC_ALERT_EPISODES_SPEC.fromRecord, NC_ALERT_EPISODES_SPEC.toValue;
|
|
10808
|
+
var NcAlertEpisodeLedger = class {
|
|
10809
|
+
ledger;
|
|
10810
|
+
constructor(deps) {
|
|
10811
|
+
this.ledger = new DurableLedger({
|
|
10812
|
+
spec: NC_ALERT_EPISODES_SPEC,
|
|
10813
|
+
kind: resolveDurableKind(NC_ALERT_EPISODES_SPEC, ncDurableRecordKinds["alert-episodes"], deps),
|
|
10814
|
+
logger: deps.logger
|
|
10815
|
+
});
|
|
10816
|
+
}
|
|
10817
|
+
static declare(store) {
|
|
10818
|
+
return declareDurableSpecLegacy(store, NC_ALERT_EPISODES_SPEC);
|
|
10819
|
+
}
|
|
10820
|
+
async load() {
|
|
10821
|
+
return (await this.ledger.load()).length;
|
|
10822
|
+
}
|
|
10823
|
+
/** Has a load READ the store once? Until then the mirror is not the truth. */
|
|
10824
|
+
hasAnswered() {
|
|
10825
|
+
return this.ledger.hasAnswered();
|
|
10826
|
+
}
|
|
10827
|
+
has(episodeId) {
|
|
10828
|
+
return this.ledger.has(episodeId);
|
|
10829
|
+
}
|
|
10830
|
+
snapshot() {
|
|
10831
|
+
return this.ledger.snapshot();
|
|
10832
|
+
}
|
|
10833
|
+
/**
|
|
10834
|
+
* Judge one announced edge and advance the ledger. Synchronous: the verdict
|
|
10835
|
+
* is a function of the mirror (D49); the durable write trails it.
|
|
10836
|
+
*/
|
|
10837
|
+
admit(edge, now) {
|
|
10838
|
+
const held = this.ledger.get(edge.episodeId);
|
|
10839
|
+
if (edge.phase === "raised") {
|
|
10840
|
+
if (edge.kind !== "alert-critical" && edge.kind !== "alert-error") return "duplicate";
|
|
10841
|
+
if (held !== void 0 && (held.kind === "alert-critical" || edge.kind === "alert-error")) return "duplicate";
|
|
10842
|
+
this.ledger.put({
|
|
10843
|
+
episodeId: edge.episodeId,
|
|
10844
|
+
kind: edge.kind,
|
|
10845
|
+
alertId: edge.alertId,
|
|
10846
|
+
since: held?.since ?? now
|
|
10847
|
+
});
|
|
10848
|
+
return "notify";
|
|
10849
|
+
}
|
|
10850
|
+
if (held === void 0) return "unknown-episode";
|
|
10851
|
+
this.ledger.forget(edge.episodeId);
|
|
10852
|
+
return held.kind === "alert-critical" && edge.kind === "alert-critical-resolved" ? "notify" : "closed-silently";
|
|
10853
|
+
}
|
|
10854
|
+
/** Drop rows older than the alert centre could still hold the alert. */
|
|
10855
|
+
async pruneOlderThan(cutoffMs) {
|
|
10856
|
+
const keep = new Set(this.ledger.snapshot().filter((row) => row.since >= cutoffMs).map((row) => row.episodeId));
|
|
10857
|
+
return this.ledger.pruneExcept(keep);
|
|
10858
|
+
}
|
|
10859
|
+
};
|
|
10860
|
+
//#endregion
|
|
10744
10861
|
//#region src/notification-center/device-prune-ledger.ts
|
|
10745
10862
|
/**
|
|
10746
10863
|
* @durable class=ledger owner=notification-center
|
|
@@ -16026,6 +16143,34 @@ function bestMatchingKindPreference(signal, ownerKind) {
|
|
|
16026
16143
|
}
|
|
16027
16144
|
//#endregion
|
|
16028
16145
|
//#region src/notification-center/event-intake.ts
|
|
16146
|
+
/**
|
|
16147
|
+
* The notification path's INTAKE SEAM — one event in, no knowledge of who
|
|
16148
|
+
* produced it.
|
|
16149
|
+
*
|
|
16150
|
+
* Why this file exists at all: until now every producer reached the centre
|
|
16151
|
+
* through its own record-shaped door (`onObjectEventPersisted(ObjectEvent)`,
|
|
16152
|
+
* `onSensorEventPersisted(SensorEvent)`, …). The doors did nothing but build a
|
|
16153
|
+
* subject and schedule an evaluation, yet their SIGNATURES made the notification
|
|
16154
|
+
* path look like a function of the pipeline's row types — so "notify about this"
|
|
16155
|
+
* was not expressible without first having a persisted row to hand.
|
|
16156
|
+
*
|
|
16157
|
+
* The split this file draws is between PRODUCING an event and CONSUMING one:
|
|
16158
|
+
*
|
|
16159
|
+
* - The producer owns persistence. The tracker persists its tracks, the sensor
|
|
16160
|
+
* ingest persists its rows, and a synthetic producer (the rule tester)
|
|
16161
|
+
* persists NOTHING. Whether a row exists is the producer's business and
|
|
16162
|
+
* never the consumer's.
|
|
16163
|
+
* - The consumer owns match → media → delivery. It receives an
|
|
16164
|
+
* {@link NcIncomingEvent} and cannot tell which producer built it, because
|
|
16165
|
+
* the only thing the matcher can read is `subject`, and `subject` carries no
|
|
16166
|
+
* provenance beyond the detection `source` a real record would carry anyway.
|
|
16167
|
+
*
|
|
16168
|
+
* {@link NcIncomingEvent.origin} exists for LOGS, and for the outbox row's
|
|
16169
|
+
* marker. It is deliberately NOT part of `NcRuleSubject`: a condition cannot
|
|
16170
|
+
* reach it, so no rule can be authored that behaves differently for a test. That
|
|
16171
|
+
* is the property the tester is worth nothing without — a test that takes a
|
|
16172
|
+
* different branch is a test of the branch, not of the rule.
|
|
16173
|
+
*/
|
|
16029
16174
|
/** Object-event insert (movement-state transitions + zone crossings). */
|
|
16030
16175
|
function incomingFromObjectEvent(event) {
|
|
16031
16176
|
return {
|
|
@@ -16869,9 +17014,45 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
16869
17014
|
case "system.restarting": return incomingFromNodeRestarting(facts, data);
|
|
16870
17015
|
case "recording.export.completed": return incomingFromExportCompleted(facts, data);
|
|
16871
17016
|
case "backup.run-completed": return incomingFromBackupCompleted(facts, data);
|
|
17017
|
+
case "alert.episode": return incomingFromAlertEpisode(facts);
|
|
16872
17018
|
default: return null;
|
|
16873
17019
|
}
|
|
16874
17020
|
}
|
|
17021
|
+
/**
|
|
17022
|
+
* An alert episode edge (`alert-critical`, `alert-error`, `alert-critical-resolved`).
|
|
17023
|
+
*
|
|
17024
|
+
* The WORDS are the alert's own: the alert centre composed the title and the
|
|
17025
|
+
* message when it opened the alert, and a rule's template may reword them with
|
|
17026
|
+
* `{{title}}`, `{{message}}`, `{{severity}}`, `{{category}}`, `{{source}}`,
|
|
17027
|
+
* `{{node}}` and `{{actionUrl}}`. The subject is the EPISODE, so the dedup slot
|
|
17028
|
+
* of a new episode is never the one a previous, resolved, episode held.
|
|
17029
|
+
*
|
|
17030
|
+
* Whether an edge is notified at all (once per episode, reconciled from the
|
|
17031
|
+
* alerts store) is decided before this point, by `NcAlertEpisodeLedger`.
|
|
17032
|
+
*/
|
|
17033
|
+
function incomingFromAlertEpisode(facts) {
|
|
17034
|
+
const payload = require_clip_model_registry.parseAlertEpisodePayload(facts.data);
|
|
17035
|
+
if (payload === null) return null;
|
|
17036
|
+
const kind = require_clip_model_registry.alertEpisodeKind(payload.severity, payload.phase);
|
|
17037
|
+
if (kind === null) return null;
|
|
17038
|
+
return systemSubject({
|
|
17039
|
+
facts,
|
|
17040
|
+
kind,
|
|
17041
|
+
subject: `alert:${payload.episodeId}`,
|
|
17042
|
+
titleKey: `system.${kind}.title`,
|
|
17043
|
+
bodyKey: `system.${kind}.body`,
|
|
17044
|
+
textParams: {
|
|
17045
|
+
title: payload.title,
|
|
17046
|
+
message: payload.message,
|
|
17047
|
+
severity: payload.severity,
|
|
17048
|
+
category: payload.category,
|
|
17049
|
+
source: payload.sourceId ?? "",
|
|
17050
|
+
nodeId: payload.nodeId ?? "",
|
|
17051
|
+
actionUrl: payload.actionUrl ?? ""
|
|
17052
|
+
},
|
|
17053
|
+
...payload.nodeId !== void 0 ? { nodeId: payload.nodeId } : {}
|
|
17054
|
+
});
|
|
17055
|
+
}
|
|
16875
17056
|
/** The shells whose update has its own sentence. Anything else gets the plain
|
|
16876
17057
|
* one: a body that names a shell nobody observed is worse than a vague body. */
|
|
16877
17058
|
var WRAPPER_BODY_KINDS = new Set([
|
|
@@ -19354,6 +19535,7 @@ async function declareNcPhase3SettingsStoreLegacyCollections(store) {
|
|
|
19354
19535
|
async function declareNcPhase3RecordBackedLegacyCollections(store) {
|
|
19355
19536
|
await OccupancyStore.declare(store);
|
|
19356
19537
|
await NcLivenessLedger.declare(store);
|
|
19538
|
+
await NcAlertEpisodeLedger.declare(store);
|
|
19357
19539
|
await NcAlertStore.declare(store);
|
|
19358
19540
|
await NcStateSummaryService.declare(store);
|
|
19359
19541
|
await NcSnoozeStore.declare(store);
|
|
@@ -22165,6 +22347,41 @@ function audioDbfsByBucket(events, since, bucketMs) {
|
|
|
22165
22347
|
}
|
|
22166
22348
|
return out;
|
|
22167
22349
|
}
|
|
22350
|
+
/**
|
|
22351
|
+
* PEAK dBFS per bucket index from device-history points (D431 follow-up).
|
|
22352
|
+
*
|
|
22353
|
+
* The bar answers "was it loud here", so a bucket reports the loudest window
|
|
22354
|
+
* in it. A raw point has `max === value`; a rollup row's `max` is the loudest
|
|
22355
|
+
* point it covers, which a mean would flatten. A bucket with no point is
|
|
22356
|
+
* absent — never zero. A rollup row also credits a held value across a quiet
|
|
22357
|
+
* gap, so its max can echo an earlier measured peak.
|
|
22358
|
+
*/
|
|
22359
|
+
function audioPeakByBucket(points, since, bucketMs) {
|
|
22360
|
+
const out = /* @__PURE__ */ new Map();
|
|
22361
|
+
if (bucketMs <= 0) return out;
|
|
22362
|
+
for (const p of points) {
|
|
22363
|
+
if (!Number.isFinite(p.max)) continue;
|
|
22364
|
+
const idx = Math.floor((p.t - since) / bucketMs);
|
|
22365
|
+
if (idx < 0) continue;
|
|
22366
|
+
const prev = out.get(idx);
|
|
22367
|
+
if (prev === void 0 || p.max > prev) out.set(idx, p.max);
|
|
22368
|
+
}
|
|
22369
|
+
return out;
|
|
22370
|
+
}
|
|
22371
|
+
//#endregion
|
|
22372
|
+
//#region src/pipeline-analytics/store/analytics-history-keys.ts
|
|
22373
|
+
/** Declared addon series keys (D850). */
|
|
22374
|
+
var ANALYTICS_HISTORY_BUCKET_SERIES = {
|
|
22375
|
+
motion: "addon:pipeline-analytics/motion",
|
|
22376
|
+
"audio-dbfs": "addon:pipeline-analytics/audio-dbfs",
|
|
22377
|
+
occupancy: "addon:pipeline-analytics/occupancy"
|
|
22378
|
+
};
|
|
22379
|
+
var ANALYTICS_AUDIO_LEVEL_KEY = "addon:pipeline-analytics/audio-level";
|
|
22380
|
+
/** Map the public cap enum to the device-history key (read path). */
|
|
22381
|
+
function historyKeyForLtsSeries(series) {
|
|
22382
|
+
if (series === "battery") return "cap:battery.percentage";
|
|
22383
|
+
return ANALYTICS_HISTORY_BUCKET_SERIES[series];
|
|
22384
|
+
}
|
|
22168
22385
|
//#endregion
|
|
22169
22386
|
//#region src/pipeline-analytics/store/event-store.ts
|
|
22170
22387
|
/**
|
|
@@ -22537,6 +22754,24 @@ function eventRowId(logger, operation, deviceId, key) {
|
|
|
22537
22754
|
});
|
|
22538
22755
|
return null;
|
|
22539
22756
|
}
|
|
22757
|
+
var RAW_RETENTION_MS = 7 * 864e5;
|
|
22758
|
+
var FIVE_MIN_RETENTION_MS = 14 * 864e5;
|
|
22759
|
+
var FIVE_MIN_MS = 3e5;
|
|
22760
|
+
var HOUR_MS$2 = 36e5;
|
|
22761
|
+
/** The producer writes one raw point per window of this length. */
|
|
22762
|
+
var AUDIO_LEVEL_POINT_MS = 1e4;
|
|
22763
|
+
var HISTORY_MAX_POINTS = 5e3;
|
|
22764
|
+
/**
|
|
22765
|
+
* Pick the device-history tier for a density window: raw while the buckets are
|
|
22766
|
+
* finer than the 5m rollup, the window is inside raw retention and its points
|
|
22767
|
+
* fit one query; the 5m rollup while inside its retention; else 1h.
|
|
22768
|
+
*/
|
|
22769
|
+
function audioLevelResolution(q, now) {
|
|
22770
|
+
const rawFits = Math.ceil((q.until - q.since) / AUDIO_LEVEL_POINT_MS) <= HISTORY_MAX_POINTS;
|
|
22771
|
+
if (q.bucketMs < FIVE_MIN_MS && rawFits && q.since >= now - RAW_RETENTION_MS) return "raw";
|
|
22772
|
+
if (q.bucketMs < HOUR_MS$2 && q.since >= now - FIVE_MIN_RETENTION_MS) return "5m";
|
|
22773
|
+
return "1h";
|
|
22774
|
+
}
|
|
22540
22775
|
var EventStore = class EventStore {
|
|
22541
22776
|
store;
|
|
22542
22777
|
logger;
|
|
@@ -22544,6 +22779,10 @@ var EventStore = class EventStore {
|
|
|
22544
22779
|
audioLedger;
|
|
22545
22780
|
media;
|
|
22546
22781
|
onMotionSample;
|
|
22782
|
+
audioLevelHistory;
|
|
22783
|
+
nowFn;
|
|
22784
|
+
/** Cameras whose history read already failed once (log once per camera, D315). */
|
|
22785
|
+
levelFallbackLogged = /* @__PURE__ */ new Set();
|
|
22547
22786
|
constructor(deps) {
|
|
22548
22787
|
this.store = deps.store;
|
|
22549
22788
|
this.logger = deps.logger;
|
|
@@ -22552,6 +22791,8 @@ var EventStore = class EventStore {
|
|
|
22552
22791
|
this.audioLedger = deps.audioLedger ?? wireAudioEventLedger(wiring);
|
|
22553
22792
|
this.media = deps.media;
|
|
22554
22793
|
this.onMotionSample = deps.onMotionSample;
|
|
22794
|
+
this.audioLevelHistory = deps.audioLevelHistory;
|
|
22795
|
+
this.nowFn = deps.now ?? (() => Date.now());
|
|
22555
22796
|
}
|
|
22556
22797
|
static async declareTracklessEventCollections(store) {
|
|
22557
22798
|
await store.declareCollection.mutate({
|
|
@@ -23280,8 +23521,8 @@ var EventStore = class EventStore {
|
|
|
23280
23521
|
const m = motionMap.get(i) ?? 0;
|
|
23281
23522
|
const o = objectMap.get(i) ?? 0;
|
|
23282
23523
|
const a = audioMap.get(i) ?? 0;
|
|
23283
|
-
if (m + o + a === 0) continue;
|
|
23284
23524
|
const dbfs = audioDbfsMap.get(i);
|
|
23525
|
+
if (m + o + a === 0 && dbfs === void 0) continue;
|
|
23285
23526
|
out.push({
|
|
23286
23527
|
bucketStart: q.since + i * q.bucketMs,
|
|
23287
23528
|
motion: m,
|
|
@@ -23302,6 +23543,28 @@ var EventStore = class EventStore {
|
|
|
23302
23543
|
* missing bar is the honest rendering of "not measured" (D431).
|
|
23303
23544
|
*/
|
|
23304
23545
|
async audioDbfsPerBucket(q) {
|
|
23546
|
+
const history = this.audioLevelHistory;
|
|
23547
|
+
if (history !== void 0) try {
|
|
23548
|
+
const resolution = audioLevelResolution(q, this.nowFn());
|
|
23549
|
+
const points = (await history({
|
|
23550
|
+
deviceId: q.deviceId,
|
|
23551
|
+
key: ANALYTICS_AUDIO_LEVEL_KEY,
|
|
23552
|
+
scope: "",
|
|
23553
|
+
from: q.since,
|
|
23554
|
+
to: q.until,
|
|
23555
|
+
resolution,
|
|
23556
|
+
maxPoints: HISTORY_MAX_POINTS
|
|
23557
|
+
})).series.flatMap((s) => s.points);
|
|
23558
|
+
if (points.length > 0) return audioPeakByBucket(points, q.since, q.bucketMs);
|
|
23559
|
+
} catch (err) {
|
|
23560
|
+
if (!this.levelFallbackLogged.has(q.deviceId)) {
|
|
23561
|
+
this.levelFallbackLogged.add(q.deviceId);
|
|
23562
|
+
this.logger.warn("density: audio level history unreadable — falling back to audio events", {
|
|
23563
|
+
tags: { deviceId: q.deviceId },
|
|
23564
|
+
meta: { error: String(err) }
|
|
23565
|
+
});
|
|
23566
|
+
}
|
|
23567
|
+
}
|
|
23305
23568
|
try {
|
|
23306
23569
|
return audioDbfsByBucket((await this.queryAudio({
|
|
23307
23570
|
deviceId: q.deviceId,
|
|
@@ -32881,6 +33144,12 @@ var en_default = {
|
|
|
32881
33144
|
"system.backup-completed.title": "Backup completed",
|
|
32882
33145
|
"system.backup-completed.body": "Backup saved to {{destinationsOk}} of {{destinationsTotal}} destinations ({{sizeMB}} MB)",
|
|
32883
33146
|
"system.backup-completed.body.partial": "Backup saved to {{destinationsOk}} of {{destinationsTotal}} destinations — {{destinationsFailed}} failed",
|
|
33147
|
+
"system.alert-critical.title": "{{title}}",
|
|
33148
|
+
"system.alert-critical.body": "{{message}}",
|
|
33149
|
+
"system.alert-error.title": "{{title}}",
|
|
33150
|
+
"system.alert-error.body": "{{message}}",
|
|
33151
|
+
"system.alert-critical-resolved.title": "Resolved: {{title}}",
|
|
33152
|
+
"system.alert-critical-resolved.body": "{{message}}",
|
|
32884
33153
|
"system.device-consumable-low.title": "Consumable low — {{deviceName}}",
|
|
32885
33154
|
"system.device-consumable-low.body": "{{item}} on {{deviceName}} is at {{percentage}}%",
|
|
32886
33155
|
"system.device-consumable-low.body.unmeasured": "{{item}} on {{deviceName}} needs replacing",
|
|
@@ -33261,6 +33530,12 @@ var it_default = {
|
|
|
33261
33530
|
"system.backup-completed.title": "Backup completato",
|
|
33262
33531
|
"system.backup-completed.body": "Backup salvato su {{destinationsOk}} destinazioni su {{destinationsTotal}} ({{sizeMB}} MB)",
|
|
33263
33532
|
"system.backup-completed.body.partial": "Backup salvato su {{destinationsOk}} destinazioni su {{destinationsTotal}} — {{destinationsFailed}} fallite",
|
|
33533
|
+
"system.alert-critical.title": "{{title}}",
|
|
33534
|
+
"system.alert-critical.body": "{{message}}",
|
|
33535
|
+
"system.alert-error.title": "{{title}}",
|
|
33536
|
+
"system.alert-error.body": "{{message}}",
|
|
33537
|
+
"system.alert-critical-resolved.title": "Risolto: {{title}}",
|
|
33538
|
+
"system.alert-critical-resolved.body": "{{message}}",
|
|
33264
33539
|
"system.device-consumable-low.title": "Consumabile basso — {{deviceName}}",
|
|
33265
33540
|
"system.device-consumable-low.body": "{{item}} su {{deviceName}} è al {{percentage}}%",
|
|
33266
33541
|
"system.device-consumable-low.body.unmeasured": "{{item}} su {{deviceName}} va sostituito",
|
|
@@ -35236,6 +35511,7 @@ function systemEventSubject(kind, scope) {
|
|
|
35236
35511
|
if (kind === "node-updating") return `node-updating:${scope.nodeId ?? "hub"}:1.0.1`;
|
|
35237
35512
|
if (kind === "export-completed") return `export:${scope.deviceId ?? 0}`;
|
|
35238
35513
|
if (kind === "backup-completed") return "backup:test";
|
|
35514
|
+
if (kind.startsWith("alert-")) return "alert:test";
|
|
35239
35515
|
return `alarm:${scope.deviceId ?? 0}`;
|
|
35240
35516
|
}
|
|
35241
35517
|
/**
|
|
@@ -35376,7 +35652,12 @@ function planRuleTest(input) {
|
|
|
35376
35652
|
nodes: "hub",
|
|
35377
35653
|
fromVersion: "1.0.0",
|
|
35378
35654
|
toVersion: "1.0.1",
|
|
35379
|
-
title: "Export",
|
|
35655
|
+
title: systemEventKind.startsWith("alert-") ? "Recording location full" : "Export",
|
|
35656
|
+
message: "No free space left on the recording location",
|
|
35657
|
+
severity: systemEventKind === "alert-error" ? "error" : "critical",
|
|
35658
|
+
category: "recorder.placement",
|
|
35659
|
+
source: "recorder",
|
|
35660
|
+
actionUrl: "/system/addons",
|
|
35380
35661
|
count: "1"
|
|
35381
35662
|
},
|
|
35382
35663
|
...systemScope.deviceId !== void 0 ? { deviceId: systemScope.deviceId } : {},
|
|
@@ -51962,6 +52243,10 @@ function groupedAlarmPhaseKey(ruleId, trackId) {
|
|
|
51962
52243
|
var NO_MATCH_REPORT_INTERVAL_MS = 6e4;
|
|
51963
52244
|
var DEFAULT_DRAIN_INTERVAL_MS = 2e3;
|
|
51964
52245
|
var DEFAULT_RULE_RELOAD_INTERVAL_MS = 3e4;
|
|
52246
|
+
/** How often open critical/error alerts are reconciled against the notified episodes. */
|
|
52247
|
+
var NC_ALERT_RECONCILE_INTERVAL_MS = 5 * 6e4;
|
|
52248
|
+
/** The most alerts one reconcile reads (the alert centre's own ceiling). */
|
|
52249
|
+
var NC_ALERT_RECONCILE_LIMIT = 5e3;
|
|
51965
52250
|
var DEFAULT_RECONCILE_WINDOW_MS = 15 * 6e4;
|
|
51966
52251
|
/** Overlap subtracted from the watermark on boot (idempotent, cheap). */
|
|
51967
52252
|
var RECONCILE_OVERLAP_MS = 6e4;
|
|
@@ -52534,6 +52819,15 @@ var NotificationCenter = class {
|
|
|
52534
52819
|
* seeds, only a real flip notifies.
|
|
52535
52820
|
*/
|
|
52536
52821
|
liveness;
|
|
52822
|
+
/**
|
|
52823
|
+
* Which alert episodes were already notified (`alert-episode-ledger.ts`): the
|
|
52824
|
+
* alert centre sends nothing of its own, so this is what makes its bus
|
|
52825
|
+
* announcements and the reconcile from the alerts store safe to run together.
|
|
52826
|
+
*/
|
|
52827
|
+
alertEpisodes;
|
|
52828
|
+
/** The boot load of {@link alertEpisodes} has been ATTEMPTED (bus edges before it are the reconcile's). */
|
|
52829
|
+
alertEpisodesLoaded = false;
|
|
52830
|
+
alertReconcileTimer = null;
|
|
52537
52831
|
/** Batches `addon.updated` (version-bearing) into one notification per 30 s. */
|
|
52538
52832
|
addonUpdates;
|
|
52539
52833
|
/** Trailing 5 min hold-off for camera liveness flaps (plugin restart). */
|
|
@@ -53235,6 +53529,10 @@ var NotificationCenter = class {
|
|
|
53235
53529
|
...durableStore,
|
|
53236
53530
|
logger: this.logger.child("liveness")
|
|
53237
53531
|
});
|
|
53532
|
+
this.alertEpisodes = new NcAlertEpisodeLedger({
|
|
53533
|
+
...durableStore,
|
|
53534
|
+
logger: this.logger.child("alert-episodes")
|
|
53535
|
+
});
|
|
53238
53536
|
this.addonUpdates = new AddonUpdateCoalescer((updates) => {
|
|
53239
53537
|
const incoming = incomingFromAddonUpdates({
|
|
53240
53538
|
eventId: `addon-updated:${updates.map((u) => u.packageName).join(",")}`,
|
|
@@ -53631,6 +53929,10 @@ var NotificationCenter = class {
|
|
|
53631
53929
|
this.reloadTimer = setInterval(() => {
|
|
53632
53930
|
this.reloadRules();
|
|
53633
53931
|
}, this.deps.ruleReloadIntervalMs ?? DEFAULT_RULE_RELOAD_INTERVAL_MS);
|
|
53932
|
+
this.alertReconcileTimer = setInterval(() => {
|
|
53933
|
+
this.reconcileAlertEpisodes();
|
|
53934
|
+
}, this.deps.alertReconcileIntervalMs ?? NC_ALERT_RECONCILE_INTERVAL_MS);
|
|
53935
|
+
this.alertReconcileTimer.unref?.();
|
|
53634
53936
|
const timelapseScheduler = this.timelapseScheduler;
|
|
53635
53937
|
if (timelapseScheduler !== null) await timed("timelapseProducer", () => timelapseScheduler.start()).catch((err) => {
|
|
53636
53938
|
this.logger.warn("timelapse producer did not start", { meta: { error: String(err) } });
|
|
@@ -53746,6 +54048,7 @@ var NotificationCenter = class {
|
|
|
53746
54048
|
await this.deviceDirectory.refresh();
|
|
53747
54049
|
})) return;
|
|
53748
54050
|
if (!await phase("liveness", () => this.hydrateLiveness())) return;
|
|
54051
|
+
if (!await phase("alertEpisodes", () => this.hydrateAlertEpisodes())) return;
|
|
53749
54052
|
if (!await phase("deviceAlerts", async () => {
|
|
53750
54053
|
const rows = await this.alertStore.load();
|
|
53751
54054
|
this.logger.info("device alert ledger reseeded", { meta: { rows } });
|
|
@@ -53788,6 +54091,10 @@ var NotificationCenter = class {
|
|
|
53788
54091
|
clearInterval(this.reloadTimer);
|
|
53789
54092
|
this.reloadTimer = null;
|
|
53790
54093
|
}
|
|
54094
|
+
if (this.alertReconcileTimer !== null) {
|
|
54095
|
+
clearInterval(this.alertReconcileTimer);
|
|
54096
|
+
this.alertReconcileTimer = null;
|
|
54097
|
+
}
|
|
53791
54098
|
this.timelapseScheduler?.stop();
|
|
53792
54099
|
this.summaryProducer?.stop();
|
|
53793
54100
|
await this.closeGroupedWindowsOnStop();
|
|
@@ -54799,6 +55106,10 @@ var NotificationCenter = class {
|
|
|
54799
55106
|
return;
|
|
54800
55107
|
}
|
|
54801
55108
|
this.stateSummary.observeRawEvent(event.category, event.data, this.now(), event.source.nodeId);
|
|
55109
|
+
if (event.category === "alert.episode") {
|
|
55110
|
+
if (this.alertEpisodesLoaded) this.admitAlertEpisode(event, "bus");
|
|
55111
|
+
return;
|
|
55112
|
+
}
|
|
54802
55113
|
if (event.category === "addon.started" || event.category === "addon.restarted") return;
|
|
54803
55114
|
if (event.category === "pipeline.camera-assigned") return;
|
|
54804
55115
|
const battery = batterySliceOf(event);
|
|
@@ -54832,10 +55143,6 @@ var NotificationCenter = class {
|
|
|
54832
55143
|
const system = incoming.subject.systemEvent;
|
|
54833
55144
|
if (system !== void 0) this.stateSummary.observeSystem(system, this.now());
|
|
54834
55145
|
if (system !== void 0) this.updateLandings.observe(system, this.now());
|
|
54835
|
-
if (system !== void 0 && system.kind === "addon-crash-loop") {
|
|
54836
|
-
this.logger.debug("addon-crash-loop is pushed by the hub alert centre, not by a rule", { meta: { subject: system.subject } });
|
|
54837
|
-
return;
|
|
54838
|
-
}
|
|
54839
55146
|
const observation = system !== void 0 ? ledgeredLiveness(system) : null;
|
|
54840
55147
|
if (observation !== null) {
|
|
54841
55148
|
const previous = this.liveness.stateOf(observation.subject);
|
|
@@ -57487,6 +57794,89 @@ var NotificationCenter = class {
|
|
|
57487
57794
|
await this.occupancyStore.pruneExcept(this.activeOccupancyKeys());
|
|
57488
57795
|
}
|
|
57489
57796
|
/**
|
|
57797
|
+
* One edge of an alert episode, from the bus or from the reconcile. The
|
|
57798
|
+
* ledger decides whether it is news; only news reaches the rules.
|
|
57799
|
+
*/
|
|
57800
|
+
admitAlertEpisode(event, via) {
|
|
57801
|
+
const payload = require_clip_model_registry.parseAlertEpisodePayload(event.data);
|
|
57802
|
+
if (payload === null) {
|
|
57803
|
+
this.logger.warn("alert episode event with an unreadable payload ignored", { meta: {
|
|
57804
|
+
via,
|
|
57805
|
+
eventId: event.id
|
|
57806
|
+
} });
|
|
57807
|
+
return false;
|
|
57808
|
+
}
|
|
57809
|
+
const verdict = this.alertEpisodes.admit({
|
|
57810
|
+
kind: require_clip_model_registry.alertEpisodeKind(payload.severity, payload.phase),
|
|
57811
|
+
phase: payload.phase,
|
|
57812
|
+
episodeId: payload.episodeId,
|
|
57813
|
+
alertId: payload.alertId
|
|
57814
|
+
}, this.now());
|
|
57815
|
+
const meta = {
|
|
57816
|
+
via,
|
|
57817
|
+
alertId: payload.alertId,
|
|
57818
|
+
episodeId: payload.episodeId,
|
|
57819
|
+
phase: payload.phase,
|
|
57820
|
+
severity: payload.severity,
|
|
57821
|
+
verdict
|
|
57822
|
+
};
|
|
57823
|
+
if (verdict !== "notify") {
|
|
57824
|
+
this.logger.debug("alert episode edge not notified", { meta });
|
|
57825
|
+
return false;
|
|
57826
|
+
}
|
|
57827
|
+
const incoming = incomingFromSystemEvent(event, this.deviceDirectory.lookup());
|
|
57828
|
+
if (incoming === null) return false;
|
|
57829
|
+
this.logger.info("alert episode notified to the rules", { meta });
|
|
57830
|
+
this.consumeEvent(incoming);
|
|
57831
|
+
return true;
|
|
57832
|
+
}
|
|
57833
|
+
async hydrateAlertEpisodes() {
|
|
57834
|
+
try {
|
|
57835
|
+
const rows = await this.alertEpisodes.load();
|
|
57836
|
+
this.logger.info("alert episode ledger reseeded", { meta: { rows } });
|
|
57837
|
+
} finally {
|
|
57838
|
+
this.alertEpisodesLoaded = true;
|
|
57839
|
+
}
|
|
57840
|
+
await this.reconcileAlertEpisodes();
|
|
57841
|
+
}
|
|
57842
|
+
/**
|
|
57843
|
+
* The alerts store against the notified episodes (D11, D49): every OPEN
|
|
57844
|
+
* critical/error alert not yet notified is notified, and every notified
|
|
57845
|
+
* episode whose alert is CLOSED gets its resolution. Never runs on a ledger
|
|
57846
|
+
* the store has not answered — an empty, unread mirror would re-notify every
|
|
57847
|
+
* open alert — and never reads absence as closure: an alert that is not in
|
|
57848
|
+
* the answer is left alone (rows age out by {@link NC_ALERT_EPISODE_MAX_AGE_MS}).
|
|
57849
|
+
*/
|
|
57850
|
+
async reconcileAlertEpisodes() {
|
|
57851
|
+
const read = this.deps.readAlerts;
|
|
57852
|
+
if (read === void 0 || !this.evaluationActive || !this.alertEpisodes.hasAnswered()) return;
|
|
57853
|
+
let alerts;
|
|
57854
|
+
try {
|
|
57855
|
+
alerts = await read();
|
|
57856
|
+
} catch (err) {
|
|
57857
|
+
this.logger.warn("alert episode reconcile could not read the alerts", { meta: { error: require_clip_model_registry.errMsg(err) } });
|
|
57858
|
+
return;
|
|
57859
|
+
}
|
|
57860
|
+
let notified = 0;
|
|
57861
|
+
for (const alert of alerts) {
|
|
57862
|
+
const episodeId = require_clip_model_registry.alertEpisodeId(alert);
|
|
57863
|
+
const open = require_clip_model_registry.isAlertEpisodeOpen(alert.status);
|
|
57864
|
+
if (open && !require_clip_model_registry.isAlertEpisodeSeverity(alert.severity)) continue;
|
|
57865
|
+
if (!open && !this.alertEpisodes.has(episodeId)) continue;
|
|
57866
|
+
const phase = open ? "raised" : "resolved";
|
|
57867
|
+
if (this.admitAlertEpisode(require_clip_model_registry.createEvent(require_clip_model_registry.EventCategory.AlertEpisode, {
|
|
57868
|
+
type: "addon",
|
|
57869
|
+
id: "notification-center"
|
|
57870
|
+
}, require_clip_model_registry.alertEpisodePayload(alert, phase, this.now())), "reconcile")) notified += 1;
|
|
57871
|
+
}
|
|
57872
|
+
const pruned = alerts.length < NC_ALERT_RECONCILE_LIMIT ? await this.alertEpisodes.pruneOlderThan(this.now() - NC_ALERT_EPISODE_MAX_AGE_MS) : 0;
|
|
57873
|
+
if (notified > 0 || pruned > 0) this.logger.info("alert episodes reconciled", { meta: {
|
|
57874
|
+
alerts: alerts.length,
|
|
57875
|
+
notified,
|
|
57876
|
+
pruned
|
|
57877
|
+
} });
|
|
57878
|
+
}
|
|
57879
|
+
/**
|
|
57490
57880
|
* Boot reseed of the durable liveness ledger.
|
|
57491
57881
|
*
|
|
57492
57882
|
* The count is said OUT LOUD at info, because it is the one line that
|
|
@@ -94641,6 +95031,62 @@ function pickWritableMediaLocation(locations, stickyId) {
|
|
|
94641
95031
|
return writable.toSorted((a, b) => b.headroomBytes - a.headroomBytes || a.id.localeCompare(b.id))[0]?.id ?? null;
|
|
94642
95032
|
}
|
|
94643
95033
|
//#endregion
|
|
95034
|
+
//#region src/pipeline-analytics/event-media-write-location.ts
|
|
95035
|
+
var EventMediaWriteLocator = class {
|
|
95036
|
+
deps;
|
|
95037
|
+
cached = null;
|
|
95038
|
+
/**
|
|
95039
|
+
* The last location actually picked. Survives {@link invalidate}, so a ring
|
|
95040
|
+
* keeps writing where it was writing while that stays writable — the
|
|
95041
|
+
* stickiness the class default used to provide (D383).
|
|
95042
|
+
*/
|
|
95043
|
+
lastPick = null;
|
|
95044
|
+
constructor(deps) {
|
|
95045
|
+
this.deps = deps;
|
|
95046
|
+
}
|
|
95047
|
+
/** Drop the cached pick: the next write re-reads the location set. */
|
|
95048
|
+
invalidate() {
|
|
95049
|
+
this.cached = null;
|
|
95050
|
+
}
|
|
95051
|
+
async resolve(logger, deviceId) {
|
|
95052
|
+
if (this.cached !== null) return this.cached;
|
|
95053
|
+
let locations;
|
|
95054
|
+
try {
|
|
95055
|
+
locations = await this.deps.list();
|
|
95056
|
+
} catch (err) {
|
|
95057
|
+
logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: require_clip_model_registry.errMsg(err) } });
|
|
95058
|
+
this.cached = DEFAULT_MEDIA_LOCATION;
|
|
95059
|
+
return DEFAULT_MEDIA_LOCATION;
|
|
95060
|
+
}
|
|
95061
|
+
const picked = pickWritableMediaLocation(locations.map((l) => ({
|
|
95062
|
+
id: l.id,
|
|
95063
|
+
mode: require_clip_model_registry.resolveLocationMode(l),
|
|
95064
|
+
headroomBytes: l.capacity?.availableBytes ?? 0
|
|
95065
|
+
})), this.lastPick ?? void 0);
|
|
95066
|
+
if (picked === null) {
|
|
95067
|
+
const seen = locations.length > 0 ? locations.map((l) => l.id).join(", ") : "(none registered)";
|
|
95068
|
+
logger.error("pipeline-analytics: no writable eventMedia storage location — refusing the write instead of failing far downstream as a filesystem error", {
|
|
95069
|
+
tags: { deviceId },
|
|
95070
|
+
meta: { locationsSeen: seen }
|
|
95071
|
+
});
|
|
95072
|
+
throw new Error(`no writable eventMedia storage location (seen: ${seen})`);
|
|
95073
|
+
}
|
|
95074
|
+
this.cached = picked;
|
|
95075
|
+
this.lastPick = picked;
|
|
95076
|
+
return picked;
|
|
95077
|
+
}
|
|
95078
|
+
};
|
|
95079
|
+
/**
|
|
95080
|
+
* Ring the locator from the location-set doorbell. `onRing` lets the caller drop
|
|
95081
|
+
* its other location-derived caches in the same breath.
|
|
95082
|
+
*/
|
|
95083
|
+
function subscribeEventMediaWriteLocator(eventBus, locator, onRing) {
|
|
95084
|
+
return eventBus.subscribe({ category: require_clip_model_registry.EventCategory.StorageLocationsChanged }, () => {
|
|
95085
|
+
locator.invalidate();
|
|
95086
|
+
onRing();
|
|
95087
|
+
});
|
|
95088
|
+
}
|
|
95089
|
+
//#endregion
|
|
94644
95090
|
//#region src/pipeline-analytics/media-relocate-engine.ts
|
|
94645
95091
|
/**
|
|
94646
95092
|
* Event-media relocation engine (entity-routing spec, Phase 4) — the
|
|
@@ -104634,7 +105080,8 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
|
|
|
104634
105080
|
require_clip_model_registry.EventCategory.SystemRestartCompleted,
|
|
104635
105081
|
require_clip_model_registry.EventCategory.SystemRestarting,
|
|
104636
105082
|
require_clip_model_registry.EventCategory.RecordingExportCompleted,
|
|
104637
|
-
require_clip_model_registry.EventCategory.BackupRunCompleted
|
|
105083
|
+
require_clip_model_registry.EventCategory.BackupRunCompleted,
|
|
105084
|
+
require_clip_model_registry.EventCategory.AlertEpisode
|
|
104638
105085
|
];
|
|
104639
105086
|
/**
|
|
104640
105087
|
* Install the addon's bus subscriptions. Identical set + relative order to the
|
|
@@ -107522,6 +107969,15 @@ Object.freeze({
|
|
|
107522
107969
|
"valueType": "boolean"
|
|
107523
107970
|
}],
|
|
107524
107971
|
"dishwasher": [
|
|
107972
|
+
{
|
|
107973
|
+
"path": "availability",
|
|
107974
|
+
"valueType": "enum",
|
|
107975
|
+
"enumValues": [
|
|
107976
|
+
"available",
|
|
107977
|
+
"offline",
|
|
107978
|
+
"description-missing"
|
|
107979
|
+
]
|
|
107980
|
+
},
|
|
107525
107981
|
{
|
|
107526
107982
|
"path": "state",
|
|
107527
107983
|
"valueType": "enum",
|
|
@@ -107534,12 +107990,35 @@ Object.freeze({
|
|
|
107534
107990
|
"unavailable"
|
|
107535
107991
|
]
|
|
107536
107992
|
},
|
|
107993
|
+
{
|
|
107994
|
+
"path": "operationState",
|
|
107995
|
+
"valueType": "enum",
|
|
107996
|
+
"enumValues": [
|
|
107997
|
+
"Inactive",
|
|
107998
|
+
"Ready",
|
|
107999
|
+
"DelayedStart",
|
|
108000
|
+
"Run",
|
|
108001
|
+
"Pause",
|
|
108002
|
+
"ActionRequired",
|
|
108003
|
+
"Finished",
|
|
108004
|
+
"Error",
|
|
108005
|
+
"Aborting"
|
|
108006
|
+
]
|
|
108007
|
+
},
|
|
107537
108008
|
{
|
|
107538
108009
|
"path": "program",
|
|
107539
108010
|
"valueType": "string"
|
|
107540
108011
|
},
|
|
107541
108012
|
{
|
|
107542
|
-
"path": "
|
|
108013
|
+
"path": "selectedProgram",
|
|
108014
|
+
"valueType": "string"
|
|
108015
|
+
},
|
|
108016
|
+
{
|
|
108017
|
+
"path": "remainingSeconds",
|
|
108018
|
+
"valueType": "number"
|
|
108019
|
+
},
|
|
108020
|
+
{
|
|
108021
|
+
"path": "delayedStartSeconds",
|
|
107543
108022
|
"valueType": "number"
|
|
107544
108023
|
},
|
|
107545
108024
|
{
|
|
@@ -107550,14 +108029,30 @@ Object.freeze({
|
|
|
107550
108029
|
"path": "doorOpen",
|
|
107551
108030
|
"valueType": "boolean"
|
|
107552
108031
|
},
|
|
108032
|
+
{
|
|
108033
|
+
"path": "doorLocked",
|
|
108034
|
+
"valueType": "boolean"
|
|
108035
|
+
},
|
|
107553
108036
|
{
|
|
107554
108037
|
"path": "remoteStartAllowed",
|
|
107555
108038
|
"valueType": "boolean"
|
|
107556
108039
|
},
|
|
108040
|
+
{
|
|
108041
|
+
"path": "remoteControlActive",
|
|
108042
|
+
"valueType": "boolean"
|
|
108043
|
+
},
|
|
108044
|
+
{
|
|
108045
|
+
"path": "localControlActive",
|
|
108046
|
+
"valueType": "boolean"
|
|
108047
|
+
},
|
|
107557
108048
|
{
|
|
107558
108049
|
"path": "powerOn",
|
|
107559
108050
|
"valueType": "boolean"
|
|
107560
108051
|
},
|
|
108052
|
+
{
|
|
108053
|
+
"path": "powerState",
|
|
108054
|
+
"valueType": "string"
|
|
108055
|
+
},
|
|
107561
108056
|
{
|
|
107562
108057
|
"path": "saltLevel",
|
|
107563
108058
|
"valueType": "enum",
|
|
@@ -111141,20 +111636,6 @@ var MigrationMarkerStore = class MigrationMarkerStore {
|
|
|
111141
111636
|
};
|
|
111142
111637
|
};
|
|
111143
111638
|
//#endregion
|
|
111144
|
-
//#region src/pipeline-analytics/store/analytics-history-keys.ts
|
|
111145
|
-
/** Declared addon series keys (D850). */
|
|
111146
|
-
var ANALYTICS_HISTORY_BUCKET_SERIES = {
|
|
111147
|
-
motion: "addon:pipeline-analytics/motion",
|
|
111148
|
-
"audio-dbfs": "addon:pipeline-analytics/audio-dbfs",
|
|
111149
|
-
occupancy: "addon:pipeline-analytics/occupancy"
|
|
111150
|
-
};
|
|
111151
|
-
var ANALYTICS_AUDIO_LEVEL_KEY = "addon:pipeline-analytics/audio-level";
|
|
111152
|
-
/** Map the public cap enum to the device-history key (read path). */
|
|
111153
|
-
function historyKeyForLtsSeries(series) {
|
|
111154
|
-
if (series === "battery") return "cap:battery.percentage";
|
|
111155
|
-
return ANALYTICS_HISTORY_BUCKET_SERIES[series];
|
|
111156
|
-
}
|
|
111157
|
-
//#endregion
|
|
111158
111639
|
//#region src/pipeline-analytics/store/analytics-history-producer.ts
|
|
111159
111640
|
var BUCKET_SERIES_META = [
|
|
111160
111641
|
{
|
|
@@ -111176,6 +111657,10 @@ var AUDIO_LEVEL_SERIES = {
|
|
|
111176
111657
|
displayName: "Audio level (dBFS)",
|
|
111177
111658
|
unit: "dBFS"
|
|
111178
111659
|
};
|
|
111660
|
+
/** One audio level point per window; equals device-history's 1 point / 10 s record bound. */
|
|
111661
|
+
var AUDIO_LEVEL_WINDOW_MS = 1e4;
|
|
111662
|
+
/** Stored for a measured-silent window (−∞ dBFS cannot be a history value). */
|
|
111663
|
+
var AUDIO_LEVEL_SILENT_DBFS = -120;
|
|
111179
111664
|
function refusalCounts(result) {
|
|
111180
111665
|
const counts = {};
|
|
111181
111666
|
for (const r of result.refused) counts[r.reason] = (counts[r.reason] ?? 0) + r.count;
|
|
@@ -111187,6 +111672,7 @@ var AnalyticsHistoryProducer = class {
|
|
|
111187
111672
|
logger;
|
|
111188
111673
|
nowFn;
|
|
111189
111674
|
declared = false;
|
|
111675
|
+
levelWindows = /* @__PURE__ */ new Map();
|
|
111190
111676
|
constructor(deps) {
|
|
111191
111677
|
this.deviceHistory = deps.deviceHistory;
|
|
111192
111678
|
this.logger = deps.logger;
|
|
@@ -111219,6 +111705,43 @@ var AnalyticsHistoryProducer = class {
|
|
|
111219
111705
|
deviceId
|
|
111220
111706
|
}, scope ?? "", value, at ?? this.nowFn());
|
|
111221
111707
|
}
|
|
111708
|
+
/**
|
|
111709
|
+
* Feed EVERY analysed audio sample. One point per window of
|
|
111710
|
+
* {@link AUDIO_LEVEL_WINDOW_MS} is written, stamped at the window's first
|
|
111711
|
+
* sample, carrying the window's PEAK dBFS: a 2 s spike inside a stable window
|
|
111712
|
+
* is not averaged away, and stable audio still yields a point each window
|
|
111713
|
+
* (continuity over dedup — the level bar draws a continuous line).
|
|
111714
|
+
*
|
|
111715
|
+
* Measured silence (−∞) is stored at {@link AUDIO_LEVEL_SILENT_DBFS}: it is a
|
|
111716
|
+
* measurement, not an absence (D431); NaN carries no claim and is dropped.
|
|
111717
|
+
* A window closes on the first sample past its end, so no timer is needed and
|
|
111718
|
+
* points are always at least a window apart (the history's 1 point / 10 s
|
|
111719
|
+
* bound).
|
|
111720
|
+
*/
|
|
111721
|
+
observeAudioLevel(deviceId, dbfs, at) {
|
|
111722
|
+
if (Number.isNaN(dbfs)) return;
|
|
111723
|
+
const value = Number.isFinite(dbfs) ? dbfs : AUDIO_LEVEL_SILENT_DBFS;
|
|
111724
|
+
const win = this.levelWindows.get(deviceId);
|
|
111725
|
+
if (win === void 0) {
|
|
111726
|
+
this.levelWindows.set(deviceId, {
|
|
111727
|
+
start: at,
|
|
111728
|
+
peak: value
|
|
111729
|
+
});
|
|
111730
|
+
return;
|
|
111731
|
+
}
|
|
111732
|
+
if (at - win.start < AUDIO_LEVEL_WINDOW_MS) {
|
|
111733
|
+
if (value > win.peak) this.levelWindows.set(deviceId, {
|
|
111734
|
+
start: win.start,
|
|
111735
|
+
peak: value
|
|
111736
|
+
});
|
|
111737
|
+
return;
|
|
111738
|
+
}
|
|
111739
|
+
this.levelWindows.set(deviceId, {
|
|
111740
|
+
start: at,
|
|
111741
|
+
peak: value
|
|
111742
|
+
});
|
|
111743
|
+
this.recordAudioLevel(deviceId, win.peak, win.start);
|
|
111744
|
+
}
|
|
111222
111745
|
recordAudioLevel(deviceId, dbfs, at) {
|
|
111223
111746
|
const record = this.deviceHistory.record;
|
|
111224
111747
|
if (record === void 0) return;
|
|
@@ -113879,20 +114402,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
113879
114402
|
mediaRelocate = null;
|
|
113880
114403
|
mediaLocationStorage = null;
|
|
113881
114404
|
/**
|
|
113882
|
-
*
|
|
113883
|
-
*
|
|
113884
|
-
* `
|
|
113885
|
-
*
|
|
113886
|
-
* `enabled` flags only change via operator reconfig.
|
|
113887
|
-
*/
|
|
113888
|
-
eventMediaWriteLocationCache = null;
|
|
113889
|
-
/**
|
|
113890
|
-
* The last location this process actually picked for an `eventMedia` write.
|
|
113891
|
-
* Survives the cache invalidation a migration performs, so a refresh keeps
|
|
113892
|
-
* writing where it was writing while that stays writable — the stickiness the
|
|
113893
|
-
* class default used to provide before it was deleted (D383).
|
|
114405
|
+
* Where NEW `eventMedia` writes land. Its pick is cached per location-set
|
|
114406
|
+
* generation and dropped by the `StorageLocationsChanged` doorbell (D387) and
|
|
114407
|
+
* by `refreshStorageLocationsForMigration`, so a mode change or a repoint takes
|
|
114408
|
+
* effect without a restart.
|
|
113894
114409
|
*/
|
|
113895
|
-
|
|
114410
|
+
eventMediaWriteLocator = null;
|
|
114411
|
+
eventMediaLocationDoorbellUnsub = null;
|
|
113896
114412
|
mediaWriteGate = new MediaWriteGate();
|
|
113897
114413
|
storageMigrationLeaseId = null;
|
|
113898
114414
|
/** Per-camera history of LINKED-device sensor state changes (Part B). */
|
|
@@ -115932,33 +116448,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
115932
116448
|
}
|
|
115933
116449
|
/** One line per process: this repeats on every occupancy refresh otherwise. */
|
|
115934
116450
|
mediaOccupancyUnattributedLogged = false;
|
|
115935
|
-
|
|
115936
|
-
|
|
115937
|
-
|
|
115938
|
-
let locations;
|
|
115939
|
-
try {
|
|
115940
|
-
locations = await api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION });
|
|
115941
|
-
} catch (err) {
|
|
115942
|
-
logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: require_clip_model_registry.errMsg(err) } });
|
|
115943
|
-
this.eventMediaWriteLocationCache = DEFAULT_MEDIA_LOCATION;
|
|
115944
|
-
return DEFAULT_MEDIA_LOCATION;
|
|
115945
|
-
}
|
|
115946
|
-
const picked = pickWritableMediaLocation(locations.map((l) => ({
|
|
115947
|
-
id: l.id,
|
|
115948
|
-
mode: require_clip_model_registry.resolveLocationMode(l),
|
|
115949
|
-
headroomBytes: l.capacity?.availableBytes ?? 0
|
|
115950
|
-
})), this.lastEventMediaWritePick ?? void 0);
|
|
115951
|
-
if (picked === null) {
|
|
115952
|
-
const seen = locations.length > 0 ? locations.map((l) => l.id).join(", ") : "(none registered)";
|
|
115953
|
-
logger.error("pipeline-analytics: no writable eventMedia storage location — refusing the write instead of failing far downstream as a filesystem error", {
|
|
115954
|
-
tags: { deviceId },
|
|
115955
|
-
meta: { locationsSeen: seen }
|
|
115956
|
-
});
|
|
115957
|
-
throw new Error(`no writable eventMedia storage location (seen: ${seen})`);
|
|
115958
|
-
}
|
|
115959
|
-
this.eventMediaWriteLocationCache = picked;
|
|
115960
|
-
this.lastEventMediaWritePick = picked;
|
|
115961
|
-
return picked;
|
|
116451
|
+
resolveEventMediaWriteLocation(api, logger, deviceId) {
|
|
116452
|
+
this.eventMediaWriteLocator ??= new EventMediaWriteLocator({ list: () => api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION }) });
|
|
116453
|
+
return this.eventMediaWriteLocator.resolve(logger, deviceId);
|
|
115962
116454
|
}
|
|
115963
116455
|
/** Constructs every SQLite-backed store plus the stationary/package-drop/
|
|
115964
116456
|
* sensor plumbing, in the exact pre-S7 order. Returns the non-null bundle
|
|
@@ -116059,6 +116551,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
116059
116551
|
resolveEventMediaLocation: (deviceId) => this.resolveEventMediaWriteLocation(api, logger, deviceId)
|
|
116060
116552
|
});
|
|
116061
116553
|
this.mediaStore = mediaStore;
|
|
116554
|
+
this.eventMediaWriteLocator ??= new EventMediaWriteLocator({ list: () => api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION }) });
|
|
116555
|
+
this.eventMediaLocationDoorbellUnsub?.();
|
|
116556
|
+
this.eventMediaLocationDoorbellUnsub = subscribeEventMediaWriteLocator(this.ctx.eventBus, this.eventMediaWriteLocator, () => this.mediaLocationStorage?.invalidateRoots());
|
|
116062
116557
|
const eventLedgerWiring = this.eventLedgerWiring(api.settingsStore);
|
|
116063
116558
|
const retrainFrames = new RetrainFrameStore({
|
|
116064
116559
|
store: api.settingsStore,
|
|
@@ -116209,6 +116704,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
116209
116704
|
const eventStore = new EventStore({
|
|
116210
116705
|
store: api.settingsStore,
|
|
116211
116706
|
logger: logger.child("EventStore"),
|
|
116707
|
+
audioLevelHistory: (input) => api.deviceHistory.query.query(input),
|
|
116212
116708
|
motionLedger: wireMotionEventLedger(eventLedgerWiring),
|
|
116213
116709
|
audioLedger: wireAudioEventLedger(eventLedgerWiring),
|
|
116214
116710
|
onMotionSample: (deviceId, regionCount, at) => lts.note("motion", deviceId, regionCount, at),
|
|
@@ -116641,6 +117137,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
116641
117137
|
coreBlocks: async () => (await api.coreBlocks.list.query({})).blocks,
|
|
116642
117138
|
nodeAddons: (nodeId) => api.nodes.getNodeAddons.query({ nodeId })
|
|
116643
117139
|
},
|
|
117140
|
+
readAlerts: () => api.alerts.list.query({ limit: 5e3 }),
|
|
116644
117141
|
getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
|
|
116645
117142
|
recognitionOutlook: ({ deviceId, trackId, className, labelKind, now }) => recognitionOutlook({
|
|
116646
117143
|
className,
|
|
@@ -118654,6 +119151,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
118654
119151
|
this.alarmPanelConvergence?.stop();
|
|
118655
119152
|
this.alarmPanelConvergence = null;
|
|
118656
119153
|
for (const unsub of this.subscriptionUnsubs) unsub();
|
|
119154
|
+
this.eventMediaLocationDoorbellUnsub?.();
|
|
119155
|
+
this.eventMediaLocationDoorbellUnsub = null;
|
|
118657
119156
|
this.subscriptionUnsubs = [];
|
|
118658
119157
|
await Promise.all([this.drainSensorHandlers(), this.motionEpisodes?.closeAll("addon-shutdown")]);
|
|
118659
119158
|
await this.settleTrackFusions();
|
|
@@ -121681,6 +122180,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
121681
122180
|
score: topClassification.score
|
|
121682
122181
|
} : {}
|
|
121683
122182
|
}, this.audioMarkerConfig(settings));
|
|
122183
|
+
if (level !== void 0) this.lts?.observeAudioLevel(deviceId, level.dbfs, timestamp);
|
|
121684
122184
|
this.notificationCenter?.observeAudio(deviceId, ncAudioSampleFromFrame(frame, timestamp, settings));
|
|
121685
122185
|
const route = classifyAudioFrame(topClassification ? {
|
|
121686
122186
|
className: topClassification.macroClass,
|
|
@@ -121756,8 +122256,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
121756
122256
|
});
|
|
121757
122257
|
this.levelStateByDevice.set(deviceId, res.nextState);
|
|
121758
122258
|
if (!res.emit) return;
|
|
121759
|
-
const dbfs = level?.dbfs ?? Number.NEGATIVE_INFINITY;
|
|
121760
|
-
this.lts?.recordAudioLevel(deviceId, dbfs, timestamp);
|
|
121761
122259
|
}
|
|
121762
122260
|
/**
|
|
121763
122261
|
* Persist motion-analysis bus events into the EPISODE tracker (D475): a
|
|
@@ -125908,7 +126406,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
125908
126406
|
async refreshStorageLocationsForMigration(input) {
|
|
125909
126407
|
if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
|
|
125910
126408
|
this.mediaLocationStorage?.invalidateRoots();
|
|
125911
|
-
this.
|
|
126409
|
+
this.eventMediaWriteLocator?.invalidate();
|
|
125912
126410
|
return { refreshed: true };
|
|
125913
126411
|
}
|
|
125914
126412
|
/**
|
|
@@ -126271,7 +126769,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
126271
126769
|
async startStorageMigrationMove(input) {
|
|
126272
126770
|
if (!this.mediaRelocate) throw new Error("media relocation unavailable");
|
|
126273
126771
|
if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
|
|
126274
|
-
return { jobId: this.
|
|
126772
|
+
return { jobId: await this.startMediaMover(input) };
|
|
126773
|
+
}
|
|
126774
|
+
/**
|
|
126775
|
+
* Start a media move only while no single-location repoint holds an overlapping
|
|
126776
|
+
* physical root (the storage orchestrator owns that claim). A shared recordings
|
|
126777
|
+
* root is the case: a repoint of another class there must not run beside us.
|
|
126778
|
+
*/
|
|
126779
|
+
startMediaMover(input) {
|
|
126780
|
+
const engine = this.mediaRelocate;
|
|
126781
|
+
if (!engine) throw new Error("media relocation unavailable");
|
|
126782
|
+
return require_clip_model_registry.startMoverUnderClearance({
|
|
126783
|
+
clearance: (locationIds) => this.ctx.api.storageMigration.moverClearance.query({ locationIds }),
|
|
126784
|
+
start: () => engine.start(input),
|
|
126785
|
+
cancel: (jobId) => {
|
|
126786
|
+
engine.cancel(jobId);
|
|
126787
|
+
},
|
|
126788
|
+
locationIds: [input.fromLocationId ?? "*", input.toLocationId]
|
|
126789
|
+
});
|
|
126275
126790
|
}
|
|
126276
126791
|
async getStorageMigrationMoveStatus(input) {
|
|
126277
126792
|
return this.mediaRelocate?.list().find((job) => job.jobId === input.jobId) ?? null;
|
|
@@ -126289,7 +126804,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
126289
126804
|
*/
|
|
126290
126805
|
async relocateMedia(input) {
|
|
126291
126806
|
if (!this.mediaRelocate) throw new Error("media relocation unavailable");
|
|
126292
|
-
const jobId = this.
|
|
126807
|
+
const jobId = await this.startMediaMover(input);
|
|
126293
126808
|
this.ctx.logger.info("pipeline-analytics: media relocate job enqueued", { meta: {
|
|
126294
126809
|
jobId,
|
|
126295
126810
|
mode: input.mode ?? "move",
|