@camstack/addon-post-analysis 1.2.233 → 1.2.235

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.
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.187",
39
+ version: "1.2.189",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.103",
48
+ version: "1.2.105",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.152",
84
+ version: "1.2.154",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-Dbf6ceXh.js");
5
+ const require_dist = require("../dist-IoI_ufBo.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -7445,6 +7445,16 @@ var NcAlertStore = class {
7445
7445
  };
7446
7446
  }
7447
7447
  };
7448
+ function deriveConsumableBand(input) {
7449
+ const { level, status } = input.reading;
7450
+ const measured = level !== null && Number.isFinite(level);
7451
+ if (!measured && status === null) return null;
7452
+ if (status === "replace") return "low";
7453
+ if (!measured) return "normal";
7454
+ const value = level;
7455
+ if (input.previous === "low") return value >= input.thresholdPercent + 5 ? "normal" : "low";
7456
+ return value <= input.thresholdPercent ? "low" : "normal";
7457
+ }
7448
7458
  //#endregion
7449
7459
  //#region src/notification-center/device-directory.ts
7450
7460
  /**
@@ -9845,6 +9855,40 @@ function incomingFromBatteryBand(input) {
9845
9855
  });
9846
9856
  }
9847
9857
  /**
9858
+ * A consumable BAND, as a system-event subject.
9859
+ *
9860
+ * Built exactly like {@link incomingFromBatteryBand} — the operator asked for
9861
+ * these to be treated as the battery notifications are — with two differences
9862
+ * that come from the subject and not from taste:
9863
+ *
9864
+ * - **the subject carries the ITEM.** `consumable:<deviceId>:<key>`, because a
9865
+ * vacuum has a brush AND a filter AND a mop pad, and one subject for all
9866
+ * three would let the brush's alert cancel the filter's.
9867
+ * - **the body variant follows what is KNOWN.** An item with no percentage
9868
+ * gets a finished sentence rather than a template with a stand-in number in
9869
+ * it, for the same reason a binary battery does.
9870
+ *
9871
+ * The recovery keeps the SAME subject: it is the same episode ending.
9872
+ */
9873
+ function incomingFromConsumableBand(input) {
9874
+ const kind = input.band === "low" ? "device-consumable-low" : "device-consumable-normal";
9875
+ const measured = input.item.level !== null && Number.isFinite(input.item.level);
9876
+ return systemSubject({
9877
+ facts: input.facts,
9878
+ kind,
9879
+ subject: `consumable:${input.deviceId}:${input.item.key}`,
9880
+ titleKey: `system.${kind}.title`,
9881
+ bodyKey: measured ? `system.${kind}.body` : `system.${kind}.body.unmeasured`,
9882
+ textParams: {
9883
+ ...deviceTextParams(input.deviceId, input.identity),
9884
+ item: input.item.label,
9885
+ ...measured ? { percentage: String(Math.round(input.item.level)) } : {}
9886
+ },
9887
+ deviceId: input.deviceId,
9888
+ ...input.identity !== void 0 ? { deviceType: input.identity.type } : {}
9889
+ });
9890
+ }
9891
+ /**
9848
9892
  * Normalize the infrastructure categories consumed by Notification Center.
9849
9893
  * Malformed payloads are rejected instead of guessing an identity.
9850
9894
  *
@@ -9996,6 +10040,7 @@ function incomingFromSystemEvent(event, lookup) {
9996
10040
  }
9997
10041
  case "system.restart-completed": return incomingFromServerUpdated(facts, data);
9998
10042
  case "recording.export.completed": return incomingFromExportCompleted(facts, data);
10043
+ case "backup.run-completed": return incomingFromBackupCompleted(facts, data);
9999
10044
  default: return null;
10000
10045
  }
10001
10046
  }
@@ -10159,6 +10204,37 @@ function incomingFromServerUpdated(facts, data) {
10159
10204
  nodeIds: [nodeId]
10160
10205
  });
10161
10206
  }
10207
+ /**
10208
+ * A finished backup RUN.
10209
+ *
10210
+ * Subject is the run, not the destination: `backup.completed` fires once per
10211
+ * destination and an operator with three of them would be told three times.
10212
+ * The counts travel in the text because a PARTIAL run must not read as a clean
10213
+ * one — two destinations of three is news, and a bare "backup completed" would
10214
+ * hide it.
10215
+ */
10216
+ function incomingFromBackupCompleted(facts, data) {
10217
+ const ok = numberField(data, "destinationsOk");
10218
+ const total = numberField(data, "destinationsTotal");
10219
+ if (ok === void 0 || total === void 0) return null;
10220
+ const failed = numberField(data, "destinationsFailed") ?? Math.max(0, total - ok);
10221
+ const sizeMB = numberField(data, "sizeMB");
10222
+ const backupId = stringField(data, "backupId");
10223
+ return systemSubject({
10224
+ facts,
10225
+ kind: "backup-completed",
10226
+ subject: `backup:${backupId ?? facts.timestamp}`,
10227
+ titleKey: "system.backup-completed.title",
10228
+ bodyKey: failed > 0 ? "system.backup-completed.body.partial" : "system.backup-completed.body",
10229
+ textParams: {
10230
+ destinationsOk: String(ok),
10231
+ destinationsTotal: String(total),
10232
+ destinationsFailed: String(failed),
10233
+ ...sizeMB !== void 0 ? { sizeMB: String(sizeMB) } : {},
10234
+ ...backupId !== void 0 ? { backupId } : {}
10235
+ }
10236
+ });
10237
+ }
10162
10238
  function incomingFromExportCompleted(facts, data) {
10163
10239
  const exportId = stringField(data, "exportId");
10164
10240
  if (exportId === void 0) return null;
@@ -10496,6 +10572,8 @@ var NcDeviceAlerts = class {
10496
10572
  * durable.
10497
10573
  */
10498
10574
  lastReading = /* @__PURE__ */ new Map();
10575
+ /** The last reading per consumable SUBJECT, for the reminder sweep. */
10576
+ lastConsumable = /* @__PURE__ */ new Map();
10499
10577
  constructor(deps) {
10500
10578
  this.deps = deps;
10501
10579
  this.now = deps.now ?? Date.now;
@@ -10533,6 +10611,44 @@ var NcDeviceAlerts = class {
10533
10611
  }));
10534
10612
  }
