@devicechain/widgets 0.22.0 → 0.23.0-rc.1

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 (54) hide show
  1. package/README.md +15 -0
  2. package/dist/access-strings.d.ts +6 -0
  3. package/dist/chunks/{aggregate-feed-6ZYKOQOB.js → aggregate-feed-BD3CHNFU.js} +5 -4
  4. package/dist/chunks/aggregate-feed-BD3CHNFU.js.map +7 -0
  5. package/dist/chunks/{aggregate-widgets-WVXAW3UL.js → aggregate-widgets-HZQI5HTX.js} +3 -2
  6. package/dist/chunks/{aggregate-widgets-WVXAW3UL.js.map → aggregate-widgets-HZQI5HTX.js.map} +1 -1
  7. package/dist/chunks/alarm-widgets-7UY6MIYZ.js +9 -0
  8. package/dist/chunks/alarm-widgets-7UY6MIYZ.js.map +7 -0
  9. package/dist/chunks/chunk-6OGI4UFJ.js +367 -0
  10. package/dist/chunks/chunk-6OGI4UFJ.js.map +7 -0
  11. package/dist/chunks/chunk-BRH66T5F.js +15 -0
  12. package/dist/chunks/chunk-BRH66T5F.js.map +7 -0
  13. package/dist/chunks/chunk-FM7OI5TQ.js +87 -0
  14. package/dist/chunks/chunk-FM7OI5TQ.js.map +7 -0
  15. package/dist/chunks/chunk-USRYKW2J.js +55 -0
  16. package/dist/chunks/chunk-USRYKW2J.js.map +7 -0
  17. package/dist/chunks/{chunk-S2OMBJLI.js → chunk-WBKHYZU2.js} +7 -3
  18. package/dist/chunks/{chunk-S2OMBJLI.js.map → chunk-WBKHYZU2.js.map} +2 -2
  19. package/dist/chunks/chunk-XSLFHCNI.js +419 -0
  20. package/dist/chunks/chunk-XSLFHCNI.js.map +7 -0
  21. package/dist/chunks/{control-widgets-UDS4ET3T.js → control-widgets-3V2LSSRF.js} +4 -2
  22. package/dist/chunks/{control-widgets-UDS4ET3T.js.map → control-widgets-3V2LSSRF.js.map} +1 -1
  23. package/dist/chunks/entity-selector-3JJFI4DT.js +7 -0
  24. package/dist/chunks/entity-selector-3JJFI4DT.js.map +7 -0
  25. package/dist/chunks/{fleet-feed-FK4ZMKIK.js → fleet-feed-5QGDLOV5.js} +27 -7
  26. package/dist/chunks/fleet-feed-5QGDLOV5.js.map +7 -0
  27. package/dist/chunks/table-6EVV7QZH.js +7 -0
  28. package/dist/chunks/table-6EVV7QZH.js.map +7 -0
  29. package/dist/connected-widget.d.ts +2 -0
  30. package/dist/dashboard-renderer.d.ts +1 -0
  31. package/dist/fleet-feed.d.ts +1 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +478 -1142
  34. package/dist/index.js.map +4 -4
  35. package/dist/preload.d.ts +2 -0
  36. package/dist/widgets/alarm-count.d.ts +0 -46
  37. package/dist/widgets/alarm-lazy.d.ts +123 -0
  38. package/dist/widgets/alarm-table.d.ts +0 -76
  39. package/dist/widgets/alarm-widgets.d.ts +2 -0
  40. package/dist/widgets/command-button.d.ts +0 -38
  41. package/dist/widgets/control-lazy.d.ts +38 -0
  42. package/dist/widgets/control-widgets.d.ts +1 -0
  43. package/dist/widgets/entity-selector.d.ts +0 -15
  44. package/dist/widgets/gauge.d.ts +4 -0
  45. package/dist/widgets/lazy-chunk.d.ts +2 -1
  46. package/dist/widgets/selection-lazy.d.ts +16 -0
  47. package/dist/widgets/table-lazy.d.ts +39 -0
  48. package/dist/widgets/table.d.ts +0 -38
  49. package/dist/widgets/time-series-chart.d.ts +4 -0
  50. package/package.json +10 -7
  51. package/dist/chunks/aggregate-feed-6ZYKOQOB.js.map +0 -7
  52. package/dist/chunks/chunk-YPFB6DAL.js +0 -157
  53. package/dist/chunks/chunk-YPFB6DAL.js.map +0 -7
  54. package/dist/chunks/fleet-feed-FK4ZMKIK.js.map +0 -7
@@ -1,3 +1,7 @@
1
+ import {
2
+ ACCESS_STRINGS
3
+ } from "./chunk-BRH66T5F.js";
4
+
1
5
  // src/fleet-model.ts
