@burdenoff/microfe-bigconsole 2026.619.1 → 2026.624.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.
@@ -0,0 +1,87 @@
1
+ import { memo as e, useEffect as t, useMemo as n, useState as r } from "react";
2
+ import { jsx as i, jsxs as a } from "react/jsx-runtime";
3
+ //#region src/bigconsole/components/dashboard/LiveBoardBanner.tsx
4
+ function o(e) {
5
+ let t = null;
6
+ for (let n of e) {
7
+ let e = n.refreshInterval;
8
+ typeof e == "number" && e > 0 && (t = t === null ? e : Math.min(t, e));
9
+ }
10
+ return t;
11
+ }
12
+ function s(e) {
13
+ if (e <= 0) return "just now";
14
+ if (e < 60) return `${e}s ago`;
15
+ let t = Math.floor(e / 60), n = e % 60;
16
+ return n === 0 ? `${t}m ago` : `${t}m ${n}s ago`;
17
+ }
18
+ var c = e(function({ widgets: e, isEditMode: c = !1 }) {
19
+ let l = n(() => o(e), [e]), [u, d] = r(() => Date.now()), [f, p] = r(0);
20
+ if (t(() => {
21
+ if (c || !l) return;
22
+ d(Date.now()), p(0);
23
+ let e = setInterval(() => {
24
+ p((e) => {
25
+ let t = e + 1;
26
+ return t >= l ? (d(Date.now()), 0) : t;
27
+ });
28
+ }, 1e3);
29
+ return () => clearInterval(e);
30
+ }, [
31
+ c,
32
+ l,
33
+ e.length
34
+ ]), c || !l) return null;
35
+ let m = new Date(u).toLocaleTimeString([], {
36
+ hour: "2-digit",
37
+ minute: "2-digit",
38
+ second: "2-digit"
39
+ });
40
+ return /* @__PURE__ */ a("div", {
41
+ className: "flex items-center justify-between gap-3 px-4 py-1.5 border-b border-border-default bg-status-success-bg flex-shrink-0",
42
+ role: "status",
43
+ "aria-live": "polite",
44
+ "data-testid": "live-board-banner",
45
+ children: [/* @__PURE__ */ a("div", {
46
+ className: "flex items-center gap-2 min-w-0",
47
+ children: [
48
+ /* @__PURE__ */ a("span", {
49
+ className: "relative flex h-2.5 w-2.5 flex-shrink-0",
50
+ "aria-hidden": "true",
51
+ children: [/* @__PURE__ */ i("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-status-success-text opacity-60" }), /* @__PURE__ */ i("span", { className: "relative inline-flex rounded-full h-2.5 w-2.5 bg-status-success-text" })]
52
+ }),
53
+ /* @__PURE__ */ i("span", {
54
+ className: "text-xs font-semibold uppercase tracking-wide text-status-success-text",
55
+ children: "Live"
56
+ }),
57
+ /* @__PURE__ */ a("span", {
58
+ className: "text-xs text-text-secondary truncate",
59
+ children: [
60
+ "Auto-refreshing every ",
61
+ l,
62
+ "s — no reload needed"
63
+ ]
64
+ })
65
+ ]
66
+ }), /* @__PURE__ */ a("div", {
67
+ className: "flex items-center gap-1.5 flex-shrink-0",
68
+ children: [/* @__PURE__ */ a("span", {
69
+ className: "text-xs text-text-tertiary",
70
+ "data-testid": "live-board-asof",
71
+ children: ["As of ", m]
72
+ }), /* @__PURE__ */ a("span", {
73
+ className: "text-xs text-text-tertiary tabular-nums",
74
+ "data-testid": "live-board-ago",
75
+ children: [
76
+ "(updated ",
77
+ s(f),
78
+ ")"
79
+ ]
80
+ })]
81
+ })]
82
+ });
83
+ });
84
+ //#endregion
85
+ export { c as default };
86
+
87
+ //# sourceMappingURL=LiveBoardBanner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LiveBoardBanner.js","names":[],"sources":["../../../../src/bigconsole/components/dashboard/LiveBoardBanner.tsx"],"sourcesContent":["/**\n * LiveBoardBanner\n *\n * Wall-display \"live\" freshness indicator for auto-refreshing boards\n * (BC-PFC-07 — Live Bed Board). It surfaces, to the operator standing in front\n * of a wall display, that the board is updating itself on a fixed cadence and\n * how stale the data on screen is right now.\n *\n * WHY THIS EXISTS (and why it is polling, not a WebSocket subscription):\n * The Live Bed Board spec wants push-based GraphQL subscriptions\n * (`dataSinkDataUpdated` / `dataSinkUpdated`) with a MANDATORY 30s poll\n * fallback for transports that lack a WebSocket upgrade. In this deployment\n * the subscription transport is genuinely absent — the backend GraphQL server\n * (`@burdenoff/be-sdk` createServer) is HTTP-only with no graphql-ws transport\n * or PubSub, and the two sink subscriptions are declared in the SDL but have\n * no resolvers. So the board runs on the spec's own fallback path: each data\n * widget carries `refreshInterval` (30s on the Live Bed Board), which\n * `useWidgetData` turns into an Apollo `pollInterval`. This banner makes that\n * cadence visible and ticks an \"updated …s ago\" clock between polls.\n *\n * It is intentionally generic: it lights up for ANY board whose visible widgets\n * declare a refresh interval (not just the bed board), and stays hidden in edit\n * mode and on static boards. Semantic status tokens only — no raw colors — so\n * it themes correctly on light / dark / high-contrast wall displays.\n */\n\nimport { type FC, memo, useEffect, useMemo, useState } from 'react';\nimport type { Widget } from '../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface LiveBoardBannerProps {\n /** Widgets currently rendered on the active page */\n widgets: Widget[];\n /** Hide the banner while the dashboard is being edited */\n isEditMode?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * The board's live cadence is the SHORTEST positive refreshInterval among the\n * visible widgets (in seconds). A board with no refreshing widget is static and\n * the banner does not render.\n */\nfunction deriveRefreshSeconds(widgets: Widget[]): number | null {\n let min: number | null = null;\n for (const w of widgets) {\n const interval = (w as { refreshInterval?: number | null }).refreshInterval;\n if (typeof interval === 'number' && interval > 0) {\n min = min === null ? interval : Math.min(min, interval);\n }\n }\n return min;\n}\n\nfunction formatAgo(seconds: number): string {\n if (seconds <= 0) return 'just now';\n if (seconds < 60) return `${seconds}s ago`;\n const mins = Math.floor(seconds / 60);\n const rem = seconds % 60;\n return rem === 0 ? `${mins}m ago` : `${mins}m ${rem}s ago`;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const LiveBoardBanner: FC<LiveBoardBannerProps> = memo(function LiveBoardBanner({\n widgets,\n isEditMode = false,\n}) {\n const refreshSeconds = useMemo(() => deriveRefreshSeconds(widgets), [widgets]);\n\n // Anchor each refresh cycle. We cannot observe each widget's individual poll\n // completion from here, but every widget on the board shares the same cadence,\n // so we model the board's \"as of\" clock as a cycle anchored to mount and\n // advanced every `refreshSeconds`. This keeps the displayed freshness honest\n // (it never claims to be fresher than the poll cadence allows).\n const [lastRefreshAt, setLastRefreshAt] = useState<number>(() => Date.now());\n const [agoSeconds, setAgoSeconds] = useState(0);\n\n useEffect(() => {\n if (isEditMode || !refreshSeconds) return;\n // Reset the cycle whenever the cadence or the widget set changes.\n setLastRefreshAt(Date.now());\n setAgoSeconds(0);\n\n const tick = setInterval(() => {\n setAgoSeconds((prev) => {\n const next = prev + 1;\n if (next >= refreshSeconds) {\n // A poll cycle completed — the widgets have just re-fetched.\n setLastRefreshAt(Date.now());\n return 0;\n }\n return next;\n });\n }, 1000);\n\n return () => clearInterval(tick);\n }, [isEditMode, refreshSeconds, widgets.length]);\n\n if (isEditMode || !refreshSeconds) return null;\n\n const asOf = new Date(lastRefreshAt).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n });\n\n return (\n <div\n className=\"flex items-center justify-between gap-3 px-4 py-1.5 border-b border-border-default bg-status-success-bg flex-shrink-0\"\n role=\"status\"\n aria-live=\"polite\"\n data-testid=\"live-board-banner\"\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n <span className=\"relative flex h-2.5 w-2.5 flex-shrink-0\" aria-hidden=\"true\">\n <span className=\"animate-ping absolute inline-flex h-full w-full rounded-full bg-status-success-text opacity-60\" />\n <span className=\"relative inline-flex rounded-full h-2.5 w-2.5 bg-status-success-text\" />\n </span>\n <span className=\"text-xs font-semibold uppercase tracking-wide text-status-success-text\">Live</span>\n <span className=\"text-xs text-text-secondary truncate\">\n Auto-refreshing every {refreshSeconds}s — no reload needed\n </span>\n </div>\n <div className=\"flex items-center gap-1.5 flex-shrink-0\">\n <span className=\"text-xs text-text-tertiary\" data-testid=\"live-board-asof\">\n As of {asOf}\n </span>\n <span className=\"text-xs text-text-tertiary tabular-nums\" data-testid=\"live-board-ago\">\n (updated {formatAgo(agoSeconds)})\n </span>\n </div>\n </div>\n );\n});\n\nexport default LiveBoardBanner;\n"],"mappings":";;;AAiDA,SAAS,EAAqB,GAAkC;CAC9D,IAAI,IAAqB;AACzB,MAAK,IAAM,KAAK,GAAS;EACvB,IAAM,IAAY,EAA0C;AAC5D,EAAI,OAAO,KAAa,YAAY,IAAW,MAC7C,IAAM,MAAQ,OAAO,IAAW,KAAK,IAAI,GAAK,EAAS;;AAG3D,QAAO;;AAGT,SAAS,EAAU,GAAyB;AAC1C,KAAI,KAAW,EAAG,QAAO;AACzB,KAAI,IAAU,GAAI,QAAO,GAAG,EAAQ;CACpC,IAAM,IAAO,KAAK,MAAM,IAAU,GAAG,EAC/B,IAAM,IAAU;AACtB,QAAO,MAAQ,IAAI,GAAG,EAAK,SAAS,GAAG,EAAK,IAAI,EAAI;;AAOtD,IAAa,IAA4C,EAAK,SAAyB,EACrF,YACA,gBAAa,MACZ;CACD,IAAM,IAAiB,QAAc,EAAqB,EAAQ,EAAE,CAAC,EAAQ,CAAC,EAOxE,CAAC,GAAe,KAAoB,QAAuB,KAAK,KAAK,CAAC,EACtE,CAAC,GAAY,KAAiB,EAAS,EAAE;AAuB/C,KArBA,QAAgB;AACd,MAAI,KAAc,CAAC,EAAgB;AAGnC,EADA,EAAiB,KAAK,KAAK,CAAC,EAC5B,EAAc,EAAE;EAEhB,IAAM,IAAO,kBAAkB;AAC7B,MAAe,MAAS;IACtB,IAAM,IAAO,IAAO;AAMpB,WALI,KAAQ,KAEV,EAAiB,KAAK,KAAK,CAAC,EACrB,KAEF;KACP;KACD,IAAK;AAER,eAAa,cAAc,EAAK;IAC/B;EAAC;EAAY;EAAgB,EAAQ;EAAO,CAAC,EAE5C,KAAc,CAAC,EAAgB,QAAO;CAE1C,IAAM,IAAO,IAAI,KAAK,EAAc,CAAC,mBAAmB,EAAE,EAAE;EAC1D,MAAM;EACN,QAAQ;EACR,QAAQ;EACT,CAAC;AAEF,QACE,kBAAC,OAAD;EACE,WAAU;EACV,MAAK;EACL,aAAU;EACV,eAAY;YAJd,CAME,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,QAAD;KAAM,WAAU;KAA0C,eAAY;eAAtE,CACE,kBAAC,QAAD,EAAM,WAAU,kGAAmG,CAAA,EACnH,kBAAC,QAAD,EAAM,WAAU,wEAAyE,CAAA,CACpF;;IACP,kBAAC,QAAD;KAAM,WAAU;eAAyE;KAAW,CAAA;IACpG,kBAAC,QAAD;KAAM,WAAU;eAAhB;MAAuD;MAC9B;MAAe;MACjC;;IACH;MACN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,QAAD;IAAM,WAAU;IAA6B,eAAY;cAAzD,CAA2E,UAClE,EACF;OACP,kBAAC,QAAD;IAAM,WAAU;IAA0C,eAAY;cAAtE;KAAuF;KAC3E,EAAU,EAAW;KAAC;KAC3B;MACH;KACF;;EAER"}
@@ -6,6 +6,7 @@ import "./GlobalFiltersBar/FilterChip.js";
6
6
  import "./GlobalFiltersBar/FilterEditor.js";
7
7
  import "./GlobalFiltersBar/GlobalFiltersBar.js";
8
8
  import "./GlobalFiltersBar/index.js";
9
+ import "./LiveBoardBanner.js";
9
10
  import "./PageTabsManager/PageTabsManager.js";
10
11
  import "./PageTabsManager/index.js";
11
12
  import "./ShareDialog/ShareDialog.js";