10535
10613
  /**
10614
+ * ONE consumable item, from a device's `consumables` slice.
10615
+ *
10616
+ * Per ITEM, never per device: a vacuum has a brush, a filter and a mop pad,
10617
+ * and folding them into one subject would make the brush's alert cancel the
10618
+ * filter's — the same collision D141 split `device-switch:<id>` off liveness
10619
+ * to avoid. Each item is its own episode, its own cooldown, its own recovery.
10620
+ *
10621
+ * An item that has told us nothing leaves EVERYTHING untouched, for the
10622
+ * reason `observeBattery` does: seeding it healthy is a claim, and the first
10623
+ * genuine `replace` would then read as a transition out of a state nobody
10624
+ * observed.
10625
+ */
10626
+ observeConsumable(deviceId, item) {
10627
+ const subject = `consumable:${deviceId}:${item.key}`;
10628
+ const previous = this.deps.rows.get(subject);
10629
+ const settings = this.deps.settings();
10630
+ const band = deriveConsumableBand({
10631
+ reading: {
10632
+ level: item.level,
10633
+ status: item.status
10634
+ },
10635
+ thresholdPercent: settings.consumableLowPercent,
10636
+ previous: previous?.alerting === true ? "low" : previous === void 0 ? void 0 : "normal"
10637
+ });
10638
+ if (band === null) return;
10639
+ this.lastConsumable.set(subject, {
10640
+ deviceId,
10641
+ item
10642
+ });
10643
+ this.apply(subject, band === "low", settings, (facts, alerting) => incomingFromConsumableBand({
10644
+ facts,
10645
+ deviceId,
10646
+ band: alerting ? "low" : "normal",
10647
+ item,
10648
+ identity: this.deps.lookup(deviceId)
10649
+ }));
10650
+ }
10651
+ /**
10536
10652
  * The reminder sweep. Called from an existing periodic tick; it allocates
10537
10653
  * nothing on the healthy path.
10538
10654
  *
@@ -10546,6 +10662,17 @@ var NcDeviceAlerts = class {
10546
10662
  if (settings.renotifyMinutes <= 0) return;
10547
10663
  for (const row of this.deps.rows.all()) {
10548
10664
  if (!row.alerting) continue;
10665
+ const consumable = this.lastConsumable.get(row.subject);
10666
+ if (consumable !== void 0) {
10667
+ this.apply(row.subject, true, settings, (facts) => incomingFromConsumableBand({
10668
+ facts,
10669
+ deviceId: consumable.deviceId,
10670
+ band: "low",
10671
+ item: consumable.item,
10672
+ identity: this.deps.lookup(consumable.deviceId)
10673
+ }));
10674
+ continue;
10675
+ }
10549
10676
  const restatement = this.lastReading.get(row.subject);
10550
10677
  if (restatement === void 0) continue;
10551
10678
  this.apply(row.subject, true, settings, (facts) => incomingFromBatteryBand({
@@ -11654,6 +11781,15 @@ var en_default = {
11654
11781
  "system.export-completed.title": "Export ready",
11655
11782
  "system.export-completed.body": "The footage export is ready",
11656
11783
  "system.export-completed.body.titled": "{{title}} is ready",
11784
+ "system.backup-completed.title": "Backup completed",
11785
+ "system.backup-completed.body": "Backup saved to {{destinationsOk}} of {{destinationsTotal}} destinations ({{sizeMB}} MB)",
11786
+ "system.backup-completed.body.partial": "Backup saved to {{destinationsOk}} of {{destinationsTotal}} destinations — {{destinationsFailed}} failed",
11787
+ "system.device-consumable-low.title": "Consumable low",
11788
+ "system.device-consumable-low.body": "{{item}} on {{deviceName}} is at {{percentage}}%",
11789
+ "system.device-consumable-low.body.unmeasured": "{{item}} on {{deviceName}} needs replacing",
11790
+ "system.device-consumable-normal.title": "Consumable replaced",
11791
+ "system.device-consumable-normal.body": "{{item}} on {{deviceName}} is back to {{percentage}}%",
11792
+ "system.device-consumable-normal.body.unmeasured": "{{item}} on {{deviceName}} is OK again",
11657
11793
  "system.camera-online.title": "Device online",
11658
11794
  "system.camera-online.body": "Device {{deviceName}} is online",
11659
11795
  "system.camera-offline.title": "Device offline",
@@ -11914,6 +12050,15 @@ var it_default = {
11914
12050
  "system.export-completed.title": "Export pronto",
11915
12051
  "system.export-completed.body": "L'export del filmato è pronto",
11916
12052
  "system.export-completed.body.titled": "{{title}} è pronto",
12053
+ "system.backup-completed.title": "Backup completato",
12054
+ "system.backup-completed.body": "Backup salvato su {{destinationsOk}} destinazioni su {{destinationsTotal}} ({{sizeMB}} MB)",
12055
+ "system.backup-completed.body.partial": "Backup salvato su {{destinationsOk}} destinazioni su {{destinationsTotal}} — {{destinationsFailed}} fallite",
12056
+ "system.device-consumable-low.title": "Consumabile quasi esaurito",
12057
+ "system.device-consumable-low.body": "{{item}} su {{deviceName}} è al {{percentage}}%",
12058
+ "system.device-consumable-low.body.unmeasured": "{{item}} su {{deviceName}} va sostituito",
12059
+ "system.device-consumable-normal.title": "Consumabile sostituito",
12060
+ "system.device-consumable-normal.body": "{{item}} su {{deviceName}} è tornato al {{percentage}}%",
12061
+ "system.device-consumable-normal.body.unmeasured": "{{item}} su {{deviceName}} è di nuovo a posto",
11917
12062
  "system.camera-online.title": "Dispositivo online",
11918
12063
  "system.camera-online.body": "Il dispositivo {{deviceName}} è online",
11919
12064
  "system.camera-offline.title": "Dispositivo offline",
@@ -22237,6 +22382,7 @@ function alarmButtonKind(kind) {
22237
22382
  */
