@devicechain/widgets 0.21.1-rc.1 → 0.22.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.
- package/README.md +28 -7
- package/dist/aggregate-feed.d.ts +14 -0
- package/dist/aggregate-fold.d.ts +11 -0
- package/dist/aggregate-reading.d.ts +19 -0
- package/dist/aggregate-strings.d.ts +16 -0
- package/dist/binding-policy.d.ts +3 -1
- package/dist/builtin-descriptors.d.ts +1210 -8
- package/dist/builtin-registry.d.ts +1197 -8
- package/dist/category-geometry.d.ts +19 -0
- package/dist/category-items.d.ts +10 -0
- package/dist/chunks/aggregate-feed-6ZYKOQOB.js +130 -0
- package/dist/chunks/aggregate-feed-6ZYKOQOB.js.map +7 -0
- package/dist/chunks/aggregate-widgets-WVXAW3UL.js +895 -0
- package/dist/chunks/aggregate-widgets-WVXAW3UL.js.map +7 -0
- package/dist/chunks/chunk-2D7JNKFE.js +65 -0
- package/dist/chunks/chunk-2D7JNKFE.js.map +7 -0
- package/dist/chunks/chunk-S2OMBJLI.js +200 -0
- package/dist/chunks/chunk-S2OMBJLI.js.map +7 -0
- package/dist/chunks/chunk-XQQQNTBX.js +76 -0
- package/dist/chunks/chunk-XQQQNTBX.js.map +7 -0
- package/dist/chunks/chunk-YPFB6DAL.js +157 -0
- package/dist/chunks/chunk-YPFB6DAL.js.map +7 -0
- package/dist/chunks/chunk-ZR74ORIT.js +35 -0
- package/dist/chunks/chunk-ZR74ORIT.js.map +7 -0
- package/dist/chunks/control-widgets-UDS4ET3T.js +775 -0
- package/dist/chunks/control-widgets-UDS4ET3T.js.map +7 -0
- package/dist/chunks/fleet-feed-FK4ZMKIK.js +252 -0
- package/dist/chunks/fleet-feed-FK4ZMKIK.js.map +7 -0
- package/dist/chunks/level-widgets-MJB7TOLU.js +833 -0
- package/dist/chunks/level-widgets-MJB7TOLU.js.map +7 -0
- package/dist/command-control-core.d.ts +68 -0
- package/dist/command-control-strings.d.ts +32 -0
- package/dist/command-control.d.ts +31 -0
- package/dist/dashboard-renderer.d.ts +15 -4
- package/dist/fleet-feed.d.ts +21 -0
- package/dist/fleet-model.d.ts +43 -0
- package/dist/fleet-seed.d.ts +7 -0
- package/dist/fleet-strings.d.ts +25 -0
- package/dist/heading.d.ts +3 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1774 -676
- package/dist/index.js.map +4 -4
- package/dist/level-reading.d.ts +29 -0
- package/dist/level-readout.d.ts +25 -0
- package/dist/level-strings.d.ts +13 -0
- package/dist/options.d.ts +1 -1
- package/dist/time-range-bar.d.ts +28 -0
- package/dist/use-held.d.ts +1 -0
- package/dist/value-scale.d.ts +30 -0
- package/dist/widgets/aggregate-lazy.d.ts +305 -0
- package/dist/widgets/aggregate-widgets.d.ts +4 -0
- package/dist/widgets/alarm-count.d.ts +1 -0
- package/dist/widgets/alarm-table.d.ts +4 -1
- package/dist/widgets/bar-chart.d.ts +5 -0
- package/dist/widgets/battery.d.ts +7 -0
- package/dist/widgets/command-slider.d.ts +2 -0
- package/dist/widgets/command-toggle.d.ts +4 -0
- package/dist/widgets/compass.d.ts +2 -0
- package/dist/widgets/control-lazy.d.ts +92 -0
- package/dist/widgets/control-widgets.d.ts +2 -0
- package/dist/widgets/fleet-table.d.ts +13 -0
- package/dist/widgets/gauge.d.ts +52 -0
- package/dist/widgets/label.d.ts +1 -1
- package/dist/widgets/latest-card.d.ts +44 -1
- package/dist/widgets/lazy-chunk.d.ts +8 -0
- package/dist/widgets/level-gauge.d.ts +2 -0
- package/dist/widgets/level-lazy.d.ts +647 -0
- package/dist/widgets/level-widgets.d.ts +7 -0
- package/dist/widgets/linear-gauge.d.ts +2 -0
- package/dist/widgets/pie-chart.d.ts +2 -0
- package/dist/widgets/progress-ring.d.ts +3 -0
- package/dist/widgets/severity.d.ts +2 -1
- package/dist/widgets/signal-bars.d.ts +3 -0
- package/dist/widgets/stat-card.d.ts +3 -0
- package/dist/widgets/status-light.d.ts +7 -0
- package/dist/widgets/table.d.ts +9 -1
- package/dist/widgets/time-series-chart.d.ts +62 -4
- package/package.json +8 -8
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/widgets/command-slider.tsx", "../../src/command-control-core.ts", "../../src/command-control.tsx", "../../src/command-control-strings.ts", "../../src/widgets/command-toggle.tsx"],
|
|
4
|
+
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// command-slider \u2014 sends a command carrying ONE numeric parameter, chosen with a slider. The range\n// comes from the command definition baked into the options (the same snapshot the command button\n// renders its form from); the author's min/max/step fill only what the definition leaves open.\n//\n// \uD83D\uDD34 DRAGGING SENDS NOTHING. The thumb stages a value; releasing it on the slider (or pressing\n// Enter, for a keyboard user) sends it, once. A staged value that is not committed is DISCARDED,\n// never sent later: a cancelled pointer (the browser took the gesture for a scroll), a lost\n// capture or a release off the slider, Escape, or focus leaving the slider. While a send is in\n// flight the slider is aria-disabled and ignores input, so a second release cannot queue a second\n// command; it is not `disabled`, which would drop a keyboard user's focus on the page body.\n//\n// Keyboard: the arrow, Page and Home/End keys stage a value (the native range input's own\n// behaviour, so aria-valuetext follows the staged value) and Enter sends it. That departure from a\n// plain slider is announced: the input is described by a hint saying so, as well as by the\n// last-sent caption, which reads \"Not sent yet. ... press Enter to send\" while a value is staged.\n//\n// \uD83D\uDD34 IT SHOWS WHAT WAS SENT, NOT WHAT THE DEVICE IS DOING. The thumb rests at the last value sent\n// (read off the command history) and the caption says so with that command's delivery status.\n// With nothing sent the thumb rests at the bottom of the range and the readout is a dash.\n\nimport { useEffect, useId, useMemo, useRef, useState, type ReactNode } from 'react';\n\nimport {\n css,\n FIT_MIN_FONT_PX,\n fitValueFont,\n fitValueFontCss,\n fontVar,\n optNumber,\n optString,\n spaceVar,\n useStickyChars,\n WidgetFrame,\n type CommandStreamState,\n type WidgetProps,\n type WidgetScale,\n} from '@devicechain/widget-sdk';\n\nimport {\n bindControl,\n clampToRange,\n lastSent,\n payloadFor,\n type Binding,\n} from '../command-control-core';\nimport {\n ControlEmpty,\n ControlNotes,\n readOnlyId,\n LastSentCaption,\n refusalText,\n TOUCH_TARGET_PX,\n useCommandSender,\n type CommandSender,\n} from '../command-control';\n\ntype Bound = Extract<Binding, { ok: true }>;\n\nconst withUnit = (v: number, unit: string | undefined) => (unit ? `${v} ${unit}` : String(v));\n\nexport function CommandSlider({ widget, data, actions }: WidgetProps<CommandStreamState>) {\n const commandName = optString(widget.options, 'commandName');\n const commandLabel = optString(widget.options, 'commandLabel') ?? commandName;\n const parameterSchema = optString(widget.options, 'parameterSchema') ?? null;\n const parameter = optString(widget.options, 'parameter');\n const min = optNumber(widget.options, 'min');\n const max = optNumber(widget.options, 'max');\n const step = optNumber(widget.options, 'step');\n\n const binding = useMemo(\n () => bindControl('number', { commandName, parameterSchema, parameter, min, max, step }),\n [commandName, parameterSchema, parameter, min, max, step],\n );\n const sender = useCommandSender(\n actions,\n data.deviceToken,\n commandName,\n `${commandName ?? ''}::${parameterSchema ?? ''}::${parameter ?? ''}`,\n );\n\n const last =\n binding.ok && commandName\n ? lastSent('number', data.commands, commandName, binding.param.name, sender.optimistic)\n : null;\n\n // The staged value. A ref mirrors it so a release reads what the last change set, not the value\n // the previous render closed over.\n const [draft, setDraftState] = useState<number | null>(null);\n const draftRef = useRef<number | null>(null);\n const setDraft = (v: number | null) => {\n draftRef.current = v;\n setDraftState(v);\n };\n const captionId = useId();\n const hintId = useId();\n\n // A pointer pressed on the slider whose release has not been seen. A release anywhere but on\n // the slider, a cancel or a lost capture while it is set discards the staged value.\n const pressed = useRef(false);\n const offRelease = useRef<(() => void) | null>(null);\n useEffect(() => () => offRelease.current?.(), []);\n const discard = () => {\n pressed.current = false;\n offRelease.current?.();\n offRelease.current = null;\n if (draftRef.current !== null) setDraft(null);\n };\n const press = (input: HTMLInputElement) => {\n pressed.current = true;\n offRelease.current?.();\n const doc = input.ownerDocument;\n const onRelease = (e: PointerEvent) => {\n // The slider's own release commits through onPointerUp; this one sees releases elsewhere.\n if (e.target instanceof Node && input.contains(e.target)) return;\n discard();\n };\n doc.addEventListener('pointerup', onRelease, true);\n offRelease.current = () => doc.removeEventListener('pointerup', onRelease, true);\n };\n\n const commit = () => {\n if (!binding.ok) return;\n const staged = draftRef.current;\n if (staged === null || !sender.canSend) return;\n if (sender.send(payloadFor(binding, staged), staged)) setDraft(null);\n };\n const release = () => {\n pressed.current = false;\n offRelease.current?.();\n offRelease.current = null;\n commit();\n };\n\n return (\n <WidgetFrame title={optString(widget.options, 'title')}>\n {(scale) =>\n !binding.ok ? (\n <ControlEmpty message={refusalText(binding.refusal, sender.strings, 'number')} />\n ) : (\n <SliderBody\n binding={binding}\n label={commandLabel ?? binding.param.name}\n draft={draft}\n lastValue={typeof last?.value === 'number' ? last.value : undefined}\n sender={sender}\n captionId={captionId}\n hintId={hintId}\n caption={\n <LastSentCaption\n id={captionId}\n last={last}\n strings={sender.strings}\n truncated={data.total > data.commands.length}\n format={(v) => (typeof v === 'number' ? withUnit(v, binding.param.unit) : sender.strings.noValue)}\n override={draft !== null ? sender.strings.unsent : sender.pending ? sender.strings.sending : undefined}\n />\n }\n onStage={(v) => {\n // A value moved while a send is in flight is not staged: the slider is aria-disabled.\n if (!sender.pending) setDraft(clampToRange(binding.range!, v));\n }}\n onCommit={commit}\n onPress={press}\n onRelease={release}\n onDiscard={discard}\n onLostCapture={() => {\n if (pressed.current) discard();\n }}\n scale={scale}\n />\n )\n }\n </WidgetFrame>\n );\n}\n\nfunction SliderBody({\n binding,\n label,\n draft,\n lastValue,\n sender,\n captionId,\n hintId,\n caption,\n onStage,\n onCommit,\n onPress,\n onRelease,\n onDiscard,\n onLostCapture,\n scale: ws,\n}: {\n binding: Bound;\n label: string;\n draft: number | null;\n lastValue: number | undefined;\n sender: CommandSender;\n captionId: string;\n hintId: string;\n caption: ReactNode;\n onStage: (v: number) => void;\n onCommit: () => void;\n onPress: (input: HTMLInputElement) => void;\n onRelease: () => void;\n onDiscard: () => void;\n onLostCapture: () => void;\n scale: WidgetScale;\n}) {\n const range = binding.range!;\n const unit = binding.param.unit;\n // Read-only is `disabled`; in flight is only aria-disabled, so focus stays on the slider.\n const disabled = !sender.canSend;\n const busy = sender.pending;\n const rest = lastValue !== undefined ? clampToRange(range, lastValue) : range.min;\n const shown = draft ?? rest;\n const known = draft !== null || lastValue !== undefined;\n const readout = known ? String(shown) : '\u2014';\n\n // The readout fits the room left beside the track, with the same curve the value widgets use.\n const chars = useStickyChars(readout.length + (unit && known ? Math.ceil(unit.length / 2) + 1 : 0));\n const floor = Math.round(FIT_MIN_FONT_PX * ws.scale);\n const measured = ws.size.width > 0 && ws.size.height > 0;\n const room = {\n width: Math.floor(ws.size.width * 0.34),\n height: Math.max(TOUCH_TARGET_PX, Math.floor(ws.size.height * 0.5)),\n };\n const font = measured ? fitValueFont(room, chars, { floor }) : fitValueFontCss(chars, { floor });\n\n return (\n <div\n style={{\n height: '100%',\n boxSizing: 'border-box',\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'safe center',\n gap: spaceVar(1),\n overflow: 'hidden',\n }}\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: spaceVar(2), minHeight: TOUCH_TARGET_PX }}>\n <input\n type=\"range\"\n aria-label={label}\n aria-valuetext={withUnit(shown, unit)}\n aria-describedby={`${hintId} ${captionId}${sender.readOnlyReason ? ` ${readOnlyId(captionId)}` : ''}`}\n aria-disabled={busy || undefined}\n aria-busy={busy || undefined}\n min={range.min}\n max={range.max}\n step={range.step}\n value={shown}\n disabled={disabled}\n data-staged={draft !== null ? 'true' : undefined}\n onChange={(e) => onStage(Number(e.target.value))}\n onPointerDown={(e) => onPress(e.currentTarget)}\n onPointerUp={onRelease}\n onPointerCancel={onDiscard}\n onLostPointerCapture={onLostCapture}\n onBlur={onDiscard}\n onKeyDown={(e) => {\n if (e.key === 'Enter') onCommit();\n else if (e.key === 'Escape') onDiscard();\n }}\n style={{\n flex: '1 1 0',\n minWidth: 0,\n height: TOUCH_TARGET_PX,\n margin: 0,\n accentColor: css('primary'),\n cursor: disabled || busy ? 'not-allowed' : 'pointer',\n }}\n />\n <span id={hintId} hidden>\n {sender.strings.sliderHint}\n </span>\n <output\n aria-hidden=\"true\"\n data-slider-readout=\"\"\n style={{\n flex: '0 0 auto',\n fontSize: font,\n fontWeight: 600,\n lineHeight: 1.1,\n whiteSpace: 'nowrap',\n fontVariantNumeric: 'tabular-nums',\n color: draft !== null ? css('foreground') : css('muted-foreground'),\n }}\n >\n {readout}\n {unit && known ? (\n <span style={{ fontSize: fontVar('sm'), marginLeft: spaceVar(1), color: css('muted-foreground') }}>{unit}</span>\n ) : null}\n </output>\n </div>\n {/* One row, so the short read-only caption costs no height at a two-row widget's minimum span. */}\n <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'baseline', columnGap: spaceVar(2) }}>\n {caption}\n <ControlNotes sender={sender} captionId={captionId} />\n </div>\n </div>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The pure half of the command slider and toggle: which parameter a control is bound to and why\n// it may refuse to be, the payload it sends, and the last value sent read back off the command\n// history. Nothing here reads a widget's options (the widgets hand the values in), so it is\n// tested on its own.\n//\n// \uD83D\uDD34 A CONTROL THAT CANNOT SEND SAYS SO. Every way the binding can be wrong is a named refusal\n// with the parameter it concerns, never a fallback value: a control that guesses at its range, its\n// type or a required parameter sends a plausible command the author did not mean.\n\nimport type { CommandParameter, CommandRow } from '@devicechain/dashboards';\n\nimport { buildPayload, isScalar, parseParameterSchema, validateParams } from './widgets/command-params';\n\nexport type ControlKind = 'number' | 'boolean';\n\nexport type Refusal =\n | { code: 'noCommand' }\n | { code: 'setParameter' }\n | { code: 'parameterMissing'; name: string }\n | { code: 'wrongType'; name: string; kind: ControlKind }\n | { code: 'fixedChoices'; name: string }\n | { code: 'otherRequired'; names: string[] }\n | { code: 'badDefault'; names: string[] }\n | { code: 'setRange' }\n | { code: 'badRange' };\n\nexport interface BindInput {\n commandName: string | undefined;\n parameterSchema: string | null | undefined;\n parameter: string | undefined;\n // Authored fallbacks for a slider: used only where the command definition leaves the end (or\n // the step) open. The definition is the authority on what the command accepts.\n min?: number;\n max?: number;\n step?: number;\n}\n\nexport interface Range {\n min: number;\n max: number;\n step: number;\n integer: boolean;\n}\n\nexport type Binding =\n | { ok: true; kind: ControlKind; param: CommandParameter; range?: Range; sent: CommandParameter[] }\n | { ok: false; refusal: Refusal };\n\nconst refuse = (refusal: Refusal): Binding => ({ ok: false, refusal });\n\n// 100 notches across a continuous range: the step a DOUBLE slider gets when nobody set one.\nconst DEFAULT_NOTCHES = 100;\n\nexport function bindControl(kind: ControlKind, input: BindInput): Binding {\n if (!input.commandName) return refuse({ code: 'noCommand' });\n const params = parseParameterSchema(input.parameterSchema);\n const wanted = (p: CommandParameter) =>\n isScalar(p) && (kind === 'boolean' ? p.dataType === 'BOOLEAN' : p.dataType === 'INT' || p.dataType === 'DOUBLE');\n\n let param: CommandParameter | undefined;\n if (input.parameter) {\n param = params.find((p) => p.name === input.parameter);\n if (!param) return refuse({ code: 'parameterMissing', name: input.parameter });\n if (!wanted(param)) return refuse({ code: 'wrongType', name: param.name, kind });\n } else {\n // Unambiguous or not at all: one candidate of the right type is the parameter; none or several\n // is the author's to resolve.\n const candidates = params.filter(wanted);\n if (candidates.length !== 1) return refuse({ code: 'setParameter' });\n param = candidates[0];\n }\n if (param.enum && param.enum.length > 0) return refuse({ code: 'fixedChoices', name: param.name });\n\n // Every other parameter either has a declared default (sent as is) or is optional (omitted).\n // A required one with neither would have to be invented, so the control refuses instead.\n const others = params.filter((p) => p !== param);\n const unsatisfied = others.filter((p) => p.required && (!isScalar(p) || p.default == null || p.default === ''));\n if (unsatisfied.length > 0) return refuse({ code: 'otherRequired', names: unsatisfied.map((p) => p.name) });\n const defaulted = others.filter((p) => isScalar(p) && p.default != null && p.default !== '');\n // A default is sent as written, so one the definition's own rules reject (not a number, not a\n // whole number, outside its range) would be a command the server refuses on every send.\n const invalid = new Set(\n Object.keys(validateParams(defaulted, Object.fromEntries(defaulted.map((p) => [p.name, p.default ?? ''])))),\n );\n for (const p of defaulted) if (p.enum && p.enum.length > 0 && !p.enum.includes(p.default ?? '')) invalid.add(p.name);\n if (invalid.size > 0) return refuse({ code: 'badDefault', names: defaulted.filter((p) => invalid.has(p.name)).map((p) => p.name) });\n const sent = [param, ...defaulted];\n\n if (kind === 'boolean') return { ok: true, kind, param, sent };\n\n const min = param.minValue ?? input.min;\n const max = param.maxValue ?? input.max;\n if (min === undefined || max === undefined) return refuse({ code: 'setRange' });\n if (!(max > min)) return refuse({ code: 'badRange' });\n const integer = param.dataType === 'INT';\n const step = input.step !== undefined && input.step > 0 ? input.step : integer ? 1 : (max - min) / DEFAULT_NOTCHES;\n return { ok: true, kind, param, range: { min, max, step, integer }, sent };\n}\n\n// The decimals a step carries, so snapped values do not pick up float noise (0.1 steps).\nfunction decimalsOf(n: number): number {\n const text = String(n);\n const e = /e-(\\d+)$/.exec(text);\n if (e) return Number(e[1]);\n const dot = text.indexOf('.');\n return dot < 0 ? 0 : text.length - dot - 1;\n}\n\n// clampToRange puts a raw value on the slider's grid: inside [min, max], on a whole step from min\n// (an INT rounds to a whole number whatever the step), and never NaN.\nexport function clampToRange(range: Range, raw: number): number {\n const finite = Number.isFinite(raw) ? raw : range.min;\n const bounded = Math.min(range.max, Math.max(range.min, finite));\n const steps = Math.round((bounded - range.min) / range.step);\n const decimals = Math.max(decimalsOf(range.step), decimalsOf(range.min));\n let snapped = Number((range.min + steps * range.step).toFixed(Math.min(decimals, 12)));\n if (snapped > range.max) snapped = Number((snapped - range.step).toFixed(Math.min(decimals, 12)));\n if (range.integer) snapped = Math.round(snapped);\n return Math.min(range.max, Math.max(range.min, snapped));\n}\n\n// The JSON the command carries: the bound parameter as its real type, the other parameters that\n// have a declared default, nothing else.\nexport function payloadFor(binding: Extract<Binding, { ok: true }>, value: number | boolean): string {\n const values: Record<string, string> = {};\n for (const p of binding.sent) {\n values[p.name] = p.default ?? '';\n if (p === binding.param) values[p.name] = String(value);\n }\n const payload = buildPayload(binding.sent, values);\n // sent always holds the bound parameter, so a payload always exists.\n return payload as string;\n}\n\nexport type DeliveryGroup = 'pending' | 'delivered' | 'failed' | 'unknown';\n\n// Which of the three things an operator asks of a command its status answers. An unrecognised\n// status is its own group, rendered as the raw word: it is not known to be pending.\nexport function deliveryGroup(status: string): DeliveryGroup {\n switch (status) {\n case 'QUEUED':\n case 'HELD':\n case 'SENT':\n case 'PARKED':\n return 'pending';\n case 'SUCCESSFUL':\n return 'delivered';\n case 'FAILED':\n case 'TIMEOUT':\n case 'EXPIRED':\n case 'CANCELLED':\n return 'failed';\n default:\n return 'unknown';\n }\n}\n\nexport interface LastSent {\n token: string;\n // The value the command carried for the bound parameter; undefined when the command (sent from\n // somewhere else) did not carry one this control can read.\n value: number | boolean | undefined;\n status: string;\n}\n\n// An accepted send not yet seen in the history.\nexport interface Optimistic {\n token: string;\n value: number | boolean;\n}\n\nfunction readValue(kind: ControlKind, payload: string | null, name: string): number | boolean | undefined {\n if (!payload) return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(payload);\n } catch {\n return undefined;\n }\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n const v = (parsed as Record<string, unknown>)[name];\n if (kind === 'number') return typeof v === 'number' && Number.isFinite(v) ? v : undefined;\n if (typeof v === 'boolean') return v;\n return undefined;\n}\n\n// lastSent reads the newest command of this control's name off the history. It is the newest, not\n// the newest that happens to parse: skipping to an older row would report a value that has since\n// been superseded. A send the history has not caught up with yet stands in until it does.\nexport function lastSent(\n kind: ControlKind,\n commands: readonly CommandRow[],\n commandName: string,\n parameter: string,\n optimistic: Optimistic | null,\n): LastSent | null {\n let newest: CommandRow | undefined;\n for (const c of commands) {\n if (c.name !== commandName) continue;\n if (!newest || (c.queuedTime ?? '') > (newest.queuedTime ?? '')) newest = c;\n }\n if (optimistic && !commands.some((c) => c.token === optimistic.token)) {\n return { token: optimistic.token, value: optimistic.value, status: 'QUEUED' };\n }\n if (!newest) return null;\n return { token: newest.token, value: readValue(kind, newest.payload, parameter), status: newest.status };\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// What the command slider and toggle share beyond the pure logic: the send itself (one at a time,\n// the permission gate, the refusal's reason), the \"last sent\" caption with its delivery status, and\n// the explained empty state. Neither control shows device state: the caption names what was SENT.\n\nimport type { CommandDispatch, WidgetActions } from '@devicechain/dashboards';\nimport { useCallback, useEffect, useRef, useState, type CSSProperties } from 'react';\n\nimport { css, fontVar, spaceVar } from '@devicechain/widget-sdk';\n\nimport { controlStrings, type ControlStrings } from './command-control-strings';\nimport { deliveryGroup, type LastSent, type Optimistic, type Refusal } from './command-control-core';\nimport { commandStatusColor, commandStatusLabel } from './widgets/command-status';\n\n// The smallest hit area a touch control may have, in CSS px.\nexport const TOUCH_TARGET_PX = 44;\n\nexport function refusalText(r: Refusal, s: ControlStrings, expect: 'number' | 'boolean'): string {\n switch (r.code) {\n case 'noCommand':\n return s.noCommand;\n case 'setParameter':\n return s.setParameter;\n case 'parameterMissing':\n return s.parameterMissing(r.name);\n case 'wrongType':\n return s.wrongType(r.name, expect === 'number' ? s.numberKind : s.booleanKind);\n case 'fixedChoices':\n return s.fixedChoices(r.name);\n case 'otherRequired':\n return s.otherRequired(r.names);\n case 'badDefault':\n return s.badDefault(r.names);\n case 'setRange':\n return s.setRange;\n case 'badRange':\n return s.badRange;\n }\n}\n\nexport interface CommandSender {\n strings: ControlStrings;\n // Why the control is read-only, or undefined when the viewer may send.\n readOnlyReason: string | undefined;\n canSend: boolean;\n // A dispatch is in flight.\n pending: boolean;\n error: string | null;\n // A send the platform accepted, until the history shows it.\n optimistic: Optimistic | null;\n // Returns whether a command was dispatched. At most one is in flight at a time.\n send: (payload: string, value: number | boolean) => boolean;\n}\n\n// useCommandSender owns one control's sending. `resetKey` identifies the configured command: when\n// it changes, an in-flight send's result is dropped rather than landing under the new command.\nexport function useCommandSender(\n actions: WidgetActions | undefined,\n deviceToken: string | null | undefined,\n commandName: string | undefined,\n resetKey: string,\n): CommandSender {\n const strings = controlStrings();\n const canWrite = actions?.can('command:write') ?? false;\n const hasSeam = typeof actions?.sendCommand === 'function';\n const canSend = canWrite && hasSeam && !!deviceToken && !!commandName;\n\n // The in-flight flag is a ref, not state: two clicks in the same tick both see the state before\n // either has re-rendered, and only a ref closes that window.\n const inFlight = useRef(false);\n const generation = useRef(0);\n const [pending, setPending] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [optimistic, setOptimistic] = useState<Optimistic | null>(null);\n\n useEffect(() => {\n generation.current += 1;\n inFlight.current = false;\n setPending(false);\n setError(null);\n setOptimistic(null);\n }, [resetKey]);\n\n const send = useCallback(\n (payload: string, value: number | boolean): boolean => {\n if (!canSend || inFlight.current || !actions?.sendCommand || !deviceToken || !commandName) return false;\n const gen = generation.current;\n inFlight.current = true;\n setPending(true);\n setError(null);\n const settle = () => {\n if (gen !== generation.current) return;\n inFlight.current = false;\n setPending(false);\n };\n let promise: Promise<CommandDispatch>;\n try {\n promise = actions.sendCommand(deviceToken, commandName, payload);\n } catch {\n setError(strings.sendFailed);\n settle();\n return true;\n }\n promise\n .then((dispatch) => {\n if (gen !== generation.current) return;\n // A refusal is shown with the server's reason (it is client-safe and the operator can act\n // on it); a thrown error is an availability failure whose text is internal, so it gets\n // one fixed message instead. A refused value is not \"last sent\".\n if (dispatch.status === 'rejected') setError(dispatch.reason);\n else setOptimistic({ token: dispatch.token, value });\n })\n .catch(() => {\n if (gen === generation.current) setError(strings.sendFailed);\n })\n .finally(settle);\n return true;\n },\n [actions, canSend, commandName, deviceToken, strings.sendFailed],\n );\n\n // A view with no send seam at all (a read-only embed) is told so, rather than blamed on the\n // viewer's permissions.\n const readOnlyReason = !hasSeam\n ? strings.noSeam\n : !canWrite\n ? strings.noPermission\n : !deviceToken\n ? strings.noDevice\n : undefined;\n return { strings, readOnlyReason, canSend, pending, error, optimistic, send };\n}\n\nconst note: CSSProperties = {\n fontSize: fontVar('xs'),\n lineHeight: 1.25,\n color: css('muted-foreground'),\n overflowWrap: 'anywhere',\n};\n\n// The explained empty state: why the control has nothing to offer, as an alert so it is announced.\nexport function ControlEmpty({ message }: { message: string }) {\n return (\n <div\n role=\"alert\"\n data-control-empty=\"\"\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n height: '100%',\n textAlign: 'center',\n color: css('muted-foreground'),\n fontSize: fontVar('sm'),\n overflowWrap: 'anywhere',\n }}\n >\n {message}\n </div>\n );\n}\n\n// The id of the full read-only reason, derived from the caption's id so the control can name it in\n// aria-describedby. The visible caption is the short \"View only\" so it fits a two-row widget at\n// one-column width; the full sentence lives here, hidden from the eye but not from assistive tech.\nexport const readOnlyId = (captionId: string) => `${captionId}-ro`;\n\n// The notes under a control: a short \"View only\" caption (the full reason is its title and the\n// control's description), and the last send's error.\nexport function ControlNotes({ sender, captionId }: { sender: CommandSender; captionId: string }) {\n return (\n <>\n {sender.readOnlyReason ? (\n <span id={readOnlyId(captionId)} hidden>\n {sender.readOnlyReason}\n </span>\n ) : null}\n {sender.readOnlyReason ? (\n <div data-read-only=\"\" title={sender.readOnlyReason} style={{ ...note, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>\n {sender.strings.viewOnly}\n </div>\n ) : null}\n {sender.error ? (\n <div role=\"alert\" style={{ ...note, color: commandStatusColor('FAILED') }}>\n {sender.error}\n </div>\n ) : null}\n </>\n );\n}\n\n// \"Last sent: 22 C \u00B7 Pending\". The words are the viewer's: pending, delivered or failed, with the\n// precise status on hover. `override` replaces the whole line (the slider's \"not sent yet\" while a\n// value is staged), so the caption never claims a staged value was sent.\nexport function LastSentCaption({\n id,\n last,\n format,\n strings,\n override,\n truncated = false,\n}: {\n id: string;\n last: LastSent | null;\n format: (value: number | boolean | undefined) => string;\n strings: ControlStrings;\n override?: string;\n // The device has more commands than the page read, so an empty match proves nothing.\n truncated?: boolean;\n}) {\n const group = last ? deliveryGroup(last.status) : undefined;\n const word =\n group === 'pending'\n ? strings.pending\n : group === 'delivered'\n ? strings.delivered\n : group === 'failed'\n ? strings.failed\n : last\n ? commandStatusLabel(last.status)\n : '';\n return (\n <div\n id={id}\n role=\"status\"\n aria-live=\"polite\"\n data-last-sent=\"\"\n data-delivery={group}\n style={{ ...note, fontSize: fontVar('sm'), display: 'flex', flexWrap: 'wrap', alignItems: 'center', columnGap: spaceVar(1) }}\n >\n {override ? (\n <span>{override}</span>\n ) : last ? (\n <>\n <span>{strings.lastSent}:</span>\n <span style={{ color: css('foreground'), fontWeight: 600 }}>{format(last.value)}</span>\n <span aria-hidden=\"true\">\u00B7</span>\n <span\n title={last.status}\n style={{ display: 'inline-flex', alignItems: 'center', gap: 4, color: commandStatusColor(last.status), fontWeight: 600 }}\n >\n <span aria-hidden=\"true\" style={{ width: 7, height: 7, borderRadius: '50%', background: commandStatusColor(last.status) }} />\n {word}\n </span>\n </>\n ) : (\n <span>{truncated ? strings.beyondHistory : strings.nothingSent}</span>\n )}\n </div>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The fixed words the command slider and toggle render themselves: why they will not send, what\n// they last sent, and where that command is in its delivery. The widget packages carry no i18n\n// runtime, so this is the one small per-language table, in the manner of level-strings.ts; option\n// labels are translated in the console's catalogs, not here.\n//\n// The caption is worded as what was SENT. These controls know the history of the commands issued\n// to the device, not what the device is doing, and no string here may read as the latter.\n\nimport { viewerLocale } from '@devicechain/widget-sdk';\n\nexport interface ControlStrings {\n noCommand: string;\n setParameter: string;\n parameterMissing: (name: string) => string;\n wrongType: (name: string, expected: string) => string;\n fixedChoices: (name: string) => string;\n otherRequired: (names: string[]) => string;\n badDefault: (names: string[]) => string;\n setRange: string;\n badRange: string;\n numberKind: string;\n booleanKind: string;\n // The caption.\n lastSent: string;\n nothingSent: string;\n // The history is longer than the page this control reads, and none of this command is on it.\n beyondHistory: string;\n noValue: string;\n unsent: string;\n // The slider's keyboard description: arrows choose, Enter sends.\n sliderHint: string;\n pending: string;\n delivered: string;\n failed: string;\n sending: string;\n on: string;\n off: string;\n // Read-only reasons.\n // The short visible caption beside a read-only control; the full reason is its description.\n viewOnly: string;\n noPermission: string;\n noSeam: string;\n noDevice: string;\n sendFailed: string;\n}\n\nexport const CONTROL_STRINGS: Record<'en' | 'es', ControlStrings> = {\n en: {\n noCommand: 'No command configured',\n setParameter: 'Set which parameter this control sends',\n parameterMissing: (n) => `The command has no parameter \u201C${n}\u201D`,\n wrongType: (n, e) => `Parameter \u201C${n}\u201D is not ${e}`,\n fixedChoices: (n) => `Parameter \u201C${n}\u201D takes fixed choices, not a range`,\n otherRequired: (ns) => `Not sent: the command also requires ${ns.map((n) => `\u201C${n}\u201D`).join(', ')}, which has no default value`,\n badDefault: (ns) => `Not sent: the default value of ${ns.map((n) => `\u201C${n}\u201D`).join(', ')} is not valid for the command`,\n setRange: 'Set min and max',\n badRange: 'Max must be above min',\n numberKind: 'a number',\n booleanKind: 'a boolean',\n lastSent: 'Last sent',\n nothingSent: 'Nothing sent yet',\n beyondHistory: 'Not among the recent commands',\n noValue: 'no value',\n unsent: 'Not sent yet. Release or press Enter to send',\n sliderHint: 'Arrow keys choose a value; press Enter to send it, or Escape to discard it.',\n pending: 'Pending',\n delivered: 'Delivered',\n failed: 'Failed',\n sending: 'Sending\u2026',\n on: 'On',\n off: 'Off',\n viewOnly: 'View only',\n noPermission: 'You don\u2019t have permission to send commands.',\n noSeam: 'Commands aren\u2019t available in this view.',\n noDevice: 'No device is selected for this command.',\n sendFailed: 'Couldn\u2019t send the command. Try again.',\n },\n es: {\n noCommand: 'No hay un comando configurado',\n setParameter: 'Defina qu\u00E9 par\u00E1metro env\u00EDa este control',\n parameterMissing: (n) => `El comando no tiene el par\u00E1metro \u201C${n}\u201D`,\n wrongType: (n, e) => `El par\u00E1metro \u201C${n}\u201D no es ${e}`,\n fixedChoices: (n) => `El par\u00E1metro \u201C${n}\u201D admite opciones fijas, no un rango`,\n otherRequired: (ns) => `No se env\u00EDa: el comando tambi\u00E9n exige ${ns.map((n) => `\u201C${n}\u201D`).join(', ')}, que no tiene valor por defecto`,\n badDefault: (ns) => `No se env\u00EDa: el valor por defecto de ${ns.map((n) => `\u201C${n}\u201D`).join(', ')} no es v\u00E1lido para el comando`,\n setRange: 'Defina m\u00EDn. y m\u00E1x.',\n badRange: 'El m\u00E1x. debe superar el m\u00EDn.',\n numberKind: 'un n\u00FAmero',\n booleanKind: 'un booleano',\n lastSent: '\u00DAltimo enviado',\n nothingSent: 'A\u00FAn no se ha enviado nada',\n beyondHistory: 'No figura entre los comandos recientes',\n noValue: 'sin valor',\n unsent: 'A\u00FAn no enviado. Suelte o pulse Intro para enviar',\n sliderHint: 'Las flechas eligen un valor; pulse Intro para enviarlo o Escape para descartarlo.',\n pending: 'Pendiente',\n delivered: 'Entregado',\n failed: 'Fallido',\n sending: 'Enviando\u2026',\n on: 'Encendido',\n off: 'Apagado',\n viewOnly: 'Solo lectura',\n noPermission: 'No tiene permiso para enviar comandos.',\n noSeam: 'Los comandos no est\u00E1n disponibles en esta vista.',\n noDevice: 'No hay un dispositivo seleccionado para este comando.',\n sendFailed: 'No se pudo enviar el comando. Int\u00E9ntelo de nuevo.',\n },\n};\n\nexport function controlStrings(locale: string | undefined = viewerLocale()): ControlStrings {\n return locale?.toLowerCase().split('-')[0] === 'es' ? CONTROL_STRINGS.es : CONTROL_STRINGS.en;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// command-toggle \u2014 sends a command carrying ONE boolean parameter, one command per click.\n//\n// \uD83D\uDD34 THE SWITCH SHOWS THE LAST VALUE SENT, NOT THE DEVICE'S STATE. It rests where the newest\n// command left it (read off the command history) and the caption says \"Last sent\" with that\n// command's delivery status.\n//\n// \uD83D\uDD34 IT NEVER GUESSES WHICH WAY TO FLIP. A click sends the opposite of the last value sent only\n// when that value is a live intent: a command that is pending or was delivered, carrying a value\n// this control can read. With nothing sent, a history too short to show this command, a newest\n// command with no readable value, or a newest command that FAILED (its value never reached the\n// device, so flipping relative to it would send what the operator may already have), the switch\n// is replaced by two explicit buttons, On and Off, and the operator chooses.\n//\n// A click while a send is in flight does nothing, so rapid clicks make one command, not a queue of\n// alternating ones.\n\nimport { useId, useMemo, type ReactNode } from 'react';\n\nimport {\n css,\n FIT_MIN_FONT_PX,\n fitValueFont,\n fitValueFontCss,\n optString,\n spaceVar,\n useStickyChars,\n WidgetFrame,\n type CommandStreamState,\n type WidgetProps,\n type WidgetScale,\n} from '@devicechain/widget-sdk';\n\nimport { bindControl, deliveryGroup, lastSent, payloadFor, type Binding, type LastSent } from '../command-control-core';\nimport {\n ControlEmpty,\n ControlNotes,\n readOnlyId,\n LastSentCaption,\n refusalText,\n TOUCH_TARGET_PX,\n useCommandSender,\n type CommandSender,\n} from '../command-control';\n\ntype Bound = Extract<Binding, { ok: true }>;\n\nconst TRACK_W = 52;\nconst TRACK_H = 30;\nconst THUMB = 24;\n\nexport function CommandToggle({ widget, data, actions }: WidgetProps<CommandStreamState>) {\n const commandName = optString(widget.options, 'commandName');\n const commandLabel = optString(widget.options, 'commandLabel') ?? commandName;\n const parameterSchema = optString(widget.options, 'parameterSchema') ?? null;\n const parameter = optString(widget.options, 'parameter');\n\n const binding = useMemo(\n () => bindControl('boolean', { commandName, parameterSchema, parameter }),\n [commandName, parameterSchema, parameter],\n );\n const sender = useCommandSender(\n actions,\n data.deviceToken,\n commandName,\n `${commandName ?? ''}::${parameterSchema ?? ''}::${parameter ?? ''}`,\n );\n const last =\n binding.ok && commandName\n ? lastSent('boolean', data.commands, commandName, binding.param.name, sender.optimistic)\n : null;\n const captionId = useId();\n\n return (\n <WidgetFrame title={optString(widget.options, 'title')}>\n {(scale) =>\n !binding.ok ? (\n <ControlEmpty message={refusalText(binding.refusal, sender.strings, 'boolean')} />\n ) : (\n <ToggleBody\n binding={binding}\n label={commandLabel ?? binding.param.name}\n value={intentOf(last)}\n sender={sender}\n captionId={captionId}\n caption={\n <LastSentCaption\n id={captionId}\n last={last}\n strings={sender.strings}\n truncated={data.total > data.commands.length}\n format={(v) => (typeof v === 'boolean' ? (v ? sender.strings.on : sender.strings.off) : sender.strings.noValue)}\n override={sender.pending ? sender.strings.sending : undefined}\n />\n }\n scale={scale}\n />\n )\n }\n </WidgetFrame>\n );\n}\n\n// The value a flip may be computed from, or undefined when the operator has to choose. Only a\n// command that is still on its way, or arrived, states an intent the device is acting on.\nexport function intentOf(last: LastSent | null): boolean | undefined {\n if (!last || typeof last.value !== 'boolean') return undefined;\n const group = deliveryGroup(last.status);\n return group === 'pending' || group === 'delivered' ? last.value : undefined;\n}\n\nfunction ToggleBody({\n binding,\n label,\n value,\n sender,\n captionId,\n caption,\n scale: ws,\n}: {\n binding: Bound;\n label: string;\n value: boolean | undefined;\n sender: CommandSender;\n captionId: string;\n caption: ReactNode;\n scale: WidgetScale;\n}) {\n const checked = value === true;\n const unknown = value === undefined;\n const disabled = !sender.canSend;\n const word = checked ? sender.strings.on : sender.strings.off;\n\n const chars = useStickyChars(word.length);\n const floor = Math.round(FIT_MIN_FONT_PX * ws.scale);\n const measured = ws.size.width > 0 && ws.size.height > 0;\n const room = {\n width: Math.floor(ws.size.width - TRACK_W - 16),\n height: Math.max(TOUCH_TARGET_PX, Math.floor(ws.size.height * 0.5)),\n };\n const font = measured ? fitValueFont(room, chars, { floor }) : fitValueFontCss(chars, { floor });\n\n const sendValue = (next: boolean) => {\n sender.send(payloadFor(binding, next), next);\n };\n\n return (\n <div\n style={{\n height: '100%',\n boxSizing: 'border-box',\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'safe center',\n gap: spaceVar(1),\n overflow: 'hidden',\n }}\n >\n {unknown ? (\n <div role=\"group\" aria-label={label} aria-describedby={sender.readOnlyReason ? `${captionId} ${readOnlyId(captionId)}` : captionId} data-toggle-choose=\"\" style={{ display: 'flex', gap: spaceVar(2) }}>\n {([true, false] as const).map((v) => (\n <button\n key={String(v)}\n type=\"button\"\n disabled={disabled}\n aria-busy={sender.pending}\n onClick={() => sendValue(v)}\n style={{\n flex: '1 1 0',\n minHeight: TOUCH_TARGET_PX,\n minWidth: TOUCH_TARGET_PX,\n padding: `0 ${spaceVar(2)}`,\n borderRadius: 6,\n border: `1px solid ${css('border')}`,\n background: css('background'),\n color: css('foreground'),\n fontWeight: 600,\n cursor: disabled ? 'not-allowed' : 'pointer',\n opacity: disabled ? 0.6 : 1,\n }}\n >\n {v ? sender.strings.on : sender.strings.off}\n </button>\n ))}\n </div>\n ) : (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={label}\n aria-describedby={sender.readOnlyReason ? `${captionId} ${readOnlyId(captionId)}` : captionId}\n aria-busy={sender.pending}\n disabled={disabled}\n onClick={() => sendValue(!checked)}\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: spaceVar(2),\n minHeight: TOUCH_TARGET_PX,\n minWidth: TOUCH_TARGET_PX,\n padding: 0,\n border: 'none',\n background: 'transparent',\n color: css('foreground'),\n cursor: disabled ? 'not-allowed' : 'pointer',\n opacity: disabled ? 0.6 : 1,\n textAlign: 'left',\n }}\n >\n <span\n aria-hidden=\"true\"\n style={{\n position: 'relative',\n flex: '0 0 auto',\n width: TRACK_W,\n height: TRACK_H,\n borderRadius: TRACK_H / 2,\n boxSizing: 'border-box',\n border: `1px solid ${css('border')}`,\n background: checked ? css('primary') : css('muted'),\n }}\n >\n <span\n style={{\n position: 'absolute',\n top: (TRACK_H - THUMB) / 2 - 1,\n left: checked ? TRACK_W - THUMB - 3 : 2,\n width: THUMB,\n height: THUMB,\n borderRadius: '50%',\n background: checked ? css('primary-foreground') : css('background'),\n border: `1px solid ${css('border')}`,\n boxSizing: 'border-box',\n }}\n />\n </span>\n <span\n data-toggle-word=\"\"\n style={{\n fontSize: font,\n fontWeight: 600,\n lineHeight: 1.1,\n whiteSpace: 'nowrap',\n color: css('foreground'),\n }}\n >\n {word}\n </span>\n </button>\n )}\n {/* One row, so the short read-only caption costs no height at a two-row widget's minimum span. */}\n <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'baseline', columnGap: spaceVar(2) }}>\n {caption}\n <ControlNotes sender={sender} captionId={captionId} />\n </div>\n </div>\n );\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;AAuBA,SAAS,aAAAA,YAAW,OAAO,SAAS,UAAAC,SAAQ,YAAAC,iBAAgC;AAE5E;AAAA,EACE,OAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAAC;AAAA,EACA;AAAA,EACA;AAAA,OAIK;;;ACYP,IAAM,SAAS,CAAC,aAA+B,EAAE,IAAI,OAAO,QAAQ;AAGpE,IAAM,kBAAkB;AAEjB,SAAS,YAAY,MAAmB,OAA2B;AACxE,MAAI,CAAC,MAAM,YAAa,QAAO,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3D,QAAM,SAAS,qBAAqB,MAAM,eAAe;AACzD,QAAM,SAAS,CAAC,MACd,SAAS,CAAC,MAAM,SAAS,YAAY,EAAE,aAAa,YAAY,EAAE,aAAa,SAAS,EAAE,aAAa;AAEzG,MAAI;AACJ,MAAI,MAAM,WAAW;AACnB,YAAQ,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,MAAM,SAAS;AACrD,QAAI,CAAC,MAAO,QAAO,OAAO,EAAE,MAAM,oBAAoB,MAAM,MAAM,UAAU,CAAC;AAC7E,QAAI,CAAC,OAAO,KAAK,EAAG,QAAO,OAAO,EAAE,MAAM,aAAa,MAAM,MAAM,MAAM,KAAK,CAAC;AAAA,EACjF,OAAO;AAGL,UAAM,aAAa,OAAO,OAAO,MAAM;AACvC,QAAI,WAAW,WAAW,EAAG,QAAO,OAAO,EAAE,MAAM,eAAe,CAAC;AACnE,YAAQ,WAAW,CAAC;AAAA,EACtB;AACA,MAAI,MAAM,QAAQ,MAAM,KAAK,SAAS,EAAG,QAAO,OAAO,EAAE,MAAM,gBAAgB,MAAM,MAAM,KAAK,CAAC;AAIjG,QAAM,SAAS,OAAO,OAAO,CAAC,MAAM,MAAM,KAAK;AAC/C,QAAM,cAAc,OAAO,OAAO,CAAC,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,KAAK,EAAE,WAAW,QAAQ,EAAE,YAAY,GAAG;AAC9G,MAAI,YAAY,SAAS,EAAG,QAAO,OAAO,EAAE,MAAM,iBAAiB,OAAO,YAAY,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1G,QAAM,YAAY,OAAO,OAAO,CAAC,MAAM,SAAS,CAAC,KAAK,EAAE,WAAW,QAAQ,EAAE,YAAY,EAAE;AAG3F,QAAM,UAAU,IAAI;AAAA,IAClB,OAAO,KAAK,eAAe,WAAW,OAAO,YAAY,UAAU,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;AAAA,EAC5G;AACA,aAAW,KAAK,UAAW,KAAI,EAAE,QAAQ,EAAE,KAAK,SAAS,KAAK,CAAC,EAAE,KAAK,SAAS,EAAE,WAAW,EAAE,EAAG,SAAQ,IAAI,EAAE,IAAI;AACnH,MAAI,QAAQ,OAAO,EAAG,QAAO,OAAO,EAAE,MAAM,cAAc,OAAO,UAAU,OAAO,CAAC,MAAM,QAAQ,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;AAClI,QAAM,OAAO,CAAC,OAAO,GAAG,SAAS;AAEjC,MAAI,SAAS,UAAW,QAAO,EAAE,IAAI,MAAM,MAAM,OAAO,KAAK;AAE7D,QAAM,MAAM,MAAM,YAAY,MAAM;AACpC,QAAM,MAAM,MAAM,YAAY,MAAM;AACpC,MAAI,QAAQ,UAAa,QAAQ,OAAW,QAAO,OAAO,EAAE,MAAM,WAAW,CAAC;AAC9E,MAAI,EAAE,MAAM,KAAM,QAAO,OAAO,EAAE,MAAM,WAAW,CAAC;AACpD,QAAM,UAAU,MAAM,aAAa;AACnC,QAAM,OAAO,MAAM,SAAS,UAAa,MAAM,OAAO,IAAI,MAAM,OAAO,UAAU,KAAK,MAAM,OAAO;AACnG,SAAO,EAAE,IAAI,MAAM,MAAM,OAAO,OAAO,EAAE,KAAK,KAAK,MAAM,QAAQ,GAAG,KAAK;AAC3E;AAGA,SAAS,WAAW,GAAmB;AACrC,QAAM,OAAO,OAAO,CAAC;AACrB,QAAM,IAAI,WAAW,KAAK,IAAI;AAC9B,MAAI,EAAG,QAAO,OAAO,EAAE,CAAC,CAAC;AACzB,QAAM,MAAM,KAAK,QAAQ,GAAG;AAC5B,SAAO,MAAM,IAAI,IAAI,KAAK,SAAS,MAAM;AAC3C;AAIO,SAAS,aAAa,OAAc,KAAqB;AAC9D,QAAM,SAAS,OAAO,SAAS,GAAG,IAAI,MAAM,MAAM;AAClD,QAAM,UAAU,KAAK,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM,CAAC;AAC/D,QAAM,QAAQ,KAAK,OAAO,UAAU,MAAM,OAAO,MAAM,IAAI;AAC3D,QAAM,WAAW,KAAK,IAAI,WAAW,MAAM,IAAI,GAAG,WAAW,MAAM,GAAG,CAAC;AACvE,MAAI,UAAU,QAAQ,MAAM,MAAM,QAAQ,MAAM,MAAM,QAAQ,KAAK,IAAI,UAAU,EAAE,CAAC,CAAC;AACrF,MAAI,UAAU,MAAM,IAAK,WAAU,QAAQ,UAAU,MAAM,MAAM,QAAQ,KAAK,IAAI,UAAU,EAAE,CAAC,CAAC;AAChG,MAAI,MAAM,QAAS,WAAU,KAAK,MAAM,OAAO;AAC/C,SAAO,KAAK,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,OAAO,CAAC;AACzD;AAIO,SAAS,WAAW,SAAyC,OAAiC;AACnG,QAAM,SAAiC,CAAC;AACxC,aAAW,KAAK,QAAQ,MAAM;AAC5B,WAAO,EAAE,IAAI,IAAI,EAAE,WAAW;AAC9B,QAAI,MAAM,QAAQ,MAAO,QAAO,EAAE,IAAI,IAAI,OAAO,KAAK;AAAA,EACxD;AACA,QAAM,UAAU,aAAa,QAAQ,MAAM,MAAM;AAEjD,SAAO;AACT;AAMO,SAAS,cAAc,QAA+B;AAC3D,UAAQ,QAAQ;AAAA,IACd,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAgBA,SAAS,UAAU,MAAmB,SAAwB,MAA4C;AACxG,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,OAAO;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,WAAW,YAAY,WAAW,KAAM,QAAO;AAC1D,QAAM,IAAK,OAAmC,IAAI;AAClD,MAAI,SAAS,SAAU,QAAO,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,IAAI,IAAI;AAChF,MAAI,OAAO,MAAM,UAAW,QAAO;AACnC,SAAO;AACT;AAKO,SAAS,SACd,MACA,UACA,aACA,WACA,YACiB;AACjB,MAAI;AACJ,aAAW,KAAK,UAAU;AACxB,QAAI,EAAE,SAAS,YAAa;AAC5B,QAAI,CAAC,WAAW,EAAE,cAAc,OAAO,OAAO,cAAc,IAAK,UAAS;AAAA,EAC5E;AACA,MAAI,cAAc,CAAC,SAAS,KAAK,CAAC,MAAM,EAAE,UAAU,WAAW,KAAK,GAAG;AACrE,WAAO,EAAE,OAAO,WAAW,OAAO,OAAO,WAAW,OAAO,QAAQ,SAAS;AAAA,EAC9E;AACA,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,EAAE,OAAO,OAAO,OAAO,OAAO,UAAU,MAAM,OAAO,SAAS,SAAS,GAAG,QAAQ,OAAO,OAAO;AACzG;;;ACzMA,SAAS,aAAa,WAAW,QAAQ,gBAAoC;AAE7E,SAAS,KAAK,SAAS,gBAAgB;;;ACCvC,SAAS,oBAAoB;AAsCtB,IAAM,kBAAuD;AAAA,EAClE,IAAI;AAAA,IACF,WAAW;AAAA,IACX,cAAc;AAAA,IACd,kBAAkB,CAAC,MAAM,sCAAiC,CAAC;AAAA,IAC3D,WAAW,CAAC,GAAG,MAAM,mBAAc,CAAC,iBAAY,CAAC;AAAA,IACjD,cAAc,CAAC,MAAM,mBAAc,CAAC;AAAA,IACpC,eAAe,CAAC,OAAO,uCAAuC,GAAG,IAAI,CAAC,MAAM,SAAI,CAAC,QAAG,EAAE,KAAK,IAAI,CAAC;AAAA,IAChG,YAAY,CAAC,OAAO,kCAAkC,GAAG,IAAI,CAAC,MAAM,SAAI,CAAC,QAAG,EAAE,KAAK,IAAI,CAAC;AAAA,IACxF,UAAU;AAAA,IACV,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,UAAU;AAAA,IACV,aAAa;AAAA,IACb,eAAe;AAAA,IACf,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,IAAI;AAAA,IACJ,KAAK;AAAA,IACL,UAAU;AAAA,IACV,cAAc;AAAA,IACd,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,YAAY;AAAA,EACd;AAAA,EACA,IAAI;AAAA,IACF,WAAW;AAAA,IACX,cAAc;AAAA,IACd,kBAAkB,CAAC,MAAM,6CAAqC,CAAC;AAAA,IAC/D,WAAW,CAAC,GAAG,MAAM,yBAAiB,CAAC,gBAAW,CAAC;AAAA,IACnD,cAAc,CAAC,MAAM,yBAAiB,CAAC;AAAA,IACvC,eAAe,CAAC,OAAO,+CAAyC,GAAG,IAAI,CAAC,MAAM,SAAI,CAAC,QAAG,EAAE,KAAK,IAAI,CAAC;AAAA,IAClG,YAAY,CAAC,OAAO,2CAAwC,GAAG,IAAI,CAAC,MAAM,SAAI,CAAC,QAAG,EAAE,KAAK,IAAI,CAAC;AAAA,IAC9F,UAAU;AAAA,IACV,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,UAAU;AAAA,IACV,aAAa;AAAA,IACb,eAAe;AAAA,IACf,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,IAAI;AAAA,IACJ,KAAK;AAAA,IACL,UAAU;AAAA,IACV,cAAc;AAAA,IACd,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,YAAY;AAAA,EACd;AACF;AAEO,SAAS,eAAe,SAA6B,aAAa,GAAmB;AAC1F,SAAO,QAAQ,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,MAAM,OAAO,gBAAgB,KAAK,gBAAgB;AAC7F;;;AD+BI,SA4BA,UA5BA,KA4BA,YA5BA;AAhIG,IAAM,kBAAkB;AAExB,SAAS,YAAY,GAAY,GAAmB,QAAsC;AAC/F,UAAQ,EAAE,MAAM;AAAA,IACd,KAAK;AACH,aAAO,EAAE;AAAA,IACX,KAAK;AACH,aAAO,EAAE;AAAA,IACX,KAAK;AACH,aAAO,EAAE,iBAAiB,EAAE,IAAI;AAAA,IAClC,KAAK;AACH,aAAO,EAAE,UAAU,EAAE,MAAM,WAAW,WAAW,EAAE,aAAa,EAAE,WAAW;AAAA,IAC/E,KAAK;AACH,aAAO,EAAE,aAAa,EAAE,IAAI;AAAA,IAC9B,KAAK;AACH,aAAO,EAAE,cAAc,EAAE,KAAK;AAAA,IAChC,KAAK;AACH,aAAO,EAAE,WAAW,EAAE,KAAK;AAAA,IAC7B,KAAK;AACH,aAAO,EAAE;AAAA,IACX,KAAK;AACH,aAAO,EAAE;AAAA,EACb;AACF;AAkBO,SAAS,iBACd,SACA,aACA,aACA,UACe;AACf,QAAM,UAAU,eAAe;AAC/B,QAAM,WAAW,SAAS,IAAI,eAAe,KAAK;AAClD,QAAM,UAAU,OAAO,SAAS,gBAAgB;AAChD,QAAM,UAAU,YAAY,WAAW,CAAC,CAAC,eAAe,CAAC,CAAC;AAI1D,QAAM,WAAW,OAAO,KAAK;AAC7B,QAAM,aAAa,OAAO,CAAC;AAC3B,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,YAAY,aAAa,IAAI,SAA4B,IAAI;AAEpE,YAAU,MAAM;AACd,eAAW,WAAW;AACtB,aAAS,UAAU;AACnB,eAAW,KAAK;AAChB,aAAS,IAAI;AACb,kBAAc,IAAI;AAAA,EACpB,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,OAAO;AAAA,IACX,CAAC,SAAiB,UAAqC;AACrD,UAAI,CAAC,WAAW,SAAS,WAAW,CAAC,SAAS,eAAe,CAAC,eAAe,CAAC,YAAa,QAAO;AAClG,YAAM,MAAM,WAAW;AACvB,eAAS,UAAU;AACnB,iBAAW,IAAI;AACf,eAAS,IAAI;AACb,YAAM,SAAS,MAAM;AACnB,YAAI,QAAQ,WAAW,QAAS;AAChC,iBAAS,UAAU;AACnB,mBAAW,KAAK;AAAA,MAClB;AACA,UAAI;AACJ,UAAI;AACF,kBAAU,QAAQ,YAAY,aAAa,aAAa,OAAO;AAAA,MACjE,QAAQ;AACN,iBAAS,QAAQ,UAAU;AAC3B,eAAO;AACP,eAAO;AAAA,MACT;AACA,cACG,KAAK,CAAC,aAAa;AAClB,YAAI,QAAQ,WAAW,QAAS;AAIhC,YAAI,SAAS,WAAW,WAAY,UAAS,SAAS,MAAM;AAAA,YACvD,eAAc,EAAE,OAAO,SAAS,OAAO,MAAM,CAAC;AAAA,MACrD,CAAC,EACA,MAAM,MAAM;AACX,YAAI,QAAQ,WAAW,QAAS,UAAS,QAAQ,UAAU;AAAA,MAC7D,CAAC,EACA,QAAQ,MAAM;AACjB,aAAO;AAAA,IACT;AAAA,IACA,CAAC,SAAS,SAAS,aAAa,aAAa,QAAQ,UAAU;AAAA,EACjE;AAIA,QAAM,iBAAiB,CAAC,UACpB,QAAQ,SACR,CAAC,WACC,QAAQ,eACR,CAAC,cACC,QAAQ,WACR;AACR,SAAO,EAAE,SAAS,gBAAgB,SAAS,SAAS,OAAO,YAAY,KAAK;AAC9E;AAEA,IAAM,OAAsB;AAAA,EAC1B,UAAU,QAAQ,IAAI;AAAA,EACtB,YAAY;AAAA,EACZ,OAAO,IAAI,kBAAkB;AAAA,EAC7B,cAAc;AAChB;AAGO,SAAS,aAAa,EAAE,QAAQ,GAAwB;AAC7D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,sBAAmB;AAAA,MACnB,OAAO;AAAA,QACL,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,gBAAgB;AAAA,QAChB,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,OAAO,IAAI,kBAAkB;AAAA,QAC7B,UAAU,QAAQ,IAAI;AAAA,QACtB,cAAc;AAAA,MAChB;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;AAKO,IAAM,aAAa,CAAC,cAAsB,GAAG,SAAS;AAItD,SAAS,aAAa,EAAE,QAAQ,UAAU,GAAiD;AAChG,SACE,iCACG;AAAA,WAAO,iBACN,oBAAC,UAAK,IAAI,WAAW,SAAS,GAAG,QAAM,MACpC,iBAAO,gBACV,IACE;AAAA,IACH,OAAO,iBACN,oBAAC,SAAI,kBAAe,IAAG,OAAO,OAAO,gBAAgB,OAAO,EAAE,GAAG,MAAM,YAAY,UAAU,UAAU,UAAU,cAAc,WAAW,GACvI,iBAAO,QAAQ,UAClB,IACE;AAAA,IACH,OAAO,QACN,oBAAC,SAAI,MAAK,SAAQ,OAAO,EAAE,GAAG,MAAM,OAAO,mBAAmB,QAAQ,EAAE,GACrE,iBAAO,OACV,IACE;AAAA,KACN;AAEJ;AAKO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AACd,GAQG;AACD,QAAM,QAAQ,OAAO,cAAc,KAAK,MAAM,IAAI;AAClD,QAAM,OACJ,UAAU,YACN,QAAQ,UACR,UAAU,cACR,QAAQ,YACR,UAAU,WACR,QAAQ,SACR,OACE,mBAAmB,KAAK,MAAM,IAC9B;AACZ,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL,aAAU;AAAA,MACV,kBAAe;AAAA,MACf,iBAAe;AAAA,MACf,OAAO,EAAE,GAAG,MAAM,UAAU,QAAQ,IAAI,GAAG,SAAS,QAAQ,UAAU,QAAQ,YAAY,UAAU,WAAW,SAAS,CAAC,EAAE;AAAA,MAE1H,qBACC,oBAAC,UAAM,oBAAS,IACd,OACF,iCACE;AAAA,6BAAC,UAAM;AAAA,kBAAQ;AAAA,UAAS;AAAA,WAAC;AAAA,QACzB,oBAAC,UAAK,OAAO,EAAE,OAAO,IAAI,YAAY,GAAG,YAAY,IAAI,GAAI,iBAAO,KAAK,KAAK,GAAE;AAAA,QAChF,oBAAC,UAAK,eAAY,QAAO,kBAAC;AAAA,QAC1B;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,KAAK;AAAA,YACZ,OAAO,EAAE,SAAS,eAAe,YAAY,UAAU,KAAK,GAAG,OAAO,mBAAmB,KAAK,MAAM,GAAG,YAAY,IAAI;AAAA,YAEvH;AAAA,kCAAC,UAAK,eAAY,QAAO,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG,cAAc,OAAO,YAAY,mBAAmB,KAAK,MAAM,EAAE,GAAG;AAAA,cAC1H;AAAA;AAAA;AAAA,QACH;AAAA,SACF,IAEA,oBAAC,UAAM,sBAAY,QAAQ,gBAAgB,QAAQ,aAAY;AAAA;AAAA,EAEnE;AAEJ;;;AFhHU,gBAAAC,MA4IF,QAAAC,aA5IE;AA/EV,IAAM,WAAW,CAAC,GAAW,SAA8B,OAAO,GAAG,CAAC,IAAI,IAAI,KAAK,OAAO,CAAC;AAEpF,SAAS,cAAc,EAAE,QAAQ,MAAM,QAAQ,GAAoC;AACxF,QAAM,cAAc,UAAU,OAAO,SAAS,aAAa;AAC3D,QAAM,eAAe,UAAU,OAAO,SAAS,cAAc,KAAK;AAClE,QAAM,kBAAkB,UAAU,OAAO,SAAS,iBAAiB,KAAK;AACxE,QAAM,YAAY,UAAU,OAAO,SAAS,WAAW;AACvD,QAAM,MAAM,UAAU,OAAO,SAAS,KAAK;AAC3C,QAAM,MAAM,UAAU,OAAO,SAAS,KAAK;AAC3C,QAAM,OAAO,UAAU,OAAO,SAAS,MAAM;AAE7C,QAAM,UAAU;AAAA,IACd,MAAM,YAAY,UAAU,EAAE,aAAa,iBAAiB,WAAW,KAAK,KAAK,KAAK,CAAC;AAAA,IACvF,CAAC,aAAa,iBAAiB,WAAW,KAAK,KAAK,IAAI;AAAA,EAC1D;AACA,QAAM,SAAS;AAAA,IACb;AAAA,IACA,KAAK;AAAA,IACL;AAAA,IACA,GAAG,eAAe,EAAE,KAAK,mBAAmB,EAAE,KAAK,aAAa,EAAE;AAAA,EACpE;AAEA,QAAM,OACJ,QAAQ,MAAM,cACV,SAAS,UAAU,KAAK,UAAU,aAAa,QAAQ,MAAM,MAAM,OAAO,UAAU,IACpF;AAIN,QAAM,CAAC,OAAO,aAAa,IAAIC,UAAwB,IAAI;AAC3D,QAAM,WAAWC,QAAsB,IAAI;AAC3C,QAAM,WAAW,CAAC,MAAqB;AACrC,aAAS,UAAU;AACnB,kBAAc,CAAC;AAAA,EACjB;AACA,QAAM,YAAY,MAAM;AACxB,QAAM,SAAS,MAAM;AAIrB,QAAM,UAAUA,QAAO,KAAK;AAC5B,QAAM,aAAaA,QAA4B,IAAI;AACnD,EAAAC,WAAU,MAAM,MAAM,WAAW,UAAU,GAAG,CAAC,CAAC;AAChD,QAAM,UAAU,MAAM;AACpB,YAAQ,UAAU;AAClB,eAAW,UAAU;AACrB,eAAW,UAAU;AACrB,QAAI,SAAS,YAAY,KAAM,UAAS,IAAI;AAAA,EAC9C;AACA,QAAM,QAAQ,CAAC,UAA4B;AACzC,YAAQ,UAAU;AAClB,eAAW,UAAU;AACrB,UAAM,MAAM,MAAM;AAClB,UAAM,YAAY,CAAC,MAAoB;AAErC,UAAI,EAAE,kBAAkB,QAAQ,MAAM,SAAS,EAAE,MAAM,EAAG;AAC1D,cAAQ;AAAA,IACV;AACA,QAAI,iBAAiB,aAAa,WAAW,IAAI;AACjD,eAAW,UAAU,MAAM,IAAI,oBAAoB,aAAa,WAAW,IAAI;AAAA,EACjF;AAEA,QAAM,SAAS,MAAM;AACnB,QAAI,CAAC,QAAQ,GAAI;AACjB,UAAM,SAAS,SAAS;AACxB,QAAI,WAAW,QAAQ,CAAC,OAAO,QAAS;AACxC,QAAI,OAAO,KAAK,WAAW,SAAS,MAAM,GAAG,MAAM,EAAG,UAAS,IAAI;AAAA,EACrE;AACA,QAAM,UAAU,MAAM;AACpB,YAAQ,UAAU;AAClB,eAAW,UAAU;AACrB,eAAW,UAAU;AACrB,WAAO;AAAA,EACT;AAEA,SACE,gBAAAJ,KAAC,eAAY,OAAO,UAAU,OAAO,SAAS,OAAO,GAClD,WAAC,UACA,CAAC,QAAQ,KACP,gBAAAA,KAAC,gBAAa,SAAS,YAAY,QAAQ,SAAS,OAAO,SAAS,QAAQ,GAAG,IAE/E,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,gBAAgB,QAAQ,MAAM;AAAA,MACrC;AAAA,MACA,WAAW,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ;AAAA,MAC1D;AAAA,MACA;AAAA,MACA;AAAA,MACA,SACE,gBAAAA;AAAA,QAAC;AAAA;AAAA,UACC,IAAI;AAAA,UACJ;AAAA,UACA,SAAS,OAAO;AAAA,UAChB,WAAW,KAAK,QAAQ,KAAK,SAAS;AAAA,UACtC,QAAQ,CAAC,MAAO,OAAO,MAAM,WAAW,SAAS,GAAG,QAAQ,MAAM,IAAI,IAAI,OAAO,QAAQ;AAAA,UACzF,UAAU,UAAU,OAAO,OAAO,QAAQ,SAAS,OAAO,UAAU,OAAO,QAAQ,UAAU;AAAA;AAAA,MAC/F;AAAA,MAEF,SAAS,CAAC,MAAM;AAEd,YAAI,CAAC,OAAO,QAAS,UAAS,aAAa,QAAQ,OAAQ,CAAC,CAAC;AAAA,MAC/D;AAAA,MACA,UAAU;AAAA,MACV,SAAS;AAAA,MACT,WAAW;AAAA,MACX,WAAW;AAAA,MACX,eAAe,MAAM;AACnB,YAAI,QAAQ,QAAS,SAAQ;AAAA,MAC/B;AAAA,MACA;AAAA;AAAA,EACF,GAGN;AAEJ;AAEA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AACT,GAgBG;AACD,QAAM,QAAQ,QAAQ;AACtB,QAAM,OAAO,QAAQ,MAAM;AAE3B,QAAM,WAAW,CAAC,OAAO;AACzB,QAAM,OAAO,OAAO;AACpB,QAAM,OAAO,cAAc,SAAY,aAAa,OAAO,SAAS,IAAI,MAAM;AAC9E,QAAM,QAAQ,SAAS;AACvB,QAAM,QAAQ,UAAU,QAAQ,cAAc;AAC9C,QAAM,UAAU,QAAQ,OAAO,KAAK,IAAI;AAGxC,QAAM,QAAQ,eAAe,QAAQ,UAAU,QAAQ,QAAQ,KAAK,KAAK,KAAK,SAAS,CAAC,IAAI,IAAI,EAAE;AAClG,QAAM,QAAQ,KAAK,MAAM,kBAAkB,GAAG,KAAK;AACnD,QAAM,WAAW,GAAG,KAAK,QAAQ,KAAK,GAAG,KAAK,SAAS;AACvD,QAAM,OAAO;AAAA,IACX,OAAO,KAAK,MAAM,GAAG,KAAK,QAAQ,IAAI;AAAA,IACtC,QAAQ,KAAK,IAAI,iBAAiB,KAAK,MAAM,GAAG,KAAK,SAAS,GAAG,CAAC;AAAA,EACpE;AACA,QAAM,OAAO,WAAW,aAAa,MAAM,OAAO,EAAE,MAAM,CAAC,IAAI,gBAAgB,OAAO,EAAE,MAAM,CAAC;AAE/F,SACE,gBAAAC;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,SAAS;AAAA,QACT,eAAe;AAAA,QACf,gBAAgB;AAAA,QAChB,KAAKI,UAAS,CAAC;AAAA,QACf,UAAU;AAAA,MACZ;AAAA,MAEA;AAAA,wBAAAJ,MAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,YAAY,UAAU,KAAKI,UAAS,CAAC,GAAG,WAAW,gBAAgB,GAChG;AAAA,0BAAAL;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,cAAY;AAAA,cACZ,kBAAgB,SAAS,OAAO,IAAI;AAAA,cACpC,oBAAkB,GAAG,MAAM,IAAI,SAAS,GAAG,OAAO,iBAAiB,IAAI,WAAW,SAAS,CAAC,KAAK,EAAE;AAAA,cACnG,iBAAe,QAAQ;AAAA,cACvB,aAAW,QAAQ;AAAA,cACnB,KAAK,MAAM;AAAA,cACX,KAAK,MAAM;AAAA,cACX,MAAM,MAAM;AAAA,cACZ,OAAO;AAAA,cACP;AAAA,cACA,eAAa,UAAU,OAAO,SAAS;AAAA,cACvC,UAAU,CAAC,MAAM,QAAQ,OAAO,EAAE,OAAO,KAAK,CAAC;AAAA,cAC/C,eAAe,CAAC,MAAM,QAAQ,EAAE,aAAa;AAAA,cAC7C,aAAa;AAAA,cACb,iBAAiB;AAAA,cACjB,sBAAsB;AAAA,cACtB,QAAQ;AAAA,cACR,WAAW,CAAC,MAAM;AAChB,oBAAI,EAAE,QAAQ,QAAS,UAAS;AAAA,yBACvB,EAAE,QAAQ,SAAU,WAAU;AAAA,cACzC;AAAA,cACA,OAAO;AAAA,gBACL,MAAM;AAAA,gBACN,UAAU;AAAA,gBACV,QAAQ;AAAA,gBACR,QAAQ;AAAA,gBACR,aAAaM,KAAI,SAAS;AAAA,gBAC1B,QAAQ,YAAY,OAAO,gBAAgB;AAAA,cAC7C;AAAA;AAAA,UACF;AAAA,UACA,gBAAAN,KAAC,UAAK,IAAI,QAAQ,QAAM,MACrB,iBAAO,QAAQ,YAClB;AAAA,UACA,gBAAAC;AAAA,YAAC;AAAA;AAAA,cACC,eAAY;AAAA,cACZ,uBAAoB;AAAA,cACpB,OAAO;AAAA,gBACL,MAAM;AAAA,gBACN,UAAU;AAAA,gBACV,YAAY;AAAA,gBACZ,YAAY;AAAA,gBACZ,YAAY;AAAA,gBACZ,oBAAoB;AAAA,gBACpB,OAAO,UAAU,OAAOK,KAAI,YAAY,IAAIA,KAAI,kBAAkB;AAAA,cACpE;AAAA,cAEC;AAAA;AAAA,gBACA,QAAQ,QACP,gBAAAN,KAAC,UAAK,OAAO,EAAE,UAAUO,SAAQ,IAAI,GAAG,YAAYF,UAAS,CAAC,GAAG,OAAOC,KAAI,kBAAkB,EAAE,GAAI,gBAAK,IACvG;AAAA;AAAA;AAAA,UACN;AAAA,WACF;AAAA,QAEA,gBAAAL,MAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,UAAU,QAAQ,YAAY,YAAY,WAAWI,UAAS,CAAC,EAAE,GAC7F;AAAA;AAAA,UACD,gBAAAL,KAAC,gBAAa,QAAgB,WAAsB;AAAA,WACtD;AAAA;AAAA;AAAA,EACF;AAEJ;;;AI/RA,SAAS,SAAAQ,QAAO,WAAAC,gBAA+B;AAE/C;AAAA,EACE,OAAAC;AAAA,EACA,mBAAAC;AAAA,EACA,gBAAAC;AAAA,EACA,mBAAAC;AAAA,EACA,aAAAC;AAAA,EACA,YAAAC;AAAA,EACA,kBAAAC;AAAA,EACA,eAAAC;AAAA,OAIK;AA8CG,gBAAAC,MA6GF,QAAAC,aA7GE;AA9BV,IAAM,UAAU;AAChB,IAAM,UAAU;AAChB,IAAM,QAAQ;AAEP,SAAS,cAAc,EAAE,QAAQ,MAAM,QAAQ,GAAoC;AACxF,QAAM,cAAcC,WAAU,OAAO,SAAS,aAAa;AAC3D,QAAM,eAAeA,WAAU,OAAO,SAAS,cAAc,KAAK;AAClE,QAAM,kBAAkBA,WAAU,OAAO,SAAS,iBAAiB,KAAK;AACxE,QAAM,YAAYA,WAAU,OAAO,SAAS,WAAW;AAEvD,QAAM,UAAUC;AAAA,IACd,MAAM,YAAY,WAAW,EAAE,aAAa,iBAAiB,UAAU,CAAC;AAAA,IACxE,CAAC,aAAa,iBAAiB,SAAS;AAAA,EAC1C;AACA,QAAM,SAAS;AAAA,IACb;AAAA,IACA,KAAK;AAAA,IACL;AAAA,IACA,GAAG,eAAe,EAAE,KAAK,mBAAmB,EAAE,KAAK,aAAa,EAAE;AAAA,EACpE;AACA,QAAM,OACJ,QAAQ,MAAM,cACV,SAAS,WAAW,KAAK,UAAU,aAAa,QAAQ,MAAM,MAAM,OAAO,UAAU,IACrF;AACN,QAAM,YAAYC,OAAM;AAExB,SACE,gBAAAJ,KAACK,cAAA,EAAY,OAAOH,WAAU,OAAO,SAAS,OAAO,GAClD,WAAC,UACA,CAAC,QAAQ,KACP,gBAAAF,KAAC,gBAAa,SAAS,YAAY,QAAQ,SAAS,OAAO,SAAS,SAAS,GAAG,IAEhF,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,gBAAgB,QAAQ,MAAM;AAAA,MACrC,OAAO,SAAS,IAAI;AAAA,MACpB;AAAA,MACA;AAAA,MACA,SACE,gBAAAA;AAAA,QAAC;AAAA;AAAA,UACC,IAAI;AAAA,UACJ;AAAA,UACA,SAAS,OAAO;AAAA,UAChB,WAAW,KAAK,QAAQ,KAAK,SAAS;AAAA,UACtC,QAAQ,CAAC,MAAO,OAAO,MAAM,YAAa,IAAI,OAAO,QAAQ,KAAK,OAAO,QAAQ,MAAO,OAAO,QAAQ;AAAA,UACvG,UAAU,OAAO,UAAU,OAAO,QAAQ,UAAU;AAAA;AAAA,MACtD;AAAA,MAEF;AAAA;AAAA,EACF,GAGN;AAEJ;AAIO,SAAS,SAAS,MAA4C;AACnE,MAAI,CAAC,QAAQ,OAAO,KAAK,UAAU,UAAW,QAAO;AACrD,QAAM,QAAQ,cAAc,KAAK,MAAM;AACvC,SAAO,UAAU,aAAa,UAAU,cAAc,KAAK,QAAQ;AACrE;AAEA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AACT,GAQG;AACD,QAAM,UAAU,UAAU;AAC1B,QAAM,UAAU,UAAU;AAC1B,QAAM,WAAW,CAAC,OAAO;AACzB,QAAM,OAAO,UAAU,OAAO,QAAQ,KAAK,OAAO,QAAQ;AAE1D,QAAM,QAAQM,gBAAe,KAAK,MAAM;AACxC,QAAM,QAAQ,KAAK,MAAMC,mBAAkB,GAAG,KAAK;AACnD,QAAM,WAAW,GAAG,KAAK,QAAQ,KAAK,GAAG,KAAK,SAAS;AACvD,QAAM,OAAO;AAAA,IACX,OAAO,KAAK,MAAM,GAAG,KAAK,QAAQ,UAAU,EAAE;AAAA,IAC9C,QAAQ,KAAK,IAAI,iBAAiB,KAAK,MAAM,GAAG,KAAK,SAAS,GAAG,CAAC;AAAA,EACpE;AACA,QAAM,OAAO,WAAWC,cAAa,MAAM,OAAO,EAAE,MAAM,CAAC,IAAIC,iBAAgB,OAAO,EAAE,MAAM,CAAC;AAE/F,QAAM,YAAY,CAAC,SAAkB;AACnC,WAAO,KAAK,WAAW,SAAS,IAAI,GAAG,IAAI;AAAA,EAC7C;AAEA,SACE,gBAAAR;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,SAAS;AAAA,QACT,eAAe;AAAA,QACf,gBAAgB;AAAA,QAChB,KAAKS,UAAS,CAAC;AAAA,QACf,UAAU;AAAA,MACZ;AAAA,MAEC;AAAA,kBACC,gBAAAV,KAAC,SAAI,MAAK,SAAQ,cAAY,OAAO,oBAAkB,OAAO,iBAAiB,GAAG,SAAS,IAAI,WAAW,SAAS,CAAC,KAAK,WAAW,sBAAmB,IAAG,OAAO,EAAE,SAAS,QAAQ,KAAKU,UAAS,CAAC,EAAE,GACjM,WAAC,MAAM,KAAK,EAAY,IAAI,CAAC,MAC7B,gBAAAV;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL;AAAA,YACA,aAAW,OAAO;AAAA,YAClB,SAAS,MAAM,UAAU,CAAC;AAAA,YAC1B,OAAO;AAAA,cACL,MAAM;AAAA,cACN,WAAW;AAAA,cACX,UAAU;AAAA,cACV,SAAS,KAAKU,UAAS,CAAC,CAAC;AAAA,cACzB,cAAc;AAAA,cACd,QAAQ,aAAaC,KAAI,QAAQ,CAAC;AAAA,cAClC,YAAYA,KAAI,YAAY;AAAA,cAC5B,OAAOA,KAAI,YAAY;AAAA,cACvB,YAAY;AAAA,cACZ,QAAQ,WAAW,gBAAgB;AAAA,cACnC,SAAS,WAAW,MAAM;AAAA,YAC5B;AAAA,YAEC,cAAI,OAAO,QAAQ,KAAK,OAAO,QAAQ;AAAA;AAAA,UAnBnC,OAAO,CAAC;AAAA,QAoBf,CACD,GACH,IAEA,gBAAAV;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,gBAAc;AAAA,YACd,cAAY;AAAA,YACZ,oBAAkB,OAAO,iBAAiB,GAAG,SAAS,IAAI,WAAW,SAAS,CAAC,KAAK;AAAA,YACpF,aAAW,OAAO;AAAA,YAClB;AAAA,YACA,SAAS,MAAM,UAAU,CAAC,OAAO;AAAA,YACjC,OAAO;AAAA,cACL,SAAS;AAAA,cACT,YAAY;AAAA,cACZ,KAAKS,UAAS,CAAC;AAAA,cACf,WAAW;AAAA,cACX,UAAU;AAAA,cACV,SAAS;AAAA,cACT,QAAQ;AAAA,cACR,YAAY;AAAA,cACZ,OAAOC,KAAI,YAAY;AAAA,cACvB,QAAQ,WAAW,gBAAgB;AAAA,cACnC,SAAS,WAAW,MAAM;AAAA,cAC1B,WAAW;AAAA,YACb;AAAA,YAEA;AAAA,8BAAAX;AAAA,gBAAC;AAAA;AAAA,kBACC,eAAY;AAAA,kBACZ,OAAO;AAAA,oBACL,UAAU;AAAA,oBACV,MAAM;AAAA,oBACN,OAAO;AAAA,oBACP,QAAQ;AAAA,oBACR,cAAc,UAAU;AAAA,oBACxB,WAAW;AAAA,oBACX,QAAQ,aAAaW,KAAI,QAAQ,CAAC;AAAA,oBAClC,YAAY,UAAUA,KAAI,SAAS,IAAIA,KAAI,OAAO;AAAA,kBACpD;AAAA,kBAEA,0BAAAX;AAAA,oBAAC;AAAA;AAAA,sBACC,OAAO;AAAA,wBACL,UAAU;AAAA,wBACV,MAAM,UAAU,SAAS,IAAI;AAAA,wBAC7B,MAAM,UAAU,UAAU,QAAQ,IAAI;AAAA,wBACtC,OAAO;AAAA,wBACP,QAAQ;AAAA,wBACR,cAAc;AAAA,wBACd,YAAY,UAAUW,KAAI,oBAAoB,IAAIA,KAAI,YAAY;AAAA,wBAClE,QAAQ,aAAaA,KAAI,QAAQ,CAAC;AAAA,wBAClC,WAAW;AAAA,sBACb;AAAA;AAAA,kBACF;AAAA;AAAA,cACF;AAAA,cACA,gBAAAX;AAAA,gBAAC;AAAA;AAAA,kBACC,oBAAiB;AAAA,kBACjB,OAAO;AAAA,oBACL,UAAU;AAAA,oBACV,YAAY;AAAA,oBACZ,YAAY;AAAA,oBACZ,YAAY;AAAA,oBACZ,OAAOW,KAAI,YAAY;AAAA,kBACzB;AAAA,kBAEC;AAAA;AAAA,cACH;AAAA;AAAA;AAAA,QACF;AAAA,QAGF,gBAAAV,MAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,UAAU,QAAQ,YAAY,YAAY,WAAWS,UAAS,CAAC,EAAE,GAC7F;AAAA;AAAA,UACD,gBAAAV,KAAC,gBAAa,QAAgB,WAAsB;AAAA,WACtD;AAAA;AAAA;AAAA,EACF;AAEJ;",
|
|
6
|
+
"names": ["useEffect", "useRef", "useState", "css", "fontVar", "spaceVar", "jsx", "jsxs", "useState", "useRef", "useEffect", "spaceVar", "css", "fontVar", "useId", "useMemo", "css", "FIT_MIN_FONT_PX", "fitValueFont", "fitValueFontCss", "optString", "spaceVar", "useStickyChars", "WidgetFrame", "jsx", "jsxs", "optString", "useMemo", "useId", "WidgetFrame", "useStickyChars", "FIT_MIN_FONT_PX", "fitValueFont", "fitValueFontCss", "spaceVar", "css"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildFleet,
|
|
3
|
+
fleetStrings,
|
|
4
|
+
newestMemberTime,
|
|
5
|
+
seedWindow
|
|
6
|
+
} from "./chunk-S2OMBJLI.js";
|
|
7
|
+
import {
|
|
8
|
+
LevelEmpty
|
|
9
|
+
} from "./chunk-XQQQNTBX.js";
|
|
10
|
+
import {
|
|
11
|
+
measurementSpan
|
|
12
|
+
} from "./chunk-2D7JNKFE.js";
|
|
13
|
+
|
|
14
|
+
// src/fleet-feed.tsx
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_TIME_RANGE_SECONDS
|
|
17
|
+
} from "@devicechain/dashboards";
|
|
18
|
+
import { optString, resolveStaleAfterSeconds, useWidgetTimeRange, WidgetFrame, WidgetFreshnessProvider } from "@devicechain/widget-sdk";
|
|
19
|
+
import { useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
20
|
+
|
|
21
|
+
// src/fleet-seed.ts
|
|
22
|
+
var MINUTE_MS = 6e4;
|
|
23
|
+
var TAIL_SECONDS = 300;
|
|
24
|
+
var MAX_NARROW_READS = 2;
|
|
25
|
+
var has = (o, key) => Object.prototype.hasOwnProperty.call(o, key);
|
|
26
|
+
function foldMemberSeed(deviceToken, buckets, notBefore) {
|
|
27
|
+
const floor = notBefore === void 0 ? Number.NaN : Date.parse(notBefore);
|
|
28
|
+
const latest = {};
|
|
29
|
+
const at = {};
|
|
30
|
+
const observed = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const b of buckets) {
|
|
32
|
+
if (b.count === void 0 ? b.avg != null : b.count > 0) observed.add(b.name);
|
|
33
|
+
if (b.avg == null) continue;
|
|
34
|
+
const start = Date.parse(b.bucketStart);
|
|
35
|
+
if (has(at, b.name) && at[b.name] >= start) continue;
|
|
36
|
+
at[b.name] = start;
|
|
37
|
+
latest[b.name] = {
|
|
38
|
+
id: `seed-${deviceToken}-${b.name}-${b.bucketStart}`,
|
|
39
|
+
deviceToken,
|
|
40
|
+
eventType: 0,
|
|
41
|
+
occurredTime: start < floor ? new Date(floor).toISOString() : b.bucketStart,
|
|
42
|
+
name: b.name,
|
|
43
|
+
value: b.avg,
|
|
44
|
+
classifier: null
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { latest, observed: [...observed] };
|
|
48
|
+
}
|
|
49
|
+
function mergeSeeds(newer, older) {
|
|
50
|
+
return {
|
|
51
|
+
latest: { ...older.latest, ...newer.latest },
|
|
52
|
+
observed: [.../* @__PURE__ */ new Set([...newer.observed, ...older.observed])]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function tailWindow(window) {
|
|
56
|
+
const start = Math.floor((Date.parse(window.endTime) + 1) / MINUTE_MS) * MINUTE_MS - TAIL_SECONDS * 1e3;
|
|
57
|
+
if (!(start > Date.parse(window.startTime))) return void 0;
|
|
58
|
+
return { startTime: new Date(start).toISOString(), endTime: window.endTime, intervalSeconds: 60 };
|
|
59
|
+
}
|
|
60
|
+
function memberSeedPlan(window, names) {
|
|
61
|
+
const wanted = [...new Set(names)];
|
|
62
|
+
return {
|
|
63
|
+
async seed(deviceToken, read) {
|
|
64
|
+
const fold = (c) => read(c).then((b) => foldMemberSeed(deviceToken, b, c.startTime));
|
|
65
|
+
const tail = tailWindow(window);
|
|
66
|
+
let seed;
|
|
67
|
+
let recent;
|
|
68
|
+
if (wanted.length === 0 || !tail) seed = await fold(window);
|
|
69
|
+
else {
|
|
70
|
+
recent = await fold(tail);
|
|
71
|
+
const missing = wanted.filter((name) => !has(recent.latest, name));
|
|
72
|
+
if (recent.observed.length === 0 || missing.length > MAX_NARROW_READS) seed = mergeSeeds(recent, await fold(window));
|
|
73
|
+
else {
|
|
74
|
+
seed = recent;
|
|
75
|
+
for (const name of missing) seed = mergeSeeds(seed, await fold({ ...window, name }));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return window.intervalSeconds > 60 ? sharpen(seed, recent, window, fold) : seed;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function sharpen(seed, recent, window, fold) {
|
|
83
|
+
const byStamp = /* @__PURE__ */ new Map();
|
|
84
|
+
for (const [name, sample] of Object.entries(seed.latest)) {
|
|
85
|
+
if (recent && has(recent.latest, name)) continue;
|
|
86
|
+
const at = Date.parse(sample.occurredTime ?? "");
|
|
87
|
+
byStamp.set(at, [...byStamp.get(at) ?? [], name]);
|
|
88
|
+
}
|
|
89
|
+
const latest = { ...seed.latest };
|
|
90
|
+
const unread = [];
|
|
91
|
+
const end = Date.parse(window.endTime) + 1;
|
|
92
|
+
const stamps = [...byStamp.keys()].sort((a, b) => b - a);
|
|
93
|
+
for (const [i, at] of stamps.entries()) {
|
|
94
|
+
const names = byStamp.get(at);
|
|
95
|
+
const minutes = i < MAX_NARROW_READS ? (await fold({
|
|
96
|
+
startTime: new Date(at).toISOString(),
|
|
97
|
+
endTime: new Date(Math.min(at + window.intervalSeconds * 1e3, end) - 1).toISOString(),
|
|
98
|
+
intervalSeconds: 60,
|
|
99
|
+
...names.length === 1 ? { name: names[0] } : {}
|
|
100
|
+
})).latest : {};
|
|
101
|
+
for (const name of names) {
|
|
102
|
+
if (has(minutes, name)) latest[name] = minutes[name];
|
|
103
|
+
else {
|
|
104
|
+
delete latest[name];
|
|
105
|
+
unread.push(name);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { latest, observed: seed.observed, unread };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/fleet-feed.tsx
|
|
113
|
+
import { jsx } from "react/jsx-runtime";
|
|
114
|
+
var FLUSH_MS = 250;
|
|
115
|
+
var AGE_OUT_MS = 3e4;
|
|
116
|
+
var emptyStore = (key) => ({ key, roster: null, live: /* @__PURE__ */ new Map(), seeds: /* @__PURE__ */ new Map(), forbidden: false, error: null });
|
|
117
|
+
function record(into, sample) {
|
|
118
|
+
let byName = into.get(sample.deviceToken);
|
|
119
|
+
if (!byName) into.set(sample.deviceToken, byName = /* @__PURE__ */ new Map());
|
|
120
|
+
const held = byName.get(sample.name);
|
|
121
|
+
if (held?.occurredTime && sample.occurredTime && Date.parse(sample.occurredTime) < Date.parse(held.occurredTime)) return;
|
|
122
|
+
byName.set(sample.name, sample);
|
|
123
|
+
}
|
|
124
|
+
function GroupRefusal({ widget, descriptor }) {
|
|
125
|
+
const strings = fleetStrings();
|
|
126
|
+
const perDevice = descriptor.binding.members !== void 0;
|
|
127
|
+
return /* @__PURE__ */ jsx(WidgetFrame, { title: optString(widget.options, "title"), children: /* @__PURE__ */ jsx("div", { "data-group-refused": "", style: { display: "contents" }, children: /* @__PURE__ */ jsx(LevelEmpty, { message: `${strings.oneDevice} ${perDevice ? strings.usePerDevice : strings.useFleetTable}` }) }) });
|
|
128
|
+
}
|
|
129
|
+
function shownMeasurements(widget) {
|
|
130
|
+
const one = widget.options?.measurement;
|
|
131
|
+
const named = [...widget.datasource?.measurements ?? [], ...typeof one === "string" && one !== "" ? [one] : []];
|
|
132
|
+
return [...new Set(named)];
|
|
133
|
+
}
|
|
134
|
+
function FleetEntry(props) {
|
|
135
|
+
return props.refuse ? /* @__PURE__ */ jsx(GroupRefusal, { widget: props.widget, descriptor: props.descriptor }) : /* @__PURE__ */ jsx(FleetFeed, { ...props });
|
|
136
|
+
}
|
|
137
|
+
function FleetFeed({ hub, widget, descriptor, Component, actions, ErrorFrame }) {
|
|
138
|
+
const range = useWidgetTimeRange();
|
|
139
|
+
const now = useMemo(() => hub.now ? () => hub.now() : Date.now, [hub]);
|
|
140
|
+
const spanSeconds = measurementSpan(descriptor, widget, range?.spanSeconds) ?? range?.spanSeconds ?? DEFAULT_TIME_RANGE_SECONDS;
|
|
141
|
+
const pausedAt = range?.paused ? range.pausedAt : void 0;
|
|
142
|
+
const names = shownMeasurements(widget);
|
|
143
|
+
const ds = widget.datasource && { ...widget.datasource, measurements: names };
|
|
144
|
+
const dsKey = JSON.stringify(ds ?? null);
|
|
145
|
+
const namesKey = JSON.stringify(names);
|
|
146
|
+
const supported = hub.subscribeMembers !== void 0 && hub.readMemberSeeds !== void 0;
|
|
147
|
+
const store = useRef(emptyStore(dsKey));
|
|
148
|
+
const queued = useRef(/* @__PURE__ */ new Map());
|
|
149
|
+
const pausedRef = useRef(pausedAt !== void 0);
|
|
150
|
+
const [version, bump] = useReducer((n) => n + 1, 0);
|
|
151
|
+
const [rosterKey, setRosterKey] = useState(null);
|
|
152
|
+
const flushTimer = useRef(void 0);
|
|
153
|
+
const schedule = () => {
|
|
154
|
+
if (flushTimer.current !== void 0) return;
|
|
155
|
+
flushTimer.current = setTimeout(() => {
|
|
156
|
+
flushTimer.current = void 0;
|
|
157
|
+
bump();
|
|
158
|
+
}, FLUSH_MS);
|
|
159
|
+
};
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
const s = emptyStore(dsKey);
|
|
162
|
+
store.current = s;
|
|
163
|
+
queued.current = /* @__PURE__ */ new Map();
|
|
164
|
+
setRosterKey(null);
|
|
165
|
+
bump();
|
|
166
|
+
if (!ds || !supported) return;
|
|
167
|
+
const dispose = hub.subscribeMembers(ds, {
|
|
168
|
+
roster: (roster) => {
|
|
169
|
+
s.roster = roster;
|
|
170
|
+
setRosterKey(roster.members.map((m) => m.token).join("\n"));
|
|
171
|
+
schedule();
|
|
172
|
+
},
|
|
173
|
+
next: (sample) => {
|
|
174
|
+
record(pausedRef.current ? queued.current : s.live, sample);
|
|
175
|
+
if (!pausedRef.current) schedule();
|
|
176
|
+
},
|
|
177
|
+
forbidden: () => {
|
|
178
|
+
s.forbidden = true;
|
|
179
|
+
schedule();
|
|
180
|
+
},
|
|
181
|
+
error: (err) => {
|
|
182
|
+
s.error = err;
|
|
183
|
+
schedule();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
return () => {
|
|
187
|
+
dispose();
|
|
188
|
+
if (flushTimer.current !== void 0) clearTimeout(flushTimer.current);
|
|
189
|
+
flushTimer.current = void 0;
|
|
190
|
+
};
|
|
191
|
+
}, [hub, dsKey, supported]);
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const was = pausedRef.current;
|
|
194
|
+
pausedRef.current = pausedAt !== void 0;
|
|
195
|
+
if (was && pausedAt === void 0) {
|
|
196
|
+
for (const byName of queued.current.values()) for (const sample of byName.values()) record(store.current.live, sample);
|
|
197
|
+
queued.current = /* @__PURE__ */ new Map();
|
|
198
|
+
schedule();
|
|
199
|
+
}
|
|
200
|
+
}, [pausedAt]);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const s = store.current;
|
|
203
|
+
if (!supported || !s.roster || rosterKey === null) return;
|
|
204
|
+
const tokens = s.roster.members.map((m) => m.token);
|
|
205
|
+
if (tokens.length === 0) return;
|
|
206
|
+
let cancelled = false;
|
|
207
|
+
void hub.readMemberSeeds(
|
|
208
|
+
tokens,
|
|
209
|
+
memberSeedPlan(seedWindow(spanSeconds, pausedAt ?? now()), names),
|
|
210
|
+
(token, seed, failure) => {
|
|
211
|
+
if (failure === "forbidden") s.forbidden = true;
|
|
212
|
+
else s.seeds.set(token, seed ?? "failed");
|
|
213
|
+
schedule();
|
|
214
|
+
},
|
|
215
|
+
() => cancelled
|
|
216
|
+
);
|
|
217
|
+
return () => {
|
|
218
|
+
cancelled = true;
|
|
219
|
+
};
|
|
220
|
+
}, [hub, supported, dsKey, rosterKey, spanSeconds, pausedAt, namesKey]);
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (pausedAt !== void 0) return;
|
|
223
|
+
const id = setInterval(bump, AGE_OUT_MS);
|
|
224
|
+
return () => clearInterval(id);
|
|
225
|
+
}, [pausedAt]);
|
|
226
|
+
const members = useMemo(() => {
|
|
227
|
+
const s = store.current.key === dsKey ? store.current : emptyStore(dsKey);
|
|
228
|
+
if (!supported) return { status: "unsupported", members: [], total: 0, truncated: false };
|
|
229
|
+
return buildFleet({
|
|
230
|
+
roster: s.roster,
|
|
231
|
+
seeds: s.seeds,
|
|
232
|
+
live: s.live,
|
|
233
|
+
since: (pausedAt ?? now()) - spanSeconds * 1e3,
|
|
234
|
+
forbidden: s.forbidden
|
|
235
|
+
});
|
|
236
|
+
}, [version, supported, dsKey, spanSeconds, pausedAt, now]);
|
|
237
|
+
const error = store.current.key === dsKey ? store.current.error : null;
|
|
238
|
+
const data = { latest: {}, samples: [], error, members };
|
|
239
|
+
const freshness = useMemo(
|
|
240
|
+
() => descriptor.freshness === true ? { at: newestMemberTime(members), staleAfterSeconds: resolveStaleAfterSeconds(widget.options) } : void 0,
|
|
241
|
+
[descriptor, widget.options, members]
|
|
242
|
+
);
|
|
243
|
+
if (error) return /* @__PURE__ */ jsx(ErrorFrame, { widget, error });
|
|
244
|
+
return /* @__PURE__ */ jsx(WidgetFreshnessProvider, { freshness, children: /* @__PURE__ */ jsx(Component, { widget, data, actions }) });
|
|
245
|
+
}
|
|
246
|
+
export {
|
|
247
|
+
FLUSH_MS,
|
|
248
|
+
GroupRefusal,
|
|
249
|
+
FleetEntry as default,
|
|
250
|
+
shownMeasurements
|
|
251
|
+
};
|
|
252
|
+
//# sourceMappingURL=fleet-feed-FK4ZMKIK.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fleet-feed.tsx", "../../src/fleet-seed.ts"],
|
|
4
|
+
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The data feed of a fleet widget (one row or one bar per member of an anchor): the roster, each\n// member's own live readings, and one seed read per member for what it last reported before the\n// board was opened. Loaded on first use (connected-widget.tsx imports it dynamically), so a board\n// with no fleet widget never downloads it.\n//\n// - ONE subscription per widget (hub.subscribeMembers). The samples it delivers carry their\n// device, and everything here is keyed by device and measurement together -- the measurement\n// channel's name-only `latest` is exactly what this exists to get away from;\n// - the seed is read per member, bounded hub-wide and shared between widgets that ask the same\n// question; what is read is fleet-seed.ts's plan (the last few minutes first, the span only for\n// what that left empty). The window is the widget's pinned span, else the board's, ending at the\n// freeze instant while paused;\n// - paused, live readings queue (newest per device and measurement) and merge on resume, so the\n// table on screen does not move under a \"Paused\" badge;\n// - a change of span re-reads the seeds; a reading older than the span is no longer shown.\n//\n// Rebuilding the whole state per sample would re-render a 500-row table hundreds of times a\n// second, so changes are folded into a store and flushed at most once per FLUSH_MS.\n\nimport {\n DEFAULT_TIME_RANGE_SECONDS,\n type FleetState,\n type MeasurementSample,\n type MeasurementStreamState,\n type MemberRoster,\n type MemberSeed,\n type WidgetActions,\n type WidgetDataSource,\n type WidgetDescriptor,\n type WidgetInstance,\n} from '@devicechain/dashboards';\nimport { optString, resolveStaleAfterSeconds, useWidgetTimeRange, WidgetFrame, WidgetFreshnessProvider } from '@devicechain/widget-sdk';\nimport { useEffect, useMemo, useReducer, useRef, useState, type ComponentType } from 'react';\n\nimport { measurementSpan } from './binding-policy';\nimport { buildFleet, newestMemberTime, seedWindow } from './fleet-model';\nimport { memberSeedPlan } from './fleet-seed';\nimport { fleetStrings } from './fleet-strings';\nimport { LevelEmpty } from './level-readout';\n\n// The most often the state is rebuilt from live samples.\nexport const FLUSH_MS = 250;\n// How often a reading that has aged out of the span is dropped when nothing else changes.\nconst AGE_OUT_MS = 30_000;\n\ninterface Store {\n // The selector this store was filled for: a render that carries a new selector must not show the\n // previous fleet's rows under it while the effect that resets the store has yet to run.\n key: string | null;\n roster: MemberRoster | null;\n live: Map<string, Map<string, MeasurementSample>>;\n seeds: Map<string, MemberSeed | 'failed'>;\n forbidden: boolean;\n error: unknown | null;\n}\n\nconst emptyStore = (key: string | null): Store => ({ key, roster: null, live: new Map(), seeds: new Map(), forbidden: false, error: null });\n\nfunction record(into: Map<string, Map<string, MeasurementSample>>, sample: MeasurementSample): void {\n let byName = into.get(sample.deviceToken);\n if (!byName) into.set(sample.deviceToken, (byName = new Map()));\n // Newest wins; an out-of-order older sample must not roll a cell back.\n const held = byName.get(sample.name);\n if (held?.occurredTime && sample.occurredTime && Date.parse(sample.occurredTime) < Date.parse(held.occurredTime)) return;\n byName.set(sample.name, sample);\n}\n\nexport interface FleetFeedProps {\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 ErrorFrame: ComponentType<{ widget: WidgetInstance; error: unknown }>;\n // Not a fleet widget, but bound to a group: say so instead (GroupRefusal).\n refuse?: boolean;\n}\n\n// What a widget that shows ONE device's readings keyed by measurement name renders when it is bound\n// to a group: every device reporting \"temperature\" would overwrite the others in it, so it shows a\n// sentence instead of whichever device spoke last. A widget with a per-device mode (the bar chart's\n// one bar per device) is pointed at that mode; any other at the fleet table.\nexport function GroupRefusal({ widget, descriptor }: { widget: WidgetInstance; descriptor: WidgetDescriptor }) {\n const strings = fleetStrings();\n const perDevice = (descriptor.binding as { members?: unknown }).members !== undefined;\n return (\n <WidgetFrame title={optString(widget.options, 'title')}>\n <div data-group-refused=\"\" style={{ display: 'contents' }}>\n <LevelEmpty message={`${strings.oneDevice} ${perDevice ? strings.usePerDevice : strings.useFleetTable}`} />\n </div>\n </WidgetFrame>\n );\n}\n\n// The measurements a fleet widget shows, and so the ones its seed must find AND its live feed must\n// carry: the datasource's, and the `measurement` option a per-device bar chart compares. Empty: every\n// measurement reported. One list for both, because the hub's live feed delivers only the names its\n// selector lists: a bar chart comparing a measurement its datasource does not name would otherwise\n// be seeded with it and then never hear of it again.\nexport function shownMeasurements(widget: WidgetInstance): string[] {\n const one = widget.options?.measurement;\n const named = [...(widget.datasource?.measurements ?? []), ...(typeof one === 'string' && one !== '' ? [one] : [])];\n return [...new Set(named)];\n}\n\nexport default function FleetEntry(props: FleetFeedProps) {\n return props.refuse ? <GroupRefusal widget={props.widget} descriptor={props.descriptor} /> : <FleetFeed {...props} />;\n}\n\nfunction FleetFeed({ hub, widget, descriptor, Component, actions, ErrorFrame }: FleetFeedProps) {\n const range = useWidgetTimeRange();\n const now = useMemo(() => (hub.now ? () => hub.now!() : Date.now), [hub]);\n const spanSeconds = measurementSpan(descriptor, widget, range?.spanSeconds) ?? range?.spanSeconds ?? DEFAULT_TIME_RANGE_SECONDS;\n const pausedAt = range?.paused ? range.pausedAt : undefined;\n const names = shownMeasurements(widget);\n const ds = widget.datasource && { ...widget.datasource, measurements: names };\n const dsKey = JSON.stringify(ds ?? null);\n const namesKey = JSON.stringify(names);\n const supported = hub.subscribeMembers !== undefined && hub.readMemberSeeds !== undefined;\n\n const store = useRef<Store>(emptyStore(dsKey));\n // Live samples that arrive while paused, newest per device and measurement.\n const queued = useRef<Map<string, Map<string, MeasurementSample>>>(new Map());\n const pausedRef = useRef(pausedAt !== undefined);\n const [version, bump] = useReducer((n: number) => n + 1, 0);\n const [rosterKey, setRosterKey] = useState<string | null>(null);\n const flushTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n const schedule = (): void => {\n if (flushTimer.current !== undefined) return;\n flushTimer.current = setTimeout(() => {\n flushTimer.current = undefined;\n bump();\n }, FLUSH_MS);\n };\n\n // The subscription: the roster, then the live samples. A new selector starts from nothing.\n useEffect(() => {\n const s = emptyStore(dsKey);\n store.current = s;\n queued.current = new Map();\n setRosterKey(null);\n bump();\n if (!ds || !supported) return;\n const dispose = hub.subscribeMembers!(ds, {\n roster: (roster) => {\n s.roster = roster;\n setRosterKey(roster.members.map((m) => m.token).join('\\n'));\n schedule();\n },\n next: (sample) => {\n record(pausedRef.current ? queued.current : s.live, sample);\n if (!pausedRef.current) schedule();\n },\n forbidden: () => {\n s.forbidden = true;\n schedule();\n },\n error: (err) => {\n s.error = err;\n schedule();\n },\n });\n return () => {\n dispose();\n if (flushTimer.current !== undefined) clearTimeout(flushTimer.current);\n flushTimer.current = undefined;\n };\n // `ds` is read through `dsKey` (value identity), not by reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, dsKey, supported]);\n\n // The pause gate: resuming merges what queued while frozen.\n useEffect(() => {\n const was = pausedRef.current;\n pausedRef.current = pausedAt !== undefined;\n if (was && pausedAt === undefined) {\n for (const byName of queued.current.values()) for (const sample of byName.values()) record(store.current.live, sample);\n queued.current = new Map();\n schedule();\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [pausedAt]);\n\n // The seeds: one read per member for the window, again when the span or the freeze instant changes.\n useEffect(() => {\n const s = store.current;\n if (!supported || !s.roster || rosterKey === null) return;\n const tokens = s.roster.members.map((m) => m.token);\n if (tokens.length === 0) return;\n let cancelled = false;\n void hub.readMemberSeeds!(\n tokens,\n memberSeedPlan(seedWindow(spanSeconds, pausedAt ?? now()), names),\n (token, seed, failure) => {\n if (failure === 'forbidden') s.forbidden = true;\n else s.seeds.set(token, seed ?? 'failed');\n schedule();\n },\n () => cancelled,\n );\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, supported, dsKey, rosterKey, spanSeconds, pausedAt, namesKey]);\n\n // A reading ages out of the span with no event to say so.\n useEffect(() => {\n if (pausedAt !== undefined) return;\n const id = setInterval(bump, AGE_OUT_MS);\n return () => clearInterval(id);\n }, [pausedAt]);\n\n const members = useMemo<FleetState>(() => {\n const s = store.current.key === dsKey ? store.current : emptyStore(dsKey);\n if (!supported) return { status: 'unsupported', members: [], total: 0, truncated: false };\n return buildFleet({\n roster: s.roster,\n seeds: s.seeds,\n live: s.live,\n since: (pausedAt ?? now()) - spanSeconds * 1000,\n forbidden: s.forbidden,\n });\n // `version` is the store's change counter; the store itself is mutable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [version, supported, dsKey, spanSeconds, pausedAt, now]);\n\n const error = store.current.key === dsKey ? store.current.error : null;\n const data: MeasurementStreamState = { latest: {}, samples: [], error, members };\n\n // Opt-in through the descriptor, as for any measurement widget. The frame's age is the newest\n // reading anywhere in the fleet; a widget that shows ages per row takes them from the members.\n const freshness = useMemo(\n () =>\n descriptor.freshness === true\n ? { at: newestMemberTime(members), staleAfterSeconds: resolveStaleAfterSeconds(widget.options) }\n : undefined,\n [descriptor, widget.options, members],\n );\n if (error) return <ErrorFrame widget={widget} error={error} />;\n return (\n <WidgetFreshnessProvider freshness={freshness}>\n <Component widget={widget} data={data} actions={actions} />\n </WidgetFreshnessProvider>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// What a fleet widget reads to fill its cells before the live feed has, and how it makes a seed of\n// the answer. Loaded with the fleet feed (a lazy chunk), never by a board without a fleet widget.\n//\n// A fleet of 500 is 500 devices to ask, so the cost per device is what matters. The obvious read --\n// every measurement of the device over the whole span in the span's buckets -- returns up to 240\n// buckets per measurement per device (a megabyte or more per device on a long span, tens of\n// megabytes per viewer) to find ONE number per cell. The plan instead asks the question the cell\n// asks, cheapest first:\n//\n// 1. THE LAST FEW MINUTES (TAIL_SECONDS, in one-minute buckets): at most five rows per measurement,\n// read from the store's one-minute summaries. A device that is reporting -- the ordinary case,\n// and the expensive one, because a busy device is the one with the most history -- is done here,\n// with its newest minute.\n// 2. Only for what that left empty, the whole span. One read PER missing measurement, narrowed to\n// that name (the store aggregates one name or all), when the device did report in the tail --\n// which is also what tells \"does not report this\" from \"sent nothing\", so the narrow read loses\n// nothing. Otherwise (a quiet device, or more missing names than are worth a read each) one read\n// of every measurement over the span, as before.\n//\n// A table that names no columns gets the span read alone: its columns ARE the names its devices\n// reported in the span, which only a read of the whole span can list.\n//\n// 3. A value a span read found is the AVERAGE of a span bucket (42 minutes wide on a week), not the\n// device's latest reading: a machine that read 14, was refuelled to 40 and then parked would\n// show 27. So the newest span bucket is read again in one-minute buckets -- at most a bucket's\n// worth of rows -- and its newest minute is what the cell shows, as for a device seen in the\n// tail. Names whose newest buckets coincide share one such read, and a device gets at most\n// MAX_NARROW_READS of them, newest bucket first. A name left past that bound is not shown\n// rather than shown as an average (an average is never the latest reading), and is marked\n// `unread`: the device has data for it, so its cell says \"not loaded\", never \"no data\".\n//\n// Ages. The store returns a bucket's start, not the time of its newest reading, so a seeded reading is\n// stamped at the start of its bucket (or of the span, when the bucket began before it). The reading\n// happened at or after that instant, so its age is OVERSTATED, never understated: by less than a\n// minute for a device seen in the tail, by up to one span bucket (bucketSecondsFor) otherwise. The\n// first live reading replaces the stamp with the real time.\n\nimport type {\n HistoryWindow,\n MeasurementBucket,\n MeasurementSample,\n MemberBucketCriteria,\n MemberSeed,\n MemberSeedPlan,\n} from '@devicechain/dashboards';\n\nconst MINUTE_MS = 60_000;\n\n// The tail read first: long enough to catch a device that reports every minute or two, short enough\n// that it is a handful of rows per measurement.\nexport const TAIL_SECONDS = 300;\n\n// The most narrowed (one-name) span reads one device gets before a single read of every name is the\n// cheaper question.\nexport const MAX_NARROW_READS = 2;\n\nconst has = (o: object, key: string): boolean => Object.prototype.hasOwnProperty.call(o, key);\n\n// foldMemberSeed turns one device's buckets into its seed: the newest bucket with a mean wins per\n// name, stamped at max(bucket start, `notBefore`) (see \"Ages\" above). A name whose buckets hold\n// readings is observed even when none has a mean.\nexport function foldMemberSeed(deviceToken: string, buckets: readonly MeasurementBucket[], notBefore?: string): MemberSeed {\n const floor = notBefore === undefined ? Number.NaN : Date.parse(notBefore);\n const latest: Record<string, MeasurementSample> = {};\n const at: Record<string, number> = {};\n const observed = new Set<string>();\n for (const b of buckets) {\n if (b.count === undefined ? b.avg != null : b.count > 0) observed.add(b.name);\n if (b.avg == null) continue;\n const start = Date.parse(b.bucketStart);\n if (has(at, b.name) && at[b.name] >= start) continue;\n at[b.name] = start;\n latest[b.name] = {\n id: `seed-${deviceToken}-${b.name}-${b.bucketStart}`,\n deviceToken,\n eventType: 0,\n occurredTime: start < floor ? new Date(floor).toISOString() : b.bucketStart,\n name: b.name,\n value: b.avg,\n classifier: null,\n };\n }\n return { latest, observed: [...observed] };\n}\n\n// Two seeds of one device: `newer` (read over a later stretch) wins per name; observed is the union.\nexport function mergeSeeds(newer: MemberSeed, older: MemberSeed): MemberSeed {\n return {\n latest: { ...older.latest, ...newer.latest },\n observed: [...new Set([...newer.observed, ...older.observed])],\n };\n}\n\n// The tail of a window: its last TAIL_SECONDS, on whole minutes, in one-minute buckets. Undefined\n// when the window is no longer than that (the window itself is then the cheap read).\nexport function tailWindow(window: HistoryWindow): HistoryWindow | undefined {\n const start = Math.floor((Date.parse(window.endTime) + 1) / MINUTE_MS) * MINUTE_MS - TAIL_SECONDS * 1000;\n if (!(start > Date.parse(window.startTime))) return undefined;\n return { startTime: new Date(start).toISOString(), endTime: window.endTime, intervalSeconds: 60 };\n}\n\n// The plan a fleet widget hands the hub: the window is the widget's span, `names` the measurements\n// it shows (empty: every measurement its devices report).\nexport function memberSeedPlan(window: HistoryWindow, names: readonly string[]): MemberSeedPlan {\n const wanted = [...new Set(names)];\n return {\n async seed(deviceToken, read) {\n const fold = (c: MemberBucketCriteria) => read(c).then((b) => foldMemberSeed(deviceToken, b, c.startTime));\n const tail = tailWindow(window);\n let seed: MemberSeed;\n let recent: MemberSeed | undefined;\n if (wanted.length === 0 || !tail) seed = await fold(window);\n else {\n recent = await fold(tail);\n const missing = wanted.filter((name) => !has(recent!.latest, name));\n // Nothing missing falls through the loop below with no read: the tail was the whole answer.\n if (recent.observed.length === 0 || missing.length > MAX_NARROW_READS) seed = mergeSeeds(recent, await fold(window));\n else {\n seed = recent;\n for (const name of missing) seed = mergeSeeds(seed, await fold({ ...window, name }));\n }\n }\n return window.intervalSeconds > 60 ? sharpen(seed, recent, window, fold) : seed;\n },\n };\n}\n\n// Step 3 above: replaces each value a span read found (any not from the tail's minutes) with the\n// newest minute of its span bucket. A span value is stamped at its bucket's start (or the window's),\n// so the bucket's readings lie between that stamp and one bucket width after it.\nasync function sharpen(\n seed: MemberSeed,\n recent: MemberSeed | undefined,\n window: HistoryWindow,\n fold: (c: MemberBucketCriteria) => Promise<MemberSeed>,\n): Promise<MemberSeed> {\n const byStamp = new Map<number, string[]>();\n for (const [name, sample] of Object.entries(seed.latest)) {\n if (recent && has(recent.latest, name)) continue;\n const at = Date.parse(sample.occurredTime ?? '');\n byStamp.set(at, [...(byStamp.get(at) ?? []), name]);\n }\n const latest = { ...seed.latest };\n const unread: string[] = [];\n const end = Date.parse(window.endTime) + 1;\n const stamps = [...byStamp.keys()].sort((a, b) => b - a);\n for (const [i, at] of stamps.entries()) {\n const names = byStamp.get(at)!;\n const minutes =\n i < MAX_NARROW_READS\n ? (\n await fold({\n startTime: new Date(at).toISOString(),\n endTime: new Date(Math.min(at + window.intervalSeconds * 1000, end) - 1).toISOString(),\n intervalSeconds: 60,\n ...(names.length === 1 ? { name: names[0] } : {}),\n })\n ).latest\n : {};\n for (const name of names) {\n if (has(minutes, name)) latest[name] = minutes[name];\n else {\n delete latest[name];\n unread.push(name);\n }\n }\n }\n return { latest, observed: seed.observed, unread };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|