@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.
@@ -1,4 +1,4 @@
1
- import { $ as TimelapseRuleSchema, $t as boolean, A as NcConditionDescriptorSchema, At as plateGalleryCapability, B as NcTaxonomySchema, Bt as videoclipsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as isSourceCap, D as NC_DEFAULT_SNOOZE_MINUTES, E as NC_CONDITION_CATALOG, Et as notificationRulesCapability, F as NcScheduleSchema, Ft as sceneMonitorCapability, Gt as DeviceType, Ht as errMsg$1, I as NcSnoozeInputSchema, It as storageOccupancyCapability, J as SCENE_DIVERGED, Jt as isDeviceScopedCap, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as createEvent, L as NcSnoozeSchema, Lt as subKindsOf, M as NcRulePatchSchema, Mt as readTimelapseGeneratedAt, N as NcRuleSchema, Nt as resolveLocationMode, Ot as pickClusterStepModels, P as NcRuleTargetSchema, Q as TimelapseRulePatchSchema, Qt as array, R as NcSnoozeSuppressedSchema, Rt as systemEventFilterApplies, St as isScheduleActive, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as mayWriteToLocation, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as OpsLogEntrySchema, Vt as zoneAnalyticsCapability, W as RetrainStatusSchema, Wt as CamProfileSchema, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as sleep$1, Y as SceneMonitorSchema, Yt as nodePin, Z as TimelapseRuleInputSchema, Zt as _enum, _ as FULL_IMAGE_BBOX, _t as failureContributionCapability, a as CLUSTER_MODEL_SCOPED_STEPS, an as record, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isEventOwnerType, cn as EventCategory, ct as cosineSimilarity$1, dt as deriveRecordingMode, en as discriminatedUnion, et as TrackSourceSchema, f as DeclaredDevices, g as FIRST_LEVEL_MACRO_CLASSES, gt as faceGalleryCapability, h as EVENT_PAD_MS, ht as evictionPolicyOfLocation, i as BirthDecisionRecordSchema, in as partialRecord, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readDeviceStateFrom, k as NC_TAXONOMY, kt as pipelineAnalyticsCapability, lt as customAction, m as EVENT_OWNER_TYPES, mt as evaluateSensorEdge, n as ArchivedDebugNoteSchema, nn as number, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as string, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as encodeVectorBase64, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as hydrateSchema, r as BaseDevice, rn as object, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as unknown, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as literal, tt as VISIT_MERGE_GAP_MS, u as DETECTION_MACRO_CLASSES, ut as defineCustomActions, v as FailureCounters, w as MediaFileKindEnum, wt as kebabToCamel, x as MAX_BIRTH_DECISION_RECORDS, xt as isMediaOwnerType, y as LabelAttributionSchema, yt as isDetectionMacroClass, z as NcSystemEventKindSchema, zt as vectorDimFromBase64 } from "../dist-yyQ1H5yE.mjs";
1
+ import { $ as TimelapseRuleSchema, $t as boolean, A as NcConditionDescriptorSchema, At as plateGalleryCapability, B as NcTaxonomySchema, Bt as videoclipsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as isSourceCap, D as NC_DEFAULT_SNOOZE_MINUTES, E as NC_CONDITION_CATALOG, Et as notificationRulesCapability, F as NcScheduleSchema, Ft as sceneMonitorCapability, Gt as DeviceType, Ht as errMsg$1, I as NcSnoozeInputSchema, It as storageOccupancyCapability, J as SCENE_DIVERGED, Jt as isDeviceScopedCap, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as createEvent, L as NcSnoozeSchema, Lt as subKindsOf, M as NcRulePatchSchema, Mt as readTimelapseGeneratedAt, N as NcRuleSchema, Nt as resolveLocationMode, Ot as pickClusterStepModels, P as NcRuleTargetSchema, Q as TimelapseRulePatchSchema, Qt as array, R as NcSnoozeSuppressedSchema, Rt as systemEventFilterApplies, St as isScheduleActive, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as mayWriteToLocation, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as OpsLogEntrySchema, Vt as zoneAnalyticsCapability, W as RetrainStatusSchema, Wt as CamProfileSchema, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as sleep$1, Y as SceneMonitorSchema, Yt as nodePin, Z as TimelapseRuleInputSchema, Zt as _enum, _ as FULL_IMAGE_BBOX, _t as failureContributionCapability, a as CLUSTER_MODEL_SCOPED_STEPS, an as record, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isEventOwnerType, cn as EventCategory, ct as cosineSimilarity$1, dt as deriveRecordingMode, en as discriminatedUnion, et as TrackSourceSchema, f as DeclaredDevices, g as FIRST_LEVEL_MACRO_CLASSES, gt as faceGalleryCapability, h as EVENT_PAD_MS, ht as evictionPolicyOfLocation, i as BirthDecisionRecordSchema, in as partialRecord, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readDeviceStateFrom, k as NC_TAXONOMY, kt as pipelineAnalyticsCapability, lt as customAction, m as EVENT_OWNER_TYPES, mt as evaluateSensorEdge, n as ArchivedDebugNoteSchema, nn as number, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as string, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as encodeVectorBase64, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as hydrateSchema, r as BaseDevice, rn as object, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as unknown, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as literal, tt as VISIT_MERGE_GAP_MS, u as DETECTION_MACRO_CLASSES, ut as defineCustomActions, v as FailureCounters, w as MediaFileKindEnum, wt as kebabToCamel, x as MAX_BIRTH_DECISION_RECORDS, xt as isMediaOwnerType, y as LabelAttributionSchema, yt as isDetectionMacroClass, z as NcSystemEventKindSchema, zt as vectorDimFromBase64 } from "../dist-BDpucx_W.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -7438,6 +7438,16 @@ var NcAlertStore = class {
7438
7438
  };
