@hansenexus/hud 0.3.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 +186 -4
- package/dist/access-CNvJOwKM.d.ts +54 -0
- package/dist/capabilities-CjulXFQX.js +189 -0
- package/dist/{define-hud-BHQR8Lbv.d.ts → define-hud-BBzsSl8b.d.ts} +6 -1
- 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 -36
- package/dist/index.js +119 -202
- 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 +3 -18
- package/dist/next.js +3 -28
- 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.js +3 -86
- package/dist/{plugins-1QmTmZyL.d.ts → plugins-CQ_0w1ot.d.ts} +39 -1
- package/dist/{plugins-CzUiTn_r.js → plugins-DVy_vaYr.js} +3 -1
- 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 -39
- package/dist/server.js +1 -1
- 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 +23 -3
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { i as definePlugin, n as HudApiError } from "../plugins-DVy_vaYr.js";
|
|
2
|
+
import { i as OPS_CAPABILITY, r as stateRating, t as TELEMETRY_RANGES } from "../types-QJUajwWq.js";
|
|
3
|
+
import { useCallback, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/plugins/ops/poll.ts
|
|
6
|
+
/** Entries per plugin context, so the pill and the panel share one poller per path. */
|
|
7
|
+
const entries = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
function entryFor(ctx, path) {
|
|
9
|
+
let byPath = entries.get(ctx);
|
|
10
|
+
if (!byPath) {
|
|
11
|
+
byPath = /* @__PURE__ */ new Map();
|
|
12
|
+
entries.set(ctx, byPath);
|
|
13
|
+
}
|
|
14
|
+
let entry = byPath.get(path);
|
|
15
|
+
if (!entry) {
|
|
16
|
+
entry = {
|
|
17
|
+
state: { loading: true },
|
|
18
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
19
|
+
inflight: false
|
|
20
|
+
};
|
|
21
|
+
byPath.set(path, entry);
|
|
22
|
+
}
|
|
23
|
+
return entry;
|
|
24
|
+
}
|
|
25
|
+
/** A readable message for a failed poll; 502 bodies carry the provider's own. */
|
|
26
|
+
function describeError(error) {
|
|
27
|
+
if (error instanceof HudApiError) {
|
|
28
|
+
if (error.status === 404) return "route not mounted: add ops() to createHudHandler";
|
|
29
|
+
const body = error.body;
|
|
30
|
+
if (typeof body?.error === "string") return body.error;
|
|
31
|
+
return `HTTP ${error.status}`;
|
|
32
|
+
}
|
|
33
|
+
return error instanceof Error ? error.message : "request failed";
|
|
34
|
+
}
|
|
35
|
+
function set(entry, state) {
|
|
36
|
+
entry.state = state;
|
|
37
|
+
for (const listener of entry.listeners) listener();
|
|
38
|
+
}
|
|
39
|
+
async function tick(ctx, path, entry, force = false) {
|
|
40
|
+
if (entry.inflight || !force && typeof document !== "undefined" && document.hidden) return;
|
|
41
|
+
entry.inflight = true;
|
|
42
|
+
try {
|
|
43
|
+
set(entry, {
|
|
44
|
+
data: await ctx.api(path),
|
|
45
|
+
loading: false
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
set(entry, {
|
|
49
|
+
...entry.state,
|
|
50
|
+
error: describeError(error),
|
|
51
|
+
loading: false
|
|
52
|
+
});
|
|
53
|
+
} finally {
|
|
54
|
+
entry.inflight = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Polls one of the plugin's routes while anything renders it. Pauses while
|
|
59
|
+
* the tab is hidden; subscribers of the same path share one request.
|
|
60
|
+
*/
|
|
61
|
+
function usePoll(ctx, path, intervalMs) {
|
|
62
|
+
const entry = entryFor(ctx, path);
|
|
63
|
+
const subscribe = useCallback((listener) => {
|
|
64
|
+
entry.listeners.add(listener);
|
|
65
|
+
if (entry.listeners.size === 1) {
|
|
66
|
+
tick(ctx, path, entry);
|
|
67
|
+
entry.timer = setInterval(() => void tick(ctx, path, entry), intervalMs);
|
|
68
|
+
}
|
|
69
|
+
return () => {
|
|
70
|
+
entry.listeners.delete(listener);
|
|
71
|
+
if (entry.listeners.size === 0) clearInterval(entry.timer);
|
|
72
|
+
};
|
|
73
|
+
}, [
|
|
74
|
+
ctx,
|
|
75
|
+
path,
|
|
76
|
+
entry,
|
|
77
|
+
intervalMs
|
|
78
|
+
]);
|
|
79
|
+
const state = useSyncExternalStore(subscribe, () => entry.state, () => entry.state);
|
|
80
|
+
const refresh = useCallback(() => void tick(ctx, path, entry, true), [
|
|
81
|
+
ctx,
|
|
82
|
+
path,
|
|
83
|
+
entry
|
|
84
|
+
]);
|
|
85
|
+
return {
|
|
86
|
+
...state,
|
|
87
|
+
refresh
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/plugins/ops/styles.ts
|
|
92
|
+
/** Panel styles, rendered as a `<style>` inside the panel, so inside the shadow root. */
|
|
93
|
+
const OPS_CSS = `
|
|
94
|
+
.ops { display: grid; gap: 12px; }
|
|
95
|
+
.ops section { display: grid; gap: 6px; }
|
|
96
|
+
.ops h3 {
|
|
97
|
+
margin: 0;
|
|
98
|
+
font-size: 10px;
|
|
99
|
+
font-weight: 600;
|
|
100
|
+
letter-spacing: 0.06em;
|
|
101
|
+
text-transform: uppercase;
|
|
102
|
+
color: var(--hud-fg-muted);
|
|
103
|
+
}
|
|
104
|
+
.ops .head { display: flex; align-items: center; gap: 6px; font-family: var(--hud-mono); }
|
|
105
|
+
.ops .head .branch { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
106
|
+
.ops .warn { color: var(--hud-warn); margin: 0; }
|
|
107
|
+
.ops .muted { color: var(--hud-fg-muted); }
|
|
108
|
+
.ops ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
|
|
109
|
+
.ops li a, .ops li > span {
|
|
110
|
+
display: grid;
|
|
111
|
+
grid-template-columns: 6px minmax(0, 1fr) auto auto;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
padding: 3px 4px;
|
|
115
|
+
border-radius: 4px;
|
|
116
|
+
color: inherit;
|
|
117
|
+
text-decoration: none;
|
|
118
|
+
}
|
|
119
|
+
.ops li a:hover { background: var(--hud-bg-subtle); }
|
|
120
|
+
.ops .title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
121
|
+
.ops .title small { color: var(--hud-fg-muted); margin-left: 6px; }
|
|
122
|
+
.ops .mono { font-family: var(--hud-mono); color: var(--hud-fg-muted); }
|
|
123
|
+
.ops .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
|
|
124
|
+
.ops .stat { padding: 6px 8px; border: 1px solid var(--hud-border); border-radius: 6px; }
|
|
125
|
+
.ops .stat b { display: block; font-family: var(--hud-mono); font-size: 13px; }
|
|
126
|
+
.ops .stat span { font-size: 10px; color: var(--hud-fg-muted); }
|
|
127
|
+
.ops .row { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
|
|
128
|
+
.ops .ranges { display: flex; gap: 2px; }
|
|
129
|
+
.ops button.chip { padding: 1px 6px; border-radius: 999px; font-family: var(--hud-mono); }
|
|
130
|
+
.ops button.chip:hover, .ops button.chip[aria-pressed="true"] { background: var(--hud-bg-subtle); }
|
|
131
|
+
`;
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/plugins/ops/ops-panel.tsx
|
|
134
|
+
const CI_POLL_MS = 6e4;
|
|
135
|
+
const TELEMETRY_POLL_MS = 6e4;
|
|
136
|
+
const STATE_LABEL = {
|
|
137
|
+
success: "passing",
|
|
138
|
+
failure: "failing",
|
|
139
|
+
cancelled: "cancelled",
|
|
140
|
+
running: "running",
|
|
141
|
+
queued: "queued",
|
|
142
|
+
neutral: "neutral"
|
|
143
|
+
};
|
|
144
|
+
function stateLabel(state) {
|
|
145
|
+
return state ? STATE_LABEL[state] : "no runs";
|
|
146
|
+
}
|
|
147
|
+
/** `3m`, `5h`, `2d` since `iso`. */
|
|
148
|
+
function age(iso, now = Date.now()) {
|
|
149
|
+
if (!iso) return "";
|
|
150
|
+
const seconds = Math.max(0, Math.round((now - Date.parse(iso)) / 1e3));
|
|
151
|
+
if (seconds < 60) return `${seconds}s`;
|
|
152
|
+
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
|
|
153
|
+
if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;
|
|
154
|
+
return `${Math.round(seconds / 86400)}d`;
|
|
155
|
+
}
|
|
156
|
+
function Missing({ name }) {
|
|
157
|
+
return /* @__PURE__ */ jsxs("p", {
|
|
158
|
+
className: "warn",
|
|
159
|
+
children: [
|
|
160
|
+
"Not configured: set ",
|
|
161
|
+
/* @__PURE__ */ jsx("code", { children: name ?? "its env" }),
|
|
162
|
+
" in the app's server env."
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function RunRow({ run }) {
|
|
167
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
168
|
+
/* @__PURE__ */ jsx("span", {
|
|
169
|
+
className: "dot",
|
|
170
|
+
"data-rating": stateRating(run.state),
|
|
171
|
+
title: STATE_LABEL[run.state]
|
|
172
|
+
}),
|
|
173
|
+
/* @__PURE__ */ jsxs("span", {
|
|
174
|
+
className: "title",
|
|
175
|
+
title: run.title,
|
|
176
|
+
children: [run.name, /* @__PURE__ */ jsx("small", { children: run.title })]
|
|
177
|
+
}),
|
|
178
|
+
/* @__PURE__ */ jsx("span", {
|
|
179
|
+
className: "mono",
|
|
180
|
+
children: run.sha
|
|
181
|
+
}),
|
|
182
|
+
/* @__PURE__ */ jsx("span", {
|
|
183
|
+
className: "mono",
|
|
184
|
+
children: age(run.startedAt)
|
|
185
|
+
})
|
|
186
|
+
] });
|
|
187
|
+
return /* @__PURE__ */ jsx("li", { children: run.url ? /* @__PURE__ */ jsx("a", {
|
|
188
|
+
href: run.url,
|
|
189
|
+
target: "_blank",
|
|
190
|
+
rel: "noreferrer",
|
|
191
|
+
children: content
|
|
192
|
+
}) : /* @__PURE__ */ jsx("span", { children: content }) });
|
|
193
|
+
}
|
|
194
|
+
function Deployment({ deployment }) {
|
|
195
|
+
if (!deployment) return /* @__PURE__ */ jsx("p", {
|
|
196
|
+
className: "muted",
|
|
197
|
+
children: "Nothing deployed."
|
|
198
|
+
});
|
|
199
|
+
const rows = [
|
|
200
|
+
["Environment", deployment.environment],
|
|
201
|
+
["Image", deployment.image],
|
|
202
|
+
["Sha", deployment.sha],
|
|
203
|
+
["Ready", deployment.ready],
|
|
204
|
+
["Since", deployment.at && `${age(deployment.at)} ago`]
|
|
205
|
+
];
|
|
206
|
+
return /* @__PURE__ */ jsx("table", {
|
|
207
|
+
className: "vitals",
|
|
208
|
+
children: /* @__PURE__ */ jsx("tbody", { children: rows.flatMap(([label, value]) => value ? /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", {
|
|
209
|
+
className: "label",
|
|
210
|
+
children: label
|
|
211
|
+
}), /* @__PURE__ */ jsx("td", { children: value })] }, label) : []) })
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function CiSection({ ctx }) {
|
|
215
|
+
const { data, error, loading, refresh } = usePoll(ctx, "/ci", CI_POLL_MS);
|
|
216
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("section", {
|
|
217
|
+
"aria-label": "CI",
|
|
218
|
+
children: [
|
|
219
|
+
/* @__PURE__ */ jsxs("div", {
|
|
220
|
+
className: "row",
|
|
221
|
+
children: [/* @__PURE__ */ jsxs("h3", { children: ["CI", data ? ` · ${data.repo}` : ""] }), /* @__PURE__ */ jsx("button", {
|
|
222
|
+
type: "button",
|
|
223
|
+
className: "chip",
|
|
224
|
+
onClick: refresh,
|
|
225
|
+
children: "refresh"
|
|
226
|
+
})]
|
|
227
|
+
}),
|
|
228
|
+
loading && /* @__PURE__ */ jsx("p", {
|
|
229
|
+
className: "muted",
|
|
230
|
+
children: "Loading…"
|
|
231
|
+
}),
|
|
232
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
233
|
+
className: "warn",
|
|
234
|
+
children: error
|
|
235
|
+
}),
|
|
236
|
+
data && !data.configured && /* @__PURE__ */ jsx(Missing, { name: data.missing }),
|
|
237
|
+
data?.configured && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
238
|
+
className: "head",
|
|
239
|
+
children: [
|
|
240
|
+
/* @__PURE__ */ jsx("span", {
|
|
241
|
+
className: "dot",
|
|
242
|
+
"data-rating": stateRating(data.state)
|
|
243
|
+
}),
|
|
244
|
+
/* @__PURE__ */ jsx("span", {
|
|
245
|
+
className: "branch",
|
|
246
|
+
children: data.branch ?? "no branch"
|
|
247
|
+
}),
|
|
248
|
+
/* @__PURE__ */ jsxs("span", {
|
|
249
|
+
className: "muted",
|
|
250
|
+
children: [data.branch ? stateLabel(data.state) : "latest runs, all branches", data.sha ? ` @ ${data.sha}` : ""]
|
|
251
|
+
})
|
|
252
|
+
]
|
|
253
|
+
}), data.runs.length > 0 ? /* @__PURE__ */ jsx("ul", { children: data.runs.map((run) => /* @__PURE__ */ jsx(RunRow, { run }, run.id)) }) : /* @__PURE__ */ jsxs("p", {
|
|
254
|
+
className: "muted",
|
|
255
|
+
children: [
|
|
256
|
+
"No runs",
|
|
257
|
+
data.branch ? ` on ${data.branch}` : "",
|
|
258
|
+
"."
|
|
259
|
+
]
|
|
260
|
+
})] })
|
|
261
|
+
]
|
|
262
|
+
}), data && data.deployment !== void 0 && /* @__PURE__ */ jsxs("section", {
|
|
263
|
+
"aria-label": "Deployment",
|
|
264
|
+
children: [/* @__PURE__ */ jsx("h3", { children: "Deployment" }), /* @__PURE__ */ jsx(Deployment, { deployment: data.deployment })]
|
|
265
|
+
})] });
|
|
266
|
+
}
|
|
267
|
+
function formatStat(value, unit) {
|
|
268
|
+
if (value === null) return "–";
|
|
269
|
+
return `${value.toFixed(value < 10 ? 2 : 0)}${unit}`;
|
|
270
|
+
}
|
|
271
|
+
function TelemetrySection({ ctx }) {
|
|
272
|
+
const [range, setRange] = useState("1h");
|
|
273
|
+
const { data, error, loading } = usePoll(ctx, `/telemetry?range=${range}`, TELEMETRY_POLL_MS);
|
|
274
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
275
|
+
"aria-label": "Telemetry",
|
|
276
|
+
children: [
|
|
277
|
+
/* @__PURE__ */ jsxs("div", {
|
|
278
|
+
className: "row",
|
|
279
|
+
children: [/* @__PURE__ */ jsxs("h3", { children: ["Telemetry", data ? ` · ${data.service}` : ""] }), /* @__PURE__ */ jsx("div", {
|
|
280
|
+
className: "ranges",
|
|
281
|
+
children: TELEMETRY_RANGES.map((r) => /* @__PURE__ */ jsx("button", {
|
|
282
|
+
type: "button",
|
|
283
|
+
className: "chip",
|
|
284
|
+
"aria-pressed": r === range,
|
|
285
|
+
onClick: () => setRange(r),
|
|
286
|
+
children: r
|
|
287
|
+
}, r))
|
|
288
|
+
})]
|
|
289
|
+
}),
|
|
290
|
+
loading && !data && /* @__PURE__ */ jsx("p", {
|
|
291
|
+
className: "muted",
|
|
292
|
+
children: "Loading…"
|
|
293
|
+
}),
|
|
294
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
295
|
+
className: "warn",
|
|
296
|
+
children: error
|
|
297
|
+
}),
|
|
298
|
+
data && !data.configured && /* @__PURE__ */ jsx(Missing, { name: data.missing }),
|
|
299
|
+
data?.configured && /* @__PURE__ */ jsxs("div", {
|
|
300
|
+
className: "stats",
|
|
301
|
+
children: [
|
|
302
|
+
/* @__PURE__ */ jsxs("div", {
|
|
303
|
+
className: "stat",
|
|
304
|
+
children: [/* @__PURE__ */ jsx("b", { children: formatStat(data.errorRate, "/s") }), /* @__PURE__ */ jsx("span", { children: "errors" })]
|
|
305
|
+
}),
|
|
306
|
+
/* @__PURE__ */ jsxs("div", {
|
|
307
|
+
className: "stat",
|
|
308
|
+
children: [/* @__PURE__ */ jsx("b", { children: formatStat(data.p95Ms, " ms") }), /* @__PURE__ */ jsx("span", { children: "p95" })]
|
|
309
|
+
}),
|
|
310
|
+
/* @__PURE__ */ jsxs("div", {
|
|
311
|
+
className: "stat",
|
|
312
|
+
children: [/* @__PURE__ */ jsx("b", { children: formatStat(data.requestRate, "/s") }), /* @__PURE__ */ jsx("span", { children: "requests" })]
|
|
313
|
+
})
|
|
314
|
+
]
|
|
315
|
+
})
|
|
316
|
+
]
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function createOpsPanel({ ci, telemetry }) {
|
|
320
|
+
function OpsPanel({ ctx }) {
|
|
321
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
322
|
+
className: "ops",
|
|
323
|
+
children: [
|
|
324
|
+
/* @__PURE__ */ jsx("style", { children: OPS_CSS }),
|
|
325
|
+
ci && /* @__PURE__ */ jsx(CiSection, { ctx }),
|
|
326
|
+
telemetry && /* @__PURE__ */ jsx(TelemetrySection, { ctx })
|
|
327
|
+
]
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return OpsPanel;
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/plugins/ops/index.tsx
|
|
334
|
+
function OpsPill({ ctx }) {
|
|
335
|
+
const { data, error } = usePoll(ctx, "/ci", CI_POLL_MS);
|
|
336
|
+
const label = !data && error ? "error" : !data ? "–" : !data.configured ? "off" : stateLabel(data.state);
|
|
337
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
338
|
+
className: "dot",
|
|
339
|
+
"data-rating": data ? stateRating(data.state) : void 0
|
|
340
|
+
}), `CI ${label}`] });
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* CI and telemetry: the pill shows the CI state of the branch the server runs
|
|
344
|
+
* on, the panel lists recent runs, the deployment (when the CI provider knows
|
|
345
|
+
* it) and error rate, p95 and request rate. Client half; the providers live in
|
|
346
|
+
* the server half, `@hansenexus/hud/plugins/ops/server`, which reads tokens
|
|
347
|
+
* from env and never sends them to the browser.
|
|
348
|
+
*/
|
|
349
|
+
function ops({ ci = true, telemetry = true } = {}) {
|
|
350
|
+
return definePlugin({
|
|
351
|
+
id: "ops",
|
|
352
|
+
title: "Ops",
|
|
353
|
+
capability: OPS_CAPABILITY,
|
|
354
|
+
panel: createOpsPanel({
|
|
355
|
+
ci,
|
|
356
|
+
telemetry
|
|
357
|
+
}),
|
|
358
|
+
...ci ? { pill: OpsPill } : {}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
//#endregion
|
|
362
|
+
export { ops };
|
package/dist/plugins/vitals.js
CHANGED
|
@@ -1,90 +1,7 @@
|
|
|
1
|
-
import { i as definePlugin } from "../plugins-
|
|
2
|
-
import {
|
|
1
|
+
import { i as definePlugin } from "../plugins-DVy_vaYr.js";
|
|
2
|
+
import { i as useVitals, n as formatVital, r as startVitals, t as VitalsPanel } from "../vitals-panel-Dvc5dnR-.js";
|
|
3
|
+
import { useEffect } from "react";
|
|
3
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
-
import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
|
|
5
|
-
//#region src/plugins/vitals/store.ts
|
|
6
|
-
/** Display order: the three Core Web Vitals first, then the diagnostics. */
|
|
7
|
-
const VITAL_ORDER = [
|
|
8
|
-
"LCP",
|
|
9
|
-
"INP",
|
|
10
|
-
"CLS",
|
|
11
|
-
"FCP",
|
|
12
|
-
"TTFB"
|
|
13
|
-
];
|
|
14
|
-
let snapshot = {};
|
|
15
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
16
|
-
let started = false;
|
|
17
|
-
function record(metric) {
|
|
18
|
-
snapshot = {
|
|
19
|
-
...snapshot,
|
|
20
|
-
[metric.name]: {
|
|
21
|
-
name: metric.name,
|
|
22
|
-
value: metric.value,
|
|
23
|
-
rating: metric.rating
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
for (const listener of listeners) listener();
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Starts collection once per page. Called when the HUD mounts, not when the
|
|
30
|
-
* panel opens: LCP and FCP are only reported for the initial load, so a lazy
|
|
31
|
-
* start would miss them.
|
|
32
|
-
*/
|
|
33
|
-
function startVitals() {
|
|
34
|
-
if (started) return;
|
|
35
|
-
started = true;
|
|
36
|
-
const opts = { reportAllChanges: true };
|
|
37
|
-
try {
|
|
38
|
-
onLCP(record, opts);
|
|
39
|
-
onINP(record, opts);
|
|
40
|
-
onCLS(record, opts);
|
|
41
|
-
onFCP(record, opts);
|
|
42
|
-
onTTFB(record, opts);
|
|
43
|
-
} catch {}
|
|
44
|
-
}
|
|
45
|
-
function getVitals() {
|
|
46
|
-
return snapshot;
|
|
47
|
-
}
|
|
48
|
-
function subscribeVitals(listener) {
|
|
49
|
-
listeners.add(listener);
|
|
50
|
-
return () => listeners.delete(listener);
|
|
51
|
-
}
|
|
52
|
-
function useVitals() {
|
|
53
|
-
return useSyncExternalStore(subscribeVitals, getVitals, getVitals);
|
|
54
|
-
}
|
|
55
|
-
function formatVital(name, value) {
|
|
56
|
-
if (name === "CLS") return value.toFixed(2);
|
|
57
|
-
if (value >= 1e3) return `${(value / 1e3).toFixed(1)} s`;
|
|
58
|
-
return `${Math.round(value)} ms`;
|
|
59
|
-
}
|
|
60
|
-
//#endregion
|
|
61
|
-
//#region src/plugins/vitals/vitals-panel.tsx
|
|
62
|
-
function VitalsPanel() {
|
|
63
|
-
const vitals = useVitals();
|
|
64
|
-
const rows = VITAL_ORDER.flatMap((name) => {
|
|
65
|
-
const vital = vitals[name];
|
|
66
|
-
return vital ? [vital] : [];
|
|
67
|
-
});
|
|
68
|
-
if (rows.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
69
|
-
className: "empty",
|
|
70
|
-
children: "No metrics yet. LCP reports after the first interaction or tab switch."
|
|
71
|
-
});
|
|
72
|
-
return /* @__PURE__ */ jsx("table", {
|
|
73
|
-
className: "vitals",
|
|
74
|
-
children: /* @__PURE__ */ jsx("tbody", { children: rows.map((vital) => /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", {
|
|
75
|
-
className: "label",
|
|
76
|
-
children: vital.name
|
|
77
|
-
}), /* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx("span", {
|
|
78
|
-
className: "dot",
|
|
79
|
-
"data-rating": vital.rating,
|
|
80
|
-
style: {
|
|
81
|
-
display: "inline-block",
|
|
82
|
-
marginRight: 6
|
|
83
|
-
}
|
|
84
|
-
}), formatVital(vital.name, vital.value)] })] }, vital.name)) })
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
//#endregion
|
|
88
5
|
//#region src/plugins/vitals/index.tsx
|
|
89
6
|
function VitalsPill() {
|
|
90
7
|
useEffect(() => {
|
|
@@ -1,4 +1,24 @@
|
|
|
1
|
+
import { r as GrabPayload } from "./payload-CLKDVZWl.js";
|
|
1
2
|
import { ComponentType } from "react";
|
|
3
|
+
//#region src/core/grabber/store.d.ts
|
|
4
|
+
interface HudGrabState {
|
|
5
|
+
/** The picker is active: the next click on the page completes the grab. */
|
|
6
|
+
grabbing: boolean;
|
|
7
|
+
/** The last completed grab, until a plugin clears it. */
|
|
8
|
+
payload: GrabPayload | null;
|
|
9
|
+
}
|
|
10
|
+
/** Grab mode for one HUD, shared by the shell and every plugin's `ctx.grab`. */
|
|
11
|
+
interface HudGrab {
|
|
12
|
+
start: () => void;
|
|
13
|
+
cancel: () => void;
|
|
14
|
+
/** Forgets the last payload, e.g. once it has been dispatched. */
|
|
15
|
+
clear: () => void;
|
|
16
|
+
getState: () => HudGrabState;
|
|
17
|
+
subscribe: (listener: () => void) => () => void;
|
|
18
|
+
}
|
|
19
|
+
/** Subscribes a component to a HUD's grab state: `useGrab(ctx.grab)`. */
|
|
20
|
+
declare function useGrab(grab: HudGrab): HudGrabState;
|
|
21
|
+
//#endregion
|
|
2
22
|
//#region src/core/plugins.d.ts
|
|
3
23
|
/**
|
|
4
24
|
* A capability a plugin needs, such as `"cicd.read"`. Core assumes development
|
|
@@ -21,6 +41,10 @@ interface HudPluginContext {
|
|
|
21
41
|
pin: (edge: PinEdge) => void;
|
|
22
42
|
/** Closes this plugin's panel. */
|
|
23
43
|
close: () => void;
|
|
44
|
+
/** Opens this plugin's panel. */
|
|
45
|
+
open: () => void;
|
|
46
|
+
/** The HUD's element grabber: start a grab, read or clear the last payload. See `useGrab`. */
|
|
47
|
+
grab: HudGrab;
|
|
24
48
|
}
|
|
25
49
|
interface HudPluginProps {
|
|
26
50
|
ctx: HudPluginContext;
|
|
@@ -63,7 +87,21 @@ interface HudPlugin {
|
|
|
63
87
|
/** Live content inside the plugin's pill item. Keep it inline and non-interactive. */
|
|
64
88
|
pill?: ComponentType<HudPluginProps>;
|
|
65
89
|
commands?: readonly HudCommand[];
|
|
90
|
+
/** Called when a grab (⌘⇧G or the pill's crosshair) completes. */
|
|
91
|
+
onGrab?: (payload: GrabPayload, ctx: HudPluginContext) => void;
|
|
66
92
|
routes?: HudRoutes;
|
|
93
|
+
/** Opens deployed tiers (the `access` plugin). Without one the HUD renders only in development. */
|
|
94
|
+
access?: HudAccessControl;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* What the `access` plugin hands `defineHud`. `env` is the loader's tier
|
|
98
|
+
* (`development`, `preview`, `staging`, `production`, `disabled`).
|
|
99
|
+
*/
|
|
100
|
+
interface HudAccessControl {
|
|
101
|
+
/** Capability check for the tier; outside development it narrows `can`. */
|
|
102
|
+
can: (env: string, capability: HudCapability) => boolean;
|
|
103
|
+
/** Hook: whether the operator has unlocked this tier. Until then only the access plugin shows. */
|
|
104
|
+
useUnlocked: (env: string, apiBase: string) => boolean;
|
|
67
105
|
}
|
|
68
106
|
/** Identity helper that types a plugin object without widening it. */
|
|
69
107
|
declare function definePlugin<const P extends HudPlugin>(plugin: P): P;
|
|
@@ -73,4 +111,4 @@ declare class HudApiError extends Error {
|
|
|
73
111
|
constructor(status: number, body: unknown, url: string);
|
|
74
112
|
}
|
|
75
113
|
//#endregion
|
|
76
|
-
export {
|
|
114
|
+
export { HudHttpMethod as a, HudPluginProps as c, HudRoutes as d, PinEdge as f, useGrab as g, HudGrabState as h, HudCommand as i, HudRouteContext as l, HudGrab as m, HudApiError as n, HudPlugin as o, definePlugin as p, HudCapability as r, HudPluginContext as s, HudAccessControl as t, HudRouteHandler as u };
|
|
@@ -34,13 +34,15 @@ var HudApiError = class extends Error {
|
|
|
34
34
|
this.name = "HudApiError";
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
|
-
function createPluginContext(plugin, { app, apiBase, can, pin, close }) {
|
|
37
|
+
function createPluginContext(plugin, { app, apiBase, can, pin, close, open, grab }) {
|
|
38
38
|
const prefix = `${apiBase.replace(/\/+$/, "")}/${plugin.id}`;
|
|
39
39
|
return {
|
|
40
40
|
app,
|
|
41
41
|
can,
|
|
42
42
|
pin,
|
|
43
43
|
close,
|
|
44
|
+
open,
|
|
45
|
+
grab,
|
|
44
46
|
api: async (path, init) => {
|
|
45
47
|
const url = `${prefix}/${path.replace(/^\/+/, "")}`;
|
|
46
48
|
const headers = new Headers(init?.headers);
|