@abstractframework/monitor-gpu 0.1.0

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 ADDED
@@ -0,0 +1,85 @@
1
+ # @abstractutils/monitor-gpu
2
+
3
+ Small, dependency-free GPU utilization widget that renders a mini histogram and polls a secured backend endpoint.
4
+
5
+ In AbstractFramework deployments, the default backend endpoint is AbstractGateway:
6
+ - `GET /api/gateway/host/metrics/gpu`
7
+ - Auth: `Authorization: Bearer <token>`
8
+
9
+ ## Install
10
+
11
+ - Workspace: add a dependency on `@abstractutils/monitor-gpu`
12
+ - npm (once published): `npm i @abstractutils/monitor-gpu`
13
+
14
+ ## Usage (Custom Element)
15
+
16
+ ```js
17
+ import { registerMonitorGpuWidget } from "@abstractutils/monitor-gpu";
18
+
19
+ registerMonitorGpuWidget(); // defines <monitor-gpu>
20
+
21
+ const el = document.createElement("monitor-gpu");
22
+ el.baseUrl = "http://localhost:8080"; // optional (defaults to same-origin)
23
+ el.token = "your-gateway-token"; // or el.getToken = async () => ...
24
+ el.tickMs = 1500;
25
+ el.historySize = 20;
26
+ el.mode = "full"; // "full" | "icon"
27
+ document.body.appendChild(el);
28
+ ```
29
+
30
+ You can also set the non-secret options via attributes:
31
+
32
+ ```html
33
+ <monitor-gpu base-url="http://localhost:8080" tick-ms="1500" history-size="20" mode="icon"></monitor-gpu>
34
+ ```
35
+
36
+ ## Usage (Imperative helper)
37
+
38
+ ```js
39
+ import { createMonitorGpuWidget } from "@abstractutils/monitor-gpu";
40
+
41
+ const widget = createMonitorGpuWidget(document.querySelector("#gpu"), {
42
+ baseUrl: "http://localhost:8080",
43
+ token: "your-gateway-token",
44
+ tickMs: 1500,
45
+ historySize: 20,
46
+ });
47
+
48
+ // later
49
+ widget.destroy();
50
+ ```
51
+
52
+ ## Backend contract (AbstractGateway)
53
+
54
+ The widget treats the GPU metrics payload as “supported” unless `supported === false` and extracts utilization via `extractUtilizationGpuPct(payload)`:
55
+
56
+ - `payload.utilization_gpu_pct` (number) **or**
57
+ - `payload.gpus[][].utilization_gpu_pct` (numbers; averaged)
58
+
59
+ Minimal examples:
60
+
61
+ ```json
62
+ { "supported": true, "utilization_gpu_pct": 23.0 }
63
+ ```
64
+
65
+ ```json
66
+ { "supported": true, "gpus": [{ "utilization_gpu_pct": 10.0 }, { "utilization_gpu_pct": 36.0 }] }
67
+ ```
68
+
69
+ If `supported=false`, the widget shows `N/A`.
70
+
71
+ ## Security notes
72
+ - Do not pass tokens in URLs.
73
+ - For cross-origin usage, ensure `ABSTRACTGATEWAY_ALLOWED_ORIGINS` includes your UI origin (and serve behind HTTPS in production).
74
+
75
+ ## Tests
76
+
77
+ ```bash
78
+ cd monitor-gpu
79
+ npm test
80
+ ```
81
+
82
+ ## Related docs
83
+
84
+ - Getting started: [`docs/getting-started.md`](../docs/getting-started.md)
85
+ - Architecture: [`docs/architecture.md`](../docs/architecture.md)
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@abstractframework/monitor-gpu",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight GPU utilization histogram widget for AbstractFramework clients (polls AbstractGateway host metrics with Bearer auth).",
5
+ "type": "module",
6
+ "author": "Laurent-Philippe Albou",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "abstractframework",
10
+ "gpu",
11
+ "monitoring",
12
+ "widget",
13
+ "histogram"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/lpalbou/AbstractUIC.git",
18
+ "directory": "monitor-gpu"
19
+ },
20
+ "homepage": "https://github.com/lpalbou/AbstractUIC#readme",
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "main": "./src/index.js",
25
+ "types": "./src/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./src/index.d.ts",
29
+ "default": "./src/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "src",
34
+ "README.md"
35
+ ],
36
+ "scripts": {
37
+ "test": "node --test"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "sideEffects": false
43
+ }
@@ -0,0 +1,107 @@
1
+ function _isAbsoluteUrl(s) {
2
+ try {
3
+ new URL(String(s));
4
+ return true;
5
+ } catch {
6
+ return false;
7
+ }
8
+ }
9
+
10
+ export function makeGpuMetricsUrl({ baseUrl, endpoint }) {
11
+ const ep = String(endpoint || "/api/gateway/host/metrics/gpu");
12
+ if (_isAbsoluteUrl(ep)) {
13
+ return ep;
14
+ }
15
+ if (baseUrl && String(baseUrl).trim()) {
16
+ return new URL(ep, String(baseUrl)).toString();
17
+ }
18
+ return ep;
19
+ }
20
+
21
+ export async function resolveBearerToken({ token, getToken }) {
22
+ if (typeof getToken === "function") {
23
+ const t = await getToken();
24
+ return t == null ? "" : String(t);
25
+ }
26
+ return token == null ? "" : String(token);
27
+ }
28
+
29
+ export function buildAuthHeaders({ token }) {
30
+ const t = String(token || "").trim();
31
+ return t ? { Authorization: `Bearer ${t}` } : {};
32
+ }
33
+
34
+ export function extractUtilizationGpuPct(payload) {
35
+ if (!payload || typeof payload !== "object") return null;
36
+
37
+ const direct = Number(payload.utilization_gpu_pct);
38
+ if (Number.isFinite(direct)) return direct;
39
+
40
+ const gpus = payload.gpus;
41
+ if (Array.isArray(gpus) && gpus.length) {
42
+ const vals = gpus
43
+ .map((g) => Number(g && g.utilization_gpu_pct))
44
+ .filter((n) => Number.isFinite(n));
45
+ if (!vals.length) return null;
46
+ const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
47
+ return avg;
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ export async function fetchHostGpuMetrics({
54
+ baseUrl,
55
+ endpoint,
56
+ token,
57
+ getToken,
58
+ signal,
59
+ fetchImpl,
60
+ } = {}) {
61
+ const url = makeGpuMetricsUrl({ baseUrl, endpoint });
62
+ const resolvedToken = await resolveBearerToken({ token, getToken });
63
+
64
+ const f = fetchImpl || globalThis.fetch;
65
+ if (typeof f !== "function") {
66
+ return {
67
+ ok: false,
68
+ status: 0,
69
+ error: "fetch_unavailable",
70
+ payload: null,
71
+ };
72
+ }
73
+
74
+ let res;
75
+ try {
76
+ res = await f(url, {
77
+ method: "GET",
78
+ headers: {
79
+ Accept: "application/json",
80
+ ...buildAuthHeaders({ token: resolvedToken }),
81
+ },
82
+ signal,
83
+ });
84
+ } catch (e) {
85
+ return {
86
+ ok: false,
87
+ status: 0,
88
+ error: "network_error",
89
+ detail: e instanceof Error ? e.message : String(e),
90
+ payload: null,
91
+ };
92
+ }
93
+
94
+ let payload = null;
95
+ try {
96
+ payload = await res.json();
97
+ } catch {
98
+ payload = null;
99
+ }
100
+
101
+ return {
102
+ ok: res.ok,
103
+ status: res.status,
104
+ error: res.ok ? null : "http_error",
105
+ payload,
106
+ };
107
+ }
@@ -0,0 +1,52 @@
1
+ export class HistoryBuffer {
2
+ constructor(maxSize) {
3
+ this._maxSize = HistoryBuffer._normalizeMaxSize(maxSize);
4
+ this._values = [];
5
+ }
6
+
7
+ get maxSize() {
8
+ return this._maxSize;
9
+ }
10
+
11
+ get size() {
12
+ return this._values.length;
13
+ }
14
+
15
+ static _normalizeMaxSize(maxSize) {
16
+ const n = Number(maxSize);
17
+ if (!Number.isFinite(n) || n <= 0) {
18
+ return 1;
19
+ }
20
+ return Math.min(1000, Math.floor(n));
21
+ }
22
+
23
+ setMaxSize(maxSize) {
24
+ const next = HistoryBuffer._normalizeMaxSize(maxSize);
25
+ this._maxSize = next;
26
+ if (this._values.length > next) {
27
+ this._values = this._values.slice(this._values.length - next);
28
+ }
29
+ }
30
+
31
+ clear() {
32
+ this._values = [];
33
+ }
34
+
35
+ push(value) {
36
+ const n = Number(value);
37
+ const v = Number.isFinite(n) ? n : null;
38
+ this._values.push(v);
39
+ if (this._values.length > this._maxSize) {
40
+ this._values.splice(0, this._values.length - this._maxSize);
41
+ }
42
+ }
43
+
44
+ values() {
45
+ return this._values.slice();
46
+ }
47
+
48
+ last() {
49
+ return this._values.length ? this._values[this._values.length - 1] : null;
50
+ }
51
+ }
52
+
package/src/index.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ export type MonitorGpuMode = "full" | "icon";
2
+
3
+ export type MonitorGpuWidgetOptions = {
4
+ tickMs?: number | string;
5
+ historySize?: number | string;
6
+ endpoint?: string;
7
+ baseUrl?: string;
8
+ mode?: MonitorGpuMode | string;
9
+ token?: string;
10
+ getToken?: () => string | Promise<string>;
11
+ };
12
+
13
+ export class MonitorGpuWidgetController {
14
+ constructor(target: HTMLElement, options?: MonitorGpuWidgetOptions);
15
+
16
+ get options(): { tickMs: number; historySize: number; endpoint: string; baseUrl: string; mode: MonitorGpuMode };
17
+ setOptions(next: Partial<MonitorGpuWidgetOptions>): void;
18
+
19
+ start(): void;
20
+ stop(): void;
21
+ destroy(): void;
22
+
23
+ push(value: number | null): void;
24
+
25
+ get token(): string | undefined;
26
+ set token(token: string | undefined);
27
+
28
+ get getToken(): (() => string | Promise<string>) | undefined;
29
+ set getToken(getToken: (() => string | Promise<string>) | undefined);
30
+ }
31
+
32
+ export function createMonitorGpuWidget(target: HTMLElement, options?: MonitorGpuWidgetOptions): MonitorGpuWidgetController;
33
+
34
+ export function registerMonitorGpuWidget(tagName?: string): void;
35
+
36
+ export interface MonitorGpuElement extends HTMLElement {
37
+ tickMs: number;
38
+ historySize: number;
39
+ endpoint: string;
40
+ baseUrl: string;
41
+ mode: MonitorGpuMode;
42
+
43
+ token: string | undefined;
44
+ getToken: (() => string | Promise<string>) | undefined;
45
+ }
46
+
47
+ declare global {
48
+ interface HTMLElementTagNameMap {
49
+ "monitor-gpu": MonitorGpuElement;
50
+ }
51
+ }
package/src/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { HistoryBuffer } from "./history_buffer.js";
2
+ export {
3
+ buildAuthHeaders,
4
+ extractUtilizationGpuPct,
5
+ fetchHostGpuMetrics,
6
+ makeGpuMetricsUrl,
7
+ resolveBearerToken,
8
+ } from "./gpu_metrics_api.js";
9
+ export { MonitorGpuWidgetController, createMonitorGpuWidget, registerMonitorGpuWidget } from "./monitor_gpu_widget.js";
10
+
@@ -0,0 +1,481 @@
1
+ import { HistoryBuffer } from "./history_buffer.js";
2
+ import { extractUtilizationGpuPct, fetchHostGpuMetrics } from "./gpu_metrics_api.js";
3
+
4
+ const DEFAULTS = Object.freeze({
5
+ tickMs: 1500,
6
+ historySize: 20,
7
+ endpoint: "/api/gateway/host/metrics/gpu",
8
+ baseUrl: "",
9
+ mode: "full", // "full" | "icon"
10
+ });
11
+
12
+ function _clamp01(n) {
13
+ if (!Number.isFinite(n)) return 0;
14
+ return Math.min(1, Math.max(0, n));
15
+ }
16
+
17
+ function _clamp(n, lo, hi) {
18
+ if (!Number.isFinite(n)) return lo;
19
+ return Math.min(hi, Math.max(lo, n));
20
+ }
21
+
22
+ function _toInt(n, fallback) {
23
+ const v = Number(n);
24
+ if (!Number.isFinite(v)) return fallback;
25
+ return Math.floor(v);
26
+ }
27
+
28
+ function _colorForPct(pct, alpha = 1) {
29
+ const t = _clamp(Number(pct) / 100, 0, 1);
30
+ // Hue ramp: green (140) -> red (0)
31
+ const hue = Math.round(140 - 140 * t);
32
+ const a = _clamp(Number(alpha), 0, 1);
33
+ if (a >= 1) return `hsl(${hue} 85% 55%)`;
34
+ return `hsl(${hue} 85% 55% / ${a})`;
35
+ }
36
+
37
+ function _normalizeOptions(opts) {
38
+ const o = { ...DEFAULTS, ...(opts || {}) };
39
+ o.tickMs = Math.max(250, _toInt(o.tickMs, DEFAULTS.tickMs));
40
+ o.historySize = Math.max(1, Math.min(200, _toInt(o.historySize, DEFAULTS.historySize)));
41
+ o.endpoint = String(o.endpoint || DEFAULTS.endpoint);
42
+ o.baseUrl = String(o.baseUrl || "");
43
+ o.mode = String(o.mode || DEFAULTS.mode).trim().toLowerCase();
44
+ if (o.mode !== "icon") o.mode = "full";
45
+ return o;
46
+ }
47
+
48
+ function _cssText() {
49
+ return `
50
+ :host{display:inline-block;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji";line-height:1}
51
+ .wrap{box-sizing:border-box;display:flex;flex-direction:column;gap:6px;padding:var(--monitor-gpu-padding,8px 10px);border:1px solid var(--monitor-gpu-border,#2a2f3a);border-radius:var(--monitor-gpu-radius,10px);background:var(--monitor-gpu-bg,#0b1020);color:var(--monitor-gpu-fg,#e7eaf0);width:var(--monitor-gpu-width,180px)}
52
+ .wrap.icon{gap:var(--monitor-gpu-gap,4px);padding:var(--monitor-gpu-padding,4px);border-radius:var(--monitor-gpu-radius,999px);width:var(--monitor-gpu-width,30px);background:linear-gradient(180deg,rgba(255,255,255,0.08),rgba(255,255,255,0.02)),var(--monitor-gpu-bg,#0b1020);box-shadow:0 0 14px var(--monitor-gpu-accent-glow, rgba(76,195,255,0.18))}
53
+ .wrap.icon .top{display:none}
54
+ .wrap.icon .bars{height:var(--monitor-gpu-bars-height,18px)}
55
+ .top{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
56
+ .label{font-size:var(--monitor-gpu-font-size,var(--font-size-sm,12px));opacity:.9}
57
+ .value{font-variant-numeric:tabular-nums;font-size:var(--monitor-gpu-font-size,var(--font-size-sm,12px));opacity:.95}
58
+ .bars{display:flex;align-items:flex-end;gap:2px;height:var(--monitor-gpu-bars-height,34px);overflow:hidden}
59
+ .bar{flex:1;min-width:0;border-radius:2px;background:var(--monitor-gpu-bar,#4cc3ff);height:2px;opacity:.9;transition:height .25s ease, background .25s ease, opacity .25s ease}
60
+ .bar.missing{background:var(--monitor-gpu-bar-missing,#62708a);opacity:.35}
61
+ .bar.error{background:var(--monitor-gpu-bar-error,#ff6b6b);opacity:.7}
62
+ .muted{opacity:.6}
63
+ `;
64
+ }
65
+
66
+ export class MonitorGpuWidgetController {
67
+ constructor(target, options = {}) {
68
+ if (!target || typeof target !== "object") {
69
+ throw new TypeError("target element is required");
70
+ }
71
+ this._target = target;
72
+ this._opts = _normalizeOptions(options);
73
+ this._buffer = new HistoryBuffer(this._opts.historySize);
74
+
75
+ this._timer = null;
76
+ this._timer_gen = 0;
77
+ this._running = false;
78
+ this._abort = null;
79
+ this._resumeTimer = null;
80
+
81
+ this._mounted = false;
82
+ this._els = null;
83
+
84
+ this._token = options.token;
85
+ this._getToken = options.getToken;
86
+
87
+ this._stoppedForAuth = false;
88
+ }
89
+
90
+ get options() {
91
+ return { ...this._opts };
92
+ }
93
+
94
+ set token(t) {
95
+ this._token = t;
96
+ const tok = String(this._token || "").trim();
97
+ if (tok && this._stoppedForAuth) {
98
+ this._stoppedForAuth = false;
99
+ this.start();
100
+ }
101
+ }
102
+
103
+ get token() {
104
+ return this._token;
105
+ }
106
+
107
+ set getToken(fn) {
108
+ this._getToken = fn;
109
+ }
110
+
111
+ get getToken() {
112
+ return this._getToken;
113
+ }
114
+
115
+ mount() {
116
+ if (this._mounted) return;
117
+ if (typeof document === "undefined") {
118
+ throw new Error("MonitorGpuWidget requires a browser DOM");
119
+ }
120
+
121
+ const root = this._target;
122
+ root.textContent = "";
123
+
124
+ const style = document.createElement("style");
125
+ style.textContent = _cssText();
126
+
127
+ const wrap = document.createElement("div");
128
+ wrap.className = this._opts.mode === "icon" ? "wrap icon" : "wrap";
129
+
130
+ const top = document.createElement("div");
131
+ top.className = "top";
132
+
133
+ const label = document.createElement("div");
134
+ label.className = "label";
135
+ label.textContent = "GPU";
136
+
137
+ const value = document.createElement("div");
138
+ value.className = "value muted";
139
+ value.textContent = "—";
140
+
141
+ top.appendChild(label);
142
+ top.appendChild(value);
143
+
144
+ const bars = document.createElement("div");
145
+ bars.className = "bars";
146
+
147
+ wrap.appendChild(top);
148
+ wrap.appendChild(bars);
149
+
150
+ root.appendChild(style);
151
+ root.appendChild(wrap);
152
+
153
+ this._els = { wrap, value, bars };
154
+ this._mounted = true;
155
+
156
+ this._rebuildBars();
157
+ this._render();
158
+ }
159
+
160
+ start() {
161
+ this.mount();
162
+ if (this._running) return;
163
+ this._running = true;
164
+ this._timer_gen += 1;
165
+ void this._poll_loop(this._timer_gen);
166
+ }
167
+
168
+ stop() {
169
+ this._running = false;
170
+ this._timer_gen += 1;
171
+ if (this._timer) {
172
+ clearTimeout(this._timer);
173
+ this._timer = null;
174
+ }
175
+ if (this._resumeTimer) {
176
+ clearTimeout(this._resumeTimer);
177
+ this._resumeTimer = null;
178
+ }
179
+ if (this._abort) {
180
+ this._abort.abort();
181
+ this._abort = null;
182
+ }
183
+ }
184
+
185
+ destroy() {
186
+ this.stop();
187
+ if (this._target && this._mounted) {
188
+ this._target.textContent = "";
189
+ }
190
+ this._mounted = false;
191
+ this._els = null;
192
+ }
193
+
194
+ setOptions(next) {
195
+ const merged = _normalizeOptions({ ...this._opts, ...(next || {}) });
196
+ const historyChanged = merged.historySize !== this._opts.historySize;
197
+ const tickChanged = merged.tickMs !== this._opts.tickMs;
198
+ const modeChanged = merged.mode !== this._opts.mode;
199
+
200
+ this._opts = merged;
201
+ if (modeChanged && this._mounted && this._els && this._els.wrap) {
202
+ this._els.wrap.className = merged.mode === "icon" ? "wrap icon" : "wrap";
203
+ }
204
+ if (historyChanged) {
205
+ this._buffer.setMaxSize(merged.historySize);
206
+ if (this._mounted) this._rebuildBars();
207
+ }
208
+ if (tickChanged && this._running) {
209
+ this.stop();
210
+ this.start();
211
+ } else if (this._mounted) {
212
+ this._render();
213
+ }
214
+ }
215
+
216
+ push(value) {
217
+ this._buffer.push(value);
218
+ this._render();
219
+ }
220
+
221
+ _rebuildBars() {
222
+ if (!this._els) return;
223
+ const { bars } = this._els;
224
+ bars.textContent = "";
225
+ for (let i = 0; i < this._opts.historySize; i += 1) {
226
+ const bar = document.createElement("div");
227
+ bar.className = "bar missing";
228
+ bar.style.height = "2px";
229
+ bars.appendChild(bar);
230
+ }
231
+ }
232
+
233
+ _render({ error = false } = {}) {
234
+ if (!this._els) return;
235
+ const { wrap, value, bars } = this._els;
236
+
237
+ const vals = this._buffer.values();
238
+ const last = this._buffer.last();
239
+ const pct = Number.isFinite(last) ? Math.max(0, Math.min(100, last)) : null;
240
+
241
+ if (pct == null) {
242
+ value.textContent = error ? "N/A" : "—";
243
+ value.classList.add("muted");
244
+ } else {
245
+ value.textContent = `${pct.toFixed(0)}%`;
246
+ value.classList.remove("muted");
247
+ }
248
+ if (wrap) {
249
+ wrap.title = pct == null ? "GPU —" : `GPU ${pct.toFixed(0)}%`;
250
+ if (pct == null) {
251
+ wrap.style.removeProperty("--monitor-gpu-accent");
252
+ wrap.style.removeProperty("--monitor-gpu-accent-glow");
253
+ } else {
254
+ wrap.style.setProperty("--monitor-gpu-accent", _colorForPct(pct, 0.95));
255
+ wrap.style.setProperty("--monitor-gpu-accent-glow", _colorForPct(pct, 0.35));
256
+ }
257
+ }
258
+
259
+ const barEls = Array.from(bars.children);
260
+ const missingPrefix = Math.max(0, barEls.length - vals.length);
261
+ const barsMaxPx = Math.max(2, bars && typeof bars.clientHeight === "number" && bars.clientHeight > 0 ? bars.clientHeight : 34);
262
+ for (let i = 0; i < barEls.length; i += 1) {
263
+ const el = barEls[i];
264
+ const idx = i - missingPrefix;
265
+ const v = idx >= 0 ? vals[idx] : null;
266
+ const valNum = Number.isFinite(v) ? v : null;
267
+ const h = valNum == null ? 0 : _clamp01(valNum / 100);
268
+
269
+ const px = Math.max(2, Math.min(barsMaxPx, Math.round(h * barsMaxPx)));
270
+ el.style.height = `${px}px`;
271
+ el.style.background = valNum == null ? "" : _colorForPct(valNum);
272
+ el.className = "bar";
273
+ if (valNum == null) el.classList.add("missing");
274
+ if (error) el.classList.add("error");
275
+ }
276
+ }
277
+
278
+ async _tick() {
279
+ const tokenAtStart = String(this._token || "").trim();
280
+ const getTokenAtStart = this._getToken;
281
+ const requestHadAuth = Boolean(tokenAtStart) || typeof getTokenAtStart === "function";
282
+
283
+ if (this._abort) this._abort.abort();
284
+ this._abort = new AbortController();
285
+
286
+ const res = await fetchHostGpuMetrics({
287
+ baseUrl: this._opts.baseUrl,
288
+ endpoint: this._opts.endpoint,
289
+ token: tokenAtStart,
290
+ getToken: getTokenAtStart,
291
+ signal: this._abort.signal,
292
+ });
293
+
294
+ const payload = res && res.payload;
295
+ const supported = payload && typeof payload === "object" ? payload.supported !== false : true;
296
+ const pct = extractUtilizationGpuPct(payload);
297
+
298
+ if (!res.ok) {
299
+ this._buffer.push(null);
300
+ this._render({ error: true });
301
+ if (res.status === 401 || res.status === 403) {
302
+ // If the request was unauthenticated (no token and no getToken) but auth became available
303
+ // before the 401 came back (common race when the host sets `el.token` after mount),
304
+ // do not permanently stop the widget. Let the poll loop continue and succeed on the next tick.
305
+ const tokenNow = String(this._token || "").trim();
306
+ const hasGetTokenNow = typeof this._getToken === "function";
307
+ const authNowAvailable = Boolean(tokenNow) || hasGetTokenNow;
308
+ if (!requestHadAuth && authNowAvailable) {
309
+ return;
310
+ }
311
+ this._stoppedForAuth = true;
312
+ this.stop();
313
+ } else if (res.status === 429) {
314
+ this.stop();
315
+ this._resumeTimer = setTimeout(() => {
316
+ this._resumeTimer = null;
317
+ if (!this._mounted) return;
318
+ this.start();
319
+ }, 30_000);
320
+ }
321
+ return;
322
+ }
323
+
324
+ if (!supported || pct == null) {
325
+ this._buffer.push(null);
326
+ this._render();
327
+ return;
328
+ }
329
+
330
+ this._buffer.push(pct);
331
+ this._render();
332
+ }
333
+
334
+ async _poll_loop(gen) {
335
+ if (!this._running || gen !== this._timer_gen) return;
336
+
337
+ await this._tick();
338
+
339
+ if (!this._running || gen !== this._timer_gen) return;
340
+ this._timer = setTimeout(() => {
341
+ this._timer = null;
342
+ void this._poll_loop(gen);
343
+ }, this._opts.tickMs);
344
+ }
345
+ }
346
+
347
+ export function createMonitorGpuWidget(target, options = {}) {
348
+ const c = new MonitorGpuWidgetController(target, options);
349
+ c.start();
350
+ return c;
351
+ }
352
+
353
+ export function registerMonitorGpuWidget(tagName = "monitor-gpu") {
354
+ if (typeof globalThis === "undefined") return;
355
+ if (typeof globalThis.customElements === "undefined") return;
356
+ if (globalThis.customElements.get(tagName)) return;
357
+
358
+ const HTMLElementBase = globalThis.HTMLElement || class {};
359
+
360
+ class MonitorGpuElement extends HTMLElementBase {
361
+ static get observedAttributes() {
362
+ return ["tick-ms", "history-size", "endpoint", "base-url", "mode"];
363
+ }
364
+
365
+ constructor() {
366
+ super();
367
+ this._shadow = this.attachShadow ? this.attachShadow({ mode: "open" }) : null;
368
+ const root = this._shadow || this;
369
+ this._controller = new MonitorGpuWidgetController(root, {});
370
+
371
+ // If a property was set on the element instance before it was upgraded/defined
372
+ // (e.g. React rendered `<monitor-gpu>` before `customElements.define()`),
373
+ // it becomes an "own property" and would bypass our setters. Upgrade it.
374
+ this._upgradeProperty("token");
375
+ this._upgradeProperty("getToken");
376
+ this._upgradeProperty("mode");
377
+ }
378
+
379
+ _upgradeProperty(prop) {
380
+ if (!Object.prototype.hasOwnProperty.call(this, prop)) return;
381
+ const value = this[prop];
382
+ try {
383
+ delete this[prop];
384
+ } catch {
385
+ // ignore
386
+ }
387
+ this[prop] = value;
388
+ }
389
+
390
+ connectedCallback() {
391
+ this._controller.setOptions(this._readAttrs());
392
+ const start = () => {
393
+ if (!this.isConnected) return;
394
+ this._controller.start();
395
+ };
396
+ if (typeof queueMicrotask === "function") queueMicrotask(start);
397
+ else Promise.resolve().then(start);
398
+ }
399
+
400
+ disconnectedCallback() {
401
+ this._controller.destroy();
402
+ }
403
+
404
+ attributeChangedCallback() {
405
+ this._controller.setOptions(this._readAttrs());
406
+ }
407
+
408
+ _readAttrs() {
409
+ const tickMs = this.getAttribute("tick-ms");
410
+ const historySize = this.getAttribute("history-size");
411
+ const endpoint = this.getAttribute("endpoint");
412
+ const baseUrl = this.getAttribute("base-url");
413
+ const mode = this.getAttribute("mode");
414
+ return {
415
+ ...(tickMs != null ? { tickMs } : {}),
416
+ ...(historySize != null ? { historySize } : {}),
417
+ ...(endpoint != null ? { endpoint } : {}),
418
+ ...(baseUrl != null ? { baseUrl } : {}),
419
+ ...(mode != null ? { mode } : {}),
420
+ };
421
+ }
422
+
423
+ set token(t) {
424
+ this._controller.token = t;
425
+ }
426
+
427
+ get token() {
428
+ return this._controller.token;
429
+ }
430
+
431
+ set getToken(fn) {
432
+ this._controller.getToken = fn;
433
+ }
434
+
435
+ get getToken() {
436
+ return this._controller.getToken;
437
+ }
438
+
439
+ set tickMs(v) {
440
+ this._controller.setOptions({ tickMs: v });
441
+ }
442
+
443
+ get tickMs() {
444
+ return this._controller.options.tickMs;
445
+ }
446
+
447
+ set historySize(v) {
448
+ this._controller.setOptions({ historySize: v });
449
+ }
450
+
451
+ get historySize() {
452
+ return this._controller.options.historySize;
453
+ }
454
+
455
+ set endpoint(v) {
456
+ this._controller.setOptions({ endpoint: v });
457
+ }
458
+
459
+ get endpoint() {
460
+ return this._controller.options.endpoint;
461
+ }
462
+
463
+ set baseUrl(v) {
464
+ this._controller.setOptions({ baseUrl: v });
465
+ }
466
+
467
+ get baseUrl() {
468
+ return this._controller.options.baseUrl;
469
+ }
470
+
471
+ set mode(v) {
472
+ this._controller.setOptions({ mode: v });
473
+ }
474
+
475
+ get mode() {
476
+ return this._controller.options.mode;
477
+ }
478
+ }
479
+
480
+ globalThis.customElements.define(tagName, MonitorGpuElement);
481
+ }