7439
7439
  }
7440
7440
  };
7441
+ function deriveConsumableBand(input) {
7442
+ const { level, status } = input.reading;
7443
+ const measured = level !== null && Number.isFinite(level);
7444
+ if (!measured && status === null) return null;
7445
+ if (status === "replace") return "low";
7446
+ if (!measured) return "normal";
7447
+ const value = level;
7448
+ if (input.previous === "low") return value >= input.thresholdPercent + 5 ? "normal" : "low";
7449
+ return value <= input.thresholdPercent ? "low" : "normal";
7450
+ }
7441
7451
  //#endregion
7442
7452
  //#region src/notification-center/device-directory.ts
7443
7453
  /**
@@ -9838,6 +9848,40 @@ function incomingFromBatteryBand(input) {
9838
9848
  });
9839
9849
  }
9840
9850
  /**
9851
+ * A consumable BAND, as a system-event subject.
9852
+ *
9853
+ * Built exactly like {@link incomingFromBatteryBand} — the operator asked for
9854
+ * these to be treated as the battery notifications are — with two differences
9855
+ * that come from the subject and not from taste:
9856
+ *
9857
+ * - **the subject carries the ITEM.** `consumable:<deviceId>:<key>`, because a
9858
+ * vacuum has a brush AND a filter AND a mop pad, and one subject for all
9859
+ * three would let the brush's alert cancel the filter's.
9860
+ * - **the body variant follows what is KNOWN.** An item with no percentage
9861
+ * gets a finished sentence rather than a template with a stand-in number in
9862
+ * it, for the same reason a binary battery does.
9863
+ *
9864
+ * The recovery keeps the SAME subject: it is the same episode ending.
9865
+ */
9866
+ function incomingFromConsumableBand(input) {
9867
+ const kind = input.band === "low" ? "device-consumable-low" : "device-consumable-normal";
9868
+ const measured = input.item.level !== null && Number.isFinite(input.item.level);
9869
+ return systemSubject({
9870
+ facts: input.facts,
9871
+ kind,
9872
+ subject: `consumable:${input.deviceId}:${input.item.key}`,
9873
+ titleKey: `system.${kind}.title`,
9874
+ bodyKey: measured ? `system.${kind}.body` : `system.${kind}.body.unmeasured`,
9875
+ textParams: {
9876
+ ...deviceTextParams(input.deviceId, input.identity),
9877
+ item: input.item.label,
9878
+ ...measured ? { percentage: String(Math.round(input.item.level)) } : {}
9879
+ },
9880
+ deviceId: input.deviceId,
9881
+ ...input.identity !== void 0 ? { deviceType: input.identity.type } : {}
9882
+ });
9883
+ }
9884
+ /**
9841
9885
  * Normalize the infrastructure categories consumed by Notification Center.
9842
9886
  * Malformed payloads are rejected instead of guessing an identity.
9843
9887
  *
@@ -9989,6 +10033,7 @@ function incomingFromSystemEvent(event, lookup) {
9989
10033
  }
9990
10034
  case "system.restart-completed": return incomingFromServerUpdated(facts, data);
9991
10035
  case "recording.export.completed": return incomingFromExportCompleted(facts, data);
10036
+ case "backup.run-completed": return incomingFromBackupCompleted(facts, data);
9992
10037
  default: return null;
9993
10038
  }
9994
10039
  }
@@ -10152,6 +10197,37 @@ function incomingFromServerUpdated(facts, data) {
10152
10197
  nodeIds: [nodeId]
10153
10198
  });
10154
10199
  }
10200
+ /**
10201
+ * A finished backup RUN.
10202
+ *
10203
+ * Subject is the run, not the destination: `backup.completed` fires once per
10204
+ * destination and an operator with three of them would be told three times.
10205
+ * The counts travel in the text because a PARTIAL run must not read as a clean
10206
+ * one — two destinations of three is news, and a bare "backup completed" would
10207
+ * hide it.
10208
+ */
10209
+ function incomingFromBackupCompleted(facts, data) {
10210
+ const ok = numberField(data, "destinationsOk");
10211
+ const total = numberField(data, "destinationsTotal");
10212
+ if (ok === void 0 || total === void 0) return null;
10213
+ const failed = numberField(data, "destinationsFailed") ?? Math.max(0, total - ok);
10214
+ const sizeMB = numberField(data, "sizeMB");
10215
+ const backupId = stringField(data, "backupId");
10216
+ return systemSubject({
10217
+ facts,
10218
+ kind: "backup-completed",
10219
+ subject: `backup:${backupId ?? facts.timestamp}`,
10220
+ titleKey: "system.backup-completed.title",
10221
+ bodyKey: failed > 0 ? "system.backup-completed.body.partial" : "system.backup-completed.body",
10222
+ textParams: {
10223
+ destinationsOk: String(ok),
10224
+ destinationsTotal: String(total),
10225
+ destinationsFailed: String(failed),
10226
+ ...sizeMB !== void 0 ? { sizeMB: String(sizeMB) } : {},
10227
+ ...backupId !== void 0 ? { backupId } : {}
10228
+ }
10229
+ });
10230
+ }
10155
10231
  function incomingFromExportCompleted(facts, data) {
10156
10232
  const exportId = stringField(data, "exportId");
10157
10233
  if (exportId === void 0) return null;
@@ -10489,6 +10565,8 @@ var NcDeviceAlerts = class {
10489
10565
  * durable.
10490
10566
  */
10491
10567
  lastReading = /* @__PURE__ */ new Map();
10568
+ /** The last reading per consumable SUBJECT, for the reminder sweep. */
10569
+ lastConsumable = /* @__PURE__ */ new Map();
10492
10570
  constructor(deps) {
10493
10571
  this.deps = deps;
10494
10572
  this.now = deps.now ?? Date.now;
@@ -10526,6 +10604,44 @@ var NcDeviceAlerts = class {
10526
10604
  }));
10527
10605
  }