22238
22383
  var NC_DEVICE_ALERT_FALLBACK = {
22239
22384
  batteryLowPercent: 20,
22385
+ consumableLowPercent: 15,
22240
22386
  renotifyMinutes: 720,
22241
22387
  notifyRecovery: true
22242
22388
  };
@@ -22269,6 +22415,52 @@ function batterySliceOf(event) {
22269
22415
  };
22270
22416
  }
22271
22417
  /**
22418
+ * A `device.state-changed` carrying a CONSUMABLES slice, reduced to the items
22419
+ * the band derivation reads.
22420
+ *
22421
+ * Same tap as the battery's: `DeviceStateChanged` is already subscribed, so
22422
+ * every `consumables` write already arrives here and was simply dropped. No
22423
+ * new bus category and no producer change.
22424
+ *
22425
+ * An item is kept only when it carries a usable `key` — the subject is built
22426
+ * from it, and an item with no key could not be told apart from its sibling.
22427
+ * `level` and `status` are each optional; an item with NEITHER is still
22428
+ * returned, because deciding that it has told us nothing belongs to
22429
+ * `deriveConsumableBand` and not to a parser (D393: absent is not zero, and
22430
+ * the parser is not the place that judges).
22431
+ */
22432
+ function consumablesSliceOf(event) {
22433
+ if (event.category !== "device.state-changed") return null;
22434
+ const data = event.data;
22435
+ if (data["capName"] !== "consumables") return null;
22436
+ const deviceId = data["deviceId"];
22437
+ const slice = data["slice"];
22438
+ if (typeof deviceId !== "number") return null;
22439
+ if (typeof slice !== "object" || slice === null || Array.isArray(slice)) return null;
22440
+ const rawItems = slice["items"];
22441
+ if (!Array.isArray(rawItems)) return null;
22442
+ const items = [];
22443
+ for (const entry of rawItems) {
22444
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
22445
+ const row = entry;
22446
+ const key = row["key"];
22447
+ if (typeof key !== "string" || key.length === 0) continue;
22448
+ const level = row["level"];
22449
+ const status = row["status"];
22450
+ const label = row["label"];
22451
+ items.push({
22452
+ key,
22453
+ label: typeof label === "string" && label.length > 0 ? label : key,
22454
+ level: typeof level === "number" ? level : null,
22455
+ status: status === "ok" || status === "replace" ? status : null
22456
+ });
22457
+ }
22458
+ return items.length === 0 ? null : {
22459
+ deviceId,
22460
+ items
22461
+ };
22462
+ }
22463
+ /**
22272
22464
  * What `getAlarmConfig` answers on a node with no panel.
22273
22465
  *
22274
22466
  * The DEFAULTS, not zeroes: paired with `deviceId: null` this tells a client
@@ -24317,6 +24509,11 @@ var NotificationCenter = class NotificationCenter {
24317
24509
  this.deviceAlerts.observeBattery(battery.deviceId, battery.reading);
24318
24510
  return;
24319
24511
  }
24512
+ const consumables = consumablesSliceOf(event);
24513
+ if (consumables !== null) {
24514
+ for (const item of consumables.items) this.deviceAlerts.observeConsumable(consumables.deviceId, item);
24515
+ return;
24516
+ }
24320
24517
  const lookup = this.deviceDirectory.lookup();
24321
24518
  const incoming = incomingFromSystemEvent(event, lookup);
24322
24519
  if (incoming === null) {
@@ -25929,6 +26126,7 @@ var NotificationCenter = class NotificationCenter {
25929
26126
  deviceAlertSettings() {
25930
26127
  return this.deps.readDeviceAlertSettings?.() ?? {
25931
26128
  batteryLowPercent: NC_DEVICE_ALERT_FALLBACK.batteryLowPercent,
26129
+ consumableLowPercent: NC_DEVICE_ALERT_FALLBACK.consumableLowPercent,
25932
26130
  renotifyMinutes: NC_DEVICE_ALERT_FALLBACK.renotifyMinutes,
25933
26131
  notifyRecovery: NC_DEVICE_ALERT_FALLBACK.notifyRecovery
25934
26132
  };
@@ -26544,6 +26742,7 @@ function systemEventSubject(kind, scope) {
26544
26742
  if (kind === "addon-updated") return `packages:addon-updated:${scope.packageName ?? "pkg"}`;
26545
26743
  if (kind === "server-updated") return `server-updated:${scope.nodeId ?? "hub"}`;
26546
26744
  if (kind === "export-completed") return `export:${scope.deviceId ?? 0}`;
26745
+ if (kind === "backup-completed") return "backup:test";
26547
26746
  return `alarm:${scope.deviceId ?? 0}`;
26548
26747
  }
26549
26748
  /**
@@ -49947,6 +50146,19 @@ var DeviceAlertSettingsSchema = require_dist.object({
49947
50146
  */
