@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.
Files changed (15) hide show
  1. package/dist/{clip-model-registry-VgOXFRFr.js → clip-model-registry-B11yS32V.js} +472 -47
  2. package/dist/{clip-model-registry-CTfe5v5_.mjs → clip-model-registry-BItfTZEx.mjs} +431 -48
  3. package/dist/embedding-encoder/index.js +1 -1
  4. package/dist/embedding-encoder/index.mjs +1 -1
  5. package/dist/pipeline-analytics/_stub.js +2 -2
  6. 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
  7. 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
  8. 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
  9. package/dist/pipeline-analytics/{hostInit-B8mC_3oV.mjs → hostInit-Cko0y33G.mjs} +3 -3
  10. package/dist/pipeline-analytics/index.js +457 -51
  11. package/dist/pipeline-analytics/index.mjs +457 -51
  12. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  13. package/package.json +1 -1
  14. 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
  15. 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-VgOXFRFr.js");
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);
@@ -32881,6 +33063,12 @@ var en_default = {
32881
33063
  "system.backup-completed.title": "Backup completed",
32882
33064
  "system.backup-completed.body": "Backup saved to {{destinationsOk}} of {{destinationsTotal}} destinations ({{sizeMB}} MB)",
32883
33065
  "system.backup-completed.body.partial": "Backup saved to {{destinationsOk}} of {{destinationsTotal}} destinations — {{destinationsFailed}} failed",
33066
+ "system.alert-critical.title": "{{title}}",
33067
+ "system.alert-critical.body": "{{message}}",
33068
+ "system.alert-error.title": "{{title}}",
33069
+ "system.alert-error.body": "{{message}}",
33070
+ "system.alert-critical-resolved.title": "Resolved: {{title}}",
33071
+ "system.alert-critical-resolved.body": "{{message}}",
32884
33072
  "system.device-consumable-low.title": "Consumable low — {{deviceName}}",
32885
33073
  "system.device-consumable-low.body": "{{item}} on {{deviceName}} is at {{percentage}}%",
32886
33074
  "system.device-consumable-low.body.unmeasured": "{{item}} on {{deviceName}} needs replacing",
@@ -33261,6 +33449,12 @@ var it_default = {
33261
33449
  "system.backup-completed.title": "Backup completato",
33262
33450
  "system.backup-completed.body": "Backup salvato su {{destinationsOk}} destinazioni su {{destinationsTotal}} ({{sizeMB}} MB)",
33263
33451
  "system.backup-completed.body.partial": "Backup salvato su {{destinationsOk}} destinazioni su {{destinationsTotal}} — {{destinationsFailed}} fallite",
33452
+ "system.alert-critical.title": "{{title}}",
33453
+ "system.alert-critical.body": "{{message}}",
33454
+ "system.alert-error.title": "{{title}}",
33455
+ "system.alert-error.body": "{{message}}",
33456
+ "system.alert-critical-resolved.title": "Risolto: {{title}}",
33457
+ "system.alert-critical-resolved.body": "{{message}}",
33264
33458
  "system.device-consumable-low.title": "Consumabile basso — {{deviceName}}",
33265
33459
  "system.device-consumable-low.body": "{{item}} su {{deviceName}} è al {{percentage}}%",
33266
33460
  "system.device-consumable-low.body.unmeasured": "{{item}} su {{deviceName}} va sostituito",
@@ -35236,6 +35430,7 @@ function systemEventSubject(kind, scope) {
35236
35430
  if (kind === "node-updating") return `node-updating:${scope.nodeId ?? "hub"}:1.0.1`;
35237
35431
  if (kind === "export-completed") return `export:${scope.deviceId ?? 0}`;
35238
35432
  if (kind === "backup-completed") return "backup:test";
35433
+ if (kind.startsWith("alert-")) return "alert:test";
35239
35434
  return `alarm:${scope.deviceId ?? 0}`;
35240
35435
  }
35241
35436
  /**
@@ -35376,7 +35571,12 @@ function planRuleTest(input) {
35376
35571
  nodes: "hub",
35377
35572
  fromVersion: "1.0.0",
35378
35573
  toVersion: "1.0.1",
35379
- title: "Export",
35574
+ title: systemEventKind.startsWith("alert-") ? "Recording location full" : "Export",
35575
+ message: "No free space left on the recording location",
35576
+ severity: systemEventKind === "alert-error" ? "error" : "critical",
35577
+ category: "recorder.placement",
35578
+ source: "recorder",
35579
+ actionUrl: "/system/addons",
35380
35580
  count: "1"
35381
35581
  },
35382
35582
  ...systemScope.deviceId !== void 0 ? { deviceId: systemScope.deviceId } : {},
@@ -51962,6 +52162,10 @@ function groupedAlarmPhaseKey(ruleId, trackId) {
51962
52162
  var NO_MATCH_REPORT_INTERVAL_MS = 6e4;
51963
52163
  var DEFAULT_DRAIN_INTERVAL_MS = 2e3;
51964
52164
  var DEFAULT_RULE_RELOAD_INTERVAL_MS = 3e4;
52165
+ /** How often open critical/error alerts are reconciled against the notified episodes. */
52166
+ var NC_ALERT_RECONCILE_INTERVAL_MS = 5 * 6e4;
52167
+ /** The most alerts one reconcile reads (the alert centre's own ceiling). */
52168
+ var NC_ALERT_RECONCILE_LIMIT = 5e3;
51965
52169
  var DEFAULT_RECONCILE_WINDOW_MS = 15 * 6e4;
51966
52170
  /** Overlap subtracted from the watermark on boot (idempotent, cheap). */
51967
52171
  var RECONCILE_OVERLAP_MS = 6e4;
@@ -52534,6 +52738,15 @@ var NotificationCenter = class {
52534
52738
  * seeds, only a real flip notifies.
52535
52739
  */
52536
52740
  liveness;
52741
+ /**
52742
+ * Which alert episodes were already notified (`alert-episode-ledger.ts`): the
52743
+ * alert centre sends nothing of its own, so this is what makes its bus
52744
+ * announcements and the reconcile from the alerts store safe to run together.
52745
+ */
52746
+ alertEpisodes;
52747
+ /** The boot load of {@link alertEpisodes} has been ATTEMPTED (bus edges before it are the reconcile's). */
52748
+ alertEpisodesLoaded = false;
52749
+ alertReconcileTimer = null;
52537
52750
  /** Batches `addon.updated` (version-bearing) into one notification per 30 s. */
52538
52751
  addonUpdates;
52539
52752
  /** Trailing 5 min hold-off for camera liveness flaps (plugin restart). */
@@ -53235,6 +53448,10 @@ var NotificationCenter = class {
53235
53448
  ...durableStore,
53236
53449
  logger: this.logger.child("liveness")
53237
53450
  });
53451
+ this.alertEpisodes = new NcAlertEpisodeLedger({
53452
+ ...durableStore,
53453
+ logger: this.logger.child("alert-episodes")
53454
+ });
53238
53455
  this.addonUpdates = new AddonUpdateCoalescer((updates) => {
53239
53456
  const incoming = incomingFromAddonUpdates({
53240
53457
  eventId: `addon-updated:${updates.map((u) => u.packageName).join(",")}`,
@@ -53631,6 +53848,10 @@ var NotificationCenter = class {
53631
53848
  this.reloadTimer = setInterval(() => {
53632
53849
  this.reloadRules();
53633
53850
  }, this.deps.ruleReloadIntervalMs ?? DEFAULT_RULE_RELOAD_INTERVAL_MS);
53851
+ this.alertReconcileTimer = setInterval(() => {
53852
+ this.reconcileAlertEpisodes();
53853
+ }, this.deps.alertReconcileIntervalMs ?? NC_ALERT_RECONCILE_INTERVAL_MS);
53854
+ this.alertReconcileTimer.unref?.();
53634
53855
  const timelapseScheduler = this.timelapseScheduler;
53635
53856
  if (timelapseScheduler !== null) await timed("timelapseProducer", () => timelapseScheduler.start()).catch((err) => {
53636
53857
  this.logger.warn("timelapse producer did not start", { meta: { error: String(err) } });
@@ -53746,6 +53967,7 @@ var NotificationCenter = class {
53746
53967
  await this.deviceDirectory.refresh();
53747
53968
  })) return;
53748
53969
  if (!await phase("liveness", () => this.hydrateLiveness())) return;
53970
+ if (!await phase("alertEpisodes", () => this.hydrateAlertEpisodes())) return;
53749
53971
  if (!await phase("deviceAlerts", async () => {
53750
53972
  const rows = await this.alertStore.load();
53751
53973
  this.logger.info("device alert ledger reseeded", { meta: { rows } });
@@ -53788,6 +54010,10 @@ var NotificationCenter = class {
53788
54010
  clearInterval(this.reloadTimer);
53789
54011
  this.reloadTimer = null;
53790
54012
  }
54013
+ if (this.alertReconcileTimer !== null) {
54014
+ clearInterval(this.alertReconcileTimer);
54015
+ this.alertReconcileTimer = null;
54016
+ }
53791
54017
  this.timelapseScheduler?.stop();
53792
54018
  this.summaryProducer?.stop();
53793
54019
  await this.closeGroupedWindowsOnStop();
@@ -54799,6 +55025,10 @@ var NotificationCenter = class {
54799
55025
  return;
54800
55026
  }
54801
55027
  this.stateSummary.observeRawEvent(event.category, event.data, this.now(), event.source.nodeId);
55028
+ if (event.category === "alert.episode") {
55029
+ if (this.alertEpisodesLoaded) this.admitAlertEpisode(event, "bus");
55030
+ return;
55031
+ }
54802
55032
  if (event.category === "addon.started" || event.category === "addon.restarted") return;
54803
55033
  if (event.category === "pipeline.camera-assigned") return;
54804
55034
  const battery = batterySliceOf(event);
@@ -54832,10 +55062,6 @@ var NotificationCenter = class {
54832
55062
  const system = incoming.subject.systemEvent;
54833
55063
  if (system !== void 0) this.stateSummary.observeSystem(system, this.now());
54834
55064
  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
55065
  const observation = system !== void 0 ? ledgeredLiveness(system) : null;
54840
55066
  if (observation !== null) {
54841
55067
  const previous = this.liveness.stateOf(observation.subject);
@@ -57487,6 +57713,89 @@ var NotificationCenter = class {
57487
57713
  await this.occupancyStore.pruneExcept(this.activeOccupancyKeys());
57488
57714
  }
57489
57715
  /**
57716
+ * One edge of an alert episode, from the bus or from the reconcile. The
57717
+ * ledger decides whether it is news; only news reaches the rules.
57718
+ */
57719
+ admitAlertEpisode(event, via) {
57720
+ const payload = require_clip_model_registry.parseAlertEpisodePayload(event.data);
57721
+ if (payload === null) {
57722
+ this.logger.warn("alert episode event with an unreadable payload ignored", { meta: {
57723
+ via,
57724
+ eventId: event.id
57725
+ } });
57726
+ return false;
57727
+ }
57728
+ const verdict = this.alertEpisodes.admit({
57729
+ kind: require_clip_model_registry.alertEpisodeKind(payload.severity, payload.phase),
57730
+ phase: payload.phase,
57731
+ episodeId: payload.episodeId,
57732
+ alertId: payload.alertId
57733
+ }, this.now());
57734
+ const meta = {
57735
+ via,
57736
+ alertId: payload.alertId,
57737
+ episodeId: payload.episodeId,
57738
+ phase: payload.phase,
57739
+ severity: payload.severity,
57740
+ verdict
57741
+ };
57742
+ if (verdict !== "notify") {
57743
+ this.logger.debug("alert episode edge not notified", { meta });
57744
+ return false;
57745
+ }
57746
+ const incoming = incomingFromSystemEvent(event, this.deviceDirectory.lookup());
57747
+ if (incoming === null) return false;
57748
+ this.logger.info("alert episode notified to the rules", { meta });
57749
+ this.consumeEvent(incoming);
57750
+ return true;
57751
+ }
57752
+ async hydrateAlertEpisodes() {
57753
+ try {
57754
+ const rows = await this.alertEpisodes.load();
57755
+ this.logger.info("alert episode ledger reseeded", { meta: { rows } });
57756
+ } finally {
57757
+ this.alertEpisodesLoaded = true;
57758
+ }
57759
+ await this.reconcileAlertEpisodes();
57760
+ }
57761
+ /**
57762
+ * The alerts store against the notified episodes (D11, D49): every OPEN
57763
+ * critical/error alert not yet notified is notified, and every notified
57764
+ * episode whose alert is CLOSED gets its resolution. Never runs on a ledger
57765
+ * the store has not answered — an empty, unread mirror would re-notify every
57766
+ * open alert — and never reads absence as closure: an alert that is not in
57767
+ * the answer is left alone (rows age out by {@link NC_ALERT_EPISODE_MAX_AGE_MS}).
57768
+ */
57769
+ async reconcileAlertEpisodes() {
57770
+ const read = this.deps.readAlerts;
57771
+ if (read === void 0 || !this.evaluationActive || !this.alertEpisodes.hasAnswered()) return;
57772
+ let alerts;
57773
+ try {
57774
+ alerts = await read();
57775
+ } catch (err) {
57776
+ this.logger.warn("alert episode reconcile could not read the alerts", { meta: { error: require_clip_model_registry.errMsg(err) } });
57777
+ return;
57778
+ }
57779
+ let notified = 0;
57780
+ for (const alert of alerts) {
57781
+ const episodeId = require_clip_model_registry.alertEpisodeId(alert);
57782
+ const open = require_clip_model_registry.isAlertEpisodeOpen(alert.status);
57783
+ if (open && !require_clip_model_registry.isAlertEpisodeSeverity(alert.severity)) continue;
57784
+ if (!open && !this.alertEpisodes.has(episodeId)) continue;
57785
+ const phase = open ? "raised" : "resolved";
57786
+ if (this.admitAlertEpisode(require_clip_model_registry.createEvent(require_clip_model_registry.EventCategory.AlertEpisode, {
57787
+ type: "addon",
57788
+ id: "notification-center"
57789
+ }, require_clip_model_registry.alertEpisodePayload(alert, phase, this.now())), "reconcile")) notified += 1;
57790
+ }
57791
+ const pruned = alerts.length < NC_ALERT_RECONCILE_LIMIT ? await this.alertEpisodes.pruneOlderThan(this.now() - NC_ALERT_EPISODE_MAX_AGE_MS) : 0;
57792
+ if (notified > 0 || pruned > 0) this.logger.info("alert episodes reconciled", { meta: {
57793
+ alerts: alerts.length,
57794
+ notified,
57795
+ pruned
57796
+ } });
57797
+ }
57798
+ /**
57490
57799
  * Boot reseed of the durable liveness ledger.
57491
57800
  *
57492
57801
  * The count is said OUT LOUD at info, because it is the one line that
@@ -94641,6 +94950,62 @@ function pickWritableMediaLocation(locations, stickyId) {
94641
94950
  return writable.toSorted((a, b) => b.headroomBytes - a.headroomBytes || a.id.localeCompare(b.id))[0]?.id ?? null;
94642
94951
  }
94643
94952
  //#endregion
94953
+ //#region src/pipeline-analytics/event-media-write-location.ts
94954
+ var EventMediaWriteLocator = class {
94955
+ deps;
94956
+ cached = null;
94957
+ /**
94958
+ * The last location actually picked. Survives {@link invalidate}, so a ring
94959
+ * keeps writing where it was writing while that stays writable — the
94960
+ * stickiness the class default used to provide (D383).
94961
+ */
94962
+ lastPick = null;
94963
+ constructor(deps) {
94964
+ this.deps = deps;
94965
+ }
94966
+ /** Drop the cached pick: the next write re-reads the location set. */
94967
+ invalidate() {
94968
+ this.cached = null;
94969
+ }
94970
+ async resolve(logger, deviceId) {
94971
+ if (this.cached !== null) return this.cached;
94972
+ let locations;
94973
+ try {
94974
+ locations = await this.deps.list();
94975
+ } catch (err) {
94976
+ logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: require_clip_model_registry.errMsg(err) } });
94977
+ this.cached = DEFAULT_MEDIA_LOCATION;
94978
+ return DEFAULT_MEDIA_LOCATION;
94979
+ }
94980
+ const picked = pickWritableMediaLocation(locations.map((l) => ({
94981
+ id: l.id,
94982
+ mode: require_clip_model_registry.resolveLocationMode(l),
94983
+ headroomBytes: l.capacity?.availableBytes ?? 0
94984
+ })), this.lastPick ?? void 0);
94985
+ if (picked === null) {
94986
+ const seen = locations.length > 0 ? locations.map((l) => l.id).join(", ") : "(none registered)";
94987
+ logger.error("pipeline-analytics: no writable eventMedia storage location — refusing the write instead of failing far downstream as a filesystem error", {
94988
+ tags: { deviceId },
94989
+ meta: { locationsSeen: seen }
94990
+ });
94991
+ throw new Error(`no writable eventMedia storage location (seen: ${seen})`);
94992
+ }
94993
+ this.cached = picked;
94994
+ this.lastPick = picked;
94995
+ return picked;
94996
+ }
94997
+ };
94998
+ /**
94999
+ * Ring the locator from the location-set doorbell. `onRing` lets the caller drop
95000
+ * its other location-derived caches in the same breath.
95001
+ */
95002
+ function subscribeEventMediaWriteLocator(eventBus, locator, onRing) {
95003
+ return eventBus.subscribe({ category: require_clip_model_registry.EventCategory.StorageLocationsChanged }, () => {
95004
+ locator.invalidate();
95005
+ onRing();
95006
+ });
95007
+ }
95008
+ //#endregion
94644
95009
  //#region src/pipeline-analytics/media-relocate-engine.ts
94645
95010
  /**
94646
95011
  * Event-media relocation engine (entity-routing spec, Phase 4) — the
@@ -104634,7 +104999,8 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
104634
104999
  require_clip_model_registry.EventCategory.SystemRestartCompleted,
104635
105000
  require_clip_model_registry.EventCategory.SystemRestarting,
104636
105001
  require_clip_model_registry.EventCategory.RecordingExportCompleted,
104637
- require_clip_model_registry.EventCategory.BackupRunCompleted
105002
+ require_clip_model_registry.EventCategory.BackupRunCompleted,
105003
+ require_clip_model_registry.EventCategory.AlertEpisode
104638
105004
  ];
104639
105005
  /**
104640
105006
  * Install the addon's bus subscriptions. Identical set + relative order to the
@@ -107522,6 +107888,15 @@ Object.freeze({
107522
107888
  "valueType": "boolean"
107523
107889
  }],
107524
107890
  "dishwasher": [
107891
+ {
107892
+ "path": "availability",
107893
+ "valueType": "enum",
107894
+ "enumValues": [
107895
+ "available",
107896
+ "offline",
107897
+ "description-missing"
107898
+ ]
107899
+ },
107525
107900
  {
107526
107901
  "path": "state",
107527
107902
  "valueType": "enum",
@@ -107534,12 +107909,35 @@ Object.freeze({
107534
107909
  "unavailable"
107535
107910
  ]
107536
107911
  },
107912
+ {
107913
+ "path": "operationState",
107914
+ "valueType": "enum",
107915
+ "enumValues": [
107916
+ "Inactive",
107917
+ "Ready",
107918
+ "DelayedStart",
107919
+ "Run",
107920
+ "Pause",
107921
+ "ActionRequired",
107922
+ "Finished",
107923
+ "Error",
107924
+ "Aborting"
107925
+ ]
107926
+ },
107537
107927
  {
107538
107928
  "path": "program",
107539
107929
  "valueType": "string"
107540
107930
  },
107541
107931
  {
107542
- "path": "remainingMinutes",
107932
+ "path": "selectedProgram",
107933
+ "valueType": "string"
107934
+ },
107935
+ {
107936
+ "path": "remainingSeconds",
107937
+ "valueType": "number"
107938
+ },
107939
+ {
107940
+ "path": "delayedStartSeconds",
107543
107941
  "valueType": "number"
107544
107942
  },
107545
107943
  {
@@ -107550,14 +107948,30 @@ Object.freeze({
107550
107948
  "path": "doorOpen",
107551
107949
  "valueType": "boolean"
107552
107950
  },
107951
+ {
107952
+ "path": "doorLocked",
107953
+ "valueType": "boolean"
107954
+ },
107553
107955
  {
107554
107956
  "path": "remoteStartAllowed",
107555
107957
  "valueType": "boolean"
107556
107958
  },
107959
+ {
107960
+ "path": "remoteControlActive",
107961
+ "valueType": "boolean"
107962
+ },
107963
+ {
107964
+ "path": "localControlActive",
107965
+ "valueType": "boolean"
107966
+ },
107557
107967
  {
107558
107968
  "path": "powerOn",
107559
107969
  "valueType": "boolean"
107560
107970
  },
107971
+ {
107972
+ "path": "powerState",
107973
+ "valueType": "string"
107974
+ },
107561
107975
  {
107562
107976
  "path": "saltLevel",
107563
107977
  "valueType": "enum",
@@ -113879,20 +114293,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
113879
114293
  mediaRelocate = null;
113880
114294
  mediaLocationStorage = null;
113881
114295
  /**
113882
- * Cached pick for NEW `eventMedia` writes (`pickWritableMediaLocation`).
113883
- * Resolved once, never per blob (no-RPC-per-blob, same discipline as
113884
- * `mediaLocationStorage`'s own root cache) — dropped together with it by
113885
- * `refreshStorageLocationsForMigration`, since a location's `readOnly` /
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).
114296
+ * Where NEW `eventMedia` writes land. Its pick is cached per location-set
114297
+ * generation and dropped by the `StorageLocationsChanged` doorbell (D387) and
114298
+ * by `refreshStorageLocationsForMigration`, so a mode change or a repoint takes
114299
+ * effect without a restart.
113894
114300
  */
113895
- lastEventMediaWritePick = null;
114301
+ eventMediaWriteLocator = null;
114302
+ eventMediaLocationDoorbellUnsub = null;
113896
114303
  mediaWriteGate = new MediaWriteGate();
113897
114304
  storageMigrationLeaseId = null;
113898
114305
  /** Per-camera history of LINKED-device sensor state changes (Part B). */
@@ -115932,33 +116339,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
115932
116339
  }
115933
116340
  /** One line per process: this repeats on every occupancy refresh otherwise. */
115934
116341
  mediaOccupancyUnattributedLogged = false;
115935
- async resolveEventMediaWriteLocation(api, logger, deviceId) {
115936
- const cached = this.eventMediaWriteLocationCache;
115937
- if (cached !== null) return cached;
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;
116342
+ resolveEventMediaWriteLocation(api, logger, deviceId) {
116343
+ this.eventMediaWriteLocator ??= new EventMediaWriteLocator({ list: () => api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION }) });
116344
+ return this.eventMediaWriteLocator.resolve(logger, deviceId);
115962
116345
  }
115963
116346
  /** Constructs every SQLite-backed store plus the stationary/package-drop/
115964
116347
  * sensor plumbing, in the exact pre-S7 order. Returns the non-null bundle
@@ -116059,6 +116442,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
116059
116442
  resolveEventMediaLocation: (deviceId) => this.resolveEventMediaWriteLocation(api, logger, deviceId)
116060
116443
  });
116061
116444
  this.mediaStore = mediaStore;
116445
+ this.eventMediaWriteLocator ??= new EventMediaWriteLocator({ list: () => api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION }) });
116446
+ this.eventMediaLocationDoorbellUnsub?.();
116447
+ this.eventMediaLocationDoorbellUnsub = subscribeEventMediaWriteLocator(this.ctx.eventBus, this.eventMediaWriteLocator, () => this.mediaLocationStorage?.invalidateRoots());
116062
116448
  const eventLedgerWiring = this.eventLedgerWiring(api.settingsStore);
116063
116449
  const retrainFrames = new RetrainFrameStore({
116064
116450
  store: api.settingsStore,
@@ -116641,6 +117027,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
116641
117027
  coreBlocks: async () => (await api.coreBlocks.list.query({})).blocks,
116642
117028
  nodeAddons: (nodeId) => api.nodes.getNodeAddons.query({ nodeId })
116643
117029
  },
117030
+ readAlerts: () => api.alerts.list.query({ limit: 5e3 }),
116644
117031
  getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
116645
117032
  recognitionOutlook: ({ deviceId, trackId, className, labelKind, now }) => recognitionOutlook({
116646
117033
  className,
@@ -118654,6 +119041,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
118654
119041
  this.alarmPanelConvergence?.stop();
118655
119042
  this.alarmPanelConvergence = null;
118656
119043
  for (const unsub of this.subscriptionUnsubs) unsub();
119044
+ this.eventMediaLocationDoorbellUnsub?.();
119045
+ this.eventMediaLocationDoorbellUnsub = null;
118657
119046
  this.subscriptionUnsubs = [];
118658
119047
  await Promise.all([this.drainSensorHandlers(), this.motionEpisodes?.closeAll("addon-shutdown")]);
118659
119048
  await this.settleTrackFusions();
@@ -125908,7 +126297,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
125908
126297
  async refreshStorageLocationsForMigration(input) {
125909
126298
  if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
125910
126299
  this.mediaLocationStorage?.invalidateRoots();
125911
- this.eventMediaWriteLocationCache = null;
126300
+ this.eventMediaWriteLocator?.invalidate();
125912
126301
  return { refreshed: true };
125913
126302
  }
125914
126303
  /**
@@ -126271,7 +126660,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
126271
126660
  async startStorageMigrationMove(input) {
126272
126661
  if (!this.mediaRelocate) throw new Error("media relocation unavailable");
126273
126662
  if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
126274
- return { jobId: this.mediaRelocate.start(input) };
126663
+ return { jobId: await this.startMediaMover(input) };
126664
+ }
126665
+ /**
126666
+ * Start a media move only while no single-location repoint holds an overlapping
126667
+ * physical root (the storage orchestrator owns that claim). A shared recordings
126668
+ * root is the case: a repoint of another class there must not run beside us.
126669
+ */
126670
+ startMediaMover(input) {
126671
+ const engine = this.mediaRelocate;
126672
+ if (!engine) throw new Error("media relocation unavailable");
126673
+ return require_clip_model_registry.startMoverUnderClearance({
126674
+ clearance: (locationIds) => this.ctx.api.storageMigration.moverClearance.query({ locationIds }),
126675
+ start: () => engine.start(input),
126676
+ cancel: (jobId) => {
126677
+ engine.cancel(jobId);
126678
+ },
126679
+ locationIds: [input.fromLocationId ?? "*", input.toLocationId]
126680
+ });
126275
126681
  }
126276
126682
  async getStorageMigrationMoveStatus(input) {
126277
126683
  return this.mediaRelocate?.list().find((job) => job.jobId === input.jobId) ?? null;
@@ -126289,7 +126695,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
126289
126695
  */
126290
126696
  async relocateMedia(input) {
126291
126697
  if (!this.mediaRelocate) throw new Error("media relocation unavailable");
126292
- const jobId = this.mediaRelocate.start(input);
126698
+ const jobId = await this.startMediaMover(input);
126293
126699
  this.ctx.logger.info("pipeline-analytics: media relocate job enqueued", { meta: {
126294
126700
  jobId,
126295
126701
  mode: input.mode ?? "move",