10528
10606
  /**
10607
+ * ONE consumable item, from a device's `consumables` slice.
10608
+ *
10609
+ * Per ITEM, never per device: a vacuum has a brush, a filter and a mop pad,
10610
+ * and folding them into one subject would make the brush's alert cancel the
10611
+ * filter's — the same collision D141 split `device-switch:<id>` off liveness
10612
+ * to avoid. Each item is its own episode, its own cooldown, its own recovery.
10613
+ *
10614
+ * An item that has told us nothing leaves EVERYTHING untouched, for the
10615
+ * reason `observeBattery` does: seeding it healthy is a claim, and the first
10616
+ * genuine `replace` would then read as a transition out of a state nobody
10617
+ * observed.
10618
+ */
10619
+ observeConsumable(deviceId, item) {
10620
+ const subject = `consumable:${deviceId}:${item.key}`;
10621
+ const previous = this.deps.rows.get(subject);
10622
+ const settings = this.deps.settings();
10623
+ const band = deriveConsumableBand({
10624
+ reading: {
10625
+ level: item.level,
10626
+ status: item.status
10627
+ },
10628
+ thresholdPercent: settings.consumableLowPercent,
10629
+ previous: previous?.alerting === true ? "low" : previous === void 0 ? void 0 : "normal"
10630
+ });
10631
+ if (band === null) return;
10632
+ this.lastConsumable.set(subject, {
10633
+ deviceId,
10634
+ item
10635
+ });
10636
+ this.apply(subject, band === "low", settings, (facts, alerting) => incomingFromConsumableBand({
10637
+ facts,
10638
+ deviceId,
10639
+ band: alerting ? "low" : "normal",
10640
+ item,
10641
+ identity: this.deps.lookup(deviceId)
10642
+ }));
10643
+ }
10644
+ /**
10529
10645
  * The reminder sweep. Called from an existing periodic tick; it allocates
10530
10646
  * nothing on the healthy path.
10531
10647
  *
@@ -10539,6 +10655,17 @@ var NcDeviceAlerts = class {
10539
10655
  if (settings.renotifyMinutes <= 0) return;
10540
10656
  for (const row of this.deps.rows.all()) {
10541
10657
  if (!row.alerting) continue;
10658
+ const consumable = this.lastConsumable.get(row.subject);
10659
+ if (consumable !== void 0) {
10660
+ this.apply(row.subject, true, settings, (facts) => incomingFromConsumableBand({
10661
+ facts,
10662
+ deviceId: consumable.deviceId,
10663
+ band: "low",
10664
+ item: consumable.item,
10665
+ identity: this.deps.lookup(consumable.deviceId)
10666
+ }));
10667
+ continue;
10668
+ }
10542
10669
  const restatement = this.lastReading.get(row.subject);
10543
10670
  if (restatement === void 0) continue;
10544
10671
  this.apply(row.subject, true, settings, (facts) => incomingFromBatteryBand({
@@ -11647,6 +11774,15 @@ var en_default = {
11647
11774
  "system.export-completed.title": "Export ready",
11648
11775
  "system.export-completed.body": "The footage export is ready",
11649
11776
  "system.export-completed.body.titled": "{{title}} is ready",
11777
+ "system.backup-completed.title": "Backup completed",
11778
+ "system.backup-completed.body": "Backup saved to {{destinationsOk}} of {{destinationsTotal}} destinations ({{sizeMB}} MB)",
11779
+ "system.backup-completed.body.partial": "Backup saved to {{destinationsOk}} of {{destinationsTotal}} destinations — {{destinationsFailed}} failed",
11780
+ "system.device-consumable-low.title": "Consumable low",
11781
+ "system.device-consumable-low.body": "{{item}} on {{deviceName}} is at {{percentage}}%",
11782
+ "system.device-consumable-low.body.unmeasured": "{{item}} on {{deviceName}} needs replacing",
11783
+ "system.device-consumable-normal.title": "Consumable replaced",
11784
+ "system.device-consumable-normal.body": "{{item}} on {{deviceName}} is back to {{percentage}}%",
11785
+ "system.device-consumable-normal.body.unmeasured": "{{item}} on {{deviceName}} is OK again",
11650
11786
  "system.camera-online.title": "Device online",
11651
11787
  "system.camera-online.body": "Device {{deviceName}} is online",
11652
11788
  "system.camera-offline.title": "Device offline",
@@ -11907,6 +12043,15 @@ var it_default = {
11907
12043
  "system.export-completed.title": "Export pronto",
11908
12044
  "system.export-completed.body": "L'export del filmato è pronto",
11909
12045
  "system.export-completed.body.titled": "{{title}} è pronto",
12046
+ "system.backup-completed.title": "Backup completato",
12047
+ "system.backup-completed.body": "Backup salvato su {{destinationsOk}} destinazioni su {{destinationsTotal}} ({{sizeMB}} MB)",
12048
+ "system.backup-completed.body.partial": "Backup salvato su {{destinationsOk}} destinazioni su {{destinationsTotal}} — {{destinationsFailed}} fallite",
12049
+ "system.device-consumable-low.title": "Consumabile quasi esaurito",
12050
+ "system.device-consumable-low.body": "{{item}} su {{deviceName}} è al {{percentage}}%",
12051
+ "system.device-consumable-low.body.unmeasured": "{{item}} su {{deviceName}} va sostituito",
12052
+ "system.device-consumable-normal.title": "Consumabile sostituito",
12053
+ "system.device-consumable-normal.body": "{{item}} su {{deviceName}} è tornato al {{percentage}}%",
12054
+ "system.device-consumable-normal.body.unmeasured": "{{item}} su {{deviceName}} è di nuovo a posto",
11910
12055
  "system.camera-online.title": "Dispositivo online",
11911
12056
  "system.camera-online.body": "Il dispositivo {{deviceName}} è online",
11912
12057
  "system.camera-offline.title": "Dispositivo offline",
@@ -22215,6 +22360,7 @@ function alarmButtonKind(kind) {
22215
22360
  */