2
6
  import {
3
7
  bucketSecondsFor
@@ -140,7 +144,7 @@ var FLEET_STRINGS = {
140
144
  noData: "no data in range",
141
145
  failed: "could not be read",
142
146
  unbound: "Choose a device group for this widget",
143
- forbidden: "You do not have access to this data",
147
+ forbidden: ACCESS_STRINGS.en.forbidden,
144
148
  unsupported: "This data source cannot list a fleet",
145
149
  noMembers: "No devices in this group",
146
150
  noMeasurement: "Choose the measurement to compare",
@@ -163,7 +167,7 @@ var FLEET_STRINGS = {
163
167
  noData: "sin datos en el rango",
164
168
  failed: "no se pudo leer",
165
169
  unbound: "Elija un grupo de dispositivos para este widget",
166
- forbidden: "No tiene acceso a estos datos",
170
+ forbidden: ACCESS_STRINGS.es.forbidden,
167
171
  unsupported: "Esta fuente de datos no puede listar una flota",
168
172
  noMembers: "No hay dispositivos en este grupo",
169
173
  noMeasurement: "Elija la medici\xF3n que comparar",
@@ -197,4 +201,4 @@ export {
197
201
  deviceBars,
198
202
  fleetStrings
199
203
  };
200
- //# sourceMappingURL=chunk-S2OMBJLI.js.map
204
+ //# sourceMappingURL=chunk-WBKHYZU2.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/fleet-model.ts", "../../src/fleet-strings.ts"],
4
- "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The fleet widgets' state, decided apart from the drawing and from the hub: which reading a cell\n// shows, why one is empty, how the rows are ordered and paged. Pure, so each rule is a test.\n//\n// Everything is keyed by DEVICE and measurement together. The measurement channel keys its latest\n// values by name alone, so two thermostats reporting \"temperature\" overwrite each other there;\n// nothing in this file ever looks a value up by name without the device beside it.\n\nimport {\n bucketSecondsFor,\n type FleetMember,\n type FleetState,\n type HistoryWindow,\n type MeasurementSample,\n type MemberRoster,\n type MemberSeed,\n} from '@devicechain/dashboards';\n\nconst MINUTE_MS = 60_000;\n\n// The window a fleet's seed reads: whole minutes ending at the last minute boundary at or before\n// `at`, in buckets of bucketSecondsFor(span) (a multiple of 60, so the store answers from its\n// one-minute summaries). Whole minutes are what let widgets reading the same span on the same\n// board share one request per device (the hub keys in-flight reads by window), and what keep the\n// window from holding a reading from before the span. The end is the instant just before the\n// boundary, because the store's end is inclusive.\nexport function seedWindow(spanSeconds: number, at: number): HistoryWindow {\n const end = Math.floor(at / MINUTE_MS) * MINUTE_MS;\n const start = Math.ceil((end - spanSeconds * 1000) / MINUTE_MS) * MINUTE_MS;\n return {\n startTime: new Date(start).toISOString(),\n endTime: new Date(end - 1).toISOString(),\n intervalSeconds: bucketSecondsFor(Math.max(60, (end - start) / 1000)),\n };\n}\n\n// ---- Building the state ------------------------------------------------------\n\n// What the feed has gathered. `seeds` holds a settled seed per device (`'failed'` for a read that\n// did not come back); a device absent from it is still loading. `live` is every device's newest\n// sample per measurement name since the board opened.\nexport interface FleetInputs {\n roster: MemberRoster | null;\n seeds: ReadonlyMap<string, MemberSeed | 'failed'>;\n live: ReadonlyMap<string, ReadonlyMap<string, MeasurementSample>>;\n // The start of the span (epoch ms): a reading older than this is not \"in range\".\n since: number;\n forbidden: boolean;\n}\n\nfunction timeOf(sample: MeasurementSample | undefined): number {\n if (!sample?.occurredTime) return Number.NaN;\n return Date.parse(sample.occurredTime);\n}\n\n// A sample with no readable time is taken as current: only a live sample can lack one, and it just\n// arrived.\nfunction inRange(sample: MeasurementSample, since: number): boolean {\n const at = timeOf(sample);\n return Number.isNaN(at) || at >= since;\n}\n\nfunction newer(a: MeasurementSample | undefined, b: MeasurementSample | undefined): MeasurementSample | undefined {\n if (!a) return b;\n if (!b) return a;\n const ta = timeOf(a);\n const tb = timeOf(b);\n if (Number.isNaN(ta)) return a;\n if (Number.isNaN(tb)) return b;\n return tb >= ta ? b : a;\n}\n\nexport function buildFleet(i: FleetInputs): FleetState {\n if (i.forbidden) return { status: 'forbidden', members: [], total: 0, truncated: false };\n if (!i.roster) return { status: 'loading', members: [], total: 0, truncated: false };\n if (i.roster.unbound) return { status: 'unbound', members: [], total: 0, truncated: false };\n\n // A seeded reading is stamped at the start of its minute (fleet-seed.ts, \"Ages\") and happened\n // somewhere in that minute, so it is in range while its minute is. Held to the exact instant, a\n // reading 59 minutes 45 seconds old on an hour's board was dropped as out of range.\n const seedSince = Math.floor(i.since / MINUTE_MS) * MINUTE_MS;\n const members = i.roster.members.map((ref): FleetMember => {\n const seed = i.seeds.get(ref.token);\n const seeded = seed && seed !== 'failed' ? seed : undefined;\n const live = i.live.get(ref.token);\n const latest: Record<string, MeasurementSample> = {};\n const names = new Set<string>([...Object.keys(seeded?.latest ?? {}), ...(live?.keys() ?? [])]);\n for (const name of names) {\n const best = newer(seeded?.latest[name], live?.get(name));\n if (best && inRange(best, best === seeded?.latest[name] ? seedSince : i.since)) latest[name] = best;\n }\n const observed = new Set<string>(seeded?.observed ?? []);\n for (const name of Object.keys(latest)) observed.add(name);\n return {\n token: ref.token,\n name: ref.name,\n latest,\n seed: seed === undefined ? 'pending' : seed === 'failed' ? 'failed' : 'ready',\n observed: [...observed],\n // A live reading of an unread name is its value: cellFor looks at `latest` first.\n ...(seeded?.unread?.length ? { unread: seeded.unread } : {}),\n };\n });\n return {\n status: 'ready',\n members,\n total: i.roster.total,\n truncated: i.roster.total > members.length,\n };\n}\n\n// The newest reading anywhere in the fleet (epoch ms), or undefined when none carries a time.\nexport function newestMemberTime(fleet: FleetState): number | undefined {\n let newest: number | undefined;\n for (const m of fleet.members) {\n for (const sample of Object.values(m.latest)) {\n const at = timeOf(sample);\n if (!Number.isNaN(at) && (newest === undefined || at > newest)) newest = at;\n }\n }\n return newest;\n}\n\n// ---- Cells -------------------------------------------------------------------\n\n// What one (device, measurement) cell holds, and when it holds nothing, why.\nexport type Cell =\n | { kind: 'value'; sample: MeasurementSample; value: number }\n | { kind: 'loading' }\n // The device's history read failed and no live reading has arrived since.\n | { kind: 'failed' }\n // The device reported other measurements in the span, but never this one.\n | { kind: 'not-reported' }\n // The device reported this in the span, but its newest reading was not loaded (MemberSeed.unread).\n | { kind: 'unread' }\n // The device reported nothing at all in the span.\n | { kind: 'no-data' };\n\nexport function cellFor(member: FleetMember, measurement: string): Cell {\n const sample = member.latest[measurement];\n if (sample && typeof sample.value === 'number' && Number.isFinite(sample.value)) {\n return { kind: 'value', sample, value: sample.value };\n }\n if (member.seed === 'pending') return { kind: 'loading' };\n if (member.seed === 'failed') return { kind: 'failed' };\n if (member.unread?.includes(measurement)) return { kind: 'unread' };\n return member.observed.length > 0 && !member.observed.includes(measurement) ? { kind: 'not-reported' } : { kind: 'no-data' };\n}\n\n// The columns: the measurements the datasource names, in that order; with none named, every\n// measurement any member reported, alphabetically (stable across renders however samples arrive).\nexport function columnNames(named: readonly string[], members: readonly FleetMember[]): string[] {\n if (named.length > 0) return [...new Set(named)];\n const seen = new Set<string>();\n for (const m of members) for (const name of m.observed) seen.add(name);\n return [...seen].sort((a, b) => a.localeCompare(b));\n}\n\n// ---- Order and pages ------------------------------------------------------------\n\nexport interface SortKey {\n // A measurement name, or null for the device-name column.\n by: string | null;\n descending: boolean;\n}\n\nexport function deviceLabel(member: Pick<FleetMember, 'name' | 'token'>): string {\n return member.name && member.name !== '' ? member.name : member.token;\n}\n\nconst collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });\n\n// Ordered by the key, ties broken by device name then token, so the order never depends on the\n// order samples arrived in. A device with no value for the sorted measurement goes LAST in either\n// direction: \"largest first\" must not open with the devices that reported nothing.\nexport function sortMembers(members: readonly FleetMember[], key: SortKey): FleetMember[] {\n const byDevice = (a: FleetMember, b: FleetMember): number =>\n collator.compare(deviceLabel(a), deviceLabel(b)) || (a.token < b.token ? -1 : a.token > b.token ? 1 : 0);\n const sign = key.descending ? -1 : 1;\n return [...members].sort((a, b) => {\n if (key.by === null) return sign * byDevice(a, b);\n const ca = cellFor(a, key.by);\n const cb = cellFor(b, key.by);\n if (ca.kind !== 'value' || cb.kind !== 'value') {\n if (ca.kind === cb.kind) return byDevice(a, b);\n return ca.kind === 'value' ? -1 : cb.kind === 'value' ? 1 : byDevice(a, b);\n }\n return sign * (ca.value - cb.value) || byDevice(a, b);\n });\n}\n\nexport const DEFAULT_PAGE_SIZE = 20;\n\nexport function pageCount(rows: number, pageSize: number): number {\n return Math.max(1, Math.ceil(rows / Math.max(1, pageSize)));\n}\n\n// The page asked for, held inside what exists (a fleet that shrinks must not leave the viewer on a\n// page past the end).\nexport function clampPage(page: number, rows: number, pageSize: number): number {\n return Math.min(Math.max(0, page), pageCount(rows, pageSize) - 1);\n}\n\n// ---- Bars ---------------------------------------------------------------------------\n\n// One bar per member for one measurement, in the shape the bar chart already draws (category\n// items), ordered like the categorical charts order theirs. A device with no number keeps its\n// label with no bar and sorts after those that have one.\nexport interface DeviceBar {\n label: string;\n token: string;\n value: number | null;\n}\n\nexport function deviceBars(members: readonly FleetMember[], measurement: string, sort: 'none' | 'ascending' | 'descending'): DeviceBar[] {\n const bars = members.map((m): DeviceBar => {\n const cell = cellFor(m, measurement);\n return { label: deviceLabel(m), token: m.token, value: cell.kind === 'value' ? cell.value : null };\n });\n if (sort === 'none') return bars;\n const sign = sort === 'ascending' ? 1 : -1;\n return bars\n .map((bar, i) => ({ bar, i }))\n .sort((a, b) => {\n if (a.bar.value === null || b.bar.value === null) {\n return a.bar.value === b.bar.value ? a.i - b.i : a.bar.value === null ? 1 : -1;\n }\n return sign * (a.bar.value - b.bar.value) || a.i - b.i;\n })\n .map((x) => x.bar);\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The fixed words the fleet table and the per-device bar chart render themselves: the device\n// column, the reason a cell holds no number, the truncation and paging captions. The widget\n// packages carry no i18n runtime, so this is the one small per-language table (modelled on\n// aggregate-strings.ts); option labels are translated in the console's catalogs, not here.\n\nimport { viewerLocale } from '@devicechain/widget-sdk';\n\nexport interface FleetStrings {\n device: string;\n loading: string;\n // The reason a cell holds no number (D-5): each is a different fact about the device.\n notReported: string;\n // It reported this, but its newest reading was not loaded: there IS data.\n unread: string;\n noData: string;\n failed: string;\n // Whole-widget states.\n unbound: string;\n forbidden: string;\n unsupported: string;\n noMembers: string;\n noMeasurement: string;\n // A widget that shows one device, bound to a group: refused, with where the group can be shown.\n oneDevice: string;\n useFleetTable: string;\n usePerDevice: string;\n stale: string;\n // \"Showing 500 of 812 devices\": a fleet past what is read must say so.\n truncated: (shown: number, total: number) => string;\n range: (from: number, to: number, of: number) => string;\n previous: string;\n next: string;\n sortBy: (column: string) => string;\n drill: (device: string) => string;\n}\n\nexport const FLEET_STRINGS: Record<'en' | 'es', FleetStrings> = {\n en: {\n device: 'Device',\n loading: 'Loading',\n notReported: 'does not report this',\n unread: 'not loaded',\n noData: 'no data in range',\n failed: 'could not be read',\n unbound: 'Choose a device group for this widget',\n forbidden: 'You do not have access to this data',\n unsupported: 'This data source cannot list a fleet',\n noMembers: 'No devices in this group',\n noMeasurement: 'Choose the measurement to compare',\n oneDevice: 'This widget shows one device, and it is bound to a group of devices whose readings it would mix together.',\n useFleetTable: 'Bind it to one device, or show the group in a fleet table.',\n usePerDevice: 'Set it to one bar per device to compare them, or bind it to one device.',\n stale: 'stale',\n truncated: (shown, total) => `Showing ${shown} of ${total} devices`,\n range: (from, to, of) => `${from}\u2013${to} of ${of}`,\n previous: 'Previous page',\n next: 'Next page',\n sortBy: (column) => `Sort by ${column}`,\n drill: (device) => `Show ${device} in the linked widgets`,\n },\n es: {\n device: 'Dispositivo',\n loading: 'Cargando',\n notReported: 'no informa esta medici\u00F3n',\n unread: 'no cargado',\n noData: 'sin datos en el rango',\n failed: 'no se pudo leer',\n unbound: 'Elija un grupo de dispositivos para este widget',\n forbidden: 'No tiene acceso a estos datos',\n unsupported: 'Esta fuente de datos no puede listar una flota',\n noMembers: 'No hay dispositivos en este grupo',\n noMeasurement: 'Elija la medici\u00F3n que comparar',\n oneDevice: 'Este widget muestra un solo dispositivo y est\u00E1 vinculado a un grupo de dispositivos cuyas lecturas mezclar\u00EDa.',\n useFleetTable: 'Vinc\u00FAlelo a un dispositivo o muestre el grupo en una tabla de flota.',\n usePerDevice: 'Config\u00FArelo con una barra por dispositivo para compararlos, o vinc\u00FAlelo a un dispositivo.',\n stale: 'obsoleto',\n truncated: (shown, total) => `Mostrando ${shown} de ${total} dispositivos`,\n range: (from, to, of) => `${from}\u2013${to} de ${of}`,\n previous: 'P\u00E1gina anterior',\n next: 'P\u00E1gina siguiente',\n sortBy: (column) => `Ordenar por ${column}`,\n drill: (device) => `Mostrar ${device} en los widgets enlazados`,\n },\n};\n\nexport function fleetStrings(locale: string | undefined = viewerLocale()): FleetStrings {\n return locale?.toLowerCase().split('-')[0] === 'es' ? FLEET_STRINGS.es : FLEET_STRINGS.en;\n}\n"],
5
- "mappings": ";AAUA;AAAA,EACE;AAAA,OAOK;AAEP,IAAM,YAAY;AAQX,SAAS,WAAW,aAAqB,IAA2B;AACzE,QAAM,MAAM,KAAK,MAAM,KAAK,SAAS,IAAI;AACzC,QAAM,QAAQ,KAAK,MAAM,MAAM,cAAc,OAAQ,SAAS,IAAI;AAClE,SAAO;AAAA,IACL,WAAW,IAAI,KAAK,KAAK,EAAE,YAAY;AAAA,IACvC,SAAS,IAAI,KAAK,MAAM,CAAC,EAAE,YAAY;AAAA,IACvC,iBAAiB,iBAAiB,KAAK,IAAI,KAAK,MAAM,SAAS,GAAI,CAAC;AAAA,EACtE;AACF;AAgBA,SAAS,OAAO,QAA+C;AAC7D,MAAI,CAAC,QAAQ,aAAc,QAAO,OAAO;AACzC,SAAO,KAAK,MAAM,OAAO,YAAY;AACvC;AAIA,SAAS,QAAQ,QAA2B,OAAwB;AAClE,QAAM,KAAK,OAAO,MAAM;AACxB,SAAO,OAAO,MAAM,EAAE,KAAK,MAAM;AACnC;AAEA,SAAS,MAAM,GAAkC,GAAiE;AAChH,MAAI,CAAC,EAAG,QAAO;AACf,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,KAAK,OAAO,CAAC;AACnB,QAAM,KAAK,OAAO,CAAC;AACnB,MAAI,OAAO,MAAM,EAAE,EAAG,QAAO;AAC7B,MAAI,OAAO,MAAM,EAAE,EAAG,QAAO;AAC7B,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,WAAW,GAA4B;AACrD,MAAI,EAAE,UAAW,QAAO,EAAE,QAAQ,aAAa,SAAS,CAAC,GAAG,OAAO,GAAG,WAAW,MAAM;AACvF,MAAI,CAAC,EAAE,OAAQ,QAAO,EAAE,QAAQ,WAAW,SAAS,CAAC,GAAG,OAAO,GAAG,WAAW,MAAM;AACnF,MAAI,EAAE,OAAO,QAAS,QAAO,EAAE,QAAQ,WAAW,SAAS,CAAC,GAAG,OAAO,GAAG,WAAW,MAAM;AAK1F,QAAM,YAAY,KAAK,MAAM,EAAE,QAAQ,SAAS,IAAI;AACpD,QAAM,UAAU,EAAE,OAAO,QAAQ,IAAI,CAAC,QAAqB;AACzD,UAAM,OAAO,EAAE,MAAM,IAAI,IAAI,KAAK;AAClC,UAAM,SAAS,QAAQ,SAAS,WAAW,OAAO;AAClD,UAAM,OAAO,EAAE,KAAK,IAAI,IAAI,KAAK;AACjC,UAAM,SAA4C,CAAC;AACnD,UAAM,QAAQ,oBAAI,IAAY,CAAC,GAAG,OAAO,KAAK,QAAQ,UAAU,CAAC,CAAC,GAAG,GAAI,MAAM,KAAK,KAAK,CAAC,CAAE,CAAC;AAC7F,eAAW,QAAQ,OAAO;AACxB,YAAM,OAAO,MAAM,QAAQ,OAAO,IAAI,GAAG,MAAM,IAAI,IAAI,CAAC;AACxD,UAAI,QAAQ,QAAQ,MAAM,SAAS,QAAQ,OAAO,IAAI,IAAI,YAAY,EAAE,KAAK,EAAG,QAAO,IAAI,IAAI;AAAA,IACjG;AACA,UAAM,WAAW,IAAI,IAAY,QAAQ,YAAY,CAAC,CAAC;AACvD,eAAW,QAAQ,OAAO,KAAK,MAAM,EAAG,UAAS,IAAI,IAAI;AACzD,WAAO;AAAA,MACL,OAAO,IAAI;AAAA,MACX,MAAM,IAAI;AAAA,MACV;AAAA,MACA,MAAM,SAAS,SAAY,YAAY,SAAS,WAAW,WAAW;AAAA,MACtE,UAAU,CAAC,GAAG,QAAQ;AAAA;AAAA,MAEtB,GAAI,QAAQ,QAAQ,SAAS,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,IAC5D;AAAA,EACF,CAAC;AACD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR;AAAA,IACA,OAAO,EAAE,OAAO;AAAA,IAChB,WAAW,EAAE,OAAO,QAAQ,QAAQ;AAAA,EACtC;AACF;AAGO,SAAS,iBAAiB,OAAuC;AACtE,MAAI;AACJ,aAAW,KAAK,MAAM,SAAS;AAC7B,eAAW,UAAU,OAAO,OAAO,EAAE,MAAM,GAAG;AAC5C,YAAM,KAAK,OAAO,MAAM;AACxB,UAAI,CAAC,OAAO,MAAM,EAAE,MAAM,WAAW,UAAa,KAAK,QAAS,UAAS;AAAA,IAC3E;AAAA,EACF;AACA,SAAO;AACT;AAiBO,SAAS,QAAQ,QAAqB,aAA2B;AACtE,QAAM,SAAS,OAAO,OAAO,WAAW;AACxC,MAAI,UAAU,OAAO,OAAO,UAAU,YAAY,OAAO,SAAS,OAAO,KAAK,GAAG;AAC/E,WAAO,EAAE,MAAM,SAAS,QAAQ,OAAO,OAAO,MAAM;AAAA,EACtD;AACA,MAAI,OAAO,SAAS,UAAW,QAAO,EAAE,MAAM,UAAU;AACxD,MAAI,OAAO,SAAS,SAAU,QAAO,EAAE,MAAM,SAAS;AACtD,MAAI,OAAO,QAAQ,SAAS,WAAW,EAAG,QAAO,EAAE,MAAM,SAAS;AAClE,SAAO,OAAO,SAAS,SAAS,KAAK,CAAC,OAAO,SAAS,SAAS,WAAW,IAAI,EAAE,MAAM,eAAe,IAAI,EAAE,MAAM,UAAU;AAC7H;AAIO,SAAS,YAAY,OAA0B,SAA2C;AAC/F,MAAI,MAAM,SAAS,EAAG,QAAO,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC;AAC/C,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,KAAK,QAAS,YAAW,QAAQ,EAAE,SAAU,MAAK,IAAI,IAAI;AACrE,SAAO,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AACpD;AAUO,SAAS,YAAY,QAAqD;AAC/E,SAAO,OAAO,QAAQ,OAAO,SAAS,KAAK,OAAO,OAAO,OAAO;AAClE;AAEA,IAAM,WAAW,IAAI,KAAK,SAAS,QAAW,EAAE,SAAS,MAAM,aAAa,OAAO,CAAC;AAK7E,SAAS,YAAY,SAAiC,KAA6B;AACxF,QAAM,WAAW,CAAC,GAAgB,MAChC,SAAS,QAAQ,YAAY,CAAC,GAAG,YAAY,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,KAAK,EAAE,QAAQ,EAAE,QAAQ,IAAI;AACxG,QAAM,OAAO,IAAI,aAAa,KAAK;AACnC,SAAO,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,MAAM;AACjC,QAAI,IAAI,OAAO,KAAM,QAAO,OAAO,SAAS,GAAG,CAAC;AAChD,UAAM,KAAK,QAAQ,GAAG,IAAI,EAAE;AAC5B,UAAM,KAAK,QAAQ,GAAG,IAAI,EAAE;AAC5B,QAAI,GAAG,SAAS,WAAW,GAAG,SAAS,SAAS;AAC9C,UAAI,GAAG,SAAS,GAAG,KAAM,QAAO,SAAS,GAAG,CAAC;AAC7C,aAAO,GAAG,SAAS,UAAU,KAAK,GAAG,SAAS,UAAU,IAAI,SAAS,GAAG,CAAC;AAAA,IAC3E;AACA,WAAO,QAAQ,GAAG,QAAQ,GAAG,UAAU,SAAS,GAAG,CAAC;AAAA,EACtD,CAAC;AACH;AAEO,IAAM,oBAAoB;AAE1B,SAAS,UAAU,MAAc,UAA0B;AAChE,SAAO,KAAK,IAAI,GAAG,KAAK,KAAK,OAAO,KAAK,IAAI,GAAG,QAAQ,CAAC,CAAC;AAC5D;AAIO,SAAS,UAAU,MAAc,MAAc,UAA0B;AAC9E,SAAO,KAAK,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,UAAU,MAAM,QAAQ,IAAI,CAAC;AAClE;AAaO,SAAS,WAAW,SAAiC,aAAqB,MAAwD;AACvI,QAAM,OAAO,QAAQ,IAAI,CAAC,MAAiB;AACzC,UAAM,OAAO,QAAQ,GAAG,WAAW;AACnC,WAAO,EAAE,OAAO,YAAY,CAAC,GAAG,OAAO,EAAE,OAAO,OAAO,KAAK,SAAS,UAAU,KAAK,QAAQ,KAAK;AAAA,EACnG,CAAC;AACD,MAAI,SAAS,OAAQ,QAAO;AAC5B,QAAM,OAAO,SAAS,cAAc,IAAI;AACxC,SAAO,KACJ,IAAI,CAAC,KAAK,OAAO,EAAE,KAAK,EAAE,EAAE,EAC5B,KAAK,CAAC,GAAG,MAAM;AACd,QAAI,EAAE,IAAI,UAAU,QAAQ,EAAE,IAAI,UAAU,MAAM;AAChD,aAAO,EAAE,IAAI,UAAU,EAAE,IAAI,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,UAAU,OAAO,IAAI;AAAA,IAC9E;AACA,WAAO,QAAQ,EAAE,IAAI,QAAQ,EAAE,IAAI,UAAU,EAAE,IAAI,EAAE;AAAA,EACvD,CAAC,EACA,IAAI,CAAC,MAAM,EAAE,GAAG;AACrB;;;AChOA,SAAS,oBAAoB;AA+BtB,IAAM,gBAAmD;AAAA,EAC9D,IAAI;AAAA,IACF,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,aAAa;AAAA,IACb,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,IACb,WAAW;AAAA,IACX,eAAe;AAAA,IACf,WAAW;AAAA,IACX,eAAe;AAAA,IACf,cAAc;AAAA,IACd,OAAO;AAAA,IACP,WAAW,CAAC,OAAO,UAAU,WAAW,KAAK,OAAO,KAAK;AAAA,IACzD,OAAO,CAAC,MAAM,IAAI,OAAO,GAAG,IAAI,SAAI,EAAE,OAAO,EAAE;AAAA,IAC/C,UAAU;AAAA,IACV,MAAM;AAAA,IACN,QAAQ,CAAC,WAAW,WAAW,MAAM;AAAA,IACrC,OAAO,CAAC,WAAW,QAAQ,MAAM;AAAA,EACnC;AAAA,EACA,IAAI;AAAA,IACF,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,aAAa;AAAA,IACb,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,IACb,WAAW;AAAA,IACX,eAAe;AAAA,IACf,WAAW;AAAA,IACX,eAAe;AAAA,IACf,cAAc;AAAA,IACd,OAAO;AAAA,IACP,WAAW,CAAC,OAAO,UAAU,aAAa,KAAK,OAAO,KAAK;AAAA,IAC3D,OAAO,CAAC,MAAM,IAAI,OAAO,GAAG,IAAI,SAAI,EAAE,OAAO,EAAE;AAAA,IAC/C,UAAU;AAAA,IACV,MAAM;AAAA,IACN,QAAQ,CAAC,WAAW,eAAe,MAAM;AAAA,IACzC,OAAO,CAAC,WAAW,WAAW,MAAM;AAAA,EACtC;AACF;AAEO,SAAS,aAAa,SAA6B,aAAa,GAAiB;AACtF,SAAO,QAAQ,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,MAAM,OAAO,cAAc,KAAK,cAAc;AACzF;",
4
+ "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The fleet widgets' state, decided apart from the drawing and from the hub: which reading a cell\n// shows, why one is empty, how the rows are ordered and paged. Pure, so each rule is a test.\n//\n// Everything is keyed by DEVICE and measurement together. The measurement channel keys its latest\n// values by name alone, so two thermostats reporting \"temperature\" overwrite each other there;\n// nothing in this file ever looks a value up by name without the device beside it.\n\nimport {\n bucketSecondsFor,\n type FleetMember,\n type FleetState,\n type HistoryWindow,\n type MeasurementSample,\n type MemberRoster,\n type MemberSeed,\n} from '@devicechain/dashboards';\n\nconst MINUTE_MS = 60_000;\n\n// The window a fleet's seed reads: whole minutes ending at the last minute boundary at or before\n// `at`, in buckets of bucketSecondsFor(span) (a multiple of 60, so the store answers from its\n// one-minute summaries). Whole minutes are what let widgets reading the same span on the same\n// board share one request per device (the hub keys in-flight reads by window), and what keep the\n// window from holding a reading from before the span. The end is the instant just before the\n// boundary, because the store's end is inclusive.\nexport function seedWindow(spanSeconds: number, at: number): HistoryWindow {\n const end = Math.floor(at / MINUTE_MS) * MINUTE_MS;\n const start = Math.ceil((end - spanSeconds * 1000) / MINUTE_MS) * MINUTE_MS;\n return {\n startTime: new Date(start).toISOString(),\n endTime: new Date(end - 1).toISOString(),\n intervalSeconds: bucketSecondsFor(Math.max(60, (end - start) / 1000)),\n };\n}\n\n// ---- Building the state ------------------------------------------------------\n\n// What the feed has gathered. `seeds` holds a settled seed per device (`'failed'` for a read that\n// did not come back); a device absent from it is still loading. `live` is every device's newest\n// sample per measurement name since the board opened.\nexport interface FleetInputs {\n roster: MemberRoster | null;\n seeds: ReadonlyMap<string, MemberSeed | 'failed'>;\n live: ReadonlyMap<string, ReadonlyMap<string, MeasurementSample>>;\n // The start of the span (epoch ms): a reading older than this is not \"in range\".\n since: number;\n forbidden: boolean;\n}\n\nfunction timeOf(sample: MeasurementSample | undefined): number {\n if (!sample?.occurredTime) return Number.NaN;\n return Date.parse(sample.occurredTime);\n}\n\n// A sample with no readable time is taken as current: only a live sample can lack one, and it just\n// arrived.\nfunction inRange(sample: MeasurementSample, since: number): boolean {\n const at = timeOf(sample);\n return Number.isNaN(at) || at >= since;\n}\n\nfunction newer(a: MeasurementSample | undefined, b: MeasurementSample | undefined): MeasurementSample | undefined {\n if (!a) return b;\n if (!b) return a;\n const ta = timeOf(a);\n const tb = timeOf(b);\n if (Number.isNaN(ta)) return a;\n if (Number.isNaN(tb)) return b;\n return tb >= ta ? b : a;\n}\n\nexport function buildFleet(i: FleetInputs): FleetState {\n if (i.forbidden) return { status: 'forbidden', members: [], total: 0, truncated: false };\n if (!i.roster) return { status: 'loading', members: [], total: 0, truncated: false };\n if (i.roster.unbound) return { status: 'unbound', members: [], total: 0, truncated: false };\n\n // A seeded reading is stamped at the start of its minute (fleet-seed.ts, \"Ages\") and happened\n // somewhere in that minute, so it is in range while its minute is. Held to the exact instant, a\n // reading 59 minutes 45 seconds old on an hour's board was dropped as out of range.\n const seedSince = Math.floor(i.since / MINUTE_MS) * MINUTE_MS;\n const members = i.roster.members.map((ref): FleetMember => {\n const seed = i.seeds.get(ref.token);\n const seeded = seed && seed !== 'failed' ? seed : undefined;\n const live = i.live.get(ref.token);\n const latest: Record<string, MeasurementSample> = {};\n const names = new Set<string>([...Object.keys(seeded?.latest ?? {}), ...(live?.keys() ?? [])]);\n for (const name of names) {\n const best = newer(seeded?.latest[name], live?.get(name));\n if (best && inRange(best, best === seeded?.latest[name] ? seedSince : i.since)) latest[name] = best;\n }\n const observed = new Set<string>(seeded?.observed ?? []);\n for (const name of Object.keys(latest)) observed.add(name);\n return {\n token: ref.token,\n name: ref.name,\n latest,\n seed: seed === undefined ? 'pending' : seed === 'failed' ? 'failed' : 'ready',\n observed: [...observed],\n // A live reading of an unread name is its value: cellFor looks at `latest` first.\n ...(seeded?.unread?.length ? { unread: seeded.unread } : {}),\n };\n });\n return {\n status: 'ready',\n members,\n total: i.roster.total,\n truncated: i.roster.total > members.length,\n };\n}\n\n// The newest reading anywhere in the fleet (epoch ms), or undefined when none carries a time.\nexport function newestMemberTime(fleet: FleetState): number | undefined {\n let newest: number | undefined;\n for (const m of fleet.members) {\n for (const sample of Object.values(m.latest)) {\n const at = timeOf(sample);\n if (!Number.isNaN(at) && (newest === undefined || at > newest)) newest = at;\n }\n }\n return newest;\n}\n\n// ---- Cells -------------------------------------------------------------------\n\n// What one (device, measurement) cell holds, and when it holds nothing, why.\nexport type Cell =\n | { kind: 'value'; sample: MeasurementSample; value: number }\n | { kind: 'loading' }\n // The device's history read failed and no live reading has arrived since.\n | { kind: 'failed' }\n // The device reported other measurements in the span, but never this one.\n | { kind: 'not-reported' }\n // The device reported this in the span, but its newest reading was not loaded (MemberSeed.unread).\n | { kind: 'unread' }\n // The device reported nothing at all in the span.\n | { kind: 'no-data' };\n\nexport function cellFor(member: FleetMember, measurement: string): Cell {\n const sample = member.latest[measurement];\n if (sample && typeof sample.value === 'number' && Number.isFinite(sample.value)) {\n return { kind: 'value', sample, value: sample.value };\n }\n if (member.seed === 'pending') return { kind: 'loading' };\n if (member.seed === 'failed') return { kind: 'failed' };\n if (member.unread?.includes(measurement)) return { kind: 'unread' };\n return member.observed.length > 0 && !member.observed.includes(measurement) ? { kind: 'not-reported' } : { kind: 'no-data' };\n}\n\n// The columns: the measurements the datasource names, in that order; with none named, every\n// measurement any member reported, alphabetically (stable across renders however samples arrive).\nexport function columnNames(named: readonly string[], members: readonly FleetMember[]): string[] {\n if (named.length > 0) return [...new Set(named)];\n const seen = new Set<string>();\n for (const m of members) for (const name of m.observed) seen.add(name);\n return [...seen].sort((a, b) => a.localeCompare(b));\n}\n\n// ---- Order and pages ------------------------------------------------------------\n\nexport interface SortKey {\n // A measurement name, or null for the device-name column.\n by: string | null;\n descending: boolean;\n}\n\nexport function deviceLabel(member: Pick<FleetMember, 'name' | 'token'>): string {\n return member.name && member.name !== '' ? member.name : member.token;\n}\n\nconst collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });\n\n// Ordered by the key, ties broken by device name then token, so the order never depends on the\n// order samples arrived in. A device with no value for the sorted measurement goes LAST in either\n// direction: \"largest first\" must not open with the devices that reported nothing.\nexport function sortMembers(members: readonly FleetMember[], key: SortKey): FleetMember[] {\n const byDevice = (a: FleetMember, b: FleetMember): number =>\n collator.compare(deviceLabel(a), deviceLabel(b)) || (a.token < b.token ? -1 : a.token > b.token ? 1 : 0);\n const sign = key.descending ? -1 : 1;\n return [...members].sort((a, b) => {\n if (key.by === null) return sign * byDevice(a, b);\n const ca = cellFor(a, key.by);\n const cb = cellFor(b, key.by);\n if (ca.kind !== 'value' || cb.kind !== 'value') {\n if (ca.kind === cb.kind) return byDevice(a, b);\n return ca.kind === 'value' ? -1 : cb.kind === 'value' ? 1 : byDevice(a, b);\n }\n return sign * (ca.value - cb.value) || byDevice(a, b);\n });\n}\n\nexport const DEFAULT_PAGE_SIZE = 20;\n\nexport function pageCount(rows: number, pageSize: number): number {\n return Math.max(1, Math.ceil(rows / Math.max(1, pageSize)));\n}\n\n// The page asked for, held inside what exists (a fleet that shrinks must not leave the viewer on a\n// page past the end).\nexport function clampPage(page: number, rows: number, pageSize: number): number {\n return Math.min(Math.max(0, page), pageCount(rows, pageSize) - 1);\n}\n\n// ---- Bars ---------------------------------------------------------------------------\n\n// One bar per member for one measurement, in the shape the bar chart already draws (category\n// items), ordered like the categorical charts order theirs. A device with no number keeps its\n// label with no bar and sorts after those that have one.\nexport interface DeviceBar {\n label: string;\n token: string;\n value: number | null;\n}\n\nexport function deviceBars(members: readonly FleetMember[], measurement: string, sort: 'none' | 'ascending' | 'descending'): DeviceBar[] {\n const bars = members.map((m): DeviceBar => {\n const cell = cellFor(m, measurement);\n return { label: deviceLabel(m), token: m.token, value: cell.kind === 'value' ? cell.value : null };\n });\n if (sort === 'none') return bars;\n const sign = sort === 'ascending' ? 1 : -1;\n return bars\n .map((bar, i) => ({ bar, i }))\n .sort((a, b) => {\n if (a.bar.value === null || b.bar.value === null) {\n return a.bar.value === b.bar.value ? a.i - b.i : a.bar.value === null ? 1 : -1;\n }\n return sign * (a.bar.value - b.bar.value) || a.i - b.i;\n })\n .map((x) => x.bar);\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The fixed words the fleet table and the per-device bar chart render themselves: the device\n// column, the reason a cell holds no number, the truncation and paging captions. The widget\n// packages carry no i18n runtime, so this is the one small per-language table (modelled on\n// aggregate-strings.ts); option labels are translated in the console's catalogs, not here.\n\nimport { viewerLocale } from '@devicechain/widget-sdk';\nimport { ACCESS_STRINGS } from './access-strings';\n\nexport interface FleetStrings {\n device: string;\n loading: string;\n // The reason a cell holds no number (D-5): each is a different fact about the device.\n notReported: string;\n // It reported this, but its newest reading was not loaded: there IS data.\n unread: string;\n noData: string;\n failed: string;\n // Whole-widget states.\n unbound: string;\n forbidden: string;\n unsupported: string;\n noMembers: string;\n noMeasurement: string;\n // A widget that shows one device, bound to a group: refused, with where the group can be shown.\n oneDevice: string;\n useFleetTable: string;\n usePerDevice: string;\n stale: string;\n // \"Showing 500 of 812 devices\": a fleet past what is read must say so.\n truncated: (shown: number, total: number) => string;\n range: (from: number, to: number, of: number) => string;\n previous: string;\n next: string;\n sortBy: (column: string) => string;\n drill: (device: string) => string;\n}\n\nexport const FLEET_STRINGS: Record<'en' | 'es', FleetStrings> = {\n en: {\n device: 'Device',\n loading: 'Loading',\n notReported: 'does not report this',\n unread: 'not loaded',\n noData: 'no data in range',\n failed: 'could not be read',\n unbound: 'Choose a device group for this widget',\n forbidden: ACCESS_STRINGS.en.forbidden,\n unsupported: 'This data source cannot list a fleet',\n noMembers: 'No devices in this group',\n noMeasurement: 'Choose the measurement to compare',\n oneDevice: 'This widget shows one device, and it is bound to a group of devices whose readings it would mix together.',\n useFleetTable: 'Bind it to one device, or show the group in a fleet table.',\n usePerDevice: 'Set it to one bar per device to compare them, or bind it to one device.',\n stale: 'stale',\n truncated: (shown, total) => `Showing ${shown} of ${total} devices`,\n range: (from, to, of) => `${from}\u2013${to} of ${of}`,\n previous: 'Previous page',\n next: 'Next page',\n sortBy: (column) => `Sort by ${column}`,\n drill: (device) => `Show ${device} in the linked widgets`,\n },\n es: {\n device: 'Dispositivo',\n loading: 'Cargando',\n notReported: 'no informa esta medici\u00F3n',\n unread: 'no cargado',\n noData: 'sin datos en el rango',\n failed: 'no se pudo leer',\n unbound: 'Elija un grupo de dispositivos para este widget',\n forbidden: ACCESS_STRINGS.es.forbidden,\n unsupported: 'Esta fuente de datos no puede listar una flota',\n noMembers: 'No hay dispositivos en este grupo',\n noMeasurement: 'Elija la medici\u00F3n que comparar',\n oneDevice: 'Este widget muestra un solo dispositivo y est\u00E1 vinculado a un grupo de dispositivos cuyas lecturas mezclar\u00EDa.',\n useFleetTable: 'Vinc\u00FAlelo a un dispositivo o muestre el grupo en una tabla de flota.',\n usePerDevice: 'Config\u00FArelo con una barra por dispositivo para compararlos, o vinc\u00FAlelo a un dispositivo.',\n stale: 'obsoleto',\n truncated: (shown, total) => `Mostrando ${shown} de ${total} dispositivos`,\n range: (from, to, of) => `${from}\u2013${to} de ${of}`,\n previous: 'P\u00E1gina anterior',\n next: 'P\u00E1gina siguiente',\n sortBy: (column) => `Ordenar por ${column}`,\n drill: (device) => `Mostrar ${device} en los widgets enlazados`,\n },\n};\n\nexport function fleetStrings(locale: string | undefined = viewerLocale()): FleetStrings {\n return locale?.toLowerCase().split('-')[0] === 'es' ? FLEET_STRINGS.es : FLEET_STRINGS.en;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,419 @@
1
+ // src/widgets/command-status.ts
2
+ import { isTerminalCommandStatus } from "@devicechain/dashboards";
3
+ var STATUS_COLORS = {
4
+ SUCCESSFUL: "#16a34a",
5
+ // green-600 — acknowledged by the device
6
+ FAILED: "#dc2626",
7
+ // red-600
8
+ TIMEOUT: "#dc2626",
9
+ // red-600 — never acknowledged in time
10
+ EXPIRED: "#64748b",
11
+ // slate-500 — lapsed before it was ever sent
12
+ CANCELLED: "#7c3aed",
13
+ // violet-600 — deliberately called off; a decision, not a failure
14
+ QUEUED: "#0284c7",
15
+ // sky-600 — in flight
16
+ SENT: "#0284c7",
17
+ HELD: "#d97706",
18
+ // amber-600 — withheld: the device is absent, the command still stands
19
+ PARKED: "#f59e0b"
20
+ // amber-500 — published, nobody home; held for the device's return
21
+ };
22
+ var UNKNOWN_COLOR = "#64748b";
23
+ function commandStatusColor(status) {
24
+ return STATUS_COLORS[status] ?? UNKNOWN_COLOR;
25
+ }
26
+ function isTerminalStatus(status) {
27
+ return isTerminalCommandStatus(status);
28
+ }
29
+ function commandStatusLabel(status) {
30
+ if (!status) return "";
31
+ return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
32
+ }
33
+
34
+ // src/widgets/command-params.ts
35
+ var DATA_TYPES = /* @__PURE__ */ new Set(["DOUBLE", "INT", "BOOLEAN", "STRING"]);
36
+ function parseParameterSchema(schema) {
37
+ if (!schema) return [];
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(schema);
41
+ } catch {
42
+ return [];
43
+ }
44
+ if (!Array.isArray(parsed)) return [];
45
+ const out = [];
46
+ for (const raw of parsed) {
47
+ const norm = normalizeParam(raw);
48
+ if (norm) out.push(norm);
49
+ }
50
+ return out;
51
+ }
52
+ function normalizeParam(raw) {
53
+ if (typeof raw !== "object" || raw === null) return null;
54
+ const r = raw;
55
+ if (typeof r.name !== "string") return null;
56
+ const param = { name: r.name };
57
+ if (typeof r.description === "string") param.description = r.description;
58
+ if (r.kind === "OBJECT") param.kind = "OBJECT";
59
+ else if (r.kind === "SCALAR") param.kind = "SCALAR";
60
+ if (typeof r.dataType === "string" && DATA_TYPES.has(r.dataType)) {
61
+ param.dataType = r.dataType;
62
+ }
63
+ if (typeof r.unit === "string") param.unit = r.unit;
64
+ if (r.required === true) param.required = true;
65
+ if (typeof r.default === "string") param.default = r.default;
66
+ if (typeof r.minValue === "number" && Number.isFinite(r.minValue)) param.minValue = r.minValue;
67
+ if (typeof r.maxValue === "number" && Number.isFinite(r.maxValue)) param.maxValue = r.maxValue;
68
+ if (Array.isArray(r.enum)) {
69
+ const values = r.enum.filter((v) => typeof v === "string");
70
+ if (values.length > 0) param.enum = values;
71
+ }
72
+ return param;
73
+ }
74
+ function isScalar(param) {
75
+ return param.kind !== "OBJECT";
76
+ }
77
+ function parseBool(raw) {
78
+ const s = raw.trim().toLowerCase();
79
+ return s === "true" || s === "1" || s === "t" || s === "yes" || s === "on";
80
+ }
81
+ function defaultValues(params) {
82
+ const values = {};
83
+ for (const p of params) {
84
+ if (!isScalar(p)) continue;
85
+ if (p.dataType === "BOOLEAN") values[p.name] = parseBool(p.default ?? "") ? "true" : "false";
86
+ else if (p.default != null) values[p.name] = p.default;
87
+ }
88
+ return values;
89
+ }
90
+ function validateParams(params, values) {
91
+ const errors = {};
92
+ for (const p of params) {
93
+ if (!isScalar(p)) {
94
+ if (p.required) errors[p.name] = "Structured parameter \u2014 not supported here";
95
+ continue;
96
+ }
97
+ if (p.dataType === "BOOLEAN") continue;
98
+ const raw = (values[p.name] ?? "").trim();
99
+ if (raw === "") {
100
+ if (p.required) errors[p.name] = "Required";
101
+ continue;
102
+ }
103
+ if (p.dataType === "INT" || p.dataType === "DOUBLE") {
104
+ const n = Number(raw);
105
+ if (!Number.isFinite(n)) {
106
+ errors[p.name] = "Must be a number";
107
+ } else if (p.dataType === "INT" && !Number.isInteger(n)) {
108
+ errors[p.name] = "Must be a whole number";
109
+ } else if (p.minValue != null && n < p.minValue) {
110
+ errors[p.name] = `Must be \u2265 ${p.minValue}`;
111
+ } else if (p.maxValue != null && n > p.maxValue) {
112
+ errors[p.name] = `Must be \u2264 ${p.maxValue}`;
113
+ }
114
+ }
115
+ }
116
+ return errors;
117
+ }
118
+ function buildPayload(params, values) {
119
+ const payload = {};
120
+ for (const p of params) {
121
+ if (!isScalar(p)) continue;
122
+ const raw = (values[p.name] ?? "").trim();
123
+ if (p.dataType === "BOOLEAN") {
124
+ payload[p.name] = parseBool(raw);
125
+ continue;
126
+ }
127
+ if (raw === "") continue;
128
+ payload[p.name] = coerce(p, raw);
129
+ }
130
+ return Object.keys(payload).length === 0 ? void 0 : JSON.stringify(payload);
131
+ }
132
+ function coerce(param, raw) {
133
+ switch (param.dataType) {
134
+ case "INT":
135
+ case "DOUBLE": {
136
+ const n = Number(raw);
137
+ return Number.isFinite(n) ? n : raw;
138
+ }
139
+ case "BOOLEAN":
140
+ return parseBool(raw);
141
+ default:
142
+ return raw;
143
+ }
144
+ }
145
+
146
+ // src/widgets/command-button.tsx
147
+ import { useEffect, useMemo, useRef, useState } from "react";
148
+ import {
149
+ css,
150
+ fontVar,
151
+ formatDateTime,
152
+ optString,
153
+ useWidgetTimeZone,
154
+ WidgetFrame
155
+ } from "@devicechain/widget-sdk";
156
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
157
+ function CommandButton({ widget, data, actions }) {
158
+ const commandName = optString(widget.options, "commandName");
159
+ const commandLabel = optString(widget.options, "commandLabel") ?? commandName;
160
+ const parameterSchema = optString(widget.options, "parameterSchema") ?? null;
161
+ const params = useMemo(() => parseParameterSchema(parameterSchema), [parameterSchema]);
162
+ const schemaKey = `${commandName ?? ""}::${parameterSchema ?? ""}`;
163
+ const [values, setValues] = useState(() => defaultValues(params));
164
+ const [errors, setErrors] = useState({});
165
+ const [pending, setPending] = useState(false);
166
+ const [sendError, setSendError] = useState(null);
167
+ const [sentToken, setSentToken] = useState(null);
168
+ const sendGen = useRef(0);
169
+ useEffect(() => {
170
+ sendGen.current += 1;
171
+ setValues(defaultValues(params));
172
+ setErrors({});
173
+ setSendError(null);
174
+ setSentToken(null);
175
+ }, [schemaKey]);
176
+ const canWrite = actions?.can("command:write") ?? false;
177
+ const hasSeam = typeof actions?.sendCommand === "function";
178
+ const deviceToken = data.deviceToken;
179
+ const formDisabled = !canWrite || !hasSeam || pending;
180
+ const canSend = !formDisabled && !!deviceToken && !!commandName;
181
+ const setField = (name, value) => {
182
+ setValues((prev) => ({ ...prev, [name]: value }));
183
+ setErrors((prev) => prev[name] ? { ...prev, [name]: "" } : prev);
184
+ setSendError(null);
185
+ };
186
+ const send = () => {
187
+ if (!deviceToken || !commandName || !actions?.sendCommand) return;
188
+ const found = validateParams(params, values);
189
+ const active = Object.fromEntries(Object.entries(found).filter(([, v]) => v));
190
+ if (Object.keys(active).length > 0) {
191
+ setErrors(active);
192
+ return;
193
+ }
194
+ const payload = buildPayload(params, values);
195
+ const gen = sendGen.current;
196
+ setPending(true);
197
+ setSendError(null);
198
+ let promise;
199
+ try {
200
+ promise = actions.sendCommand(deviceToken, commandName, payload);
201
+ } catch {
202
+ setSendError(SEND_FAILED);
203
+ setPending(false);
204
+ return;
205
+ }
206
+ promise.then((dispatch) => {
207
+ if (gen !== sendGen.current) return;
208
+ if (dispatch.status === "rejected") {
209
+ setSendError(dispatch.reason);
210
+ return;
211
+ }
212
+ setSentToken(dispatch.token);
213
+ }).catch(() => {
214
+ if (gen === sendGen.current) setSendError(SEND_FAILED);
215
+ }).finally(() => {
216
+ if (gen === sendGen.current) setPending(false);
217
+ });
218
+ };
219
+ return /* @__PURE__ */ jsx(WidgetFrame, { title: optString(widget.options, "title"), children: /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", height: "100%", gap: 10, overflowY: "auto" }, children: !commandName ? /* @__PURE__ */ jsx("div", { style: { ...hint, margin: "auto" }, children: "No command configured." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
220
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, flexShrink: 0 }, children: [
221
+ params.map(
222
+ (param) => isScalar(param) ? /* @__PURE__ */ jsx(
223
+ ParamField,
224
+ {
225
+ widgetId: widget.id,
226
+ param,
227
+ value: values[param.name] ?? "",
228
+ error: errors[param.name],
229
+ disabled: formDisabled,
230
+ onChange: (v) => setField(param.name, v)
231
+ },
232
+ param.name
233
+ ) : (
234
+ // A structured (OBJECT) parameter isn't form-editable. A REQUIRED one
235
+ // blocks sending (validateParams flags it); flag that in the note.
236
+ /* @__PURE__ */ jsxs("div", { style: { ...hint, color: param.required ? commandStatusColor("FAILED") : css("muted-foreground") }, children: [
237
+ param.name,
238
+ ": ",
239
+ param.required ? "required " : "",
240
+ "structured parameter \u2014 not supported here."
241
+ ] }, param.name)
242
+ )
243
+ ),
244
+ /* @__PURE__ */ jsx(
245
+ "button",
246
+ {
247
+ type: "button",
248
+ style: { ...sendButton, opacity: canSend ? 1 : 0.55, cursor: canSend ? "pointer" : "not-allowed" },
249
+ disabled: !canSend,
250
+ "aria-busy": pending,
251
+ onClick: send,
252
+ children: pending ? "Sending\u2026" : `Send ${commandLabel}`
253
+ }
254
+ ),
255
+ !canWrite ? /* @__PURE__ */ jsx("div", { style: hint, children: "You don\u2019t have permission to issue commands." }) : !hasSeam ? /* @__PURE__ */ jsx("div", { style: hint, children: "Commands aren\u2019t available in this view." }) : !deviceToken ? /* @__PURE__ */ jsx("div", { style: hint, children: "No device is selected for this command." }) : null,
256
+ sendError ? /* @__PURE__ */ jsx("div", { role: "alert", style: { ...hint, color: commandStatusColor("FAILED") }, children: sendError }) : null
257
+ ] }),
258
+ /* @__PURE__ */ jsx(CommandHistory, { commands: data.commands, sentToken, loading: data.loading, error: data.error })
259
+ ] }) }) });
260
+ }
261
+ function ParamField({
262
+ widgetId,
263
+ param,
264
+ value,
265
+ error,
266
+ disabled,
267
+ onChange
268
+ }) {
269
+ const labelId = `cmd-${widgetId}-${param.name}`;
270
+ const unit = param.unit ? ` (${param.unit})` : "";
271
+ return /* @__PURE__ */ jsxs("label", { htmlFor: labelId, style: { display: "flex", flexDirection: "column", gap: 3 }, children: [
272
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: fontVar("sm"), color: css("muted-foreground") }, children: [
273
+ param.name,
274
+ unit,
275
+ param.required ? /* @__PURE__ */ jsx("span", { style: { color: commandStatusColor("FAILED") }, children: " *" }) : null
276
+ ] }),
277
+ /* @__PURE__ */ jsx(ParamInput, { id: labelId, param, value, disabled, onChange }),
278
+ error ? /* @__PURE__ */ jsx("span", { style: { fontSize: fontVar("xs"), color: commandStatusColor("FAILED") }, children: error }) : null
279
+ ] });
280
+ }
281
+ function ParamInput({
282
+ id,
283
+ param,
284
+ value,
285
+ disabled,
286
+ onChange
287
+ }) {
288
+ if (param.enum && param.enum.length > 0) {
289
+ return /* @__PURE__ */ jsxs("select", { id, style: inputStyle, value, disabled, onChange: (e) => onChange(e.target.value), children: [
290
+ !param.required ? /* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }) : null,
291
+ param.enum.map((opt) => /* @__PURE__ */ jsx("option", { value: opt, children: opt }, opt))
292
+ ] });
293
+ }
294
+ if (param.dataType === "BOOLEAN") {
295
+ return /* @__PURE__ */ jsx(
296
+ "input",
297
+ {
298
+ id,
299
+ type: "checkbox",
300
+ style: { width: 16, height: 16 },
301
+ checked: parseBool(value),
302
+ disabled,
303
+ onChange: (e) => onChange(e.target.checked ? "true" : "false")
304
+ }
305
+ );
306
+ }
307
+ const numeric = param.dataType === "INT" || param.dataType === "DOUBLE";
308
+ return /* @__PURE__ */ jsx(
309
+ "input",
310
+ {
311
+ id,
312
+ type: numeric ? "number" : "text",
313
+ style: inputStyle,
314
+ value,
315
+ disabled,
316
+ min: param.minValue ?? void 0,
317
+ max: param.maxValue ?? void 0,
318
+ step: param.dataType === "INT" ? 1 : "any",
319
+ placeholder: param.default ?? void 0,
320
+ onChange: (e) => onChange(e.target.value)
321
+ }
322
+ );
323
+ }
324
+ function CommandHistory({
325
+ commands,
326
+ sentToken,
327
+ loading,
328
+ error
329
+ }) {
330
+ const empty = commands.length === 0;
331
+ const timeZone = useWidgetTimeZone();
332
+ return /* @__PURE__ */ jsx(
333
+ "div",
334
+ {
335
+ style: {
336
+ flex: 1,
337
+ minHeight: 0,
338
+ overflow: "auto",
339
+ borderTop: `1px solid ${css("border")}`,
340
+ paddingTop: 6
341
+ },
342
+ children: empty ? /* @__PURE__ */ jsx("div", { style: { ...hint, textAlign: "center", color: error ? commandStatusColor("FAILED") : css("muted-foreground") }, children: error ? "Couldn\u2019t load command history." : loading ? "Loading\u2026" : "No commands issued yet." }) : /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 4 }, children: commands.map((command) => /* @__PURE__ */ jsxs(
343
+ "li",
344
+ {
345
+ style: {
346
+ display: "flex",
347
+ alignItems: "center",
348
+ gap: 8,
349
+ fontSize: fontVar("sm"),
350
+ // Highlight the command just issued from this widget.
351
+ fontWeight: command.token === sentToken ? 600 : 400
352
+ },
353
+ children: [
354
+ /* @__PURE__ */ jsx(StatusBadge, { status: command.status }),
355
+ /* @__PURE__ */ jsx("span", { style: { color: css("foreground"), whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: command.name }),
356
+ /* @__PURE__ */ jsx("span", { style: { marginLeft: "auto", color: css("muted-foreground"), whiteSpace: "nowrap" }, children: formatDateTime(command.queuedTime, timeZone) })
357
+ ]
358
+ },
359
+ command.token
360
+ )) })
361
+ }
362
+ );
363
+ }
364
+ function StatusBadge({ status }) {
365
+ const color = commandStatusColor(status);
366
+ return /* @__PURE__ */ jsxs(
367
+ "span",
368
+ {
369
+ style: {
370
+ display: "inline-flex",
371
+ alignItems: "center",
372
+ gap: 4,
373
+ flexShrink: 0,
374
+ fontSize: fontVar("xs"),
375
+ fontWeight: 600,
376
+ color
377
+ },
378
+ children: [
379
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { width: 7, height: 7, borderRadius: "50%", background: color } }),
380
+ commandStatusLabel(status)
381
+ ]
382
+ }
383
+ );
384
+ }
385
+ var hint = { fontSize: fontVar("xs"), color: css("muted-foreground") };
386
+ var SEND_FAILED = "Couldn\u2019t issue the command. Try again.";
387
+ var inputStyle = {
388
+ width: "100%",
389
+ boxSizing: "border-box",
390
+ padding: "4px 8px",
391
+ fontSize: fontVar("md"),
392
+ borderRadius: 4,
393
+ border: `1px solid ${css("border")}`,
394
+ background: css("background"),
395
+ color: css("foreground")
396
+ };
397
+ var sendButton = {
398
+ padding: "6px 12px",
399
+ fontSize: fontVar("md"),
400
+ fontWeight: 600,
401
+ borderRadius: 4,
402
+ border: "none",
403
+ background: css("primary"),
404
+ color: css("background"),
405
+ marginTop: 2
406
+ };
407
+
408
+ export {
409
+ commandStatusColor,
410
+ isTerminalStatus,
411
+ commandStatusLabel,
412
+ parseParameterSchema,
413
+ isScalar,
414
+ defaultValues,
415
+ validateParams,
416
+ buildPayload,
417
+ CommandButton
418
+ };
419
+ //# sourceMappingURL=chunk-XSLFHCNI.js.map