@camstack/addon-post-analysis 1.2.629 → 1.2.630

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-M_QM7l6j.js → clip-model-registry-DB0TSd14.js} +141 -86
  2. package/dist/{clip-model-registry-SrfKJ0e8.mjs → clip-model-registry-wZ4Nt5jo.mjs} +132 -83
  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-CUsaUp1g.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BjktQPk_.mjs} +2 -2
  7. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BxPKSyZo.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-DdbV-R1z.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-q1u97PmF.mjs → hostInit-BMCiXaWO.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +340 -266
  11. package/dist/pipeline-analytics/index.mjs +339 -265
  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-DxapkUgD.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-D7KeW_uB.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-M_QM7l6j.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-DB0TSd14.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");
@@ -9026,27 +9026,13 @@ var en_default = {
9026
9026
  "system.device-consumable-low.title": "Consumable low — {{deviceName}}",
9027
9027
  "system.device-consumable-low.body": "{{item}} on {{deviceName}} is at {{percentage}}%",
9028
9028
  "system.device-consumable-low.body.unmeasured": "{{item}} on {{deviceName}} needs replacing",
9029
+ "system.device-consumable-low.body.tank": "{{item}} on {{deviceName}} is nearly empty",
9030
+ "system.device-consumable-low.body.tank-full": "{{item}} on {{deviceName}} is full",
9031
+ "system.device-consumable-low.body.product": "{{item}} on {{deviceName}} is running out",
9032
+ "system.device-consumable-low.body.bin": "{{item}} on {{deviceName}} is full",
9029
9033
  "system.device-consumable-normal.title": "Consumable replaced — {{deviceName}}",
9030
9034
  "system.device-consumable-normal.body": "{{item}} on {{deviceName}} is back to {{percentage}}%",
9031
9035
  "system.device-consumable-normal.body.unmeasured": "{{item}} on {{deviceName}} is OK again",
9032
- "system.device-tank-low.title": "Tank low — {{deviceName}}",
9033
- "system.device-tank-full.title": "Tank full — {{deviceName}}",
9034
- "system.device-tank-normal.title": "Tank OK — {{deviceName}}",
9035
- "system.device-tank-low.body.cleanWater": "The clean water tank on {{deviceName}} is running low",
9036
- "system.device-tank-full.body.cleanWater": "The clean water tank on {{deviceName}} is full",
9037
- "system.device-tank-normal.body.cleanWater": "The clean water tank on {{deviceName}} is OK again",
9038
- "system.device-tank-low.body.dirtyWater": "The dirty water tank on {{deviceName}} is running low",
9039
- "system.device-tank-full.body.dirtyWater": "The dirty water tank on {{deviceName}} is full",
9040
- "system.device-tank-normal.body.dirtyWater": "The dirty water tank on {{deviceName}} is OK again",
9041
- "system.device-tank-low.body.detergent": "The detergent tank on {{deviceName}} is running low",
9042
- "system.device-tank-full.body.detergent": "The detergent tank on {{deviceName}} is full",
9043
- "system.device-tank-normal.body.detergent": "The detergent tank on {{deviceName}} is OK again",
9044
- "system.device-tank-low.body.dustBin": "The dust bin on {{deviceName}} is running low",
9045
- "system.device-tank-full.body.dustBin": "The dust bin on {{deviceName}} is full",
9046
- "system.device-tank-normal.body.dustBin": "The dust bin on {{deviceName}} is OK again",
9047
- "system.device-tank-low.body.item": "{{item}} on {{deviceName}} is running low",
9048
- "system.device-tank-full.body.item": "{{item}} on {{deviceName}} is full",
9049
- "system.device-tank-normal.body.item": "{{item}} on {{deviceName}} is OK again",
9050
9036
  "system.camera-online.title": "Device online",
9051
9037
  "system.camera-online.body": "Device {{deviceName}} is online",
9052
9038
  "system.camera-offline.title": "Device offline",
@@ -9201,8 +9187,7 @@ var en_default = {
9201
9187
  "summary.section.device-state": "Offline or off",
9202
9188
  "summary.section.streams": "Streams offline",
9203
9189
  "summary.section.battery": "Low batteries",
9204
- "summary.section.consumables": "Consumables to replace",
9205
- "summary.section.tanks": "Tanks low",
9190
+ "summary.section.consumables": "Consumables",
9206
9191
  "summary.section.detection-blind": "Detecting nothing",
9207
9192
  "summary.section.nodes": "Nodes with problems",
9208
9193
  "summary.section.addons": "Addons stopped",
@@ -9220,7 +9205,12 @@ var en_default = {
9220
9205
  "summary.line.addon-crash-loop": "{{nodeId}} — {{addons}} stopped after repeated crashes (since {{since}})",
9221
9206
  "summary.line.update": "{{packageName}} {{currentVersion}} → {{latestVersion}}",
9222
9207
  "summary.line.wrapper-update": "Container {{currentVersion}} → {{latestVersion}}",
9223
- "summary.line.tank-unread": "{{deviceName}} — the {{tank}} tank needs attention",
9208
+ "summary.line.consumable.part": "{{item}} {{percentage}}%",
9209
+ "summary.line.consumable.part.unmeasured": "{{item}} needs replacing",
9210
+ "summary.line.consumable.tank": "{{item}} nearly empty",
9211
+ "summary.line.consumable.tank-full": "{{item}} full",
9212
+ "summary.line.consumable.product": "{{item}} running out",
9213
+ "summary.line.consumable.bin": "{{item}} full",
9224
9214
  "calendar-events.title": "{{rule}}: {{count}} upcoming",
9225
9215
  "calendar-events.empty": "No events in the next {{horizonDays}} day(s).",
9226
9216
  "calendar-events.day.today": "Today — {{date}}",
@@ -9432,27 +9422,13 @@ var it_default = {
9432
9422
  "system.device-consumable-low.title": "Consumabile basso — {{deviceName}}",
9433
9423
  "system.device-consumable-low.body": "{{item}} su {{deviceName}} è al {{percentage}}%",
9434
9424
  "system.device-consumable-low.body.unmeasured": "{{item}} su {{deviceName}} va sostituito",
9425
+ "system.device-consumable-low.body.tank": "{{item}} di {{deviceName}} è quasi vuota",
9426
+ "system.device-consumable-low.body.tank-full": "{{item}} di {{deviceName}} è piena",
9427
+ "system.device-consumable-low.body.product": "{{item}} di {{deviceName}} è in esaurimento",
9428
+ "system.device-consumable-low.body.bin": "{{item}} di {{deviceName}} è pieno",
9435
9429
  "system.device-consumable-normal.title": "Consumabile sostituito — {{deviceName}}",
9436
9430
  "system.device-consumable-normal.body": "{{item}} su {{deviceName}} è tornato al {{percentage}}%",
9437
9431
  "system.device-consumable-normal.body.unmeasured": "{{item}} su {{deviceName}} è di nuovo a posto",
9438
- "system.device-tank-low.title": "Serbatoio quasi vuoto — {{deviceName}}",
9439
- "system.device-tank-full.title": "Serbatoio pieno — {{deviceName}}",
9440
- "system.device-tank-normal.title": "Serbatoio a posto — {{deviceName}}",
9441
- "system.device-tank-low.body.cleanWater": "Il serbatoio dell'acqua pulita di {{deviceName}} è quasi vuoto",
9442
- "system.device-tank-full.body.cleanWater": "Il serbatoio dell'acqua pulita di {{deviceName}} è pieno",
9443
- "system.device-tank-normal.body.cleanWater": "Il serbatoio dell'acqua pulita di {{deviceName}} è di nuovo a posto",
9444
- "system.device-tank-low.body.dirtyWater": "Il serbatoio dell'acqua sporca di {{deviceName}} è quasi vuoto",
9445
- "system.device-tank-full.body.dirtyWater": "Il serbatoio dell'acqua sporca di {{deviceName}} è pieno",
9446
- "system.device-tank-normal.body.dirtyWater": "Il serbatoio dell'acqua sporca di {{deviceName}} è di nuovo a posto",
9447
- "system.device-tank-low.body.detergent": "Il serbatoio del detergente di {{deviceName}} è quasi vuoto",
9448
- "system.device-tank-full.body.detergent": "Il serbatoio del detergente di {{deviceName}} è pieno",
9449
- "system.device-tank-normal.body.detergent": "Il serbatoio del detergente di {{deviceName}} è di nuovo a posto",
9450
- "system.device-tank-low.body.dustBin": "Il contenitore della polvere di {{deviceName}} è quasi vuoto",
9451
- "system.device-tank-full.body.dustBin": "Il contenitore della polvere di {{deviceName}} è pieno",
9452
- "system.device-tank-normal.body.dustBin": "Il contenitore della polvere di {{deviceName}} è di nuovo a posto",
9453
- "system.device-tank-low.body.item": "{{item}} di {{deviceName}} sta finendo",
9454
- "system.device-tank-full.body.item": "{{item}} di {{deviceName}} è pieno",
9455
- "system.device-tank-normal.body.item": "{{item}} di {{deviceName}} è di nuovo a posto",
9456
9432
  "system.camera-online.title": "Dispositivo online",
9457
9433
  "system.camera-online.body": "Il dispositivo {{deviceName}} è online",
9458
9434
  "system.camera-offline.title": "Dispositivo offline",
@@ -9607,8 +9583,7 @@ var it_default = {
9607
9583
  "summary.section.device-state": "Offline o spenti",
9608
9584
  "summary.section.streams": "Stream offline",
9609
9585
  "summary.section.battery": "Batterie scariche",
9610
- "summary.section.consumables": "Consumabili da sostituire",
9611
- "summary.section.tanks": "Serbatoi bassi",
9586
+ "summary.section.consumables": "Consumabili",
9612
9587
  "summary.section.detection-blind": "Senza detection",
9613
9588
  "summary.section.nodes": "Nodi con problemi",
9614
9589
  "summary.section.addons": "Addon fermati",
@@ -9626,7 +9601,12 @@ var it_default = {
9626
9601
  "summary.line.addon-crash-loop": "{{nodeId}} — {{addons}} fermati dopo crash ripetuti (da {{since}})",
9627
9602
  "summary.line.update": "{{packageName}} {{currentVersion}} → {{latestVersion}}",
9628
9603
  "summary.line.wrapper-update": "Container {{currentVersion}} → {{latestVersion}}",
9629
- "summary.line.tank-unread": "{{deviceName}} — il serbatoio {{tank}} richiede attenzione",
9604
+ "summary.line.consumable.part": "{{item}} {{percentage}}%",
9605
+ "summary.line.consumable.part.unmeasured": "{{item}} da sostituire",
9606
+ "summary.line.consumable.tank": "{{item}} quasi vuota",
9607
+ "summary.line.consumable.tank-full": "{{item}} piena",
9608
+ "summary.line.consumable.product": "{{item}} in esaurimento",
9609
+ "summary.line.consumable.bin": "{{item}} pieno",
9630
9610
  "calendar-events.title": "{{rule}}: {{count}} in arrivo",
9631
9611
  "calendar-events.empty": "Nessun evento nei prossimi {{horizonDays}} giorno/i.",
9632
9612
  "calendar-events.day.today": "Oggi — {{date}}",
@@ -11908,9 +11888,9 @@ function systemEventKeys(kind) {
11908
11888
  titleKey: `system.${kind}.title`,
11909
11889
  bodyKey: `system.${kind}.body.ignored`
11910
11890
  };
11911
- if (require_clip_model_registry.NC_TANK_SYSTEM_EVENT_KINDS.includes(kind)) return {
11891
+ if (kind === "device-consumable-low") return {
11912
11892
  titleKey: `system.${kind}.title`,
11913
- bodyKey: `system.${kind}.body.cleanWater`
11893
+ bodyKey: "system.device-consumable-low.body.tank"
11914
11894
  };
11915
11895
  if (kind === "rule-broken") return {
11916
11896
  titleKey: `system.${kind}.title`,
@@ -71570,6 +71550,25 @@ Object.freeze({
71570
71550
  "path": "reason",
71571
71551
  "valueType": "string"
71572
71552
  }],
71553
+ "running-state": [
71554
+ {
71555
+ "path": "running",
71556
+ "valueType": "boolean"
71557
+ },
71558
+ {
71559
+ "path": "since",
71560
+ "valueType": "number"
71561
+ },
71562
+ {
71563
+ "path": "source",
71564
+ "valueType": "enum",
71565
+ "enumValues": ["native", "assigned"]
71566
+ },
71567
+ {
71568
+ "path": "phase",
71569
+ "valueType": "string"
71570
+ }
71571
+ ],
71573
71572
  "script-runner": [
71574
71573
  {
71575
71574
  "path": "isRunning",
@@ -81077,83 +81076,6 @@ function deriveConsumableBand(input) {
81077
81076
  return value <= input.thresholdPercent ? "low" : "normal";
81078
81077
  }
81079
81078
  //#endregion
81080
- //#region src/notification-center/alerts/tank-band.ts
81081
- /**
81082
- * A TANK reduced to the only thing worth storing: its band (D642).
81083
- *
81084
- * A tank is a `supply` or `waste` item of a device's `consumables` slice
81085
- * (D915 — before it, the four vacuum tanks lived on `vacuum-control`): a
81086
- * vacuum's clean water, dirty water, detergent and dust bin, a dishwasher's
81087
- * salt and rinse aid. `consumable-routing.ts` narrows the item's status to the
81088
- * band vocabulary below. The operator asked for them to notify like
81089
- * consumables: a supply running low, a waste tank full.
81090
- *
81091
- * ## The verdict is the provider's
81092
- *
81093
- * A tank's `status` is the device's own sensor (`ok` | `low` | `full`),
81094
- * and it is the ONLY thing judged. The hub has no threshold for a tank level:
81095
- * the rule for thresholds is that they belong to whoever generates the event,
81096
- * and a tank that says `low` has already decided. A tank that reports only a
81097
- * `level` is therefore not judged at all (`null`) rather than judged against a
81098
- * number the hub would have to invent.
81099
- *
81100
- * ## `null` is the third answer, and it is not `normal`
81101
- *
81102
- * An unknown status says nothing — it neither opens nor closes an episode, for
81103
- * the reason `battery-band.ts` gives: a reading nobody took is not evidence
81104
- * that anything changed.
81105
- *
81106
- * Pure. No clock, no I/O.
81107
- */
81108
- /**
81109
- * The tanks with a sentence of their own (`system.device-tank-*.body.<key>`),
81110
- * by their item key — the four a vacuum reports. Any other tank (salt, rinse
81111
- * aid, …) is worded by its label (`body.item`).
81112
- */
81113
- var NC_TANK_KEYS = [
81114
- "cleanWater",
81115
- "dirtyWater",
81116
- "detergent",
81117
- "dustBin"
81118
- ];
81119
- function isNamedTankKey(key) {
81120
- return NC_TANK_KEYS.some((k) => k === key);
81121
- }
81122
- /**
81123
- * How long a tank change must HOLD before it counts (D642 review M2).
81124
- *
81125
- * A float switch sloshing at the edge of `low` / `full` flips its status back
81126
- * and forth while the robot moves; each flip is a durable transition, and
81127
- * without a hold each one is a raise and a recovery on the operator's phone.
81128
- * One minute is two reload ticks: long enough to outlast a slosh, short enough
81129
- * that a genuinely full dust bin is said within a minute and a half.
81130
- */
81131
- var NC_TANK_HOLD_MS = 6e4;
81132
- function deriveTankBand(reading) {
81133
- switch (reading.status) {
81134
- case "low": return "low";
81135
- case "full": return "full";
81136
- case "ok": return "normal";
81137
- case null: return null;
81138
- }
81139
- }
81140
- /** `tank:<deviceId>:<key>` — the subject of one tank's episode. */
81141
- function tankSubject(deviceId, key) {
81142
- return `tank:${String(deviceId)}:${key}`;
81143
- }
81144
- /** The parts of a tank subject, or `null` when it is not one. */
81145
- function parseTankSubject(subject) {
81146
- const match = /^tank:(\d+):(.+)$/.exec(subject);
81147
- if (match === null) return null;
81148
- const deviceId = Number(match[1]);
81149
- const key = match[2];
81150
- if (!Number.isSafeInteger(deviceId) || key === void 0) return null;
81151
- return {
81152
- deviceId,
81153
- key
81154
- };
81155
- }
81156
- //#endregion
81157
81079
  //#region src/notification-center/device-directory.ts
81158
81080
  /**
81159
81081
  * WHO a device id is — the mirrored directory the system-event intake reads.
@@ -81915,6 +81837,7 @@ function incomingFromConsumableBand(input) {
81915
81837
  textParams: {
81916
81838
  ...deviceTextParams(input.deviceId, input.identity),
81917
81839
  item: input.item.label,
81840
+ form: "part",
81918
81841
  ...percentage !== null ? { percentage: String(percentage) } : {}
81919
81842
  },
81920
81843
  deviceId: input.deviceId,
@@ -81922,29 +81845,33 @@ function incomingFromConsumableBand(input) {
81922
81845
  ...input.identity !== void 0 ? { deviceType: input.identity.type } : {}
81923
81846
  });
81924
81847
  }
81848
+ /** Instant body keys for a consumable form (D934). */
81849
+ function consumableLowBodyKey(form, attention, measured) {
81850
+ if (form === "part") return measured ? "system.device-consumable-low.body" : "system.device-consumable-low.body.unmeasured";
81851
+ if (form === "tank") return attention === "full" ? "system.device-consumable-low.body.tank-full" : "system.device-consumable-low.body.tank";
81852
+ if (form === "product") return "system.device-consumable-low.body.product";
81853
+ return "system.device-consumable-low.body.bin";
81854
+ }
81925
81855
  /**
81926
- * A TANK band, as a system-event subject (D642, D915).
81856
+ * A supply / waste item judged on the provider's verdict (D934).
81927
81857
  *
81928
- * Built like {@link incomingFromConsumableBand}: `tank:<deviceId>:<tank>` is
81929
- * one subject per tank (the dust bin's alert must not cancel the clean water's),
81930
- * and the recovery keeps it. For the four named vacuum tanks the BODY VARIANT
81931
- * names the tank in the reader's language — the params are frozen before the
81932
- * language is known, so the tank's name cannot be a param; `tank` carries the
81933
- * stable token for a custom template. Any other tank (a dishwasher's salt, …)
81934
- * is worded by the provider's label through `body.item`.
81858
+ * Subject `consumable:<deviceId>:<key>` — no `consumableLowPercent` so rules
81859
+ * at 15 % still hear salt and water.
81935
81860
  */
81936
- function incomingFromTankBand(input) {
81937
- const kind = input.band === "low" ? "device-tank-low" : input.band === "full" ? "device-tank-full" : "device-tank-normal";
81861
+ function incomingFromProviderConsumableBand(input) {
81862
+ const kind = input.band === "low" ? "device-consumable-low" : "device-consumable-normal";
81863
+ const bodyKey = input.band === "low" ? consumableLowBodyKey(input.item.form, input.attention, false) : "system.device-consumable-normal.body.unmeasured";
81938
81864
  return systemSubject({
81939
81865
  facts: input.facts,
81940
81866
  kind,
81941
- subject: tankSubject(input.deviceId, input.tank),
81867
+ subject: legacyConsumableSubject(input.deviceId, input.item.key),
81942
81868
  titleKey: `system.${kind}.title`,
81943
- bodyKey: isNamedTankKey(input.tank) ? `system.${kind}.body.${input.tank}` : `system.${kind}.body.item`,
81869
+ bodyKey,
81944
81870
  textParams: {
81945
81871
  ...deviceTextParams(input.deviceId, input.identity),
81946
- tank: input.tank,
81947
- item: input.label ?? input.tank
81872
+ item: input.item.label,
81873
+ form: input.item.form,
81874
+ attention: input.attention
81948
81875
  },
81949
81876
  deviceId: input.deviceId,
81950
81877
  ...input.identity !== void 0 ? { deviceType: input.identity.type } : {}
@@ -82829,13 +82756,43 @@ function deliveryForKind(kind) {
82829
82756
  return kind;
82830
82757
  }
82831
82758
  //#endregion
82832
- //#region src/notification-center/alerts/open-alert.ts
82759
+ //#region src/notification-center/alerts/tank-band.ts
82833
82760
  /**
82834
- * The raise of an episode whose reading has not arrived since the restart —
82835
- * worded with what the row KNOWS: no invented percentage (the binary battery
82836
- * sentence), the item key as its name, and a tank kind that only the filters
82837
- * read (the summary words an unmeasured tank without a band).
82761
+ * How long a tank change must HOLD before it counts (D642 review M2).
82762
+ *
82763
+ * A float switch sloshing at the edge of `low` / `full` flips its status back
82764
+ * and forth while the robot moves; each flip is a durable transition, and
82765
+ * without a hold each one is a raise and a recovery on the operator's phone.
82766
+ * One minute is two reload ticks: long enough to outlast a slosh, short enough
82767
+ * that a genuinely full dust bin is said within a minute and a half.
82838
82768
  */
82769
+ var NC_TANK_HOLD_MS = 6e4;
82770
+ function deriveTankBand(reading) {
82771
+ switch (reading.status) {
82772
+ case "low": return "low";
82773
+ case "full": return "full";
82774
+ case "ok": return "normal";
82775
+ case null: return null;
82776
+ }
82777
+ }
82778
+ /** `tank:<deviceId>:<key>` — the subject of one tank's episode. */
82779
+ function tankSubject(deviceId, key) {
82780
+ return `tank:${String(deviceId)}:${key}`;
82781
+ }
82782
+ /** The parts of a tank subject, or `null` when it is not one. */
82783
+ function parseTankSubject(subject) {
82784
+ const match = /^tank:(\d+):(.+)$/.exec(subject);
82785
+ if (match === null) return null;
82786
+ const deviceId = Number(match[1]);
82787
+ const key = match[2];
82788
+ if (!Number.isSafeInteger(deviceId) || key === void 0) return null;
82789
+ return {
82790
+ deviceId,
82791
+ key
82792
+ };
82793
+ }
82794
+ //#endregion
82795
+ //#region src/notification-center/alerts/open-alert.ts
82839
82796
  function unmeasuredEpisode(input) {
82840
82797
  const deviceId = alertSubjectDeviceId(input.subject);
82841
82798
  if (deviceId === void 0) return null;
@@ -82853,7 +82810,23 @@ function unmeasuredEpisode(input) {
82853
82810
  identity
82854
82811
  });
82855
82812
  const parts = parseConsumableSubject(input.subject);
82856
- if (input.family === "consumable" && parts !== null) return incomingFromConsumableBand({
82813
+ if (parts !== null && parts.legacy) {
82814
+ const attention = input.detail === "full" ? "full" : "low";
82815
+ const form = providerFormFromDetail(input.detail);
82816
+ return incomingFromProviderConsumableBand({
82817
+ facts,
82818
+ deviceId,
82819
+ band: "low",
82820
+ item: {
82821
+ key: parts.key,
82822
+ label: parts.key,
82823
+ form
82824
+ },
82825
+ attention,
82826
+ identity
82827
+ });
82828
+ }
82829
+ if (parts !== null) return incomingFromConsumableBand({
82857
82830
  facts,
82858
82831
  deviceId,
82859
82832
  band: "low",
@@ -82867,25 +82840,30 @@ function unmeasuredEpisode(input) {
82867
82840
  identity
82868
82841
  });
82869
82842
  const tank = parseTankSubject(input.subject);
82870
- if (tank === null || tank.deviceId !== deviceId) return null;
82871
- return incomingFromTankBand({
82872
- facts,
82873
- deviceId,
82874
- band: input.detail === "full" ? "full" : "low",
82875
- tank: tank.key,
82876
- identity
82877
- });
82843
+ if (tank !== null && tank.deviceId === deviceId) {
82844
+ const attention = input.detail === "full" ? "full" : "low";
82845
+ return incomingFromProviderConsumableBand({
82846
+ facts,
82847
+ deviceId,
82848
+ band: "low",
82849
+ item: {
82850
+ key: tank.key,
82851
+ label: tank.key,
82852
+ form: "tank"
82853
+ },
82854
+ attention,
82855
+ identity
82856
+ });
82857
+ }
82858
+ return null;
82859
+ }
82860
+ function providerFormFromDetail(detail) {
82861
+ if (detail === "full") return "bin";
82862
+ return "tank";
82878
82863
  }
82879
- /**
82880
- * One alerting row as an open episode, or `null` when it cannot be worded (the
82881
- * caller logs it). A legacy consumable row is skipped: it is adopted or
82882
- * retired first. A tank whose band the row remembers is worded by it
82883
- * (review M6), even without a reading.
82884
- */
82885
82864
  function openAlertOf(row, input) {
82886
82865
  const parts = parseConsumableSubject(row.subject);
82887
- if (parts?.legacy === true) return null;
82888
- const family = parts !== null ? "consumable" : row.subject.startsWith("tank:") ? "tank" : "battery";
82866
+ const family = parts !== null || row.subject.startsWith("tank:") ? "consumable" : "battery";
82889
82867
  const subject = (input.restated ?? unmeasuredEpisode({
82890
82868
  subject: row.subject,
82891
82869
  family,
@@ -82894,7 +82872,7 @@ function openAlertOf(row, input) {
82894
82872
  ...row.detail !== void 0 ? { detail: row.detail } : {}
82895
82873
  }))?.subject.systemEvent;
82896
82874
  if (subject === void 0) return null;
82897
- const measured = input.restated !== null || family === "tank" && row.detail !== void 0;
82875
+ const measured = input.restated !== null || row.detail !== void 0 || parts !== null && !parts.legacy && subject.textParams?.["percentage"] !== void 0;
82898
82876
  return {
82899
82877
  family,
82900
82878
  since: row.since,
@@ -83010,8 +82988,8 @@ var NcDeviceAlerts = class {
83010
82988
  * reminder sweep and for judging a threshold a rule gains later (D642).
83011
82989
  */
83012
82990
  lastConsumable = /* @__PURE__ */ new Map();
83013
- /** The last reading per tank SUBJECT, for the reminder sweep. */
83014
- lastTank = /* @__PURE__ */ new Map();
82991
+ /** Provider-judged items, keyed by episode subject (`consumable:<id>:<key>`). */
82992
+ lastProvider = /* @__PURE__ */ new Map();
83015
82993
  /**
83016
82994
  * Tank changes on HOLD (review M2): a band that would move the episode,
83017
82995
  * first seen at `since`. It counts once it has held for `NC_TANK_HOLD_MS`,
@@ -83083,6 +83061,15 @@ var NcDeviceAlerts = class {
83083
83061
  * genuine `replace` would then read as a transition out of a state nobody
83084
83062
  * observed.
83085
83063
  */
83064
+ /** One item from the consumables slice — every form (D934). */
83065
+ observeConsumableItem(deviceId, sliceItem) {
83066
+ this.closeLegacyTankEpisode(deviceId, sliceItem.key);
83067
+ if (sliceItem.wear !== null) {
83068
+ this.observeConsumable(deviceId, sliceItem.wear);
83069
+ return;
83070
+ }
83071
+ this.observeProviderConsumable(deviceId, sliceItem);
83072
+ }
83086
83073
  observeConsumable(deviceId, item) {
83087
83074
  this.lastConsumable.set(itemKeyOf(deviceId, item.key), {
83088
83075
  deviceId,
@@ -83099,31 +83086,51 @@ var NcDeviceAlerts = class {
83099
83086
  const settings = this.deps.settings();
83100
83087
  for (const thresholdPercent of thresholds) this.judgeConsumable(deviceId, item, thresholdPercent, settings);
83101
83088
  }
83102
- /**
83103
- * ONE tank — a `supply` / `waste` item of a device's `consumables` slice
83104
- * (D642; read from `consumables` since D915, `consumable-routing.ts`).
83105
- *
83106
- * The provider's verdict is the band (`tank-band.ts`); an unknown status
83107
- * leaves everything untouched, an open episode included.
83108
- */
83109
- observeTank(deviceId, tank) {
83110
- const band = deriveTankBand(tank);
83089
+ /** A supply / waste / tank-form item — provider verdict only (D934). */
83090
+ observeProviderConsumable(deviceId, sliceItem) {
83091
+ const band = deriveTankBand(providerTankReading(sliceItem));
83111
83092
  if (band === null) return;
83112
- const subject = tankSubject(deviceId, tank.key);
83113
- this.lastTank.set(subject, {
83093
+ const subject = legacyConsumableSubject(deviceId, sliceItem.key);
83094
+ const attention = band === "full" ? "full" : "low";
83095
+ this.lastProvider.set(subject, {
83114
83096
  deviceId,
83115
- tank
83097
+ item: sliceItem,
83098
+ attention
83116
83099
  });
83117
83100
  if (!this.tankChangeHeld(subject, deviceId, band)) return;
83118
- this.apply(subject, band !== "normal", this.deps.settings(), (facts, alerting) => incomingFromTankBand({
83101
+ this.apply(subject, band !== "normal", this.deps.settings(), (facts, alerting) => incomingFromProviderConsumableBand({
83119
83102
  facts,
83120
83103
  deviceId,
83121
- band: alerting ? band : "normal",
83122
- tank: tank.key,
83123
- ...tank.label !== void 0 ? { label: tank.label } : {},
83104
+ band: alerting ? "low" : "normal",
83105
+ item: {
83106
+ key: sliceItem.key,
83107
+ label: sliceItem.label,
83108
+ form: sliceItem.form
83109
+ },
83110
+ attention,
83124
83111
  identity: this.deps.lookup(deviceId)
83125
83112
  }), band === "normal" ? void 0 : band);
83126
83113
  }
83114
+ /** A leftover `tank:<deviceId>:<key>` row is closed silently when the item is seen as a consumable. */
83115
+ closeLegacyTankEpisode(deviceId, key) {
83116
+ const subject = tankSubject(deviceId, key);
83117
+ const row = this.deps.rows.get(subject);
83118
+ if (row === void 0 || !row.alerting) return;
83119
+ this.logger()?.info("legacy tank episode closed — the consumables cap owns this item now (D934)", {
83120
+ tags: { deviceId },
83121
+ meta: {
83122
+ subject,
83123
+ key
83124
+ }
83125
+ });
83126
+ this.deps.rows.put({
83127
+ ...row,
83128
+ alerting: false
83129
+ });
83130
+ this.lastProvider.delete(legacyConsumableSubject(deviceId, key));
83131
+ this.tankHold.delete(subject);
83132
+ this.tankHold.delete(legacyConsumableSubject(deviceId, key));
83133
+ }
83127
83134
  /**
83128
83135
  * Has this tank band held long enough to move its episode? A band that
83129
83136
  * would not move it (a repeat, or low ⇄ full inside one open episode) passes
@@ -83164,12 +83171,12 @@ var NcDeviceAlerts = class {
83164
83171
  const nowMs = this.now();
83165
83172
  for (const [subject, held] of [...this.tankHold]) {
83166
83173
  if (nowMs - held.since < NC_TANK_HOLD_MS) continue;
83167
- const reading = this.lastTank.get(subject);
83174
+ const reading = this.lastProvider.get(subject);
83168
83175
  if (reading === void 0) {
83169
83176
  this.tankHold.delete(subject);
83170
83177
  continue;
83171
83178
  }
83172
- this.observeTank(reading.deviceId, reading.tank);
83179
+ this.observeProviderConsumable(reading.deviceId, reading.item);
83173
83180
  }
83174
83181
  }
83175
83182
  /** One item against ONE threshold: its own episode, its own hysteresis. */
@@ -83293,14 +83300,14 @@ var NcDeviceAlerts = class {
83293
83300
  const subjects = this.deps.rows.all().map((row) => row.subject).filter((subject) => alertSubjectDeviceId(subject) === deviceId);
83294
83301
  for (const subject of subjects) {
83295
83302
  this.lastReading.delete(subject);
83296
- this.lastTank.delete(subject);
83303
+ this.lastProvider.delete(subject);
83297
83304
  this.offeredAt.delete(subject);
83298
83305
  await this.deps.rows.remove(subject);
83299
83306
  }
83300
83307
  this.lastReading.delete(`battery:${deviceId}`);
83301
83308
  for (const [key, reading] of this.lastConsumable) if (reading.deviceId === deviceId) this.lastConsumable.delete(key);
83302
83309
  for (const subject of this.tankHold.keys()) if (alertSubjectDeviceId(subject) === deviceId) this.tankHold.delete(subject);
83303
- for (const subject of this.lastTank.keys()) if (alertSubjectDeviceId(subject) === deviceId) this.lastTank.delete(subject);
83310
+ for (const subject of this.lastProvider.keys()) if (alertSubjectDeviceId(subject) === deviceId) this.lastProvider.delete(subject);
83304
83311
  return subjects.length;
83305
83312
  }
83306
83313
  /**
@@ -83377,8 +83384,23 @@ var NcDeviceAlerts = class {
83377
83384
  /** How to re-word a subject's alert from the last reading, or `null`. */
83378
83385
  restatementOf(subject) {
83379
83386
  const parts = parseConsumableSubject(subject);
83387
+ if (parts !== null && parts.legacy) {
83388
+ const provider = this.lastProvider.get(subject);
83389
+ if (provider !== void 0) return (facts) => incomingFromProviderConsumableBand({
83390
+ facts,
83391
+ deviceId: provider.deviceId,
83392
+ band: "low",
83393
+ item: {
83394
+ key: provider.item.key,
83395
+ label: provider.item.label,
83396
+ form: provider.item.form
83397
+ },
83398
+ attention: provider.attention,
83399
+ identity: this.deps.lookup(provider.deviceId)
83400
+ });
83401
+ }
83380
83402
  const consumable = parts === null || parts.legacy ? void 0 : this.lastConsumable.get(itemKeyOf(parts.deviceId, parts.key));
83381
- if (parts !== null && consumable !== void 0) return (facts) => incomingFromConsumableBand({
83403
+ if (parts !== null && !parts.legacy && consumable !== void 0) return (facts) => incomingFromConsumableBand({
83382
83404
  facts,
83383
83405
  deviceId: consumable.deviceId,
83384
83406
  band: "low",
@@ -83386,16 +83408,6 @@ var NcDeviceAlerts = class {
83386
83408
  thresholdPercent: parts.thresholdPercent,
83387
83409
  identity: this.deps.lookup(consumable.deviceId)
83388
83410
  });
83389
- const tank = this.lastTank.get(subject);
83390
- const tankBand = tank === void 0 ? null : deriveTankBand(tank.tank);
83391
- if (tank !== void 0 && (tankBand === "low" || tankBand === "full")) return (facts) => incomingFromTankBand({
83392
- facts,
83393
- deviceId: tank.deviceId,
83394
- band: tankBand,
83395
- tank: tank.tank.key,
83396
- ...tank.tank.label !== void 0 ? { label: tank.tank.label } : {},
83397
- identity: this.deps.lookup(tank.deviceId)
83398
- });
83399
83411
  const battery = this.lastReading.get(subject);
83400
83412
  if (battery !== void 0) return (facts) => incomingFromBatteryBand({
83401
83413
  facts,
@@ -83528,8 +83540,52 @@ function tagsOf(subject) {
83528
83540
  const deviceId = alertSubjectDeviceId(subject);
83529
83541
  return deviceId !== void 0 ? { tags: { deviceId } } : {};
83530
83542
  }
83543
+ function providerTankReading(item) {
83544
+ const base = {
83545
+ key: item.key,
83546
+ label: item.label,
83547
+ level: item.level,
83548
+ status: null
83549
+ };
83550
+ const status = item.status;
83551
+ if (item.kind === "supply") switch (status) {
83552
+ case "low":
83553
+ case "empty":
83554
+ case "replace": return {
83555
+ ...base,
83556
+ status: "low"
83557
+ };
83558
+ case "ok":
83559
+ case "full": return {
83560
+ ...base,
83561
+ status: "ok"
83562
+ };
83563
+ default: return base;
83564
+ }
83565
+ if (item.kind === "waste") switch (status) {
83566
+ case "full":
83567
+ case "replace": return {
83568
+ ...base,
83569
+ status: "full"
83570
+ };
83571
+ case "ok":
83572
+ case "low":
83573
+ case "empty": return {
83574
+ ...base,
83575
+ status: "ok"
83576
+ };
83577
+ default: return base;
83578
+ }
83579
+ return base;
83580
+ }
83531
83581
  //#endregion
83532
83582
  //#region src/notification-center/alerts/consumable-routing.ts
83583
+ /**
83584
+ * The `consumables` slice — one vocabulary for every form (D934).
83585
+ *
83586
+ * Every item is a consumable reading: wear parts judged at the rule's threshold,
83587
+ * supply and waste judged on the provider's verdict (held like a float switch).
83588
+ */
83533
83589
  function wearStatus(status) {
83534
83590
  switch (status) {
83535
83591
  case "replace":
@@ -83539,59 +83595,44 @@ function wearStatus(status) {
83539
83595
  default: return null;
83540
83596
  }
83541
83597
  }
83542
- function supplyStatus(status) {
83543
- switch (status) {
83544
- case "low":
83545
- case "empty":
83546
- case "replace": return "low";
83547
- case "ok":
83548
- case "full": return "ok";
83549
- default: return null;
83550
- }
83598
+ function consumableKindOf(raw) {
83599
+ return typeof raw.kind === "string" ? raw.kind : void 0;
83551
83600
  }
83552
- function wasteStatus(status) {
83553
- switch (status) {
83554
- case "full":
83555
- case "replace": return "full";
83556
- case "ok":
83557
- case "low":
83558
- case "empty": return "ok";
83559
- default: return null;
83560
- }
83601
+ function consumableFormOfRaw(raw) {
83602
+ const kind = consumableKindOf(raw);
83603
+ const formField = typeof raw.form === "string" ? raw.form : void 0;
83604
+ return require_clip_model_registry.consumableFormOf({
83605
+ ...kind === "wear" || kind === "supply" || kind === "waste" ? { kind } : {},
83606
+ ...formField === "part" || formField === "tank" || formField === "product" || formField === "bin" ? { form: formField } : {}
83607
+ });
83561
83608
  }
83562
- /** Which family judges this item, and the reading that family reads. */
83563
- function routeConsumableItem(raw) {
83564
- const { key, label, level } = raw;
83565
- if (raw.kind === "supply" || raw.kind === "waste") return {
83566
- family: "tank",
83567
- tank: {
83568
- key,
83569
- label,
83570
- level,
83571
- status: raw.kind === "supply" ? supplyStatus(raw.status) : wasteStatus(raw.status)
83572
- }
83573
- };
83609
+ /** Parse one slice entry into the unified consumable item shape. */
83610
+ function parseConsumableSliceItem(raw) {
83611
+ const kind = consumableKindOf(raw);
83612
+ const form = consumableFormOfRaw(raw);
83613
+ const status = typeof raw.status === "string" ? raw.status : null;
83614
+ const wear = kind === void 0 || kind === "wear" ? {
83615
+ key: raw.key,
83616
+ label: raw.label,
83617
+ level: raw.level,
83618
+ status: wearStatus(status)
83619
+ } : null;
83574
83620
  return {
83575
- family: "consumable",
83576
- item: {
83577
- key,
83578
- label,
83579
- level,
83580
- status: wearStatus(raw.status)
83581
- }
83621
+ key: raw.key,
83622
+ label: raw.label,
83623
+ level: raw.level,
83624
+ status,
83625
+ kind,
83626
+ form,
83627
+ wear
83582
83628
  };
83583
83629
  }
83584
83630
  function isRecord$1(value) {
83585
83631
  return typeof value === "object" && value !== null && !Array.isArray(value);
83586
83632
  }
83587
83633
  /**
83588
- * A `device.state-changed` carrying a `consumables` slice, routed by family;
83589
- * `null` for any other event, a malformed slice or an empty list.
83590
- *
83591
- * An item is kept only when it carries a usable `key` — the subject is built
83592
- * from it. `level` and `status` are each optional; an item with NEITHER is
83593
- * still returned, because deciding that it has told us nothing belongs to the
83594
- * band derivation and not to a parser (D393: absent is not zero).
83634
+ * A `device.state-changed` carrying a `consumables` slice; `null` for any other
83635
+ * event, a malformed slice or an empty list.
83595
83636
  */
83596
83637
  function consumablesSliceOf(event) {
83597
83638
  if (event.category !== "device.state-changed") return null;
@@ -83602,8 +83643,7 @@ function consumablesSliceOf(event) {
83602
83643
  if (typeof deviceId !== "number" || !isRecord$1(slice)) return null;
83603
83644
  const rawItems = slice["items"];
83604
83645
  if (!Array.isArray(rawItems)) return null;
83605
- const consumables = [];
83606
- const tanks = [];
83646
+ const items = [];
83607
83647
  for (const entry of rawItems) {
83608
83648
  if (!isRecord$1(entry)) continue;
83609
83649
  const key = entry["key"];
@@ -83612,21 +83652,20 @@ function consumablesSliceOf(event) {
83612
83652
  const level = entry["level"];
83613
83653
  const status = entry["status"];
83614
83654
  const kind = entry["kind"];
83615
- const routed = routeConsumableItem({
83655
+ const form = entry["form"];
83656
+ items.push(parseConsumableSliceItem({
83616
83657
  key,
83617
83658
  label: typeof label === "string" && label.length > 0 ? label : key,
83618
83659
  level: typeof level === "number" && Number.isFinite(level) ? level : null,
83619
83660
  status: typeof status === "string" ? status : null,
83620
- kind: typeof kind === "string" ? kind : void 0
83621
- });
83622
- if (routed.family === "tank") tanks.push(routed.tank);
83623
- else consumables.push(routed.item);
83661
+ kind: typeof kind === "string" ? kind : void 0,
83662
+ form: typeof form === "string" ? form : void 0
83663
+ }));
83624
83664
  }
83625
- if (consumables.length === 0 && tanks.length === 0) return null;
83665
+ if (items.length === 0) return null;
83626
83666
  return {
83627
83667
  deviceId,
83628
- consumables,
83629
- tanks
83668
+ items
83630
83669
  };
83631
83670
  }
83632
83671
  //#endregion
@@ -85274,12 +85313,10 @@ var NcDeviceStateCache = class {
85274
85313
  * a job that COMPLETED, and the state it leaves behind is not a condition that
85275
85314
  * is still true. The ADR records why it is excluded rather than approximated.
85276
85315
  */
85277
- /** One source: the cap's Zod enum (D698 LOW-6). */
85278
- var NC_STATE_SUMMARY_SECTION_IDS = require_clip_model_registry.NcStateSummarySectionIdSchema.options;
85279
85316
  /** The kind groups that have no section, and why — said on the preview. */
85280
85317
  var NC_STATE_SUMMARY_EXCLUDED_GROUPS = { "backups-exports": "a backup or an export is a completed job, not a state that is still true" };
85281
85318
  function isStateSectionId(value) {
85282
- return NC_STATE_SUMMARY_SECTION_IDS.some((id) => id === value);
85319
+ return require_clip_model_registry.NC_STATE_SUMMARY_SECTION_IDS.some((id) => id === value);
85283
85320
  }
85284
85321
  /** The template variable that carries one section's lines (`template-vars.ts`). */
85285
85322
  var NC_STATE_SECTION_VAR = {
@@ -85287,7 +85324,6 @@ var NC_STATE_SECTION_VAR = {
85287
85324
  streams: "summaryStreams",
85288
85325
  battery: "summaryBattery",
85289
85326
  consumables: "summaryConsumables",
85290
- tanks: "summaryTanks",
85291
85327
  "detection-blind": "summaryDetection",
85292
85328
  nodes: "summaryNodes",
85293
85329
  addons: "summaryAddons",
@@ -85305,11 +85341,13 @@ function sectionKinds(section) {
85305
85341
  * direction, only the event did.
85306
85342
  */
85307
85343
  function sectionsOfRule(condition) {
85308
- return NC_STATE_SUMMARY_SECTION_IDS.filter((section) => sectionKinds(section).some((kind) => condition.kinds.includes(kind)));
85344
+ const kinds = condition.kinds;
85345
+ return require_clip_model_registry.NC_STATE_SUMMARY_SECTION_IDS.filter((section) => sectionKinds(section).some((kind) => kinds.includes(kind)));
85309
85346
  }
85310
85347
  /** The kind groups the rule selected that have no section. */
85311
85348
  function excludedGroupsOfRule(condition) {
85312
- return require_clip_model_registry.NC_SYSTEM_EVENT_KIND_GROUP_SPECS.filter((group) => NC_STATE_SUMMARY_EXCLUDED_GROUPS[group.id] !== void 0 && group.kinds.some((kind) => condition.kinds.includes(kind))).map((group) => group.id);
85349
+ const kinds = condition.kinds;
85350
+ return require_clip_model_registry.NC_SYSTEM_EVENT_KIND_GROUP_SPECS.filter((group) => NC_STATE_SUMMARY_EXCLUDED_GROUPS[group.id] !== void 0 && group.kinds.some((kind) => kinds.includes(kind))).map((group) => group.id);
85313
85351
  }
85314
85352
  /**
85315
85353
  * The condition the summary judges its items with: the rule's own, with its
@@ -90056,6 +90094,12 @@ function classCounts(members) {
90056
90094
  * params go last because they are the row's own truth about a system event and
90057
90095
  * outrank the live vocabulary for exactly those names.
90058
90096
  */
90097
+ /** Intake-only keys for body variant selection — not operator template vars (D934). */
90098
+ function systemEventParamsForTemplate(params) {
90099
+ if (params === void 0) return {};
90100
+ const { form: _form, attention: _attention, ...rest } = params;
90101
+ return rest;
90102
+ }
90059
90103
  function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
90060
90104
  const subject = entry.payload.subject;
90061
90105
  const occupancy = subject.occupancy;
@@ -90103,7 +90147,7 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
90103
90147
  latestVersion: typeof systemEvent?.data["latestVersion"] === "string" ? systemEvent.data["latestVersion"] : "",
90104
90148
  ...systemEvent?.deviceId !== void 0 ? { deviceName: String(systemEvent.deviceId) } : {},
90105
90149
  ...systemEvent?.nodeId !== void 0 && (systemEvent.kind === "node-online" || systemEvent.kind === "node-offline") ? { nodeName: systemEvent.nodeId } : {},
90106
- ...systemEvent?.textParams
90150
+ ...systemEventParamsForTemplate(systemEvent?.textParams)
90107
90151
  };
90108
90152
  }
90109
90153
  /**
@@ -99566,15 +99610,30 @@ function deviceParams(deviceId, identity) {
99566
99610
  deviceName: name.length > 0 ? name : String(deviceId)
99567
99611
  };
99568
99612
  }
99569
- function alertItem(section, open) {
99613
+ function consumableSummaryLineKey(params, measured) {
99614
+ const form = params["form"] ?? "part";
99615
+ if (form === "part") return measured && params["percentage"] !== void 0 ? "summary.line.consumable.part" : "summary.line.consumable.part.unmeasured";
99616
+ if (form === "tank") return params["attention"] === "full" ? "summary.line.consumable.tank-full" : "summary.line.consumable.tank";
99617
+ if (form === "product") return "summary.line.consumable.product";
99618
+ return "summary.line.consumable.bin";
99619
+ }
99620
+ function alertItem(section, open, lookup) {
99570
99621
  const params = open.subject.textParams ?? {};
99571
- const unreadTank = open.family === "tank" && !open.measured;
99622
+ const lineKey = section === "consumables" ? consumableSummaryLineKey(params, open.measured) : open.subject.bodyKey ?? "";
99623
+ const lineParams = section === "consumables" ? (({ deviceName: _deviceName, ...rest }) => rest)(params) : params;
99624
+ const deviceId = open.subject.deviceId;
99625
+ const identity = deviceId !== void 0 ? lookup(deviceId) : void 0;
99626
+ const deviceName = params["deviceName"] ?? (identity !== void 0 && identity.name.length > 0 ? identity.name : deviceId !== void 0 ? String(deviceId) : void 0);
99572
99627
  return {
99573
99628
  section,
99574
99629
  subject: open.subject,
99575
- lineKey: unreadTank ? "summary.line.tank-unread" : open.subject.bodyKey ?? "",
99576
- params,
99577
- since: open.since
99630
+ lineKey,
99631
+ params: lineParams,
99632
+ since: open.since,
99633
+ ...section === "consumables" && deviceId !== void 0 && deviceName !== void 0 ? { device: {
99634
+ id: deviceId,
99635
+ name: deviceName
99636
+ } } : {}
99578
99637
  };
99579
99638
  }
99580
99639
  function livenessItem(row, lookup) {
@@ -99703,7 +99762,7 @@ function stateItem(row, lookup, nodes) {
99703
99762
  * (`parentDeviceId` set) still owns those alerts — filtering every accessory
99704
99763
  * here dropped every tank line of the 2026-10-05 evening summary for
99705
99764
  * deviceId 4385 while the episode was open and the instant path had already
99706
- * held `device-tank-low` for that rule.
99765
+ * held a legacy tank kind for that rule.
99707
99766
  */
99708
99767
  function stillTrue(item, ports) {
99709
99768
  const deviceId = item.subject.deviceId;
@@ -99743,16 +99802,16 @@ function readSection(section, ports, nowMs) {
99743
99802
  }
99744
99803
  /** Which ports a section needs, and why each one is unavailable. */
99745
99804
  function readRaw(section, ports, nowMs) {
99746
- if (section === "battery" || section === "consumables" || section === "tanks") {
99805
+ if (section === "battery" || section === "consumables") {
99747
99806
  const alerts = ports.alerts();
99748
99807
  if (alerts === null) return {
99749
99808
  answered: false,
99750
99809
  reason: UNREAD_ALERTS
99751
99810
  };
99752
- const family = section === "battery" ? "battery" : section === "tanks" ? "tank" : "consumable";
99811
+ const family = section === "battery" ? "battery" : "consumable";
99753
99812
  return {
99754
99813
  answered: true,
99755
- items: alerts.filter((open) => open.family === family).map((open) => alertItem(section, open))
99814
+ items: alerts.filter((open) => open.family === family).map((open) => alertItem(section, open, ports.lookup))
99756
99815
  };
99757
99816
  }
99758
99817
  const state = ports.state();
@@ -99912,11 +99971,12 @@ function wordsOf(input, section) {
99912
99971
  /** One bullet per item; an item with a `node` is listed under its node's heading (D708). */
99913
99972
  function linesOf(input, items) {
99914
99973
  const bullet = (item) => `• ${lineOf(input, item)}`;
99915
- if (!items.some((item) => item.node !== void 0)) return items.map(bullet).join("\n");
99916
- return groupByNode(items, input.texts.language).flatMap((group) => [input.texts.text({
99974
+ if (items.some((item) => item.node !== void 0)) return groupByNode(items, input.texts.language).flatMap((group) => [input.texts.text({
99917
99975
  key: "summary.node.heading",
99918
99976
  vars: { node: group.node.name }
99919
99977
  }), ...group.items.map(bullet)]).join("\n");
99978
+ if (items.some((item) => item.device !== void 0)) return groupByDevice(items, input.texts.language).flatMap((group) => [`${group.device.name}:`, ...group.items.map(bullet)]).join("\n");
99979
+ return items.map(bullet).join("\n");
99920
99980
  }
99921
99981
  /**
99922
99982
  * The items cut per node, in a STABLE order: the hub first, then by name, then
@@ -99941,6 +100001,21 @@ var HUB_HEADING = {
99941
100001
  name: "hub",
99942
100002
  isHub: true
99943
100003
  };
100004
+ /** Consumables items under each device, devices ordered by name (D934). */
100005
+ function groupByDevice(items, language) {
100006
+ const groups = /* @__PURE__ */ new Map();
100007
+ for (const item of items) {
100008
+ const device = item.device;
100009
+ if (device === void 0) continue;
100010
+ const group = groups.get(device.id);
100011
+ if (group === void 0) groups.set(device.id, {
100012
+ device,
100013
+ items: [item]
100014
+ });
100015
+ else group.items.push(item);
100016
+ }
100017
+ return [...groups.values()].toSorted((a, b) => a.device.name.localeCompare(b.device.name, language));
100018
+ }
99944
100019
  function stateSummaryText(input) {
99945
100020
  const sectionVars = {};
99946
100021
  const sections = [];
@@ -104007,8 +104082,7 @@ var NotificationCenter = class {
104007
104082
  }
104008
104083
  const consumables = consumablesSliceOf(event);
104009
104084
  if (consumables !== null) {
104010
- for (const item of consumables.consumables) this.deviceAlerts.observeConsumable(consumables.deviceId, item);
104011
- for (const tank of consumables.tanks) this.deviceAlerts.observeTank(consumables.deviceId, tank);
104085
+ for (const item of consumables.items) this.deviceAlerts.observeConsumableItem(consumables.deviceId, item);
104012
104086
  return;
104013
104087
  }
104014
104088
  const lookup = this.deviceDirectory.lookup();