22216
22361
  var NC_DEVICE_ALERT_FALLBACK = {
22217
22362
  batteryLowPercent: 20,
22363
+ consumableLowPercent: 15,
22218
22364
  renotifyMinutes: 720,
22219
22365
  notifyRecovery: true
22220
22366
  };
@@ -22247,6 +22393,52 @@ function batterySliceOf(event) {
22247
22393
  };
22248
22394
  }
22249
22395
  /**
22396
+ * A `device.state-changed` carrying a CONSUMABLES slice, reduced to the items
22397
+ * the band derivation reads.
22398
+ *
22399
+ * Same tap as the battery's: `DeviceStateChanged` is already subscribed, so
22400
+ * every `consumables` write already arrives here and was simply dropped. No
22401
+ * new bus category and no producer change.
22402
+ *
22403
+ * An item is kept only when it carries a usable `key` — the subject is built
22404
+ * from it, and an item with no key could not be told apart from its sibling.
22405
+ * `level` and `status` are each optional; an item with NEITHER is still
22406
+ * returned, because deciding that it has told us nothing belongs to
22407
+ * `deriveConsumableBand` and not to a parser (D393: absent is not zero, and
22408
+ * the parser is not the place that judges).
22409
+ */
22410
+ function consumablesSliceOf(event) {
22411
+ if (event.category !== "device.state-changed") return null;
22412
+ const data = event.data;
22413
+ if (data["capName"] !== "consumables") return null;
22414
+ const deviceId = data["deviceId"];
22415
+ const slice = data["slice"];
22416
+ if (typeof deviceId !== "number") return null;
22417
+ if (typeof slice !== "object" || slice === null || Array.isArray(slice)) return null;
22418
+ const rawItems = slice["items"];
22419
+ if (!Array.isArray(rawItems)) return null;
22420
+ const items = [];
22421
+ for (const entry of rawItems) {
22422
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
22423
+ const row = entry;
22424
+ const key = row["key"];
22425
+ if (typeof key !== "string" || key.length === 0) continue;
22426
+ const level = row["level"];
22427
+ const status = row["status"];
22428
+ const label = row["label"];
22429
+ items.push({
22430
+ key,
22431
+ label: typeof label === "string" && label.length > 0 ? label : key,
22432
+ level: typeof level === "number" ? level : null,
22433
+ status: status === "ok" || status === "replace" ? status : null
22434
+ });
22435
+ }
22436
+ return items.length === 0 ? null : {
22437
+ deviceId,
22438
+ items
22439
+ };
22440
+ }
22441
+ /**
22250
22442
  * What `getAlarmConfig` answers on a node with no panel.
22251
22443
  *
22252
22444
  * The DEFAULTS, not zeroes: paired with `deviceId: null` this tells a client
@@ -24295,6 +24487,11 @@ var NotificationCenter = class NotificationCenter {
24295
24487
  this.deviceAlerts.observeBattery(battery.deviceId, battery.reading);
24296
24488
  return;
24297
24489
  }
24490
+ const consumables = consumablesSliceOf(event);
24491
+ if (consumables !== null) {
24492
+ for (const item of consumables.items) this.deviceAlerts.observeConsumable(consumables.deviceId, item);
24493
+ return;
24494
+ }
24298
24495
  const lookup = this.deviceDirectory.lookup();
24299
24496
  const incoming = incomingFromSystemEvent(event, lookup);
24300
24497
  if (incoming === null) {
@@ -25907,6 +26104,7 @@ var NotificationCenter = class NotificationCenter {
25907
26104
  deviceAlertSettings() {
25908
26105
  return this.deps.readDeviceAlertSettings?.() ?? {
25909
26106
  batteryLowPercent: NC_DEVICE_ALERT_FALLBACK.batteryLowPercent,
26107
+ consumableLowPercent: NC_DEVICE_ALERT_FALLBACK.consumableLowPercent,
25910
26108
  renotifyMinutes: NC_DEVICE_ALERT_FALLBACK.renotifyMinutes,
25911
26109
  notifyRecovery: NC_DEVICE_ALERT_FALLBACK.notifyRecovery
25912
26110
  };
@@ -26522,6 +26720,7 @@ function systemEventSubject(kind, scope) {
26522
26720
  if (kind === "addon-updated") return `packages:addon-updated:${scope.packageName ?? "pkg"}`;
26523
26721
  if (kind === "server-updated") return `server-updated:${scope.nodeId ?? "hub"}`;
26524
26722
  if (kind === "export-completed") return `export:${scope.deviceId ?? 0}`;
26723
+ if (kind === "backup-completed") return "backup:test";
26525
26724
  return `alarm:${scope.deviceId ?? 0}`;
26526
26725
  }
26527
26726
  /**
@@ -49873,6 +50072,19 @@ var DeviceAlertSettingsSchema = object({
49873
50072
  */