49948
50147
  batteryLowPercent: require_dist.number().int().min(1).max(99).default(20),
49949
50148
  /**
50149
+ * What counts as a nearly-exhausted CONSUMABLE — a filter, a brush, a mop pad.
50150
+ *
50151
+ * Its own number and not the battery's, because the two are different chores
50152
+ * on different clocks: a battery at 20 % is hours from flat, a brush at 20 %
50153
+ * is weeks from worn, and an operator who wants to be told early about one
50154
+ * does not necessarily want to be told early about the other.
50155
+ *
50156
+ * Applies to the MEASURED side only. An item whose provider states a
50157
+ * `replace` verdict is low whatever this says — the provider decided, and a
50158
+ * threshold cannot talk it out of it (`alerts/consumable-band.ts`).
50159
+ */
50160
+ consumableLowPercent: require_dist.number().int().min(1).max(99).default(15),
50161
+ /**
49950
50162
  * How long a still-true alert waits before it says itself again.
49951
50163
  *
49952
50164
  * **0 = say it once.** That is the behaviour of every other notification this
@@ -58592,7 +58804,9 @@ function stationaryEvidenceKey(deviceId, className, anchorId) {
58592
58804
  * refused is the FIRST thing it failed, not the last.
58593
58805
  */
58594
58806
  function evaluateStationaryEvidence(row, policy, classEnabled) {
58595
- const ratio = row.seen > 0 ? row.agreed / row.seen : 0;
58807
+ const windowSeen = row.windowSeen + row.prevWindowSeen;
58808
+ const windowAgreed = row.windowAgreed + row.prevWindowAgreed;
58809
+ const ratio = windowSeen > 0 ? windowAgreed / windowSeen : 0;
58596
58810
  const spanMs = row.lastAgreedAt - row.firstAgreedAt;
58597
58811
  const base = {
58598
58812
  agreed: row.agreed,
@@ -58626,6 +58840,36 @@ function evaluateStationaryEvidence(row, policy, classEnabled) {
58626
58840
  };
58627
58841
  }
58628
58842
  /**
58843
+ * Slide the window forward to `now`.
58844
+ *
58845
+ * One bucket width past: the current bucket becomes the previous one and a
58846
+ * fresh one opens. TWO or more past: the candidate was not observed for the
58847
+ * whole window, so both buckets are empty — and an empty window answers `0`,
58848
+ * which is an honest "nothing recent to judge by" rather than a stale verdict.
58849
+ * The lifetime counters are untouched, so nothing a candidate has earned is
58850
+ * lost — only its claim about the present expires.
58851
+ */
58852
+ function rotateWindow(row, now) {
58853
+ const elapsed = now - row.windowStartedAt;
58854
+ if (elapsed < 9e5) return row;
58855
+ if (elapsed < 2 * 9e5) return {
58856
+ ...row,
58857
+ prevWindowAgreed: row.windowAgreed,
58858
+ prevWindowSeen: row.windowSeen,
58859
+ windowAgreed: 0,
58860
+ windowSeen: 0,
58861
+ windowStartedAt: now
58862
+ };
58863
+ return {
58864
+ ...row,
58865
+ prevWindowAgreed: 0,
58866
+ prevWindowSeen: 0,
58867
+ windowAgreed: 0,
58868
+ windowSeen: 0,
58869
+ windowStartedAt: now
58870
+ };
58871
+ }
58872
+ /**
58629
58873
  * The candidate this observation belongs to: the same-class row whose anchor it
58630
58874
  * overlaps most, provided the overlap reaches `agreeIou`. Overlap is the ONLY
58631
58875
  * notion of sameness here, and it is the same {@link iou} the suppression gate
@@ -58659,15 +58903,19 @@ function matchEvidenceCandidate(rows, className, bbox, agreeIou) {
58659
58903
  */
58660
58904
  function foldObservation(row, observation, agrees, overlap) {
58661
58905
  const seen = row.seen + 1;
58906
+ const rotated = rotateWindow(row, observation.timestamp);
58662
58907
  if (!agrees) return {
58663
- ...row,
58908
+ ...rotated,
58664
58909
  seen,
58910
+ windowSeen: rotated.windowSeen + 1,
58665
58911
  updatedAt: observation.timestamp
58666
58912
  };
58667
58913
  const improves = overlap >= iou(row.anchor, row.bestBbox);
58668
58914
  return {
58669
- ...row,
58915
+ ...rotated,
58670
58916
  seen,
58917
+ windowSeen: rotated.windowSeen + 1,
58918
+ windowAgreed: rotated.windowAgreed + 1,
58671
58919
  agreed: row.agreed + 1,
58672
58920
  firstAgreedAt: Math.min(row.firstAgreedAt, observation.timestamp),
58673
58921
  lastAgreedAt: Math.max(row.lastAgreedAt, observation.timestamp),
@@ -58691,6 +58939,11 @@ function newEvidenceRow(observation, anchorId) {
58691
58939
  anchor: { ...observation.bbox },
58692
58940
  agreed: 1,
58693
58941
  seen: 1,
58942
+ windowAgreed: 1,
58943
+ windowSeen: 1,
58944
+ prevWindowAgreed: 0,
58945
+ prevWindowSeen: 0,
58946
+ windowStartedAt: observation.timestamp,
58694
58947
  firstAgreedAt: observation.timestamp,
58695
58948
  lastAgreedAt: observation.timestamp,
58696
58949
  bestBbox: { ...observation.bbox },
@@ -58829,6 +59082,11 @@ var StationaryEvidenceLedger = class {
58829
59082
  anchor: { ...row.anchor },
58830
59083
  agreed: row.agreed,
58831
59084
  seen: row.seen,
59085
+ windowAgreed: row.windowAgreed,
59086
+ windowSeen: row.windowSeen,
59087
+ prevWindowAgreed: row.prevWindowAgreed,
59088
+ prevWindowSeen: row.prevWindowSeen,
59089
+ windowStartedAt: row.windowStartedAt,
58832
59090
  firstAgreedAt: row.firstAgreedAt,
58833
59091
  lastAgreedAt: row.lastAgreedAt,
58834
59092
  bestBbox: { ...row.bestBbox },
@@ -58857,6 +59115,11 @@ var StationaryEvidenceLedger = class {
58857
59115
  anchor,
58858
59116
  agreed: numberOr(data.agreed, 0),
58859
59117
  seen: numberOr(data.seen, 0),
59118
+ windowAgreed: numberOr(data.windowAgreed, 0),
59119
+ windowSeen: numberOr(data.windowSeen, 0),
59120
+ prevWindowAgreed: numberOr(data.prevWindowAgreed, 0),
59121
+ prevWindowSeen: numberOr(data.prevWindowSeen, 0),
59122
+ windowStartedAt: numberOr(data.windowStartedAt, updatedAt),
58860
59123
  firstAgreedAt: numberOr(data.firstAgreedAt, updatedAt),
58861
59124
  lastAgreedAt: numberOr(data.lastAgreedAt, updatedAt),
58862
59125
  bestBbox,
@@ -67561,7 +67824,8 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
67561
67824
  require_dist.EventCategory.UpdateAvailable,
67562
67825
  require_dist.EventCategory.AddonUpdated,
67563
67826
  require_dist.EventCategory.SystemRestartCompleted,
67564
- require_dist.EventCategory.RecordingExportCompleted
67827
+ require_dist.EventCategory.RecordingExportCompleted,
67828
+ require_dist.EventCategory.BackupRunCompleted
67565
67829
  ];
67566
67830
  /**
67567
67831
  * Install the addon's bus subscriptions. Identical set + relative order to the
@@ -70644,6 +70908,17 @@ function buildGlobalSettingsSchema(cap) {
70644
70908
  default: DEVICE_ALERT_DEFAULTS.batteryLowPercent,
70645
70909
  unit: "%"
70646
70910
  },
70911
+ {
70912
+ type: "number",
70913
+ key: "consumableLowPercent",
70914
+ label: "Low consumable at",
70915
+ description: "At or below this, a measured consumable — a filter, a brush, a mop pad — is nearly exhausted. Its own number rather than the battery’s: a battery at 20% is hours from flat, a brush at 20% is weeks from worn. An item whose device reports “replace” outright is announced whatever this says.",
70916
+ min: 1,
70917
+ max: 99,
70918
+ step: 1,
70919
+ default: DEVICE_ALERT_DEFAULTS.consumableLowPercent,
70920
+ unit: "%"
70921
+ },
70647
70922
  {
70648
70923
  type: "number",
70649
70924
  key: "alertRenotifyMinutes",
@@ -76237,6 +76512,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
76237
76512
  },
76238
76513
  readDeviceAlertSettings: () => ({
76239
76514
  batteryLowPercent: this.deviceAlertSettingsMirror.batteryLowPercent,
76515
+ consumableLowPercent: this.deviceAlertSettingsMirror.consumableLowPercent,
76240
76516
  renotifyMinutes: this.deviceAlertSettingsMirror.alertRenotifyMinutes,
76241
76517
  notifyRecovery: this.deviceAlertSettingsMirror.alertNotifyRecovery
76242
76518
  }),