@hansenexus/hud 0.2.0 → 0.4.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 +253 -9
- package/dist/access-CNvJOwKM.d.ts +54 -0
- package/dist/capabilities-CjulXFQX.js +189 -0
- package/dist/define-hud-BBzsSl8b.d.ts +36 -0
- package/dist/env-DfxpPfs9.js +28 -0
- package/dist/env-vEjyJfPW.d.ts +17 -0
- package/dist/force-state-BaqWsmrf.js +153 -0
- package/dist/force-state-CCqoTfVB.d.ts +23 -0
- package/dist/handler-WmE4Lu7l.d.ts +39 -0
- package/dist/index-BYsdS-7d.d.ts +55 -0
- package/dist/index.d.ts +6 -39
- package/dist/index.js +980 -264
- package/dist/marker-C66LxOhJ.d.ts +9 -0
- package/dist/marker-CMrvxPmS.d.ts +12 -0
- package/dist/marker-DjeGvL-F.js +9 -0
- package/dist/marker-uZYPOJ4M.js +12 -0
- package/dist/markers.d.ts +3 -0
- package/dist/markers.js +3 -0
- package/dist/next.d.ts +33 -0
- package/dist/next.js +52 -0
- package/dist/payload-CLKDVZWl.d.ts +68 -0
- package/dist/plugins/access/server.d.ts +122 -0
- package/dist/plugins/access/server.js +339 -0
- package/dist/plugins/access.d.ts +41 -0
- package/dist/plugins/access.js +250 -0
- package/dist/plugins/agent/server.d.ts +129 -0
- package/dist/plugins/agent/server.js +429 -0
- package/dist/plugins/agent.d.ts +37 -0
- package/dist/plugins/agent.js +411 -0
- package/dist/plugins/design.d.ts +125 -0
- package/dist/plugins/design.js +619 -0
- package/dist/plugins/observe.d.ts +64 -0
- package/dist/plugins/observe.js +617 -0
- package/dist/plugins/ops/server.d.ts +82 -0
- package/dist/plugins/ops/server.js +380 -0
- package/dist/plugins/ops.d.ts +26 -0
- package/dist/plugins/ops.js +362 -0
- package/dist/plugins/vitals.d.ts +25 -0
- package/dist/plugins/vitals.js +26 -0
- package/dist/plugins-CQ_0w1ot.d.ts +114 -0
- package/dist/plugins-DVy_vaYr.js +70 -0
- package/dist/review.d.ts +181 -0
- package/dist/review.js +1017 -0
- package/dist/selector-NSYSe7AZ.js +28 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +106 -0
- package/dist/shortcut-U6ftVY06.js +222 -0
- package/dist/state-boundary.d.ts +26 -0
- package/dist/state-boundary.js +61 -0
- package/dist/styles--slk86ZQ.js +332 -0
- package/dist/types-DADBRiGk.d.ts +50 -0
- package/dist/types-QJUajwWq.js +35 -0
- package/dist/types-f3jlNxzw.d.ts +92 -0
- package/dist/vitals-panel-Dvc5dnR-.js +87 -0
- package/package.json +36 -3
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import { i as definePlugin } from "../plugins-DVy_vaYr.js";
|
|
2
|
+
import { r as startVitals, t as VitalsPanel } from "../vitals-panel-Dvc5dnR-.js";
|
|
3
|
+
import { useEffect, useLayoutEffect, useState, useSyncExternalStore, version } from "react";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/plugins/observe/capture-buffer.ts
|
|
6
|
+
/**
|
|
7
|
+
* Fixed-size ring buffer with a referentially-stable snapshot, shaped for
|
|
8
|
+
* `useSyncExternalStore`: the snapshot array is replaced on every mutation
|
|
9
|
+
* and reused between mutations.
|
|
10
|
+
*/
|
|
11
|
+
var CaptureBuffer = class {
|
|
12
|
+
entries = [];
|
|
13
|
+
snapshot = [];
|
|
14
|
+
listeners = /* @__PURE__ */ new Set();
|
|
15
|
+
nextId = 1;
|
|
16
|
+
maxEntries;
|
|
17
|
+
constructor(maxEntries = 500) {
|
|
18
|
+
this.maxEntries = maxEntries;
|
|
19
|
+
}
|
|
20
|
+
push(input) {
|
|
21
|
+
const entry = {
|
|
22
|
+
id: this.nextId++,
|
|
23
|
+
ts: Date.now(),
|
|
24
|
+
...input
|
|
25
|
+
};
|
|
26
|
+
this.entries.push(entry);
|
|
27
|
+
if (this.entries.length > this.maxEntries) this.entries.splice(0, this.entries.length - this.maxEntries);
|
|
28
|
+
this.emit();
|
|
29
|
+
return entry;
|
|
30
|
+
}
|
|
31
|
+
clear() {
|
|
32
|
+
if (this.entries.length === 0) return;
|
|
33
|
+
this.entries = [];
|
|
34
|
+
this.emit();
|
|
35
|
+
}
|
|
36
|
+
getSnapshot = () => this.snapshot;
|
|
37
|
+
subscribe = (listener) => {
|
|
38
|
+
this.listeners.add(listener);
|
|
39
|
+
return () => this.listeners.delete(listener);
|
|
40
|
+
};
|
|
41
|
+
emit() {
|
|
42
|
+
this.snapshot = [...this.entries];
|
|
43
|
+
for (const listener of this.listeners) listener();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/** Shared buffer the capture patches write into and the console tab reads from. */
|
|
47
|
+
const captureBuffer = new CaptureBuffer(500);
|
|
48
|
+
/**
|
|
49
|
+
* What the pill counts as an error: `console.error`, uncaught errors and
|
|
50
|
+
* rejections, and requests that failed outright or answered 5xx. A 4xx is the
|
|
51
|
+
* app's business (a 404 probe, a 401 before login), so it stays a plain fetch.
|
|
52
|
+
*/
|
|
53
|
+
function isError(entry) {
|
|
54
|
+
if (entry.kind === "error") return true;
|
|
55
|
+
return entry.kind === "fetch" && entry.status !== void 0 && (entry.status === 0 || entry.status >= 500);
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/plugins/observe/capture.ts
|
|
59
|
+
const MAX_TEXT = 300;
|
|
60
|
+
let installs = 0;
|
|
61
|
+
const restorers = [];
|
|
62
|
+
const isHudRoute = (url) => url.includes("/api/hud/");
|
|
63
|
+
function toText(args) {
|
|
64
|
+
return args.map((arg) => {
|
|
65
|
+
if (typeof arg === "string") return arg;
|
|
66
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
|
|
67
|
+
try {
|
|
68
|
+
return JSON.stringify(arg) ?? String(arg);
|
|
69
|
+
} catch {
|
|
70
|
+
return String(arg);
|
|
71
|
+
}
|
|
72
|
+
}).join(" ").slice(0, MAX_TEXT);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Pushes on a microtask. React logs dev warnings with `console.error` while it
|
|
76
|
+
* renders; a synchronous push would notify the console tab's store in the
|
|
77
|
+
* middle of that render, React would warn about the update, and the warning
|
|
78
|
+
* would land here again.
|
|
79
|
+
*/
|
|
80
|
+
function record(input) {
|
|
81
|
+
queueMicrotask(() => captureBuffer.push(input));
|
|
82
|
+
}
|
|
83
|
+
function patchConsole() {
|
|
84
|
+
for (const kind of [
|
|
85
|
+
"log",
|
|
86
|
+
"warn",
|
|
87
|
+
"error"
|
|
88
|
+
]) {
|
|
89
|
+
const original = console[kind];
|
|
90
|
+
console[kind] = (...args) => {
|
|
91
|
+
record({
|
|
92
|
+
kind,
|
|
93
|
+
text: toText(args)
|
|
94
|
+
});
|
|
95
|
+
original.apply(console, args);
|
|
96
|
+
};
|
|
97
|
+
restorers.push(() => {
|
|
98
|
+
console[kind] = original;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function listenErrors() {
|
|
103
|
+
const onError = (event) => {
|
|
104
|
+
record({
|
|
105
|
+
kind: "error",
|
|
106
|
+
text: toText([event.error instanceof Error ? event.error : event.message])
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
const onRejection = (event) => {
|
|
110
|
+
record({
|
|
111
|
+
kind: "error",
|
|
112
|
+
text: `Unhandled rejection: ${toText([event.reason])}`
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
window.addEventListener("error", onError);
|
|
116
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
117
|
+
restorers.push(() => {
|
|
118
|
+
window.removeEventListener("error", onError);
|
|
119
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function hasInternalHeader(input, init) {
|
|
123
|
+
try {
|
|
124
|
+
if (new Headers(init?.headers).has("x-hud-internal")) return true;
|
|
125
|
+
return input instanceof Request && input.headers.has("x-hud-internal");
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function patchFetch(ignore) {
|
|
131
|
+
const originalFetch = window.fetch;
|
|
132
|
+
const callOriginal = originalFetch.bind(window);
|
|
133
|
+
const patched = async (input, init) => {
|
|
134
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
135
|
+
if (ignore(url) || hasInternalHeader(input, init)) return callOriginal(input, init);
|
|
136
|
+
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
137
|
+
const started = performance.now();
|
|
138
|
+
const elapsed = () => Math.round(performance.now() - started);
|
|
139
|
+
try {
|
|
140
|
+
const response = await callOriginal(input, init);
|
|
141
|
+
record({
|
|
142
|
+
kind: "fetch",
|
|
143
|
+
text: `${method} ${url}`.slice(0, MAX_TEXT),
|
|
144
|
+
status: response.status,
|
|
145
|
+
durationMs: elapsed()
|
|
146
|
+
});
|
|
147
|
+
return response;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
record({
|
|
150
|
+
kind: "fetch",
|
|
151
|
+
text: `${method} ${url} — ${toText([error])}`.slice(0, MAX_TEXT),
|
|
152
|
+
status: 0,
|
|
153
|
+
durationMs: elapsed()
|
|
154
|
+
});
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
window.fetch = Object.assign(patched, originalFetch);
|
|
159
|
+
restorers.push(() => {
|
|
160
|
+
window.fetch = originalFetch;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function observeLongTasks() {
|
|
164
|
+
if (typeof PerformanceObserver === "undefined") return;
|
|
165
|
+
try {
|
|
166
|
+
const observer = new PerformanceObserver((list) => {
|
|
167
|
+
for (const entry of list.getEntries()) {
|
|
168
|
+
const durationMs = Math.round(entry.duration);
|
|
169
|
+
record({
|
|
170
|
+
kind: "longtask",
|
|
171
|
+
text: `long task ${durationMs} ms`,
|
|
172
|
+
durationMs
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
observer.observe({
|
|
177
|
+
type: "longtask",
|
|
178
|
+
buffered: true
|
|
179
|
+
});
|
|
180
|
+
restorers.push(() => observer.disconnect());
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Installs the patches on the first call and returns a release function; the
|
|
185
|
+
* last release restores the originals. Reference counted so StrictMode's
|
|
186
|
+
* mount-unmount-mount and two HUDs on one page neither double-patch nor
|
|
187
|
+
* unpatch under each other. The first caller's options win.
|
|
188
|
+
*/
|
|
189
|
+
function installCapture({ ignore = isHudRoute } = {}) {
|
|
190
|
+
if (typeof window === "undefined") return () => {};
|
|
191
|
+
if (installs === 0) {
|
|
192
|
+
patchConsole();
|
|
193
|
+
listenErrors();
|
|
194
|
+
patchFetch(ignore);
|
|
195
|
+
observeLongTasks();
|
|
196
|
+
}
|
|
197
|
+
installs++;
|
|
198
|
+
let released = false;
|
|
199
|
+
return () => {
|
|
200
|
+
if (released) return;
|
|
201
|
+
released = true;
|
|
202
|
+
installs--;
|
|
203
|
+
if (installs === 0) while (restorers.length > 0) restorers.pop()?.();
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/plugins/observe/view.ts
|
|
208
|
+
const FILTERS = [
|
|
209
|
+
"all",
|
|
210
|
+
"errors",
|
|
211
|
+
"warn",
|
|
212
|
+
"log",
|
|
213
|
+
"fetch",
|
|
214
|
+
"longtask"
|
|
215
|
+
];
|
|
216
|
+
function matches(entry, filter) {
|
|
217
|
+
if (filter === "all") return true;
|
|
218
|
+
if (filter === "errors") return isError(entry);
|
|
219
|
+
return entry.kind === filter;
|
|
220
|
+
}
|
|
221
|
+
let view = {
|
|
222
|
+
tab: "console",
|
|
223
|
+
filter: "all"
|
|
224
|
+
};
|
|
225
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
226
|
+
function setView(next) {
|
|
227
|
+
view = {
|
|
228
|
+
...view,
|
|
229
|
+
...next
|
|
230
|
+
};
|
|
231
|
+
for (const listener of listeners) listener();
|
|
232
|
+
}
|
|
233
|
+
function subscribe(listener) {
|
|
234
|
+
listeners.add(listener);
|
|
235
|
+
return () => listeners.delete(listener);
|
|
236
|
+
}
|
|
237
|
+
const getView = () => view;
|
|
238
|
+
function useView() {
|
|
239
|
+
return useSyncExternalStore(subscribe, getView, getView);
|
|
240
|
+
}
|
|
241
|
+
function useCapture() {
|
|
242
|
+
return useSyncExternalStore(captureBuffer.subscribe, captureBuffer.getSnapshot, captureBuffer.getSnapshot);
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/plugins/observe/console-tab.tsx
|
|
246
|
+
function statusLevel(status) {
|
|
247
|
+
if (status === 0 || status >= 500) return "error";
|
|
248
|
+
return status >= 400 ? "warn" : "ok";
|
|
249
|
+
}
|
|
250
|
+
/** Console, fetch and long-task capture, newest first. */
|
|
251
|
+
function ConsoleTab() {
|
|
252
|
+
const entries = useCapture();
|
|
253
|
+
const { filter } = useView();
|
|
254
|
+
const rows = entries.filter((entry) => matches(entry, filter)).slice(-200).reverse();
|
|
255
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
256
|
+
className: "obs-toolbar",
|
|
257
|
+
role: "toolbar",
|
|
258
|
+
"aria-label": "Filter",
|
|
259
|
+
children: [FILTERS.map((f) => /* @__PURE__ */ jsxs("button", {
|
|
260
|
+
type: "button",
|
|
261
|
+
className: "obs-chip",
|
|
262
|
+
"aria-pressed": filter === f,
|
|
263
|
+
onClick: () => setView({ filter: f }),
|
|
264
|
+
children: [f, f !== "all" && /* @__PURE__ */ jsx("span", {
|
|
265
|
+
className: "obs-count",
|
|
266
|
+
children: entries.filter((e) => matches(e, f)).length
|
|
267
|
+
})]
|
|
268
|
+
}, f)), /* @__PURE__ */ jsx("button", {
|
|
269
|
+
type: "button",
|
|
270
|
+
className: "obs-chip obs-clear",
|
|
271
|
+
onClick: () => captureBuffer.clear(),
|
|
272
|
+
children: "clear"
|
|
273
|
+
})]
|
|
274
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
275
|
+
className: "obs-log",
|
|
276
|
+
children: rows.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
277
|
+
className: "empty",
|
|
278
|
+
children: filter === "all" ? "Nothing captured yet. Console output, requests and long tasks land here." : `No ${filter} captured.`
|
|
279
|
+
}) : rows.map((entry) => /* @__PURE__ */ jsx(EntryRow, { entry }, entry.id))
|
|
280
|
+
})] });
|
|
281
|
+
}
|
|
282
|
+
function EntryRow({ entry }) {
|
|
283
|
+
const time = new Date(entry.ts).toLocaleTimeString(void 0, { hour12: false });
|
|
284
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
285
|
+
className: "obs-row",
|
|
286
|
+
"data-kind": entry.kind,
|
|
287
|
+
children: [
|
|
288
|
+
/* @__PURE__ */ jsx("span", {
|
|
289
|
+
className: "obs-time",
|
|
290
|
+
children: time
|
|
291
|
+
}),
|
|
292
|
+
/* @__PURE__ */ jsx("span", {
|
|
293
|
+
className: "obs-kind",
|
|
294
|
+
"data-kind": entry.kind,
|
|
295
|
+
children: entry.kind
|
|
296
|
+
}),
|
|
297
|
+
/* @__PURE__ */ jsx("span", {
|
|
298
|
+
className: "obs-text",
|
|
299
|
+
title: entry.text,
|
|
300
|
+
children: entry.text
|
|
301
|
+
}),
|
|
302
|
+
entry.status !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
303
|
+
className: "obs-status",
|
|
304
|
+
"data-level": statusLevel(entry.status),
|
|
305
|
+
children: entry.status || "ERR"
|
|
306
|
+
}),
|
|
307
|
+
entry.durationMs !== void 0 && /* @__PURE__ */ jsxs("span", {
|
|
308
|
+
className: "obs-ms",
|
|
309
|
+
children: [entry.durationMs, " ms"]
|
|
310
|
+
})
|
|
311
|
+
]
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/plugins/observe/info-tab.tsx
|
|
316
|
+
/** App, build, browser and user snapshot: what runs here right now. */
|
|
317
|
+
function InfoTab({ app, env, useUser }) {
|
|
318
|
+
const user = useUser();
|
|
319
|
+
const viewport = useViewportLabel();
|
|
320
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
321
|
+
/* @__PURE__ */ jsxs(Section, {
|
|
322
|
+
title: "App",
|
|
323
|
+
children: [
|
|
324
|
+
/* @__PURE__ */ jsx(Row, {
|
|
325
|
+
label: "App",
|
|
326
|
+
value: app
|
|
327
|
+
}),
|
|
328
|
+
/* @__PURE__ */ jsx(Row, {
|
|
329
|
+
label: "Origin",
|
|
330
|
+
value: window.location.origin,
|
|
331
|
+
copy: true
|
|
332
|
+
}),
|
|
333
|
+
/* @__PURE__ */ jsx(Row, {
|
|
334
|
+
label: "Path",
|
|
335
|
+
value: window.location.pathname + window.location.search,
|
|
336
|
+
copy: true
|
|
337
|
+
})
|
|
338
|
+
]
|
|
339
|
+
}),
|
|
340
|
+
/* @__PURE__ */ jsxs(Section, {
|
|
341
|
+
title: "Build",
|
|
342
|
+
children: [/* @__PURE__ */ jsx(Row, {
|
|
343
|
+
label: "React",
|
|
344
|
+
value: version
|
|
345
|
+
}), Object.entries(env).map(([label, value]) => /* @__PURE__ */ jsx(Row, {
|
|
346
|
+
label,
|
|
347
|
+
value: value ?? "not set",
|
|
348
|
+
copy: value !== void 0
|
|
349
|
+
}, label))]
|
|
350
|
+
}),
|
|
351
|
+
user && /* @__PURE__ */ jsxs(Section, {
|
|
352
|
+
title: "User",
|
|
353
|
+
children: [
|
|
354
|
+
user.name !== void 0 && /* @__PURE__ */ jsx(Row, {
|
|
355
|
+
label: "Name",
|
|
356
|
+
value: user.name
|
|
357
|
+
}),
|
|
358
|
+
user.email !== void 0 && /* @__PURE__ */ jsx(Row, {
|
|
359
|
+
label: "Email",
|
|
360
|
+
value: user.email,
|
|
361
|
+
copy: true
|
|
362
|
+
}),
|
|
363
|
+
user.id !== void 0 && /* @__PURE__ */ jsx(Row, {
|
|
364
|
+
label: "ID",
|
|
365
|
+
value: user.id,
|
|
366
|
+
copy: true
|
|
367
|
+
}),
|
|
368
|
+
user.role !== void 0 && /* @__PURE__ */ jsx(Row, {
|
|
369
|
+
label: "Role",
|
|
370
|
+
value: user.role
|
|
371
|
+
}),
|
|
372
|
+
Object.entries(user.extra ?? {}).map(([label, value]) => /* @__PURE__ */ jsx(Row, {
|
|
373
|
+
label,
|
|
374
|
+
value
|
|
375
|
+
}, label))
|
|
376
|
+
]
|
|
377
|
+
}),
|
|
378
|
+
/* @__PURE__ */ jsxs(Section, {
|
|
379
|
+
title: "Browser",
|
|
380
|
+
children: [
|
|
381
|
+
/* @__PURE__ */ jsx(Row, {
|
|
382
|
+
label: "Viewport",
|
|
383
|
+
value: viewport
|
|
384
|
+
}),
|
|
385
|
+
/* @__PURE__ */ jsx(Row, {
|
|
386
|
+
label: "Language",
|
|
387
|
+
value: navigator.language
|
|
388
|
+
}),
|
|
389
|
+
/* @__PURE__ */ jsx(Row, {
|
|
390
|
+
label: "User agent",
|
|
391
|
+
value: navigator.userAgent,
|
|
392
|
+
copy: true
|
|
393
|
+
})
|
|
394
|
+
]
|
|
395
|
+
})
|
|
396
|
+
] });
|
|
397
|
+
}
|
|
398
|
+
function viewportLabel() {
|
|
399
|
+
const dark = window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
|
|
400
|
+
return `${window.innerWidth}×${window.innerHeight} @${window.devicePixelRatio}x · ${dark ? "dark" : "light"}`;
|
|
401
|
+
}
|
|
402
|
+
function useViewportLabel() {
|
|
403
|
+
const [label, setLabel] = useState(viewportLabel);
|
|
404
|
+
useEffect(() => {
|
|
405
|
+
const update = () => setLabel(viewportLabel());
|
|
406
|
+
window.addEventListener("resize", update);
|
|
407
|
+
return () => window.removeEventListener("resize", update);
|
|
408
|
+
}, []);
|
|
409
|
+
return label;
|
|
410
|
+
}
|
|
411
|
+
function Section({ title, children }) {
|
|
412
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
413
|
+
className: "obs-section",
|
|
414
|
+
"aria-label": title,
|
|
415
|
+
children: [/* @__PURE__ */ jsx("h4", {
|
|
416
|
+
className: "obs-heading",
|
|
417
|
+
children: title
|
|
418
|
+
}), /* @__PURE__ */ jsx("dl", {
|
|
419
|
+
className: "obs-info",
|
|
420
|
+
children
|
|
421
|
+
})]
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function Row({ label, value, copy = false }) {
|
|
425
|
+
const [copied, setCopied] = useState(false);
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
if (!copied) return;
|
|
428
|
+
const timer = setTimeout(() => setCopied(false), 1200);
|
|
429
|
+
return () => clearTimeout(timer);
|
|
430
|
+
}, [copied]);
|
|
431
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
|
|
432
|
+
title: label,
|
|
433
|
+
children: label
|
|
434
|
+
}), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsxs("div", {
|
|
435
|
+
className: "obs-value",
|
|
436
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
437
|
+
title: value,
|
|
438
|
+
children: value
|
|
439
|
+
}), copy && /* @__PURE__ */ jsx("button", {
|
|
440
|
+
type: "button",
|
|
441
|
+
className: "obs-copy",
|
|
442
|
+
"aria-label": `Copy ${label}`,
|
|
443
|
+
onClick: () => {
|
|
444
|
+
navigator.clipboard?.writeText(value).then(() => setCopied(true), () => {});
|
|
445
|
+
},
|
|
446
|
+
children: copied ? "copied" : "copy"
|
|
447
|
+
})]
|
|
448
|
+
}) })] });
|
|
449
|
+
}
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/plugins/observe/styles.ts
|
|
452
|
+
/**
|
|
453
|
+
* The observe panel's styles. The panel renders them in its own `<style>`,
|
|
454
|
+
* which still sits inside the HUD's shadow root, so they ship only with this
|
|
455
|
+
* plugin and build on the shell's `--hud-*` tokens.
|
|
456
|
+
*/
|
|
457
|
+
const OBSERVE_CSS = `
|
|
458
|
+
/* Fixed width, so switching tabs does not resize the floating panel. */
|
|
459
|
+
.obs { display: flex; flex-direction: column; gap: 8px; width: 396px; max-width: 100%; }
|
|
460
|
+
|
|
461
|
+
.obs-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--hud-border); }
|
|
462
|
+
.hud .obs-tab {
|
|
463
|
+
padding: 3px 10px 5px;
|
|
464
|
+
margin-bottom: -1px;
|
|
465
|
+
border-bottom: 2px solid transparent;
|
|
466
|
+
color: var(--hud-fg-muted);
|
|
467
|
+
}
|
|
468
|
+
.hud .obs-tab[aria-selected="true"] { border-bottom-color: var(--hud-accent); color: var(--hud-fg); font-weight: 600; }
|
|
469
|
+
|
|
470
|
+
.obs-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; }
|
|
471
|
+
.hud .obs-chip {
|
|
472
|
+
padding: 1px 7px;
|
|
473
|
+
border-radius: 999px;
|
|
474
|
+
font-family: var(--hud-mono);
|
|
475
|
+
font-size: 11px;
|
|
476
|
+
color: var(--hud-fg-muted);
|
|
477
|
+
}
|
|
478
|
+
.hud .obs-chip:hover { background: var(--hud-bg-subtle); }
|
|
479
|
+
.hud .obs-chip[aria-pressed="true"] { background: var(--hud-bg-subtle); color: var(--hud-fg); }
|
|
480
|
+
.obs-chip .obs-count { margin-left: 4px; opacity: 0.6; }
|
|
481
|
+
.hud .obs-clear { margin-left: auto; }
|
|
482
|
+
|
|
483
|
+
.obs-log {
|
|
484
|
+
max-height: 320px;
|
|
485
|
+
overflow: auto;
|
|
486
|
+
border: 1px solid var(--hud-border);
|
|
487
|
+
border-radius: 4px;
|
|
488
|
+
font-family: var(--hud-mono);
|
|
489
|
+
font-size: 11px;
|
|
490
|
+
}
|
|
491
|
+
.obs-row { display: flex; align-items: baseline; gap: 8px; padding: 2px 6px; }
|
|
492
|
+
.obs-row:hover { background: var(--hud-bg-subtle); }
|
|
493
|
+
.obs-row + .obs-row { border-top: 1px solid var(--hud-border); }
|
|
494
|
+
.obs-time, .obs-ms { flex-shrink: 0; color: var(--hud-fg-muted); }
|
|
495
|
+
.obs-kind { flex-shrink: 0; width: 56px; text-transform: uppercase; color: var(--hud-fg-muted); }
|
|
496
|
+
.obs-kind[data-kind="warn"] { color: var(--hud-warn); }
|
|
497
|
+
.obs-kind[data-kind="error"] { color: var(--hud-poor); }
|
|
498
|
+
.obs-kind[data-kind="fetch"] { color: var(--hud-accent); }
|
|
499
|
+
.obs-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
500
|
+
.obs-status { flex-shrink: 0; color: var(--hud-good); }
|
|
501
|
+
.obs-status[data-level="warn"] { color: var(--hud-warn); }
|
|
502
|
+
.obs-status[data-level="error"] { color: var(--hud-poor); }
|
|
503
|
+
.obs-log .empty { margin: 0; padding: 6px; font-family: var(--hud-font); }
|
|
504
|
+
|
|
505
|
+
.obs-section + .obs-section { margin-top: 10px; }
|
|
506
|
+
.obs-heading {
|
|
507
|
+
margin: 0 0 4px;
|
|
508
|
+
font-size: 10px;
|
|
509
|
+
font-weight: 600;
|
|
510
|
+
letter-spacing: 0.06em;
|
|
511
|
+
text-transform: uppercase;
|
|
512
|
+
color: var(--hud-fg-muted);
|
|
513
|
+
}
|
|
514
|
+
.obs-info {
|
|
515
|
+
display: grid;
|
|
516
|
+
grid-template-columns: 88px minmax(0, 1fr);
|
|
517
|
+
gap: 2px 12px;
|
|
518
|
+
margin: 0;
|
|
519
|
+
}
|
|
520
|
+
.obs-info dt { color: var(--hud-fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
521
|
+
.obs-info dd { margin: 0; min-width: 0; font-family: var(--hud-mono); }
|
|
522
|
+
.obs-value { display: flex; align-items: center; gap: 6px; }
|
|
523
|
+
.obs-value span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
524
|
+
.hud .obs-copy { flex-shrink: 0; padding: 0 4px; border-radius: 4px; font-size: 10px; color: var(--hud-fg-muted); }
|
|
525
|
+
.hud .obs-copy:hover { background: var(--hud-bg-subtle); color: var(--hud-fg); }
|
|
526
|
+
`;
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/plugins/observe/index.tsx
|
|
529
|
+
const TABS = [
|
|
530
|
+
{
|
|
531
|
+
id: "console",
|
|
532
|
+
label: "Console"
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
id: "info",
|
|
536
|
+
label: "Info"
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
id: "vitals",
|
|
540
|
+
label: "Vitals"
|
|
541
|
+
}
|
|
542
|
+
];
|
|
543
|
+
const NO_ENV = {};
|
|
544
|
+
const noUser = () => void 0;
|
|
545
|
+
function useErrorCount() {
|
|
546
|
+
return useCapture().filter(isError).length;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Console and network capture, an app/build/browser/user snapshot and the
|
|
550
|
+
* page's Web Vitals. The pill counts errors; opening the panel while there are
|
|
551
|
+
* any lands on the console filtered to them.
|
|
552
|
+
*/
|
|
553
|
+
function observe({ env = NO_ENV, useUser = noUser, ignore } = {}) {
|
|
554
|
+
function ObservePill() {
|
|
555
|
+
useEffect(() => installCapture(ignore ? { ignore } : {}), []);
|
|
556
|
+
useEffect(() => startVitals(), []);
|
|
557
|
+
const errors = useErrorCount();
|
|
558
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
559
|
+
className: "dot",
|
|
560
|
+
"data-rating": errors > 0 ? "poor" : "good"
|
|
561
|
+
}), errors === 1 ? "1 error" : `${errors} errors`] });
|
|
562
|
+
}
|
|
563
|
+
function ObservePanel({ ctx }) {
|
|
564
|
+
const { tab } = useView();
|
|
565
|
+
useLayoutEffect(() => {
|
|
566
|
+
if (captureBuffer.getSnapshot().some(isError)) setView({
|
|
567
|
+
tab: "console",
|
|
568
|
+
filter: "errors"
|
|
569
|
+
});
|
|
570
|
+
}, []);
|
|
571
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
572
|
+
className: "obs",
|
|
573
|
+
children: [
|
|
574
|
+
/* @__PURE__ */ jsx("style", { children: OBSERVE_CSS }),
|
|
575
|
+
/* @__PURE__ */ jsx("div", {
|
|
576
|
+
className: "obs-tabs",
|
|
577
|
+
role: "tablist",
|
|
578
|
+
"aria-label": "Observe",
|
|
579
|
+
children: TABS.map(({ id, label }) => /* @__PURE__ */ jsx("button", {
|
|
580
|
+
type: "button",
|
|
581
|
+
role: "tab",
|
|
582
|
+
className: "obs-tab",
|
|
583
|
+
"aria-selected": tab === id,
|
|
584
|
+
onClick: () => setView({ tab: id }),
|
|
585
|
+
children: label
|
|
586
|
+
}, id))
|
|
587
|
+
}),
|
|
588
|
+
/* @__PURE__ */ jsxs("div", {
|
|
589
|
+
role: "tabpanel",
|
|
590
|
+
"aria-label": TABS.find((t) => t.id === tab)?.label,
|
|
591
|
+
children: [
|
|
592
|
+
tab === "console" && /* @__PURE__ */ jsx(ConsoleTab, {}),
|
|
593
|
+
tab === "info" && /* @__PURE__ */ jsx(InfoTab, {
|
|
594
|
+
app: ctx.app,
|
|
595
|
+
env,
|
|
596
|
+
useUser
|
|
597
|
+
}),
|
|
598
|
+
tab === "vitals" && /* @__PURE__ */ jsx(VitalsPanel, {})
|
|
599
|
+
]
|
|
600
|
+
})
|
|
601
|
+
]
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
return definePlugin({
|
|
605
|
+
id: "observe",
|
|
606
|
+
title: "Observe",
|
|
607
|
+
pill: ObservePill,
|
|
608
|
+
panel: ObservePanel,
|
|
609
|
+
commands: [{
|
|
610
|
+
id: "clear",
|
|
611
|
+
title: "Clear console capture",
|
|
612
|
+
run: () => captureBuffer.clear()
|
|
613
|
+
}]
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
617
|
+
export { observe };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { d as HudRoutes } from "../../plugins-CQ_0w1ot.js";
|
|
2
|
+
import { a as CiSnapshot, c as TelemetryRange, i as CiRun, l as TelemetryResponse, n as CiProvider, o as CiState, r as CiResponse, s as TelemetryProvider, t as CiDeployment, u as TelemetrySnapshot } from "../../types-f3jlNxzw.js";
|
|
3
|
+
//#region src/plugins/ops/errors.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A provider failure whose message is safe to send to the client (it names
|
|
6
|
+
* the upstream and its status, never a URL with credentials or a token).
|
|
7
|
+
* The routes answer 502 with it; any other error becomes a generic 502.
|
|
8
|
+
*/
|
|
9
|
+
export declare class OpsProviderError extends Error {
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/plugins/ops/github-actions.d.ts
|
|
14
|
+
interface GithubActionsOptions {
|
|
15
|
+
/** `owner/name`. */
|
|
16
|
+
repo: string;
|
|
17
|
+
/**
|
|
18
|
+
* Branch whose runs the pill shows. Default: the first of `HUD_GIT_BRANCH`,
|
|
19
|
+
* `VERCEL_GIT_COMMIT_REF`, `GITHUB_HEAD_REF`, `GITHUB_REF_NAME`, then the
|
|
20
|
+
* branch checked out where the server runs (`.git/HEAD`). With no branch
|
|
21
|
+
* the panel lists the repo's latest runs and the pill shows no state.
|
|
22
|
+
*/
|
|
23
|
+
branch?: string | (() => string | null | Promise<string | null>);
|
|
24
|
+
/** Env var holding the token, read on each request. Default `GITHUB_TOKEN`. */
|
|
25
|
+
tokenEnv?: string;
|
|
26
|
+
/** Runs listed in the panel. Default 10. */
|
|
27
|
+
limit?: number;
|
|
28
|
+
/** Seconds a GitHub answer is reused, to stay inside the rate limit. Default 15. */
|
|
29
|
+
cacheSeconds?: number;
|
|
30
|
+
/** API root, for GitHub Enterprise Server. Default `https://api.github.com`. */
|
|
31
|
+
apiUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* GitHub Actions as the `ops` plugin's CI provider. Server only: the token is
|
|
35
|
+
* read from env on each request and only ever sent to GitHub.
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* ops({ ci: githubActions({ repo: "hansenexus/hud" }) })
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function githubActions(options: GithubActionsOptions): CiProvider;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/plugins/ops/signoz.d.ts
|
|
44
|
+
interface SignozOptions {
|
|
45
|
+
/** OpenTelemetry `service.name` of the app. */
|
|
46
|
+
service: string;
|
|
47
|
+
/** Env var holding the SigNoz query URL, read on each request. Default `SIGNOZ_QUERY_URL`. */
|
|
48
|
+
urlEnv?: string;
|
|
49
|
+
/** Env var holding an API key, sent as `SIGNOZ-API-KEY` when set. Default `SIGNOZ_API_KEY`. */
|
|
50
|
+
apiKeyEnv?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* SigNoz as the `ops` plugin's telemetry provider: error rate, p95 latency and
|
|
54
|
+
* request rate of one service over its PromQL `query_range` API. Server only:
|
|
55
|
+
* the URL and API key come from env on each request.
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* ops({ telemetry: signoz({ service: "hansenexus" }) })
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function signoz(options: SignozOptions): TelemetryProvider;
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/plugins/ops/server.d.ts
|
|
64
|
+
export interface OpsServerOptions {
|
|
65
|
+
/** CI provider behind `GET /ops/ci`, such as `githubActions({ repo })`. */
|
|
66
|
+
ci?: CiProvider;
|
|
67
|
+
/** Telemetry provider behind `GET /ops/telemetry`, such as `signoz({ service })`. */
|
|
68
|
+
telemetry?: TelemetryProvider;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Server half of the `ops` plugin. Pass it to `createHudHandler`; each
|
|
72
|
+
* provider adds its route, so an app without telemetry serves no
|
|
73
|
+
* `/telemetry`. Tokens and keys stay in the providers: responses carry
|
|
74
|
+
* run data, numbers and the name of a missing env var, never a secret.
|
|
75
|
+
*/
|
|
76
|
+
export declare function ops({ ci, telemetry }?: OpsServerOptions): {
|
|
77
|
+
readonly id: "ops";
|
|
78
|
+
readonly capability: "cicd.read";
|
|
79
|
+
readonly routes: HudRoutes;
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export type { CiDeployment, CiProvider, CiResponse, CiRun, CiSnapshot, CiState, GithubActionsOptions, SignozOptions, TelemetryProvider, TelemetryRange, TelemetryResponse, TelemetrySnapshot };
|