49874
50073
  batteryLowPercent: number().int().min(1).max(99).default(20),
49875
50074
  /**
50075
+ * What counts as a nearly-exhausted CONSUMABLE — a filter, a brush, a mop pad.
50076
+ *
50077
+ * Its own number and not the battery's, because the two are different chores
50078
+ * on different clocks: a battery at 20 % is hours from flat, a brush at 20 %
50079
+ * is weeks from worn, and an operator who wants to be told early about one
50080
+ * does not necessarily want to be told early about the other.
50081
+ *
50082
+ * Applies to the MEASURED side only. An item whose provider states a
50083
+ * `replace` verdict is low whatever this says — the provider decided, and a
50084
+ * threshold cannot talk it out of it (`alerts/consumable-band.ts`).
50085
+ */
50086
+ consumableLowPercent: number().int().min(1).max(99).default(15),
50087
+ /**
49876
50088
  * How long a still-true alert waits before it says itself again.
49877
50089
  *
49878
50090
  * **0 = say it once.** That is the behaviour of every other notification this
@@ -58518,7 +58730,9 @@ function stationaryEvidenceKey(deviceId, className, anchorId) {
58518
58730
  * refused is the FIRST thing it failed, not the last.
58519
58731
  */
58520
58732
  function evaluateStationaryEvidence(row, policy, classEnabled) {
58521
- const ratio = row.seen > 0 ? row.agreed / row.seen : 0;
58733
+ const windowSeen = row.windowSeen + row.prevWindowSeen;
58734
+ const windowAgreed = row.windowAgreed + row.prevWindowAgreed;
58735
+ const ratio = windowSeen > 0 ? windowAgreed / windowSeen : 0;
58522
58736
  const spanMs = row.lastAgreedAt - row.firstAgreedAt;
58523
58737
  const base = {
58524
58738
  agreed: row.agreed,
@@ -58552,6 +58766,36 @@ function evaluateStationaryEvidence(row, policy, classEnabled) {
58552
58766
  };
58553
58767
  }
58554
58768
  /**
58769
+ * Slide the window forward to `now`.
58770
+ *
58771
+ * One bucket width past: the current bucket becomes the previous one and a
58772
+ * fresh one opens. TWO or more past: the candidate was not observed for the
58773
+ * whole window, so both buckets are empty — and an empty window answers `0`,
58774
+ * which is an honest "nothing recent to judge by" rather than a stale verdict.
58775
+ * The lifetime counters are untouched, so nothing a candidate has earned is
58776
+ * lost — only its claim about the present expires.
58777
+ */
58778
+ function rotateWindow(row, now) {
58779
+ const elapsed = now - row.windowStartedAt;
58780
+ if (elapsed < 9e5) return row;
58781
+ if (elapsed < 2 * 9e5) return {
58782
+ ...row,
58783
+ prevWindowAgreed: row.windowAgreed,
58784
+ prevWindowSeen: row.windowSeen,
58785
+ windowAgreed: 0,
58786
+ windowSeen: 0,
58787
+ windowStartedAt: now
58788
+ };
58789
+ return {
58790
+ ...row,
58791
+ prevWindowAgreed: 0,
58792
+ prevWindowSeen: 0,
58793
+ windowAgreed: 0,
58794
+ windowSeen: 0,
58795
+ windowStartedAt: now
58796
+ };
58797
+ }
58798
+ /**
58555
58799
  * The candidate this observation belongs to: the same-class row whose anchor it
58556
58800
  * overlaps most, provided the overlap reaches `agreeIou`. Overlap is the ONLY
58557
58801
  * notion of sameness here, and it is the same {@link iou} the suppression gate
@@ -58585,15 +58829,19 @@ function matchEvidenceCandidate(rows, className, bbox, agreeIou) {
58585
58829
  */
58586
58830
  function foldObservation(row, observation, agrees, overlap) {
58587
58831
  const seen = row.seen + 1;
58832
+ const rotated = rotateWindow(row, observation.timestamp);
58588
58833
  if (!agrees) return {
58589
- ...row,
58834
+ ...rotated,
58590
58835
  seen,
58836
+ windowSeen: rotated.windowSeen + 1,
58591
58837
  updatedAt: observation.timestamp
58592
58838
  };
58593
58839
  const improves = overlap >= iou(row.anchor, row.bestBbox);
58594
58840
  return {
58595
- ...row,
58841
+ ...rotated,
58596
58842
  seen,
58843
+ windowSeen: rotated.windowSeen + 1,
58844
+ windowAgreed: rotated.windowAgreed + 1,
58597
58845
  agreed: row.agreed + 1,
58598
58846
  firstAgreedAt: Math.min(row.firstAgreedAt, observation.timestamp),
58599
58847
  lastAgreedAt: Math.max(row.lastAgreedAt, observation.timestamp),
@@ -58617,6 +58865,11 @@ function newEvidenceRow(observation, anchorId) {
58617
58865
  anchor: { ...observation.bbox },
58618
58866
  agreed: 1,
58619
58867
  seen: 1,
58868
+ windowAgreed: 1,
58869
+ windowSeen: 1,
58870
+ prevWindowAgreed: 0,
58871
+ prevWindowSeen: 0,
58872
+ windowStartedAt: observation.timestamp,
58620
58873
  firstAgreedAt: observation.timestamp,
58621
58874
  lastAgreedAt: observation.timestamp,
58622
58875
  bestBbox: { ...observation.bbox },
@@ -58755,6 +59008,11 @@ var StationaryEvidenceLedger = class {
58755
59008
  anchor: { ...row.anchor },
58756
59009
  agreed: row.agreed,
58757
59010
  seen: row.seen,
59011
+ windowAgreed: row.windowAgreed,
59012
+ windowSeen: row.windowSeen,
59013
+ prevWindowAgreed: row.prevWindowAgreed,
59014
+ prevWindowSeen: row.prevWindowSeen,
59015
+ windowStartedAt: row.windowStartedAt,
58758
59016
  firstAgreedAt: row.firstAgreedAt,
58759
59017
  lastAgreedAt: row.lastAgreedAt,
58760
59018
  bestBbox: { ...row.bestBbox },
@@ -58783,6 +59041,11 @@ var StationaryEvidenceLedger = class {
58783
59041
  anchor,
58784
59042
  agreed: numberOr(data.agreed, 0),
58785
59043
  seen: numberOr(data.seen, 0),
59044
+ windowAgreed: numberOr(data.windowAgreed, 0),
59045
+ windowSeen: numberOr(data.windowSeen, 0),
59046
+ prevWindowAgreed: numberOr(data.prevWindowAgreed, 0),
59047
+ prevWindowSeen: numberOr(data.prevWindowSeen, 0),
59048
+ windowStartedAt: numberOr(data.windowStartedAt, updatedAt),
58786
59049
  firstAgreedAt: numberOr(data.firstAgreedAt, updatedAt),
58787
59050
  lastAgreedAt: numberOr(data.lastAgreedAt, updatedAt),
58788
59051
  bestBbox,
@@ -67487,7 +67750,8 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
67487
67750
  EventCategory.UpdateAvailable,
67488
67751
  EventCategory.AddonUpdated,
67489
67752
  EventCategory.SystemRestartCompleted,
67490
- EventCategory.RecordingExportCompleted
67753
+ EventCategory.RecordingExportCompleted,
67754
+ EventCategory.BackupRunCompleted
67491
67755
  ];
67492
67756
  /**
67493
67757
  * Install the addon's bus subscriptions. Identical set + relative order to the
@@ -70570,6 +70834,17 @@ function buildGlobalSettingsSchema(cap) {
70570
70834
  default: DEVICE_ALERT_DEFAULTS.batteryLowPercent,
70571
70835
  unit: "%"
70572
70836
  },
70837
+ {
70838
+ type: "number",
70839
+ key: "consumableLowPercent",
70840
+ label: "Low consumable at",
70841
+ 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.",
70842
+ min: 1,
70843
+ max: 99,
70844
+ step: 1,
70845
+ default: DEVICE_ALERT_DEFAULTS.consumableLowPercent,
70846
+ unit: "%"
70847
+ },
70573
70848
  {
70574
70849
  type: "number",
70575
70850
  key: "alertRenotifyMinutes",
@@ -76163,6 +76438,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
76163
76438
  },
76164
76439
  readDeviceAlertSettings: () => ({
76165
76440
  batteryLowPercent: this.deviceAlertSettingsMirror.batteryLowPercent,
76441
+ consumableLowPercent: this.deviceAlertSettingsMirror.consumableLowPercent,
76166
76442
  renotifyMinutes: this.deviceAlertSettingsMirror.alertRenotifyMinutes,
76167
76443
  notifyRecovery: this.deviceAlertSettingsMirror.alertNotifyRecovery
76168
76444
  }),
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BvU7JaBW.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DIQIycqg.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.233",
3
+ "version": "1.2.235",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",