@devicechain/widgets 0.25.2-rc.1 → 0.26.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.
@@ -105,17 +105,19 @@ function AggregateFeed({ hub, widget, descriptor, Component, actions, ErrorFrame
105
105
  useEffect(() => {
106
106
  if (!supported || !ds) return;
107
107
  let current = true;
108
+ let latest = 0;
108
109
  const run = () => {
110
+ const mine = ++latest;
109
111
  const { window, spanSeconds: covered } = aggregateWindow(spanSeconds, pausedAt ?? now(), trend ? bucketSecondsFor2(spanSeconds) : void 0);
110
112
  hub.bucketMeasurements(ds, window).then((buckets) => ({ byName: aggregateBuckets(buckets), buckets })).then(
111
113
  ({ byName, buckets }) => {
112
- if (!current) return;
114
+ if (!current || mine !== latest) return;
113
115
  const { startTime, endTime } = window;
114
116
  const series = trend ? { series: aggregateSeries(buckets) } : {};
115
117
  setTagged({ key, result: { status: "ready", byName, ...series, startTime, endTime, spanSeconds: covered } });
116
118
  },
117
119
  (err) => {
118
- if (!current) return;
120
+ if (!current || mine !== latest) return;
119
121
  const message = err instanceof Error ? err.message : String(err);
120
122
  setTagged({ key, result: { status: "unavailable", reason: "failed", message } });
121
123
  }
@@ -156,4 +158,4 @@ function AggregateFeed({ hub, widget, descriptor, Component, actions, ErrorFrame
156
158
  export {
157
159
  AggregateFeed as default
158
160
  };
159
- //# sourceMappingURL=aggregate-feed-XX36GNY6.js.map
161
+ //# sourceMappingURL=aggregate-feed-7JBQIYJE.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/aggregate-feed.tsx", "../../src/aggregate-fold.ts"],
4
+ "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The data feed of an aggregate widget: the exact aggregates of its measurements over its span,\n// plus the newest live reading of each. Loaded on first use (connected-widget.tsx imports it\n// dynamically), so a board with no aggregate widget never downloads it.\n//\n// The aggregates are folded from the source's own buckets (WidgetDataSource.bucketMeasurements: each\n// carries the exact count, min, max and sum), never from the chart seed: a seed is a series of bucket\n// AVERAGES, and a minimum or maximum cannot be recovered from those. A source that produces no\n// buckets is an explained state, not a guess.\n//\n// - the span is the widget's pinned one, else the board's (DEFAULT_TIME_RANGE_SECONDS off a board);\n// - the window is whole minutes (aggregateWindow), so it holds no reading from before the span;\n// - live, the read ends at the last minute and repeats on the boundaries of aggregateRefreshMs\n// (one chart bucket of the span: a minute for an hour, 42 minutes for a week), so every card on\n// the board reads at the same moment and the hub serves identical reads once; paused, it ends\n// at the freeze instant and is read once, so the number on screen does not move;\n// - a card with `sparkline` set reads the SAME window in the span's chart buckets\n// (bucketSecondsFor) instead of one, and keeps each bucket as a trend point; the figure is\n// folded from those buckets and is the same number either way;\n// - a hidden tab does not read; it reads once when it is shown again;\n// - a live feed that resumes after an interruption reads again at once, so the figures take in\n// the readings sent during the outage (and a read that failed during it stops saying so) without\n// waiting for the next refresh, which on a week's span is 42 minutes away;\n// - a read for a different selector, span or freeze instant is not shown under the new one: a\n// result is tagged with the key it was read for, and a mismatch reads as loading.\n\nimport {\n bucketSecondsFor,\n DEFAULT_TIME_RANGE_SECONDS,\n type AggregateResult,\n type AggregateStreamState,\n type WidgetActions,\n type WidgetDataSource,\n type WidgetDescriptor,\n type WidgetInstance,\n} from '@devicechain/dashboards';\nimport { optBoolean, useMeasurementStream, useWidgetTimeRange } from '@devicechain/widget-sdk';\nimport { useEffect, useMemo, useReducer, useState, type ComponentType } from 'react';\n\nimport { aggregateBuckets, aggregateRefreshMs, aggregateSeries, aggregateWindow } from './aggregate-fold';\nimport { measurementSpan } from './binding-policy';\n\nconst LOADING: AggregateResult = { status: 'loading' };\n\nexport interface AggregateFeedProps {\n hub: WidgetDataSource;\n widget: WidgetInstance;\n descriptor: WidgetDescriptor;\n // The widget's component, and the host's pane for a stream that failed (before any figure).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Component: ComponentType<any>;\n actions: WidgetActions | undefined;\n ErrorFrame: ComponentType<{ widget: WidgetInstance; error: unknown }>;\n}\n\nexport default function AggregateFeed({ hub, widget, descriptor, Component, actions, ErrorFrame }: AggregateFeedProps) {\n const range = useWidgetTimeRange();\n const now = useMemo(() => (hub.now ? () => hub.now!() : Date.now), [hub]);\n const spanSeconds = measurementSpan(descriptor, widget, range?.spanSeconds) ?? DEFAULT_TIME_RANGE_SECONDS;\n const pausedAt = range?.paused ? range.pausedAt : undefined;\n const ds = widget.datasource;\n\n // The newest live reading per measurement. It holds one sample: only `latest` is read, and the\n // pause keeps it from moving.\n const [resumes, resumed] = useReducer((n: number) => n + 1, 0);\n const stream = useMeasurementStream(hub, ds, { window: 1, pausedAt, now, onReconnect: resumed });\n\n const trend = optBoolean(widget.options, 'sparkline');\n const key = JSON.stringify([ds ?? null, spanSeconds, pausedAt ?? null, trend]);\n const supported = hub.bucketMeasurements !== undefined;\n const [tagged, setTagged] = useState<{ key: string; result: AggregateResult }>({ key, result: LOADING });\n\n useEffect(() => {\n if (!supported || !ds) return;\n let current = true;\n // Reads overlap (a boundary, a tab shown, a reconnect); only the NEWEST one's outcome is applied.\n let latest = 0;\n const run = (): void => {\n const mine = ++latest;\n const { window, spanSeconds: covered } = aggregateWindow(spanSeconds, pausedAt ?? now(), trend ? bucketSecondsFor(spanSeconds) : undefined);\n // Folded inside the chain, so a bucket that cannot be trusted fails the read like a network error.\n hub.bucketMeasurements!(ds, window).then((buckets) => ({ byName: aggregateBuckets(buckets), buckets })).then(\n ({ byName, buckets }) => {\n if (!current || mine !== latest) return;\n const { startTime, endTime } = window;\n const series = trend ? { series: aggregateSeries(buckets) } : {};\n setTagged({ key, result: { status: 'ready', byName, ...series, startTime, endTime, spanSeconds: covered } });\n },\n (err: unknown) => {\n if (!current || mine !== latest) return;\n const message = err instanceof Error ? err.message : String(err);\n setTagged({ key, result: { status: 'unavailable', reason: 'failed', message } });\n },\n );\n };\n run();\n // Paused, the window is fixed at the freeze instant: nothing new can land in it.\n if (pausedAt !== undefined) return () => void (current = false);\n const hidden = () => document.hidden;\n const every = aggregateRefreshMs(spanSeconds);\n let timer: ReturnType<typeof setTimeout>;\n const schedule = (): void => {\n timer = setTimeout(\n () => {\n if (!hidden()) run();\n schedule();\n },\n every - (now() % every),\n );\n };\n schedule();\n const shown = (): void => {\n if (!hidden()) run();\n };\n document.addEventListener('visibilitychange', shown);\n return () => {\n current = false;\n clearTimeout(timer);\n document.removeEventListener('visibilitychange', shown);\n };\n // `ds` is read through `key` (value identity), not by reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, supported, key, resumes]);\n\n let aggregates: AggregateResult;\n if (!supported) aggregates = { status: 'unavailable', reason: 'unsupported' };\n else if (!ds) aggregates = { status: 'ready', byName: {}, startTime: '', endTime: '', spanSeconds };\n else aggregates = tagged.key === key ? tagged.result : LOADING;\n\n const data: AggregateStreamState = { aggregates, latest: stream.latest, error: stream.error };\n return data.error ? <ErrorFrame widget={widget} error={data.error} /> : <Component widget={widget} data={data} actions={actions} />;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Aggregates over a span, folded from the backend's time buckets. Each bucket carries the exact\n// count, minimum, maximum and sum of the readings in it, so the span's aggregates are exact too:\n// the min of the mins, the max of the maxes, the sum of the sums, the count of the counts, and a\n// mean that is the total sum over the total count (which is the count-weighted mean of the\n// buckets' averages, without ever reading an average).\n//\n// \uD83D\uDD34 A MINIMUM OR MAXIMUM IS NEVER DERIVED FROM AVERAGES. A bucket's average hides its extremes, so\n// the \"max of the avgs\" is a plausible number that is not the maximum. A bucket that holds\n// readings but lacks a field the aggregate needs is therefore a loud failure here, never a\n// quiet substitution.\n\nimport {\n bucketSecondsFor,\n type AggregateTrendPoint,\n type HistoryWindow,\n type MeasurementAggregate,\n type MeasurementBucket,\n} from '@devicechain/dashboards';\n\nexport class AggregateFieldsError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AggregateFieldsError';\n }\n}\n\n// aggregateBuckets folds the buckets (any number of devices) into one aggregate per measurement\n// name. A name with no readings in any bucket is absent from the result, not zero.\nexport function aggregateBuckets(buckets: readonly MeasurementBucket[]): Record<string, MeasurementAggregate> {\n const out: Record<string, MeasurementAggregate> = {};\n for (const b of buckets) {\n if (typeof b.count !== 'number') {\n throw new AggregateFieldsError(`the bucket at ${b.bucketStart} for \"${b.name}\" carries no reading count`);\n }\n if (b.count === 0) continue;\n const { min, max, sum } = b;\n if (min == null || max == null || sum == null) {\n const missing = [min == null && 'min', max == null && 'max', sum == null && 'sum'].filter(Boolean);\n throw new AggregateFieldsError(\n `the bucket at ${b.bucketStart} for \"${b.name}\" holds ${b.count} readings but no ${missing.join(', ')}`,\n );\n }\n const a = out[b.name];\n if (!a) out[b.name] = { count: b.count, min, max, sum, avg: sum / b.count };\n else {\n a.count += b.count;\n a.sum += sum;\n a.min = Math.min(a.min, min);\n a.max = Math.max(a.max, max);\n a.avg = a.sum / a.count;\n }\n }\n return out;\n}\n\n// aggregateSeries lays the same buckets out as a trend: per measurement name, one point per bucket\n// that holds readings, oldest first. Buckets of several devices at one instant merge exactly as\n// aggregateBuckets merges them. Call it only on buckets aggregateBuckets accepted (it does not\n// re-validate), so a bucket that cannot be trusted has already failed the read.\nexport function aggregateSeries(buckets: readonly MeasurementBucket[]): Record<string, AggregateTrendPoint[]> {\n const byName = new Map<string, Map<string, AggregateTrendPoint>>();\n for (const b of buckets) {\n if (!b.count || b.min == null || b.max == null || b.sum == null) continue;\n let points = byName.get(b.name);\n if (!points) byName.set(b.name, (points = new Map()));\n const at = points.get(b.bucketStart);\n if (!at) {\n points.set(b.bucketStart, { bucketStart: b.bucketStart, count: b.count, min: b.min, max: b.max, sum: b.sum, avg: b.sum / b.count });\n } else {\n at.count += b.count;\n at.sum += b.sum;\n at.min = Math.min(at.min, b.min);\n at.max = Math.max(at.max, b.max);\n at.avg = at.sum / at.count;\n }\n }\n const out: Record<string, AggregateTrendPoint[]> = {};\n for (const [name, points] of byName) {\n out[name] = [...points.values()].sort((a, b) => Date.parse(a.bucketStart) - Date.parse(b.bucketStart));\n }\n return out;\n}\n\n// ---- The window a stat reads ------------------------------------------------------------------\n\nconst MINUTE_MS = 60_000;\n\n// aggregateRefreshMs is how often a live stat reads again: one bucket of the span's chart\n// (bucketSecondsFor), so never more often than once a minute. Finer is wasted -- the window it\n// reads moves in whole minutes (aggregateWindow) -- and a 7-day figure re-read every few seconds\n// costs the event store a scan of a week of summaries to move by a few seconds' readings.\nexport function aggregateRefreshMs(spanSeconds: number): number {\n return bucketSecondsFor(spanSeconds) * 1000;\n}\n\nexport interface AggregateWindow {\n window: HistoryWindow;\n // The length the window actually covers, in seconds: the span, or less when the span is not a\n // whole number of minutes. This, not the span asked for, is what the card says it covers.\n spanSeconds: number;\n}\n\n// aggregateWindow is the window a stat over `spanSeconds` reads at `at` (epoch ms): whole minutes,\n// ending at the last minute boundary at or before `at`.\n//\n// \uD83D\uDD34 Whole minutes because the event store answers from one-minute summaries, and a summary is\n// read whole: a window starting mid-minute would also count the readings from that minute's\n// start, BEFORE the span, so \"Maximum \u00B7 1h\" could be the maximum of an hour and a minute. Both\n// edges fall on minute boundaries instead, and the end is asked for as the instant just before\n// its boundary (the store's end is inclusive), so the summaries read are exactly the window's.\n// The start rounds UP, so a span that is not a whole number of minutes covers less, never more,\n// and says so (`spanSeconds`).\n//\n// Live cards read again on the boundaries of aggregateRefreshMs (a whole number of minutes), so\n// every card of one span ends its window on the same boundary: cards that read the same\n// measurement ask the same question at the same moment, and the hub answers them with one read.\n// One bucket wide: the card needs only the totals, so the store returns one or two rows per\n// measurement rather than a chart's worth.\n//\n// `bucketSeconds` asks for the window in finer buckets (a trend needs a point per bucket); the\n// totals fold identically from any number of buckets, so the figure is the same either way. Unset\n// the window is one bucket wide, as above.\nexport function aggregateWindow(spanSeconds: number, at: number, bucketSeconds?: number): AggregateWindow {\n const end = Math.floor(at / MINUTE_MS) * MINUTE_MS;\n const start = Math.ceil((end - spanSeconds * 1000) / MINUTE_MS) * MINUTE_MS;\n const covered = (end - start) / 1000;\n return {\n window: {\n startTime: new Date(start).toISOString(),\n endTime: new Date(end - 1).toISOString(),\n intervalSeconds: Math.max(60, bucketSeconds === undefined ? covered : Math.min(covered, bucketSeconds)),\n },\n spanSeconds: covered,\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["bucketSecondsFor", "bucketSecondsFor"]
7
+ }
@@ -279,14 +279,16 @@ function HistoryFeed({ hub, widget, descriptor, Component, actions }) {
279
279
  useEffect(() => {
280
280
  if (!supported || !ds || ds.kind === "unbound") return;
281
281
  let current = true;
282
+ let latest = 0;
282
283
  const run = () => {
284
+ const mine = ++latest;
283
285
  const window = historyWindow(spanSeconds, pausedAt ?? now());
284
286
  hub.bucketMeasurements(ds, window).then(
285
287
  (buckets) => {
286
- if (current) setRead({ key, view: { status: "ready", buckets, window }, readAt: Date.now() });
288
+ if (current && mine === latest) setRead({ key, view: { status: "ready", buckets, window }, readAt: Date.now() });
287
289
  },
288
290
  (err) => {
289
- if (!current) return;
291
+ if (!current || mine !== latest) return;
290
292
  const status = isForbiddenError(err) ? "forbidden" : "failed";
291
293
  setRead((prev) => status === "failed" && prev.key === key && prev.view.status === "ready" ? prev : { key, view: { status }, readAt: null });
292
294
  }
@@ -333,4 +335,4 @@ export {
333
335
  HistoryFeed,
334
336
  HistoryTable
335
337
  };
336
- //# sourceMappingURL=history-widgets-NSNXDKEC.js.map
338
+ //# sourceMappingURL=history-widgets-MBR4YRBV.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/widgets/history-table.tsx", "../../src/history-strings.ts", "../../src/history-feed.tsx"],
4
- "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// history-table -- one device's stored history as rows: a row per time bucket over the board's span,\n// a column per measurement, each cell the AVERAGE of that measurement's readings in the bucket. It\n// answers \"give me the numbers\" without leaving the board, and its frame menu exports them as CSV.\n//\n// \uD83D\uDD34 A BUCKET AVERAGE IS NOT A READING. The table says so wherever a figure could be taken for one:\n// the footer reads \"avg per 5m\" (with the sentence in its title), the time is the START of the\n// bucket, and the export's headings and file name carry the same words. There is no live tail: the\n// newest row is a bucket that is still filling, and the table reads it again on the bucket's\n// boundaries (history-feed.tsx).\n//\n// - a cell with no number is \"\u2014\" with a reason: that measurement has no readings in that bucket;\n// - newest first unless `oldestFirst`; long histories are paged (`pageSize`), and the export holds\n// every row, not the page on screen;\n// - on a narrow board the table becomes a card per bucket, so nothing is cut off at the right edge.\n\nimport {\n css,\n fontVar,\n formatDateTime,\n formatTimestamp,\n formatValue,\n optBoolean,\n optNumber,\n optString,\n spaceVar,\n useWidgetScale,\n useWidgetTimeZone,\n WidgetFrame,\n type WidgetProps,\n} from '@devicechain/widget-sdk';\nimport { useMemo, useState, type CSSProperties, type ReactNode } from 'react';\n\nimport {\n DEFAULT_PAGE_SIZE,\n bucketLabel,\n clampPage,\n columnHeading,\n historyColumns,\n historyExport,\n historyRows,\n observedNames,\n pageCount,\n spansDays,\n type HistoryColumn,\n type HistoryRow,\n type HistoryView,\n} from '../history-model';\nimport { historyStrings, type HistoryStrings } from '../history-strings';\nimport { LevelEmpty } from '../level-readout';\n\n// Below this width (px of the widget body) the table becomes a card per bucket.\nexport const STACK_BELOW_PX = 480;\n\n// The author's column list, read defensively: a hand-edited definition may carry anything, and an\n// entry that cannot be read drops out rather than take the table down.\nexport function authoredColumns(raw: unknown): HistoryColumn[] {\n if (!Array.isArray(raw)) return [];\n const out: HistoryColumn[] = [];\n for (const item of raw) {\n if (typeof item !== 'object' || item === null || Array.isArray(item)) continue;\n const entry = item as Record<string, unknown>;\n if (typeof entry.measurement !== 'string' || entry.measurement === '') continue;\n const column: HistoryColumn = { name: entry.measurement };\n if (typeof entry.label === 'string' && entry.label !== '') column.label = entry.label;\n if (typeof entry.unit === 'string' && entry.unit !== '') column.unit = entry.unit;\n if (typeof entry.precision === 'number' && Number.isInteger(entry.precision) && entry.precision >= 0 && entry.precision <= 100) {\n column.precision = entry.precision;\n }\n out.push(column);\n }\n return out;\n}\n\nconst cellBox: CSSProperties = { padding: `${spaceVar(1)} ${spaceVar(3)}`, textAlign: 'left', whiteSpace: 'nowrap', verticalAlign: 'top' };\nconst headBox: CSSProperties = {\n ...cellBox,\n fontSize: fontVar('xs'),\n fontWeight: 600,\n letterSpacing: '0.04em',\n color: css('muted-foreground'),\n borderBottom: `1px solid ${css('border')}`,\n background: css('card'),\n position: 'sticky',\n top: 0,\n zIndex: 1,\n};\n// The time column stays in view while the measurement columns scroll under it.\nconst stickyFirst: CSSProperties = { position: 'sticky', left: 0, background: css('card') };\nconst pagerButton: CSSProperties = {\n minWidth: '2em',\n padding: `0 ${spaceVar(2)}`,\n border: `1px solid ${css('border')}`,\n borderRadius: 4,\n background: 'none',\n color: 'inherit',\n font: 'inherit',\n cursor: 'pointer',\n};\n\nexport function HistoryTable({ widget, history }: WidgetProps & { history?: HistoryView }) {\n const strings = historyStrings();\n const title = optString(widget.options, 'title');\n const message = (text: string) => (\n <WidgetFrame title={title} flush>\n <LevelEmpty message={text} />\n </WidgetFrame>\n );\n if (!history) return message(strings.loading);\n if (history.status !== 'ready') return message(strings[history.status]);\n return <Ready widget={widget} history={history} strings={strings} />;\n}\n\nfunction Ready({\n widget,\n history,\n strings,\n}: {\n widget: WidgetProps['widget'];\n history: Extract<HistoryView, { status: 'ready' }>;\n strings: HistoryStrings;\n}) {\n const timeZone = useWidgetTimeZone();\n const title = optString(widget.options, 'title');\n const precision = optNumber(widget.options, 'precision');\n const oldestFirst = optBoolean(widget.options, 'oldestFirst') === true;\n const pageSize = optNumber(widget.options, 'pageSize') ?? DEFAULT_PAGE_SIZE;\n const authored = useMemo(() => authoredColumns(widget.options?.columns), [widget.options?.columns]);\n const columns = useMemo(\n () => historyColumns(authored, widget.datasource?.measurements ?? [], observedNames(history.buckets)),\n [authored, widget.datasource, history.buckets],\n );\n const rows = useMemo(() => historyRows(history.buckets, columns.map((c) => c.name), oldestFirst), [history.buckets, columns, oldestFirst]);\n const [page, setPage] = useState(0);\n const shown = clampPage(page, rows.length, pageSize);\n const pages = pageCount(rows.length, pageSize);\n const pageRows = rows.slice(shown * pageSize, (shown + 1) * pageSize);\n const bucket = bucketLabel(history.window.intervalSeconds);\n const dated = spansDays(rows, timeZone);\n const timeOf = (row: HistoryRow): string => (dated ? formatDateTime(row.bucketStart, timeZone) : formatTimestamp(row.bucketStart, timeZone));\n\n const subject = [title ?? 'history', widget.datasource?.kind === 'device' ? widget.datasource.deviceToken : undefined].filter(Boolean).join('-');\n // Built when the viewer picks the export, from the rows the table holds now: every row, in the order\n // on screen, over the window these buckets were read for.\n const exportRows = () =>\n historyExport({\n subject,\n columns,\n rows,\n window: history.window,\n precision,\n heading: { time: strings.exportTime, average: strings.average },\n });\n\n const cell = (row: HistoryRow, c: HistoryColumn): ReactNode => {\n const v = row.averages.get(c.name);\n if (v === undefined) {\n return (\n <span style={{ color: css('muted-foreground') }} title={strings.noReadings} data-cell-empty=\"\">\n \u2014\n </span>\n );\n }\n return <span style={{ fontVariantNumeric: 'tabular-nums' }}>{formatValue(v, c.precision ?? precision)}</span>;\n };\n\n const wide = (\n <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: fontVar('md'), color: css('foreground') }}>\n <thead>\n <tr>\n <th scope=\"col\" style={{ ...headBox, ...stickyFirst, zIndex: 2 }}>\n {strings.time}\n </th>\n {columns.map((c) => (\n <th key={c.name} scope=\"col\" style={headBox}>\n {columnHeading(c)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {pageRows.map((row) => (\n <tr key={row.bucketStart} data-bucket={row.bucketStart} style={{ borderBottom: `1px solid ${css('border')}` }}>\n <th scope=\"row\" style={{ ...cellBox, ...stickyFirst, fontWeight: 500, fontVariantNumeric: 'tabular-nums' }}>\n {timeOf(row)}\n </th>\n {columns.map((c) => (\n <td key={c.name} data-measurement={c.name} style={cellBox}>\n {cell(row, c)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n );\n\n const narrow = (\n <div data-history-stacked=\"\" style={{ color: css('foreground'), fontSize: fontVar('md') }}>\n {pageRows.map((row) => (\n <div key={row.bucketStart} data-bucket={row.bucketStart} style={{ padding: `${spaceVar(2)} ${spaceVar(3)}`, borderTop: `1px solid ${css('border')}` }}>\n <div style={{ fontWeight: 500, overflowWrap: 'anywhere' }}>{timeOf(row)}</div>\n {columns.map((c) => (\n <div key={c.name} data-measurement={c.name} style={{ display: 'flex', justifyContent: 'space-between', gap: spaceVar(3), alignItems: 'baseline' }}>\n <span style={{ color: css('muted-foreground'), fontSize: fontVar('sm'), overflowWrap: 'anywhere' }}>{columnHeading(c)}</span>\n <span style={{ textAlign: 'right', overflowWrap: 'anywhere' }}>{cell(row, c)}</span>\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n\n return (\n <WidgetFrame title={title} flush exportRows={exportRows}>\n <div style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>\n {rows.length === 0 ? (\n <LevelEmpty message={strings.empty} />\n ) : (\n // A focusable scroll region: a keyboard user can scroll a wide table sideways.\n <div role=\"region\" aria-label={title ?? strings.time} tabIndex={0} data-history-scroll=\"\" style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>\n <Responsive wide={wide} narrow={narrow} />\n </div>\n )}\n <Footer\n strings={strings}\n bucket={bucket}\n oldestFirst={oldestFirst}\n pages={pages}\n shown={shown}\n from={shown * pageSize + 1}\n to={shown * pageSize + pageRows.length}\n of={rows.length}\n onPage={setPage}\n />\n </div>\n </WidgetFrame>\n );\n}\n\nfunction Responsive({ wide, narrow }: { wide: ReactNode; narrow: ReactNode }) {\n const { width } = useWidgetScale().size;\n // An unmeasured body (width 0: before the first layout, or under a test renderer) takes the table.\n return width > 0 && width < STACK_BELOW_PX ? narrow : wide;\n}\n\n// Below this width (px of the widget body) the footer keeps only what cannot be left out: the basis of\n// the figures and the page buttons. The rest moves to their titles, so a table at the smallest span\n// still has a footer that fits on one line.\nconst COMPACT_FOOTER_BELOW_PX = 260;\n\nfunction Footer({\n strings,\n bucket,\n oldestFirst,\n pages,\n shown,\n from,\n to,\n of,\n onPage,\n}: {\n strings: HistoryStrings;\n bucket: string;\n oldestFirst: boolean;\n pages: number;\n shown: number;\n from: number;\n to: number;\n of: number;\n onPage: (page: number) => void;\n}) {\n const { width } = useWidgetScale().size;\n const compact = width > 0 && width < COMPACT_FOOTER_BELOW_PX;\n const order = oldestFirst ? strings.oldestFirst : strings.newestFirst;\n const range = strings.range(from, to, of);\n return (\n <div\n data-history-footer=\"\"\n style={{\n display: 'flex',\n flexWrap: 'wrap',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: spaceVar(2),\n padding: `${spaceVar(1)} ${spaceVar(3)}`,\n fontSize: fontVar('sm'),\n color: css('muted-foreground'),\n borderTop: `1px solid ${css('border')}`,\n }}\n >\n <span data-history-bucket={bucket} title={`${strings.averageTitle(bucket)} (${order})`}>\n {compact ? strings.average(bucket) : `${strings.average(bucket)} \u00B7 ${order}`}\n </span>\n {pages > 1 && (\n <span style={{ display: 'inline-flex', alignItems: 'center', gap: spaceVar(2) }} title={compact ? range : undefined}>\n <button type=\"button\" style={pagerButton} disabled={shown === 0} onClick={() => onPage(shown - 1)} aria-label={strings.previous}>\n \u2039\n </button>\n {compact ? null : <span>{range}</span>}\n <button type=\"button\" style={pagerButton} disabled={shown >= pages - 1} onClick={() => onPage(shown + 1)} aria-label={strings.next}>\n \u203A\n </button>\n </span>\n )}\n </div>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The fixed words the history table renders itself and exports with. The widget packages carry no i18n\n// runtime, so this is the one small per-language table (modelled on fleet-strings.ts); the option labels\n// are translated in the console's catalogs, not here. Loaded with the history table's chunk, never by a\n// board without one.\n\nimport { viewerLocale } from '@devicechain/widget-sdk';\n\nimport { ACCESS_STRINGS } from './access-strings';\n\nexport interface HistoryStrings {\n time: string;\n // The export's first heading: the instant is a bucket's START (written ISO 8601, in UTC).\n exportTime: string;\n // \"avg per 5m\": what every figure in the table is.\n average: (bucket: string) => string;\n averageTitle: (bucket: string) => string;\n loading: string;\n unbound: string;\n forbidden: string;\n unsupported: string;\n failed: string;\n empty: string;\n // A cell for a measurement with no readings in that bucket.\n noReadings: string;\n newestFirst: string;\n oldestFirst: string;\n range: (from: number, to: number, of: number) => string;\n previous: string;\n next: string;\n}\n\nexport const HISTORY_STRINGS: Record<'en' | 'es', HistoryStrings> = {\n en: {\n time: 'Time',\n exportTime: 'Bucket start (UTC)',\n average: (bucket) => `avg per ${bucket}`,\n averageTitle: (bucket) =>\n `Each figure is the average of the readings in a ${bucket} bucket, not a reading: the time is the start of its bucket.`,\n loading: 'Loading',\n unbound: 'Choose a device for this widget',\n forbidden: ACCESS_STRINGS.en.forbidden,\n unsupported: 'This data source cannot read stored history',\n failed: 'The history could not be read',\n empty: 'No readings in this range',\n noReadings: 'no readings in this bucket',\n newestFirst: 'newest first',\n oldestFirst: 'oldest first',\n range: (from, to, of) => `${from}\u2013${to} of ${of}`,\n previous: 'Previous page',\n next: 'Next page',\n },\n es: {\n time: 'Hora',\n exportTime: 'Inicio del intervalo (UTC)',\n average: (bucket) => `prom. por ${bucket}`,\n averageTitle: (bucket) =>\n `Cada cifra es el promedio de las lecturas de un intervalo de ${bucket}, no una lectura: la hora es el inicio de su intervalo.`,\n loading: 'Cargando',\n unbound: 'Elija un dispositivo para este widget',\n forbidden: ACCESS_STRINGS.es.forbidden,\n unsupported: 'Esta fuente de datos no puede leer el historial almacenado',\n failed: 'No se pudo leer el historial',\n empty: 'No hay lecturas en este intervalo',\n noReadings: 'sin lecturas en este intervalo',\n newestFirst: 'm\u00E1s recientes primero',\n oldestFirst: 'm\u00E1s antiguas primero',\n range: (from, to, of) => `${from}\u2013${to} de ${of}`,\n previous: 'P\u00E1gina anterior',\n next: 'P\u00E1gina siguiente',\n },\n};\n\nexport function historyStrings(): HistoryStrings {\n return viewerLocale()?.toLowerCase().startsWith('es') ? HISTORY_STRINGS.es : HISTORY_STRINGS.en;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The data feed of the history table: one device's stored history over its span, in time buckets.\n// Loaded with the table's chunk (connected-widget.tsx imports it dynamically), so a board without a\n// history table never downloads it.\n//\n// - the span is the widget's pinned one, else the board's (DEFAULT_TIME_RANGE_SECONDS off a board);\n// - the window is whole minutes ending at the last minute boundary, in the buckets of the span's chart\n// (bucketSecondsFor), read through the source's own bucketMeasurements: the same call a stat card\n// makes, so the hub shares identical reads. The table shows each bucket's AVERAGE and says so;\n// - there is no live tail. A reading that arrived since the last bucket read belongs to a bucket that\n// is still filling, and a figure that moved under the viewer between two reads would be a reading\n// passed off as an average. The table reads again on the boundaries of its bucket (live) and says\n// how old its read is (the frame's freshness is the last SUCCESSFUL read);\n// - paused, the window ends at the freeze instant and is read once;\n// - a hidden tab does not read; it reads once when it is shown again;\n// - a read for a different selector, span or freeze instant is not shown under the new one: a result\n// is tagged with the key it was read for, and a mismatch reads as loading. A read that fails keeps\n// the buckets it would have replaced (they are still true) and the frame ages from the last good one.\n\nimport { DEFAULT_TIME_RANGE_SECONDS, type WidgetActions, type WidgetDataSource, type WidgetDescriptor, type WidgetInstance } from '@devicechain/dashboards';\nimport { isForbiddenError } from '@devicechain/client';\nimport { resolveStaleAfterSeconds, useWidgetTimeRange, WidgetFreshnessProvider } from '@devicechain/widget-sdk';\nimport { useEffect, useMemo, useState, type ComponentType } from 'react';\n\nimport { measurementSpan } from './binding-policy';\nimport { historyRefreshMs, historyWindow, type HistoryView } from './history-model';\n\nexport interface HistoryFeedProps {\n hub: WidgetDataSource;\n widget: WidgetInstance;\n descriptor: WidgetDescriptor;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Component: ComponentType<any>;\n actions: WidgetActions | undefined;\n}\n\ninterface Read {\n key: string;\n view: HistoryView;\n // When the last successful read landed (epoch ms): the age of the figures on screen.\n readAt: number | null;\n}\n\nexport default function HistoryFeed({ hub, widget, descriptor, Component, actions }: HistoryFeedProps) {\n const range = useWidgetTimeRange();\n const now = useMemo(() => (hub.now ? () => hub.now!() : Date.now), [hub]);\n const spanSeconds = measurementSpan(descriptor, widget, range?.spanSeconds) ?? DEFAULT_TIME_RANGE_SECONDS;\n const pausedAt = range?.paused ? range.pausedAt : undefined;\n const ds = widget.datasource;\n const key = JSON.stringify([ds ?? null, spanSeconds, pausedAt ?? null]);\n const supported = hub.bucketMeasurements !== undefined;\n const [read, setRead] = useState<Read>({ key, view: { status: 'loading' }, readAt: null });\n\n useEffect(() => {\n if (!supported || !ds || ds.kind === 'unbound') return;\n let current = true;\n const run = (): void => {\n const window = historyWindow(spanSeconds, pausedAt ?? now());\n hub.bucketMeasurements!(ds, window).then(\n (buckets) => {\n if (current) setRead({ key, view: { status: 'ready', buckets, window }, readAt: Date.now() });\n },\n (err: unknown) => {\n if (!current) return;\n // A refusal is a state, not a failure; a failure keeps what is on screen for THIS read's key.\n const status = isForbiddenError(err) ? 'forbidden' : 'failed';\n setRead((prev) => (status === 'failed' && prev.key === key && prev.view.status === 'ready' ? prev : { key, view: { status }, readAt: null }));\n },\n );\n };\n run();\n // Paused, the window is fixed at the freeze instant: nothing new can land in it.\n if (pausedAt !== undefined) return () => void (current = false);\n const every = historyRefreshMs(spanSeconds);\n let timer: ReturnType<typeof setTimeout>;\n const schedule = (): void => {\n timer = setTimeout(\n () => {\n if (!document.hidden) run();\n schedule();\n },\n every - (now() % every),\n );\n };\n schedule();\n const shown = (): void => {\n if (!document.hidden) run();\n };\n document.addEventListener('visibilitychange', shown);\n return () => {\n current = false;\n clearTimeout(timer);\n document.removeEventListener('visibilitychange', shown);\n };\n // `ds` is read through `key` (value identity), not by reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, supported, key]);\n\n let view: HistoryView;\n let readAt: number | null = null;\n if (!supported) view = { status: 'unsupported' };\n else if (!ds || ds.kind === 'unbound') view = { status: 'unbound' };\n else if (read.key === key) ({ view, readAt } = read);\n else view = { status: 'loading' };\n\n const freshness = useMemo(\n () => (descriptor.freshness === true ? { at: readAt ?? undefined, staleAfterSeconds: resolveStaleAfterSeconds(widget.options) } : undefined),\n [descriptor, widget.options, readAt],\n );\n const data = { latest: {}, samples: [], error: null };\n return (\n <WidgetFreshnessProvider freshness={freshness}>\n <Component widget={widget} data={data} actions={actions} history={view} />\n </WidgetFreshnessProvider>\n );\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// history-table -- one device's stored history as rows: a row per time bucket over the board's span,\n// a column per measurement, each cell the AVERAGE of that measurement's readings in the bucket. It\n// answers \"give me the numbers\" without leaving the board, and its frame menu exports them as CSV.\n//\n// \uD83D\uDD34 A BUCKET AVERAGE IS NOT A READING. The table says so wherever a figure could be taken for one:\n// the footer reads \"avg per 5m\" (with the sentence in its title), the time is the START of the\n// bucket, and the export's headings and file name carry the same words. There is no live tail: the\n// newest row is a bucket that is still filling, and the table reads it again on the bucket's\n// boundaries (history-feed.tsx).\n//\n// - a cell with no number is \"\u2014\" with a reason: that measurement has no readings in that bucket;\n// - newest first unless `oldestFirst`; long histories are paged (`pageSize`), and the export holds\n// every row, not the page on screen;\n// - on a narrow board the table becomes a card per bucket, so nothing is cut off at the right edge.\n\nimport {\n css,\n fontVar,\n formatDateTime,\n formatTimestamp,\n formatValue,\n optBoolean,\n optNumber,\n optString,\n spaceVar,\n useWidgetScale,\n useWidgetTimeZone,\n WidgetFrame,\n type WidgetProps,\n} from '@devicechain/widget-sdk';\nimport { useMemo, useState, type CSSProperties, type ReactNode } from 'react';\n\nimport {\n DEFAULT_PAGE_SIZE,\n bucketLabel,\n clampPage,\n columnHeading,\n historyColumns,\n historyExport,\n historyRows,\n observedNames,\n pageCount,\n spansDays,\n type HistoryColumn,\n type HistoryRow,\n type HistoryView,\n} from '../history-model';\nimport { historyStrings, type HistoryStrings } from '../history-strings';\nimport { LevelEmpty } from '../level-readout';\n\n// Below this width (px of the widget body) the table becomes a card per bucket.\nexport const STACK_BELOW_PX = 480;\n\n// The author's column list, read defensively: a hand-edited definition may carry anything, and an\n// entry that cannot be read drops out rather than take the table down.\nexport function authoredColumns(raw: unknown): HistoryColumn[] {\n if (!Array.isArray(raw)) return [];\n const out: HistoryColumn[] = [];\n for (const item of raw) {\n if (typeof item !== 'object' || item === null || Array.isArray(item)) continue;\n const entry = item as Record<string, unknown>;\n if (typeof entry.measurement !== 'string' || entry.measurement === '') continue;\n const column: HistoryColumn = { name: entry.measurement };\n if (typeof entry.label === 'string' && entry.label !== '') column.label = entry.label;\n if (typeof entry.unit === 'string' && entry.unit !== '') column.unit = entry.unit;\n if (typeof entry.precision === 'number' && Number.isInteger(entry.precision) && entry.precision >= 0 && entry.precision <= 100) {\n column.precision = entry.precision;\n }\n out.push(column);\n }\n return out;\n}\n\nconst cellBox: CSSProperties = { padding: `${spaceVar(1)} ${spaceVar(3)}`, textAlign: 'left', whiteSpace: 'nowrap', verticalAlign: 'top' };\nconst headBox: CSSProperties = {\n ...cellBox,\n fontSize: fontVar('xs'),\n fontWeight: 600,\n letterSpacing: '0.04em',\n color: css('muted-foreground'),\n borderBottom: `1px solid ${css('border')}`,\n background: css('card'),\n position: 'sticky',\n top: 0,\n zIndex: 1,\n};\n// The time column stays in view while the measurement columns scroll under it.\nconst stickyFirst: CSSProperties = { position: 'sticky', left: 0, background: css('card') };\nconst pagerButton: CSSProperties = {\n minWidth: '2em',\n padding: `0 ${spaceVar(2)}`,\n border: `1px solid ${css('border')}`,\n borderRadius: 4,\n background: 'none',\n color: 'inherit',\n font: 'inherit',\n cursor: 'pointer',\n};\n\nexport function HistoryTable({ widget, history }: WidgetProps & { history?: HistoryView }) {\n const strings = historyStrings();\n const title = optString(widget.options, 'title');\n const message = (text: string) => (\n <WidgetFrame title={title} flush>\n <LevelEmpty message={text} />\n </WidgetFrame>\n );\n if (!history) return message(strings.loading);\n if (history.status !== 'ready') return message(strings[history.status]);\n return <Ready widget={widget} history={history} strings={strings} />;\n}\n\nfunction Ready({\n widget,\n history,\n strings,\n}: {\n widget: WidgetProps['widget'];\n history: Extract<HistoryView, { status: 'ready' }>;\n strings: HistoryStrings;\n}) {\n const timeZone = useWidgetTimeZone();\n const title = optString(widget.options, 'title');\n const precision = optNumber(widget.options, 'precision');\n const oldestFirst = optBoolean(widget.options, 'oldestFirst') === true;\n const pageSize = optNumber(widget.options, 'pageSize') ?? DEFAULT_PAGE_SIZE;\n const authored = useMemo(() => authoredColumns(widget.options?.columns), [widget.options?.columns]);\n const columns = useMemo(\n () => historyColumns(authored, widget.datasource?.measurements ?? [], observedNames(history.buckets)),\n [authored, widget.datasource, history.buckets],\n );\n const rows = useMemo(() => historyRows(history.buckets, columns.map((c) => c.name), oldestFirst), [history.buckets, columns, oldestFirst]);\n const [page, setPage] = useState(0);\n const shown = clampPage(page, rows.length, pageSize);\n const pages = pageCount(rows.length, pageSize);\n const pageRows = rows.slice(shown * pageSize, (shown + 1) * pageSize);\n const bucket = bucketLabel(history.window.intervalSeconds);\n const dated = spansDays(rows, timeZone);\n const timeOf = (row: HistoryRow): string => (dated ? formatDateTime(row.bucketStart, timeZone) : formatTimestamp(row.bucketStart, timeZone));\n\n const subject = [title ?? 'history', widget.datasource?.kind === 'device' ? widget.datasource.deviceToken : undefined].filter(Boolean).join('-');\n // Built when the viewer picks the export, from the rows the table holds now: every row, in the order\n // on screen, over the window these buckets were read for.\n const exportRows = () =>\n historyExport({\n subject,\n columns,\n rows,\n window: history.window,\n precision,\n heading: { time: strings.exportTime, average: strings.average },\n });\n\n const cell = (row: HistoryRow, c: HistoryColumn): ReactNode => {\n const v = row.averages.get(c.name);\n if (v === undefined) {\n return (\n <span style={{ color: css('muted-foreground') }} title={strings.noReadings} data-cell-empty=\"\">\n \u2014\n </span>\n );\n }\n return <span style={{ fontVariantNumeric: 'tabular-nums' }}>{formatValue(v, c.precision ?? precision)}</span>;\n };\n\n const wide = (\n <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: fontVar('md'), color: css('foreground') }}>\n <thead>\n <tr>\n <th scope=\"col\" style={{ ...headBox, ...stickyFirst, zIndex: 2 }}>\n {strings.time}\n </th>\n {columns.map((c) => (\n <th key={c.name} scope=\"col\" style={headBox}>\n {columnHeading(c)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {pageRows.map((row) => (\n <tr key={row.bucketStart} data-bucket={row.bucketStart} style={{ borderBottom: `1px solid ${css('border')}` }}>\n <th scope=\"row\" style={{ ...cellBox, ...stickyFirst, fontWeight: 500, fontVariantNumeric: 'tabular-nums' }}>\n {timeOf(row)}\n </th>\n {columns.map((c) => (\n <td key={c.name} data-measurement={c.name} style={cellBox}>\n {cell(row, c)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n );\n\n const narrow = (\n <div data-history-stacked=\"\" style={{ color: css('foreground'), fontSize: fontVar('md') }}>\n {pageRows.map((row) => (\n <div key={row.bucketStart} data-bucket={row.bucketStart} style={{ padding: `${spaceVar(2)} ${spaceVar(3)}`, borderTop: `1px solid ${css('border')}` }}>\n <div style={{ fontWeight: 500, overflowWrap: 'anywhere' }}>{timeOf(row)}</div>\n {columns.map((c) => (\n <div key={c.name} data-measurement={c.name} style={{ display: 'flex', justifyContent: 'space-between', gap: spaceVar(3), alignItems: 'baseline' }}>\n <span style={{ color: css('muted-foreground'), fontSize: fontVar('sm'), overflowWrap: 'anywhere' }}>{columnHeading(c)}</span>\n <span style={{ textAlign: 'right', overflowWrap: 'anywhere' }}>{cell(row, c)}</span>\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n\n return (\n <WidgetFrame title={title} flush exportRows={exportRows}>\n <div style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>\n {rows.length === 0 ? (\n <LevelEmpty message={strings.empty} />\n ) : (\n // A focusable scroll region: a keyboard user can scroll a wide table sideways.\n <div role=\"region\" aria-label={title ?? strings.time} tabIndex={0} data-history-scroll=\"\" style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>\n <Responsive wide={wide} narrow={narrow} />\n </div>\n )}\n <Footer\n strings={strings}\n bucket={bucket}\n oldestFirst={oldestFirst}\n pages={pages}\n shown={shown}\n from={shown * pageSize + 1}\n to={shown * pageSize + pageRows.length}\n of={rows.length}\n onPage={setPage}\n />\n </div>\n </WidgetFrame>\n );\n}\n\nfunction Responsive({ wide, narrow }: { wide: ReactNode; narrow: ReactNode }) {\n const { width } = useWidgetScale().size;\n // An unmeasured body (width 0: before the first layout, or under a test renderer) takes the table.\n return width > 0 && width < STACK_BELOW_PX ? narrow : wide;\n}\n\n// Below this width (px of the widget body) the footer keeps only what cannot be left out: the basis of\n// the figures and the page buttons. The rest moves to their titles, so a table at the smallest span\n// still has a footer that fits on one line.\nconst COMPACT_FOOTER_BELOW_PX = 260;\n\nfunction Footer({\n strings,\n bucket,\n oldestFirst,\n pages,\n shown,\n from,\n to,\n of,\n onPage,\n}: {\n strings: HistoryStrings;\n bucket: string;\n oldestFirst: boolean;\n pages: number;\n shown: number;\n from: number;\n to: number;\n of: number;\n onPage: (page: number) => void;\n}) {\n const { width } = useWidgetScale().size;\n const compact = width > 0 && width < COMPACT_FOOTER_BELOW_PX;\n const order = oldestFirst ? strings.oldestFirst : strings.newestFirst;\n const range = strings.range(from, to, of);\n return (\n <div\n data-history-footer=\"\"\n style={{\n display: 'flex',\n flexWrap: 'wrap',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: spaceVar(2),\n padding: `${spaceVar(1)} ${spaceVar(3)}`,\n fontSize: fontVar('sm'),\n color: css('muted-foreground'),\n borderTop: `1px solid ${css('border')}`,\n }}\n >\n <span data-history-bucket={bucket} title={`${strings.averageTitle(bucket)} (${order})`}>\n {compact ? strings.average(bucket) : `${strings.average(bucket)} \u00B7 ${order}`}\n </span>\n {pages > 1 && (\n <span style={{ display: 'inline-flex', alignItems: 'center', gap: spaceVar(2) }} title={compact ? range : undefined}>\n <button type=\"button\" style={pagerButton} disabled={shown === 0} onClick={() => onPage(shown - 1)} aria-label={strings.previous}>\n \u2039\n </button>\n {compact ? null : <span>{range}</span>}\n <button type=\"button\" style={pagerButton} disabled={shown >= pages - 1} onClick={() => onPage(shown + 1)} aria-label={strings.next}>\n \u203A\n </button>\n </span>\n )}\n </div>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The fixed words the history table renders itself and exports with. The widget packages carry no i18n\n// runtime, so this is the one small per-language table (modelled on fleet-strings.ts); the option labels\n// are translated in the console's catalogs, not here. Loaded with the history table's chunk, never by a\n// board without one.\n\nimport { viewerLocale } from '@devicechain/widget-sdk';\n\nimport { ACCESS_STRINGS } from './access-strings';\n\nexport interface HistoryStrings {\n time: string;\n // The export's first heading: the instant is a bucket's START (written ISO 8601, in UTC).\n exportTime: string;\n // \"avg per 5m\": what every figure in the table is.\n average: (bucket: string) => string;\n averageTitle: (bucket: string) => string;\n loading: string;\n unbound: string;\n forbidden: string;\n unsupported: string;\n failed: string;\n empty: string;\n // A cell for a measurement with no readings in that bucket.\n noReadings: string;\n newestFirst: string;\n oldestFirst: string;\n range: (from: number, to: number, of: number) => string;\n previous: string;\n next: string;\n}\n\nexport const HISTORY_STRINGS: Record<'en' | 'es', HistoryStrings> = {\n en: {\n time: 'Time',\n exportTime: 'Bucket start (UTC)',\n average: (bucket) => `avg per ${bucket}`,\n averageTitle: (bucket) =>\n `Each figure is the average of the readings in a ${bucket} bucket, not a reading: the time is the start of its bucket.`,\n loading: 'Loading',\n unbound: 'Choose a device for this widget',\n forbidden: ACCESS_STRINGS.en.forbidden,\n unsupported: 'This data source cannot read stored history',\n failed: 'The history could not be read',\n empty: 'No readings in this range',\n noReadings: 'no readings in this bucket',\n newestFirst: 'newest first',\n oldestFirst: 'oldest first',\n range: (from, to, of) => `${from}\u2013${to} of ${of}`,\n previous: 'Previous page',\n next: 'Next page',\n },\n es: {\n time: 'Hora',\n exportTime: 'Inicio del intervalo (UTC)',\n average: (bucket) => `prom. por ${bucket}`,\n averageTitle: (bucket) =>\n `Cada cifra es el promedio de las lecturas de un intervalo de ${bucket}, no una lectura: la hora es el inicio de su intervalo.`,\n loading: 'Cargando',\n unbound: 'Elija un dispositivo para este widget',\n forbidden: ACCESS_STRINGS.es.forbidden,\n unsupported: 'Esta fuente de datos no puede leer el historial almacenado',\n failed: 'No se pudo leer el historial',\n empty: 'No hay lecturas en este intervalo',\n noReadings: 'sin lecturas en este intervalo',\n newestFirst: 'm\u00E1s recientes primero',\n oldestFirst: 'm\u00E1s antiguas primero',\n range: (from, to, of) => `${from}\u2013${to} de ${of}`,\n previous: 'P\u00E1gina anterior',\n next: 'P\u00E1gina siguiente',\n },\n};\n\nexport function historyStrings(): HistoryStrings {\n return viewerLocale()?.toLowerCase().startsWith('es') ? HISTORY_STRINGS.es : HISTORY_STRINGS.en;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The data feed of the history table: one device's stored history over its span, in time buckets.\n// Loaded with the table's chunk (connected-widget.tsx imports it dynamically), so a board without a\n// history table never downloads it.\n//\n// - the span is the widget's pinned one, else the board's (DEFAULT_TIME_RANGE_SECONDS off a board);\n// - the window is whole minutes ending at the last minute boundary, in the buckets of the span's chart\n// (bucketSecondsFor), read through the source's own bucketMeasurements: the same call a stat card\n// makes, so the hub shares identical reads. The table shows each bucket's AVERAGE and says so;\n// - there is no live tail. A reading that arrived since the last bucket read belongs to a bucket that\n// is still filling, and a figure that moved under the viewer between two reads would be a reading\n// passed off as an average. The table reads again on the boundaries of its bucket (live) and says\n// how old its read is (the frame's freshness is the last SUCCESSFUL read);\n// - paused, the window ends at the freeze instant and is read once;\n// - a hidden tab does not read; it reads once when it is shown again;\n// - a read for a different selector, span or freeze instant is not shown under the new one: a result\n// is tagged with the key it was read for, and a mismatch reads as loading. A read that fails keeps\n// the buckets it would have replaced (they are still true) and the frame ages from the last good one.\n\nimport { DEFAULT_TIME_RANGE_SECONDS, type WidgetActions, type WidgetDataSource, type WidgetDescriptor, type WidgetInstance } from '@devicechain/dashboards';\nimport { isForbiddenError } from '@devicechain/client';\nimport { resolveStaleAfterSeconds, useWidgetTimeRange, WidgetFreshnessProvider } from '@devicechain/widget-sdk';\nimport { useEffect, useMemo, useState, type ComponentType } from 'react';\n\nimport { measurementSpan } from './binding-policy';\nimport { historyRefreshMs, historyWindow, type HistoryView } from './history-model';\n\nexport interface HistoryFeedProps {\n hub: WidgetDataSource;\n widget: WidgetInstance;\n descriptor: WidgetDescriptor;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Component: ComponentType<any>;\n actions: WidgetActions | undefined;\n}\n\ninterface Read {\n key: string;\n view: HistoryView;\n // When the last successful read landed (epoch ms): the age of the figures on screen.\n readAt: number | null;\n}\n\nexport default function HistoryFeed({ hub, widget, descriptor, Component, actions }: HistoryFeedProps) {\n const range = useWidgetTimeRange();\n const now = useMemo(() => (hub.now ? () => hub.now!() : Date.now), [hub]);\n const spanSeconds = measurementSpan(descriptor, widget, range?.spanSeconds) ?? DEFAULT_TIME_RANGE_SECONDS;\n const pausedAt = range?.paused ? range.pausedAt : undefined;\n const ds = widget.datasource;\n const key = JSON.stringify([ds ?? null, spanSeconds, pausedAt ?? null]);\n const supported = hub.bucketMeasurements !== undefined;\n const [read, setRead] = useState<Read>({ key, view: { status: 'loading' }, readAt: null });\n\n useEffect(() => {\n if (!supported || !ds || ds.kind === 'unbound') return;\n let current = true;\n // Reads overlap (a boundary, a tab shown, a rebind); only the NEWEST one's outcome is applied, so a\n // slow older read can neither replace newer buckets, nor raise an error over them, nor move the age.\n let latest = 0;\n const run = (): void => {\n const mine = ++latest;\n const window = historyWindow(spanSeconds, pausedAt ?? now());\n hub.bucketMeasurements!(ds, window).then(\n (buckets) => {\n if (current && mine === latest) setRead({ key, view: { status: 'ready', buckets, window }, readAt: Date.now() });\n },\n (err: unknown) => {\n if (!current || mine !== latest) return;\n // A refusal is a state, not a failure; a failure keeps what is on screen for THIS read's key.\n const status = isForbiddenError(err) ? 'forbidden' : 'failed';\n setRead((prev) => (status === 'failed' && prev.key === key && prev.view.status === 'ready' ? prev : { key, view: { status }, readAt: null }));\n },\n );\n };\n run();\n // Paused, the window is fixed at the freeze instant: nothing new can land in it.\n if (pausedAt !== undefined) return () => void (current = false);\n const every = historyRefreshMs(spanSeconds);\n let timer: ReturnType<typeof setTimeout>;\n const schedule = (): void => {\n timer = setTimeout(\n () => {\n if (!document.hidden) run();\n schedule();\n },\n every - (now() % every),\n );\n };\n schedule();\n const shown = (): void => {\n if (!document.hidden) run();\n };\n document.addEventListener('visibilitychange', shown);\n return () => {\n current = false;\n clearTimeout(timer);\n document.removeEventListener('visibilitychange', shown);\n };\n // `ds` is read through `key` (value identity), not by reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, supported, key]);\n\n let view: HistoryView;\n let readAt: number | null = null;\n if (!supported) view = { status: 'unsupported' };\n else if (!ds || ds.kind === 'unbound') view = { status: 'unbound' };\n else if (read.key === key) ({ view, readAt } = read);\n else view = { status: 'loading' };\n\n const freshness = useMemo(\n () => (descriptor.freshness === true ? { at: readAt ?? undefined, staleAfterSeconds: resolveStaleAfterSeconds(widget.options) } : undefined),\n [descriptor, widget.options, readAt],\n );\n const data = { latest: {}, samples: [], error: null };\n return (\n <WidgetFreshnessProvider freshness={freshness}>\n <Component widget={widget} data={data} actions={actions} history={view} />\n </WidgetFreshnessProvider>\n );\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["useMemo", "useState", "jsx", "useMemo", "useState"]
7
7
  }
package/dist/index.js CHANGED
@@ -1330,7 +1330,7 @@ var commandButtonWidget = defineWidget3({
1330
1330
 
1331
1331
  // src/widgets/history-lazy.tsx
1332
1332
  import { defineWidget as defineWidget4 } from "@devicechain/widget-sdk";
1333
- var chunk4 = lazyChunk(() => import("./chunks/history-widgets-NSNXDKEC.js"));
1333
+ var chunk4 = lazyChunk(() => import("./chunks/history-widgets-MBR4YRBV.js"));
1334
1334
  var loadHistoryWidgets = chunk4.load;
1335
1335
  var historyTableWidget = defineWidget4({
1336
1336
  descriptor: builtinWidgetDescriptors["history-table"],
@@ -2657,7 +2657,7 @@ function StaticConnectedWidget({ widget, Component: Component2, actions }) {
2657
2657
  }
2658
2658
  var FleetFeed = lazyChunk(() => import("./chunks/fleet-feed-D4T3LC25.js")).component("default", void 0, ChunkLoadingFrame);
2659
2659
  var PresenceFeed = lazyChunk(() => import("./chunks/presence-widgets-M5VGG3OO.js")).component("PresenceFeed");
2660
- var HistoryFeed = lazyChunk(() => import("./chunks/history-widgets-NSNXDKEC.js")).component("HistoryFeed");
2660
+ var HistoryFeed = lazyChunk(() => import("./chunks/history-widgets-MBR4YRBV.js")).component("HistoryFeed");
2661
2661
  function MeasurementConnectedWidget(props) {
2662
2662
  if (presenceMode(props.descriptor)) {
2663
2663
  return /* @__PURE__ */ jsx10(PresenceFeed, { hub: props.hub, widget: props.widget, descriptor: props.descriptor, Component: props.Component, actions: props.actions, ErrorFrame: WidgetErrorFrame });
@@ -2697,7 +2697,7 @@ function StreamConnectedWidget({ widget, hub, descriptor, Component: Component2,
2697
2697
  return /* @__PURE__ */ jsx10(WidgetFreshnessProvider, { freshness, children: wantsEventMarkers(descriptor, widget) ? /* @__PURE__ */ jsx10(EventMarkersFeed, { hub, widget, descriptor, data, Component: Component2, actions }) : /* @__PURE__ */ jsx10(Component2, { widget, data, actions }) });
2698
2698
  }
2699
2699
  var EventMarkersFeed = lazyChunk(() => import("./chunks/event-markers-feed-B3MVFYUT.js")).component("default");
2700
- var AggregateFeed = lazyChunk(() => import("./chunks/aggregate-feed-XX36GNY6.js")).component("default");
2700
+ var AggregateFeed = lazyChunk(() => import("./chunks/aggregate-feed-7JBQIYJE.js")).component("default");
2701
2701
  function AggregateConnectedWidget({ widget, hub, descriptor, Component: Component2, actions }) {
2702
2702
  const props = { hub, widget, descriptor, Component: Component2, actions, ErrorFrame: WidgetErrorFrame };
2703
2703
  return widget.datasource?.kind === "anchor" ? /* @__PURE__ */ jsx10(FleetFeed, { ...props, refuse: true }) : /* @__PURE__ */ jsx10(AggregateFeed, { ...props });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devicechain/widgets",
3
- "version": "0.25.2-rc.1",
3
+ "version": "0.26.0-rc.1",
4
4
  "description": "DeviceChain's built-in dashboard widgets across six channels — telemetry (time-series chart + gauge over Apache ECharts, latest card, table, and lazily loaded level, indicator and radial widgets: linear and level gauges, battery, status light, signal bars, compass, progress ring), static (label, image; no data), alarm (table, count, severity breakdown), control (command button), selection (entity selector), and location (map over MapLibre GL, lazily loaded, tile source supplied by the host app) — plus the renderer that lays them out. Each is a registration built on @devicechain/widget-sdk. Themed via CSS custom properties, no Tailwind leakage.",
5
5
  "keywords": [
6
6
  "devicechain",
@@ -53,18 +53,18 @@
53
53
  "test": "vitest run"
54
54
  },
55
55
  "peerDependencies": {
56
- "@devicechain/client": "0.25.2-rc.1",
57
- "@devicechain/dashboards": "0.25.2-rc.1",
58
- "@devicechain/widget-sdk": "0.25.2-rc.1",
56
+ "@devicechain/client": "0.26.0-rc.1",
57
+ "@devicechain/dashboards": "0.26.0-rc.1",
58
+ "@devicechain/widget-sdk": "0.26.0-rc.1",
59
59
  "echarts": "^6.0.0",
60
60
  "maplibre-gl": "^6.13.0",
61
61
  "react": "^19.3.0",
62
62
  "react-dom": "^19.3.0"
63
63
  },
64
64
  "devDependencies": {
65
- "@devicechain/client": "0.25.2-rc.1",
66
- "@devicechain/dashboards": "0.25.2-rc.1",
67
- "@devicechain/widget-sdk": "0.25.2-rc.1",
65
+ "@devicechain/client": "0.26.0-rc.1",
66
+ "@devicechain/dashboards": "0.26.0-rc.1",
67
+ "@devicechain/widget-sdk": "0.26.0-rc.1",
68
68
  "@testing-library/react": "^16.3.3",
69
69
  "@types/node": "^26.6.5",
70
70
  "@types/react": "^19.3.0",
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/aggregate-feed.tsx", "../../src/aggregate-fold.ts"],
4
- "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The data feed of an aggregate widget: the exact aggregates of its measurements over its span,\n// plus the newest live reading of each. Loaded on first use (connected-widget.tsx imports it\n// dynamically), so a board with no aggregate widget never downloads it.\n//\n// The aggregates are folded from the source's own buckets (WidgetDataSource.bucketMeasurements: each\n// carries the exact count, min, max and sum), never from the chart seed: a seed is a series of bucket\n// AVERAGES, and a minimum or maximum cannot be recovered from those. A source that produces no\n// buckets is an explained state, not a guess.\n//\n// - the span is the widget's pinned one, else the board's (DEFAULT_TIME_RANGE_SECONDS off a board);\n// - the window is whole minutes (aggregateWindow), so it holds no reading from before the span;\n// - live, the read ends at the last minute and repeats on the boundaries of aggregateRefreshMs\n// (one chart bucket of the span: a minute for an hour, 42 minutes for a week), so every card on\n// the board reads at the same moment and the hub serves identical reads once; paused, it ends\n// at the freeze instant and is read once, so the number on screen does not move;\n// - a card with `sparkline` set reads the SAME window in the span's chart buckets\n// (bucketSecondsFor) instead of one, and keeps each bucket as a trend point; the figure is\n// folded from those buckets and is the same number either way;\n// - a hidden tab does not read; it reads once when it is shown again;\n// - a live feed that resumes after an interruption reads again at once, so the figures take in\n// the readings sent during the outage (and a read that failed during it stops saying so) without\n// waiting for the next refresh, which on a week's span is 42 minutes away;\n// - a read for a different selector, span or freeze instant is not shown under the new one: a\n// result is tagged with the key it was read for, and a mismatch reads as loading.\n\nimport {\n bucketSecondsFor,\n DEFAULT_TIME_RANGE_SECONDS,\n type AggregateResult,\n type AggregateStreamState,\n type WidgetActions,\n type WidgetDataSource,\n type WidgetDescriptor,\n type WidgetInstance,\n} from '@devicechain/dashboards';\nimport { optBoolean, useMeasurementStream, useWidgetTimeRange } from '@devicechain/widget-sdk';\nimport { useEffect, useMemo, useReducer, useState, type ComponentType } from 'react';\n\nimport { aggregateBuckets, aggregateRefreshMs, aggregateSeries, aggregateWindow } from './aggregate-fold';\nimport { measurementSpan } from './binding-policy';\n\nconst LOADING: AggregateResult = { status: 'loading' };\n\nexport interface AggregateFeedProps {\n hub: WidgetDataSource;\n widget: WidgetInstance;\n descriptor: WidgetDescriptor;\n // The widget's component, and the host's pane for a stream that failed (before any figure).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Component: ComponentType<any>;\n actions: WidgetActions | undefined;\n ErrorFrame: ComponentType<{ widget: WidgetInstance; error: unknown }>;\n}\n\nexport default function AggregateFeed({ hub, widget, descriptor, Component, actions, ErrorFrame }: AggregateFeedProps) {\n const range = useWidgetTimeRange();\n const now = useMemo(() => (hub.now ? () => hub.now!() : Date.now), [hub]);\n const spanSeconds = measurementSpan(descriptor, widget, range?.spanSeconds) ?? DEFAULT_TIME_RANGE_SECONDS;\n const pausedAt = range?.paused ? range.pausedAt : undefined;\n const ds = widget.datasource;\n\n // The newest live reading per measurement. It holds one sample: only `latest` is read, and the\n // pause keeps it from moving.\n const [resumes, resumed] = useReducer((n: number) => n + 1, 0);\n const stream = useMeasurementStream(hub, ds, { window: 1, pausedAt, now, onReconnect: resumed });\n\n const trend = optBoolean(widget.options, 'sparkline');\n const key = JSON.stringify([ds ?? null, spanSeconds, pausedAt ?? null, trend]);\n const supported = hub.bucketMeasurements !== undefined;\n const [tagged, setTagged] = useState<{ key: string; result: AggregateResult }>({ key, result: LOADING });\n\n useEffect(() => {\n if (!supported || !ds) return;\n let current = true;\n const run = (): void => {\n const { window, spanSeconds: covered } = aggregateWindow(spanSeconds, pausedAt ?? now(), trend ? bucketSecondsFor(spanSeconds) : undefined);\n // Folded inside the chain, so a bucket that cannot be trusted fails the read like a network error.\n hub.bucketMeasurements!(ds, window).then((buckets) => ({ byName: aggregateBuckets(buckets), buckets })).then(\n ({ byName, buckets }) => {\n if (!current) return;\n const { startTime, endTime } = window;\n const series = trend ? { series: aggregateSeries(buckets) } : {};\n setTagged({ key, result: { status: 'ready', byName, ...series, startTime, endTime, spanSeconds: covered } });\n },\n (err: unknown) => {\n if (!current) return;\n const message = err instanceof Error ? err.message : String(err);\n setTagged({ key, result: { status: 'unavailable', reason: 'failed', message } });\n },\n );\n };\n run();\n // Paused, the window is fixed at the freeze instant: nothing new can land in it.\n if (pausedAt !== undefined) return () => void (current = false);\n const hidden = () => document.hidden;\n const every = aggregateRefreshMs(spanSeconds);\n let timer: ReturnType<typeof setTimeout>;\n const schedule = (): void => {\n timer = setTimeout(\n () => {\n if (!hidden()) run();\n schedule();\n },\n every - (now() % every),\n );\n };\n schedule();\n const shown = (): void => {\n if (!hidden()) run();\n };\n document.addEventListener('visibilitychange', shown);\n return () => {\n current = false;\n clearTimeout(timer);\n document.removeEventListener('visibilitychange', shown);\n };\n // `ds` is read through `key` (value identity), not by reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, supported, key, resumes]);\n\n let aggregates: AggregateResult;\n if (!supported) aggregates = { status: 'unavailable', reason: 'unsupported' };\n else if (!ds) aggregates = { status: 'ready', byName: {}, startTime: '', endTime: '', spanSeconds };\n else aggregates = tagged.key === key ? tagged.result : LOADING;\n\n const data: AggregateStreamState = { aggregates, latest: stream.latest, error: stream.error };\n return data.error ? <ErrorFrame widget={widget} error={data.error} /> : <Component widget={widget} data={data} actions={actions} />;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Aggregates over a span, folded from the backend's time buckets. Each bucket carries the exact\n// count, minimum, maximum and sum of the readings in it, so the span's aggregates are exact too:\n// the min of the mins, the max of the maxes, the sum of the sums, the count of the counts, and a\n// mean that is the total sum over the total count (which is the count-weighted mean of the\n// buckets' averages, without ever reading an average).\n//\n// \uD83D\uDD34 A MINIMUM OR MAXIMUM IS NEVER DERIVED FROM AVERAGES. A bucket's average hides its extremes, so\n// the \"max of the avgs\" is a plausible number that is not the maximum. A bucket that holds\n// readings but lacks a field the aggregate needs is therefore a loud failure here, never a\n// quiet substitution.\n\nimport {\n bucketSecondsFor,\n type AggregateTrendPoint,\n type HistoryWindow,\n type MeasurementAggregate,\n type MeasurementBucket,\n} from '@devicechain/dashboards';\n\nexport class AggregateFieldsError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AggregateFieldsError';\n }\n}\n\n// aggregateBuckets folds the buckets (any number of devices) into one aggregate per measurement\n// name. A name with no readings in any bucket is absent from the result, not zero.\nexport function aggregateBuckets(buckets: readonly MeasurementBucket[]): Record<string, MeasurementAggregate> {\n const out: Record<string, MeasurementAggregate> = {};\n for (const b of buckets) {\n if (typeof b.count !== 'number') {\n throw new AggregateFieldsError(`the bucket at ${b.bucketStart} for \"${b.name}\" carries no reading count`);\n }\n if (b.count === 0) continue;\n const { min, max, sum } = b;\n if (min == null || max == null || sum == null) {\n const missing = [min == null && 'min', max == null && 'max', sum == null && 'sum'].filter(Boolean);\n throw new AggregateFieldsError(\n `the bucket at ${b.bucketStart} for \"${b.name}\" holds ${b.count} readings but no ${missing.join(', ')}`,\n );\n }\n const a = out[b.name];\n if (!a) out[b.name] = { count: b.count, min, max, sum, avg: sum / b.count };\n else {\n a.count += b.count;\n a.sum += sum;\n a.min = Math.min(a.min, min);\n a.max = Math.max(a.max, max);\n a.avg = a.sum / a.count;\n }\n }\n return out;\n}\n\n// aggregateSeries lays the same buckets out as a trend: per measurement name, one point per bucket\n// that holds readings, oldest first. Buckets of several devices at one instant merge exactly as\n// aggregateBuckets merges them. Call it only on buckets aggregateBuckets accepted (it does not\n// re-validate), so a bucket that cannot be trusted has already failed the read.\nexport function aggregateSeries(buckets: readonly MeasurementBucket[]): Record<string, AggregateTrendPoint[]> {\n const byName = new Map<string, Map<string, AggregateTrendPoint>>();\n for (const b of buckets) {\n if (!b.count || b.min == null || b.max == null || b.sum == null) continue;\n let points = byName.get(b.name);\n if (!points) byName.set(b.name, (points = new Map()));\n const at = points.get(b.bucketStart);\n if (!at) {\n points.set(b.bucketStart, { bucketStart: b.bucketStart, count: b.count, min: b.min, max: b.max, sum: b.sum, avg: b.sum / b.count });\n } else {\n at.count += b.count;\n at.sum += b.sum;\n at.min = Math.min(at.min, b.min);\n at.max = Math.max(at.max, b.max);\n at.avg = at.sum / at.count;\n }\n }\n const out: Record<string, AggregateTrendPoint[]> = {};\n for (const [name, points] of byName) {\n out[name] = [...points.values()].sort((a, b) => Date.parse(a.bucketStart) - Date.parse(b.bucketStart));\n }\n return out;\n}\n\n// ---- The window a stat reads ------------------------------------------------------------------\n\nconst MINUTE_MS = 60_000;\n\n// aggregateRefreshMs is how often a live stat reads again: one bucket of the span's chart\n// (bucketSecondsFor), so never more often than once a minute. Finer is wasted -- the window it\n// reads moves in whole minutes (aggregateWindow) -- and a 7-day figure re-read every few seconds\n// costs the event store a scan of a week of summaries to move by a few seconds' readings.\nexport function aggregateRefreshMs(spanSeconds: number): number {\n return bucketSecondsFor(spanSeconds) * 1000;\n}\n\nexport interface AggregateWindow {\n window: HistoryWindow;\n // The length the window actually covers, in seconds: the span, or less when the span is not a\n // whole number of minutes. This, not the span asked for, is what the card says it covers.\n spanSeconds: number;\n}\n\n// aggregateWindow is the window a stat over `spanSeconds` reads at `at` (epoch ms): whole minutes,\n// ending at the last minute boundary at or before `at`.\n//\n// \uD83D\uDD34 Whole minutes because the event store answers from one-minute summaries, and a summary is\n// read whole: a window starting mid-minute would also count the readings from that minute's\n// start, BEFORE the span, so \"Maximum \u00B7 1h\" could be the maximum of an hour and a minute. Both\n// edges fall on minute boundaries instead, and the end is asked for as the instant just before\n// its boundary (the store's end is inclusive), so the summaries read are exactly the window's.\n// The start rounds UP, so a span that is not a whole number of minutes covers less, never more,\n// and says so (`spanSeconds`).\n//\n// Live cards read again on the boundaries of aggregateRefreshMs (a whole number of minutes), so\n// every card of one span ends its window on the same boundary: cards that read the same\n// measurement ask the same question at the same moment, and the hub answers them with one read.\n// One bucket wide: the card needs only the totals, so the store returns one or two rows per\n// measurement rather than a chart's worth.\n//\n// `bucketSeconds` asks for the window in finer buckets (a trend needs a point per bucket); the\n// totals fold identically from any number of buckets, so the figure is the same either way. Unset\n// the window is one bucket wide, as above.\nexport function aggregateWindow(spanSeconds: number, at: number, bucketSeconds?: number): AggregateWindow {\n const end = Math.floor(at / MINUTE_MS) * MINUTE_MS;\n const start = Math.ceil((end - spanSeconds * 1000) / MINUTE_MS) * MINUTE_MS;\n const covered = (end - start) / 1000;\n return {\n window: {\n startTime: new Date(start).toISOString(),\n endTime: new Date(end - 1).toISOString(),\n intervalSeconds: Math.max(60, bucketSeconds === undefined ? covered : Math.min(covered, bucketSeconds)),\n },\n spanSeconds: covered,\n };\n}\n"],
5
- "mappings": ";;;;;AA4BA;AAAA,EACE,oBAAAA;AAAA,EACA;AAAA,OAOK;AACP,SAAS,YAAY,sBAAsB,0BAA0B;AACrE,SAAS,WAAW,SAAS,YAAY,gBAAoC;;;ACzB7E;AAAA,EACE;AAAA,OAKK;AAEA,IAAM,uBAAN,cAAmC,MAAM;AAAA,EAC9C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAIO,SAAS,iBAAiB,SAA6E;AAC5G,QAAM,MAA4C,CAAC;AACnD,aAAW,KAAK,SAAS;AACvB,QAAI,OAAO,EAAE,UAAU,UAAU;AAC/B,YAAM,IAAI,qBAAqB,iBAAiB,EAAE,WAAW,SAAS,EAAE,IAAI,4BAA4B;AAAA,IAC1G;AACA,QAAI,EAAE,UAAU,EAAG;AACnB,UAAM,EAAE,KAAK,KAAK,IAAI,IAAI;AAC1B,QAAI,OAAO,QAAQ,OAAO,QAAQ,OAAO,MAAM;AAC7C,YAAM,UAAU,CAAC,OAAO,QAAQ,OAAO,OAAO,QAAQ,OAAO,OAAO,QAAQ,KAAK,EAAE,OAAO,OAAO;AACjG,YAAM,IAAI;AAAA,QACR,iBAAiB,EAAE,WAAW,SAAS,EAAE,IAAI,WAAW,EAAE,KAAK,oBAAoB,QAAQ,KAAK,IAAI,CAAC;AAAA,MACvG;AAAA,IACF;AACA,UAAM,IAAI,IAAI,EAAE,IAAI;AACpB,QAAI,CAAC,EAAG,KAAI,EAAE,IAAI,IAAI,EAAE,OAAO,EAAE,OAAO,KAAK,KAAK,KAAK,KAAK,MAAM,EAAE,MAAM;AAAA,SACrE;AACH,QAAE,SAAS,EAAE;AACb,QAAE,OAAO;AACT,QAAE,MAAM,KAAK,IAAI,EAAE,KAAK,GAAG;AAC3B,QAAE,MAAM,KAAK,IAAI,EAAE,KAAK,GAAG;AAC3B,QAAE,MAAM,EAAE,MAAM,EAAE;AAAA,IACpB;AAAA,EACF;AACA,SAAO;AACT;AAMO,SAAS,gBAAgB,SAA8E;AAC5G,QAAM,SAAS,oBAAI,IAA8C;AACjE,aAAW,KAAK,SAAS;AACvB,QAAI,CAAC,EAAE,SAAS,EAAE,OAAO,QAAQ,EAAE,OAAO,QAAQ,EAAE,OAAO,KAAM;AACjE,QAAI,SAAS,OAAO,IAAI,EAAE,IAAI;AAC9B,QAAI,CAAC,OAAQ,QAAO,IAAI,EAAE,MAAO,SAAS,oBAAI,IAAI,CAAE;AACpD,UAAM,KAAK,OAAO,IAAI,EAAE,WAAW;AACnC,QAAI,CAAC,IAAI;AACP,aAAO,IAAI,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,OAAO,EAAE,OAAO,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC;AAAA,IACpI,OAAO;AACL,SAAG,SAAS,EAAE;AACd,SAAG,OAAO,EAAE;AACZ,SAAG,MAAM,KAAK,IAAI,GAAG,KAAK,EAAE,GAAG;AAC/B,SAAG,MAAM,KAAK,IAAI,GAAG,KAAK,EAAE,GAAG;AAC/B,SAAG,MAAM,GAAG,MAAM,GAAG;AAAA,IACvB;AAAA,EACF;AACA,QAAM,MAA6C,CAAC;AACpD,aAAW,CAAC,MAAM,MAAM,KAAK,QAAQ;AACnC,QAAI,IAAI,IAAI,CAAC,GAAG,OAAO,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,KAAK,MAAM,EAAE,WAAW,IAAI,KAAK,MAAM,EAAE,WAAW,CAAC;AAAA,EACvG;AACA,SAAO;AACT;AAIA,IAAM,YAAY;AAMX,SAAS,mBAAmB,aAA6B;AAC9D,SAAO,iBAAiB,WAAW,IAAI;AACzC;AA6BO,SAAS,gBAAgB,aAAqB,IAAY,eAAyC;AACxG,QAAM,MAAM,KAAK,MAAM,KAAK,SAAS,IAAI;AACzC,QAAM,QAAQ,KAAK,MAAM,MAAM,cAAc,OAAQ,SAAS,IAAI;AAClE,QAAM,WAAW,MAAM,SAAS;AAChC,SAAO;AAAA,IACL,QAAQ;AAAA,MACN,WAAW,IAAI,KAAK,KAAK,EAAE,YAAY;AAAA,MACvC,SAAS,IAAI,KAAK,MAAM,CAAC,EAAE,YAAY;AAAA,MACvC,iBAAiB,KAAK,IAAI,IAAI,kBAAkB,SAAY,UAAU,KAAK,IAAI,SAAS,aAAa,CAAC;AAAA,IACxG;AAAA,IACA,aAAa;AAAA,EACf;AACF;;;ADRsB;AArFtB,IAAM,UAA2B,EAAE,QAAQ,UAAU;AAatC,SAAR,cAA+B,EAAE,KAAK,QAAQ,YAAY,WAAW,SAAS,WAAW,GAAuB;AACrH,QAAM,QAAQ,mBAAmB;AACjC,QAAM,MAAM,QAAQ,MAAO,IAAI,MAAM,MAAM,IAAI,IAAK,IAAI,KAAK,KAAM,CAAC,GAAG,CAAC;AACxE,QAAM,cAAc,gBAAgB,YAAY,QAAQ,OAAO,WAAW,KAAK;AAC/E,QAAM,WAAW,OAAO,SAAS,MAAM,WAAW;AAClD,QAAM,KAAK,OAAO;AAIlB,QAAM,CAAC,SAAS,OAAO,IAAI,WAAW,CAAC,MAAc,IAAI,GAAG,CAAC;AAC7D,QAAM,SAAS,qBAAqB,KAAK,IAAI,EAAE,QAAQ,GAAG,UAAU,KAAK,aAAa,QAAQ,CAAC;AAE/F,QAAM,QAAQ,WAAW,OAAO,SAAS,WAAW;AACpD,QAAM,MAAM,KAAK,UAAU,CAAC,MAAM,MAAM,aAAa,YAAY,MAAM,KAAK,CAAC;AAC7E,QAAM,YAAY,IAAI,uBAAuB;AAC7C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAmD,EAAE,KAAK,QAAQ,QAAQ,CAAC;AAEvG,YAAU,MAAM;AACd,QAAI,CAAC,aAAa,CAAC,GAAI;AACvB,QAAI,UAAU;AACd,UAAM,MAAM,MAAY;AACtB,YAAM,EAAE,QAAQ,aAAa,QAAQ,IAAI,gBAAgB,aAAa,YAAY,IAAI,GAAG,QAAQC,kBAAiB,WAAW,IAAI,MAAS;AAE1I,UAAI,mBAAoB,IAAI,MAAM,EAAE,KAAK,CAAC,aAAa,EAAE,QAAQ,iBAAiB,OAAO,GAAG,QAAQ,EAAE,EAAE;AAAA,QACtG,CAAC,EAAE,QAAQ,QAAQ,MAAM;AACvB,cAAI,CAAC,QAAS;AACd,gBAAM,EAAE,WAAW,QAAQ,IAAI;AAC/B,gBAAM,SAAS,QAAQ,EAAE,QAAQ,gBAAgB,OAAO,EAAE,IAAI,CAAC;AAC/D,oBAAU,EAAE,KAAK,QAAQ,EAAE,QAAQ,SAAS,QAAQ,GAAG,QAAQ,WAAW,SAAS,aAAa,QAAQ,EAAE,CAAC;AAAA,QAC7G;AAAA,QACA,CAAC,QAAiB;AAChB,cAAI,CAAC,QAAS;AACd,gBAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,oBAAU,EAAE,KAAK,QAAQ,EAAE,QAAQ,eAAe,QAAQ,UAAU,QAAQ,EAAE,CAAC;AAAA,QACjF;AAAA,MACF;AAAA,IACF;AACA,QAAI;AAEJ,QAAI,aAAa,OAAW,QAAO,MAAM,MAAM,UAAU;AACzD,UAAM,SAAS,MAAM,SAAS;AAC9B,UAAM,QAAQ,mBAAmB,WAAW;AAC5C,QAAI;AACJ,UAAM,WAAW,MAAY;AAC3B,cAAQ;AAAA,QACN,MAAM;AACJ,cAAI,CAAC,OAAO,EAAG,KAAI;AACnB,mBAAS;AAAA,QACX;AAAA,QACA,QAAS,IAAI,IAAI;AAAA,MACnB;AAAA,IACF;AACA,aAAS;AACT,UAAM,QAAQ,MAAY;AACxB,UAAI,CAAC,OAAO,EAAG,KAAI;AAAA,IACrB;AACA,aAAS,iBAAiB,oBAAoB,KAAK;AACnD,WAAO,MAAM;AACX,gBAAU;AACV,mBAAa,KAAK;AAClB,eAAS,oBAAoB,oBAAoB,KAAK;AAAA,IACxD;AAAA,EAGF,GAAG,CAAC,KAAK,WAAW,KAAK,OAAO,CAAC;AAEjC,MAAI;AACJ,MAAI,CAAC,UAAW,cAAa,EAAE,QAAQ,eAAe,QAAQ,cAAc;AAAA,WACnE,CAAC,GAAI,cAAa,EAAE,QAAQ,SAAS,QAAQ,CAAC,GAAG,WAAW,IAAI,SAAS,IAAI,YAAY;AAAA,MAC7F,cAAa,OAAO,QAAQ,MAAM,OAAO,SAAS;AAEvD,QAAM,OAA6B,EAAE,YAAY,QAAQ,OAAO,QAAQ,OAAO,OAAO,MAAM;AAC5F,SAAO,KAAK,QAAQ,oBAAC,cAAW,QAAgB,OAAO,KAAK,OAAO,IAAK,oBAAC,aAAU,QAAgB,MAAY,SAAkB;AACnI;",
6
- "names": ["bucketSecondsFor", "bucketSecondsFor"]
7
- }