@hansenexus/hud 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/LICENSE +21 -0
- package/README.md +48 -0
- package/chunk-0cj8dkk7.js +127 -0
- package/chunk-1atxwdhw.js +48 -0
- package/chunk-24vrg16w.js +39 -0
- package/chunk-3j9sm249.js +26 -0
- package/chunk-50pq4sg6.js +41 -0
- package/chunk-5gtx3pza.js +9 -0
- package/chunk-6gw4x08j.js +264 -0
- package/chunk-6jt8yg4m.js +117 -0
- package/chunk-71se605m.js +85 -0
- package/chunk-82nm9wcg.js +316 -0
- package/chunk-8tam7r3h.js +68 -0
- package/chunk-babjpqkb.js +52 -0
- package/chunk-c5y856ad.js +98 -0
- package/chunk-ghxh8926.js +172 -0
- package/chunk-hc2g0n35.js +24 -0
- package/chunk-k0vr8dsy.js +179 -0
- package/chunk-k50yfp5d.js +166 -0
- package/chunk-kyeb6qeq.js +95 -0
- package/chunk-msqkkqw7.js +140 -0
- package/chunk-nm82wr8r.js +58 -0
- package/chunk-r06bpmy6.js +79 -0
- package/chunk-t9eh4t0b.js +89 -0
- package/chunk-tf774c4w.js +115 -0
- package/chunk-v086j53b.js +19 -0
- package/chunk-wspz08q7.js +67 -0
- package/chunk-xezxydm3.js +47 -0
- package/chunk-xjn0896g.js +574 -0
- package/chunk-xztw0xyt.js +113 -0
- package/chunk-zzrpd92r.js +172 -0
- package/core/access.d.mts +53 -0
- package/core/access.js +27 -0
- package/core/api-types.d.mts +146 -0
- package/core/api-types.js +9 -0
- package/core/capabilities.d.mts +27 -0
- package/core/capabilities.js +14 -0
- package/core/capture-buffer.d.mts +37 -0
- package/core/capture-buffer.js +9 -0
- package/core/force-state.d.mts +68 -0
- package/core/force-state.js +21 -0
- package/core/mock-cookie.d.mts +30 -0
- package/core/mock-cookie.js +15 -0
- package/core/types.d.mts +59 -0
- package/core/types.js +9 -0
- package/hud-provider.d.mts +38 -0
- package/hud-provider.js +15 -0
- package/hud.d.mts +15 -0
- package/hud.js +18 -0
- package/index.d.mts +94 -0
- package/index.js +170 -0
- package/lazy-panel.d.mts +28 -0
- package/lazy-panel.js +11 -0
- package/package.json +153 -0
- package/panels/cicd-panel.d.mts +9 -0
- package/panels/cicd-panel.js +13 -0
- package/panels/debug-info-panel.d.mts +21 -0
- package/panels/debug-info-panel.js +9 -0
- package/panels/dispatch-panel.d.mts +10 -0
- package/panels/dispatch-panel.js +14 -0
- package/panels/env-info-panel.d.mts +9 -0
- package/panels/env-info-panel.js +12 -0
- package/panels/feature-flags-panel.d.mts +26 -0
- package/panels/feature-flags-panel.js +9 -0
- package/panels/force-state-panel.d.mts +11 -0
- package/panels/force-state-panel.js +113 -0
- package/panels/info-row.d.mts +21 -0
- package/panels/info-row.js +14 -0
- package/panels/mock-seed-panel.d.mts +15 -0
- package/panels/mock-seed-panel.js +14 -0
- package/panels/network-console-panel.d.mts +6 -0
- package/panels/network-console-panel.js +9 -0
- package/panels/role-switcher-panel.d.mts +28 -0
- package/panels/role-switcher-panel.js +9 -0
- package/panels/sessions-panel.d.mts +6 -0
- package/panels/sessions-panel.js +13 -0
- package/panels/tokens-panel.d.mts +38 -0
- package/panels/tokens-panel.js +9 -0
- package/panels/unlock-panel.d.mts +33 -0
- package/panels/unlock-panel.js +14 -0
- package/panels/variants-panel.d.mts +51 -0
- package/panels/variants-panel.js +12 -0
- package/panels/vitals-panel.d.mts +11 -0
- package/panels/vitals-panel.js +15 -0
- package/server/guard.d.mts +42 -0
- package/server/guard.js +12 -0
- package/server/unlock.d.mts +28 -0
- package/server/unlock.js +108 -0
- package/state-boundary.d.mts +25 -0
- package/state-boundary.js +54 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// core/force-state.ts
|
|
2
|
+
var FORCED_STATES = ["loading", "empty", "error"];
|
|
3
|
+
var FORCE_STATE_PARAM = "__state";
|
|
4
|
+
var FORCE_STATE_MARKER = "__HN_FORCE_STATE__";
|
|
5
|
+
function isForcedState(value) {
|
|
6
|
+
return FORCED_STATES.includes(value);
|
|
7
|
+
}
|
|
8
|
+
function parseForceStateParam(value) {
|
|
9
|
+
if (!value)
|
|
10
|
+
return null;
|
|
11
|
+
const separator = value.lastIndexOf(":");
|
|
12
|
+
if (separator === -1)
|
|
13
|
+
return isForcedState(value) ? { state: value, target: null } : null;
|
|
14
|
+
const target = value.slice(0, separator);
|
|
15
|
+
const state = value.slice(separator + 1);
|
|
16
|
+
if (!target || !isForcedState(state))
|
|
17
|
+
return null;
|
|
18
|
+
return { state, target };
|
|
19
|
+
}
|
|
20
|
+
function forceStateParamFromSearch(search) {
|
|
21
|
+
return parseForceStateParam(new URLSearchParams(search).get(FORCE_STATE_PARAM));
|
|
22
|
+
}
|
|
23
|
+
function resolveForcedState(snapshot, boundary) {
|
|
24
|
+
const override = snapshot.overrides[boundary.id];
|
|
25
|
+
if (override)
|
|
26
|
+
return override;
|
|
27
|
+
const { param } = snapshot;
|
|
28
|
+
if (!param)
|
|
29
|
+
return null;
|
|
30
|
+
if (param.target !== null)
|
|
31
|
+
return param.target === boundary.id ? param.state : null;
|
|
32
|
+
return boundary.parentId === null ? param.state : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class ForceStateStore {
|
|
36
|
+
registrations = new Map;
|
|
37
|
+
overrides = {};
|
|
38
|
+
search = "";
|
|
39
|
+
snapshot = { boundaries: [], overrides: {}, param: null };
|
|
40
|
+
listeners = new Set;
|
|
41
|
+
getSnapshot = () => this.snapshot;
|
|
42
|
+
subscribe = (listener) => {
|
|
43
|
+
this.listeners.add(listener);
|
|
44
|
+
return () => {
|
|
45
|
+
this.listeners.delete(listener);
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
register(id, parentId) {
|
|
49
|
+
const existing = this.registrations.get(id);
|
|
50
|
+
this.registrations.set(id, { parentId, count: (existing?.count ?? 0) + 1 });
|
|
51
|
+
this.emit();
|
|
52
|
+
return () => {
|
|
53
|
+
const current = this.registrations.get(id);
|
|
54
|
+
if (!current)
|
|
55
|
+
return;
|
|
56
|
+
if (current.count > 1)
|
|
57
|
+
this.registrations.set(id, { ...current, count: current.count - 1 });
|
|
58
|
+
else
|
|
59
|
+
this.registrations.delete(id);
|
|
60
|
+
this.emit();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
force(id, state) {
|
|
64
|
+
const next = { ...this.overrides };
|
|
65
|
+
if (state)
|
|
66
|
+
next[id] = state;
|
|
67
|
+
else
|
|
68
|
+
delete next[id];
|
|
69
|
+
this.overrides = next;
|
|
70
|
+
this.emit();
|
|
71
|
+
}
|
|
72
|
+
clearOverrides() {
|
|
73
|
+
this.overrides = {};
|
|
74
|
+
this.emit();
|
|
75
|
+
}
|
|
76
|
+
syncSearch(search) {
|
|
77
|
+
if (search === this.search)
|
|
78
|
+
return;
|
|
79
|
+
this.search = search;
|
|
80
|
+
this.emit();
|
|
81
|
+
}
|
|
82
|
+
emit() {
|
|
83
|
+
this.snapshot = {
|
|
84
|
+
boundaries: [...this.registrations].map(([id, { parentId }]) => ({ id, parentId })),
|
|
85
|
+
overrides: this.overrides,
|
|
86
|
+
param: forceStateParamFromSearch(this.search)
|
|
87
|
+
};
|
|
88
|
+
for (const listener of this.listeners)
|
|
89
|
+
listener();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function getForceStateStore() {
|
|
93
|
+
const holder = globalThis;
|
|
94
|
+
holder[FORCE_STATE_MARKER] ??= new ForceStateStore;
|
|
95
|
+
return holder[FORCE_STATE_MARKER];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { FORCED_STATES, FORCE_STATE_PARAM, FORCE_STATE_MARKER, parseForceStateParam, forceStateParamFromSearch, resolveForcedState, ForceStateStore, getForceStateStore };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
usePoll
|
|
3
|
+
} from "./chunk-xezxydm3.js";
|
|
4
|
+
import {
|
|
5
|
+
useHud
|
|
6
|
+
} from "./chunk-wspz08q7.js";
|
|
7
|
+
import {
|
|
8
|
+
LoadingSpinner,
|
|
9
|
+
PanelSection
|
|
10
|
+
} from "./chunk-kyeb6qeq.js";
|
|
11
|
+
import {
|
|
12
|
+
Badge
|
|
13
|
+
} from "./chunk-xztw0xyt.js";
|
|
14
|
+
import {
|
|
15
|
+
__require
|
|
16
|
+
} from "./chunk-5gtx3pza.js";
|
|
17
|
+
|
|
18
|
+
// panels/vitals-panel.tsx
|
|
19
|
+
import { Activity, ServerCrash } from "lucide-react";
|
|
20
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
21
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
22
|
+
"use client";
|
|
23
|
+
var VITAL_ORDER = ["LCP", "INP", "CLS", "TTFB", "FCP"];
|
|
24
|
+
var vitals = new Map;
|
|
25
|
+
var vitalsSnapshot = new Map;
|
|
26
|
+
var vitalListeners = new Set;
|
|
27
|
+
var vitalsStarted = false;
|
|
28
|
+
function recordVital(name, value, rating) {
|
|
29
|
+
vitals.set(name, { value, rating });
|
|
30
|
+
vitalsSnapshot = new Map(vitals);
|
|
31
|
+
for (const listener of vitalListeners)
|
|
32
|
+
listener();
|
|
33
|
+
}
|
|
34
|
+
function startVitals() {
|
|
35
|
+
if (vitalsStarted || typeof window === "undefined")
|
|
36
|
+
return;
|
|
37
|
+
vitalsStarted = true;
|
|
38
|
+
import("web-vitals").then(({ onCLS, onFCP, onLCP, onTTFB, onINP }) => {
|
|
39
|
+
const report = (metric) => recordVital(metric.name, metric.value, metric.rating);
|
|
40
|
+
onCLS(report, { reportAllChanges: true });
|
|
41
|
+
onINP(report, { reportAllChanges: true });
|
|
42
|
+
onLCP(report);
|
|
43
|
+
onFCP(report);
|
|
44
|
+
onTTFB(report);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function subscribeVitals(listener) {
|
|
48
|
+
vitalListeners.add(listener);
|
|
49
|
+
return () => vitalListeners.delete(listener);
|
|
50
|
+
}
|
|
51
|
+
function getVitalsSnapshot() {
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const [name, sample] of vitals)
|
|
54
|
+
out[name] = Number(sample.value.toFixed(2));
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function formatVital(name, value) {
|
|
58
|
+
if (name === "CLS")
|
|
59
|
+
return value.toFixed(3);
|
|
60
|
+
if (value >= 1000)
|
|
61
|
+
return `${(value / 1000).toFixed(2)} s`;
|
|
62
|
+
return `${Math.round(value)} ms`;
|
|
63
|
+
}
|
|
64
|
+
var RATING_CLASS = {
|
|
65
|
+
good: "text-emerald-500",
|
|
66
|
+
"needs-improvement": "text-amber-500",
|
|
67
|
+
poor: "text-red-500"
|
|
68
|
+
};
|
|
69
|
+
function VitalsPanel() {
|
|
70
|
+
const { config } = useHud();
|
|
71
|
+
const samples = useSyncExternalStore(subscribeVitals, () => vitalsSnapshot, () => vitalsSnapshot);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
startVitals();
|
|
74
|
+
}, []);
|
|
75
|
+
const apiBase = config.apiBase ?? "/api/hud";
|
|
76
|
+
const signoz = usePoll(`${apiBase}/signoz?service=${encodeURIComponent(config.appSlug)}`, 60000);
|
|
77
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
78
|
+
className: "space-y-4",
|
|
79
|
+
children: [
|
|
80
|
+
/* @__PURE__ */ jsxs(PanelSection, {
|
|
81
|
+
icon: Activity,
|
|
82
|
+
title: "Web Vitals · this page load",
|
|
83
|
+
children: [
|
|
84
|
+
/* @__PURE__ */ jsx("div", {
|
|
85
|
+
className: "flex flex-wrap gap-2 px-2",
|
|
86
|
+
children: VITAL_ORDER.map((name) => {
|
|
87
|
+
const sample = samples.get(name);
|
|
88
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
89
|
+
className: "min-w-[92px] rounded-md border bg-muted/20 px-3 py-2",
|
|
90
|
+
children: [
|
|
91
|
+
/* @__PURE__ */ jsx("div", {
|
|
92
|
+
className: "text-[10px] uppercase tracking-wider text-muted-foreground",
|
|
93
|
+
children: name
|
|
94
|
+
}),
|
|
95
|
+
/* @__PURE__ */ jsx("div", {
|
|
96
|
+
className: `font-mono text-sm font-semibold ${sample ? RATING_CLASS[sample.rating] : "text-muted-foreground/50"}`,
|
|
97
|
+
children: sample ? formatVital(name, sample.value) : "—"
|
|
98
|
+
})
|
|
99
|
+
]
|
|
100
|
+
}, name);
|
|
101
|
+
})
|
|
102
|
+
}),
|
|
103
|
+
/* @__PURE__ */ jsx("p", {
|
|
104
|
+
className: "px-2 text-[10px] text-muted-foreground/60",
|
|
105
|
+
children: "CLS/INP update live; LCP/FCP/TTFB are per page load. History lands in SigNoz via /api/otel/vitals."
|
|
106
|
+
})
|
|
107
|
+
]
|
|
108
|
+
}),
|
|
109
|
+
/* @__PURE__ */ jsxs(PanelSection, {
|
|
110
|
+
icon: ServerCrash,
|
|
111
|
+
title: `Server · ${config.appSlug} (1h)`,
|
|
112
|
+
children: [
|
|
113
|
+
signoz.loading && /* @__PURE__ */ jsx(LoadingSpinner, {}),
|
|
114
|
+
signoz.error && /* @__PURE__ */ jsxs("p", {
|
|
115
|
+
className: "px-2 text-[10px] text-amber-500",
|
|
116
|
+
children: [
|
|
117
|
+
"hub unreachable — ",
|
|
118
|
+
signoz.error
|
|
119
|
+
]
|
|
120
|
+
}),
|
|
121
|
+
signoz.data && /* @__PURE__ */ jsxs("div", {
|
|
122
|
+
className: "space-y-1 px-2",
|
|
123
|
+
children: [
|
|
124
|
+
/* @__PURE__ */ jsxs("div", {
|
|
125
|
+
className: "flex items-center gap-2",
|
|
126
|
+
children: [
|
|
127
|
+
/* @__PURE__ */ jsx(ServerStat, {
|
|
128
|
+
label: "error rate",
|
|
129
|
+
value: signoz.data.errorRate,
|
|
130
|
+
unit: "/s"
|
|
131
|
+
}),
|
|
132
|
+
/* @__PURE__ */ jsx(ServerStat, {
|
|
133
|
+
label: "p95",
|
|
134
|
+
value: signoz.data.p95Latency,
|
|
135
|
+
unit: " ms"
|
|
136
|
+
}),
|
|
137
|
+
/* @__PURE__ */ jsx(ServerStat, {
|
|
138
|
+
label: "req rate",
|
|
139
|
+
value: signoz.data.requestCount,
|
|
140
|
+
unit: "/s"
|
|
141
|
+
})
|
|
142
|
+
]
|
|
143
|
+
}),
|
|
144
|
+
signoz.data.source === "mock" && /* @__PURE__ */ jsx(Badge, {
|
|
145
|
+
variant: "outline",
|
|
146
|
+
className: "h-4 px-1.5 text-[9px] text-muted-foreground",
|
|
147
|
+
children: "mock data — SIGNOZ_QUERY_URL unset"
|
|
148
|
+
})
|
|
149
|
+
]
|
|
150
|
+
})
|
|
151
|
+
]
|
|
152
|
+
})
|
|
153
|
+
]
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function ServerStat({ label, value, unit }) {
|
|
157
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
158
|
+
className: "min-w-[92px] rounded-md border bg-muted/20 px-3 py-2",
|
|
159
|
+
children: [
|
|
160
|
+
/* @__PURE__ */ jsx("div", {
|
|
161
|
+
className: "text-[10px] uppercase tracking-wider text-muted-foreground",
|
|
162
|
+
children: label
|
|
163
|
+
}),
|
|
164
|
+
/* @__PURE__ */ jsx("div", {
|
|
165
|
+
className: "font-mono text-sm font-semibold",
|
|
166
|
+
children: value === null ? "—" : `${value.toFixed(value < 10 ? 2 : 0)}${unit}`
|
|
167
|
+
})
|
|
168
|
+
]
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { getVitalsSnapshot, VitalsPanel };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// core/mock-cookie.ts
|
|
2
|
+
var HUD_MOCK_COOKIE = "hud-mock";
|
|
3
|
+
function isHudMockEnabled(cookieValue, env) {
|
|
4
|
+
const hudAllowed = env.nodeEnv === "development" || env.hudEnabled === "true";
|
|
5
|
+
return hudAllowed && cookieValue === "1";
|
|
6
|
+
}
|
|
7
|
+
function hudMockCookieString(enabled) {
|
|
8
|
+
if (!enabled)
|
|
9
|
+
return `${HUD_MOCK_COOKIE}=; path=/; max-age=0; samesite=lax`;
|
|
10
|
+
return `${HUD_MOCK_COOKIE}=1; path=/; max-age=${60 * 60 * 24 * 30}; samesite=lax`;
|
|
11
|
+
}
|
|
12
|
+
function readHudMockCookie() {
|
|
13
|
+
if (typeof document === "undefined")
|
|
14
|
+
return;
|
|
15
|
+
const match = document.cookie.split("; ").find((part) => part.startsWith(`${HUD_MOCK_COOKIE}=`));
|
|
16
|
+
return match?.slice(HUD_MOCK_COOKIE.length + 1);
|
|
17
|
+
}
|
|
18
|
+
function setHudMockCookie(enabled) {
|
|
19
|
+
if (typeof document === "undefined")
|
|
20
|
+
return;
|
|
21
|
+
document.cookie = hudMockCookieString(enabled);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { HUD_MOCK_COOKIE, isHudMockEnabled, hudMockCookieString, readHudMockCookie, setHudMockCookie };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bridgeFetch,
|
|
3
|
+
getBridgeInfo
|
|
4
|
+
} from "./chunk-50pq4sg6.js";
|
|
5
|
+
import {
|
|
6
|
+
useHud
|
|
7
|
+
} from "./chunk-wspz08q7.js";
|
|
8
|
+
import {
|
|
9
|
+
LoadingSpinner,
|
|
10
|
+
PanelSection
|
|
11
|
+
} from "./chunk-kyeb6qeq.js";
|
|
12
|
+
import {
|
|
13
|
+
Badge,
|
|
14
|
+
Button,
|
|
15
|
+
Switch
|
|
16
|
+
} from "./chunk-xztw0xyt.js";
|
|
17
|
+
import {
|
|
18
|
+
readHudMockCookie,
|
|
19
|
+
setHudMockCookie
|
|
20
|
+
} from "./chunk-hc2g0n35.js";
|
|
21
|
+
|
|
22
|
+
// panels/mock-seed-panel.tsx
|
|
23
|
+
import { Database, FlaskConical, RefreshCw } from "lucide-react";
|
|
24
|
+
import { useEffect, useState } from "react";
|
|
25
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
26
|
+
"use client";
|
|
27
|
+
function MockSeedPanel({ seeds = [] }) {
|
|
28
|
+
const { config, can } = useHud();
|
|
29
|
+
const [mockCookie, setMockCookie] = useState(undefined);
|
|
30
|
+
const [bridge, setBridge] = useState(undefined);
|
|
31
|
+
const [runs, setRuns] = useState([]);
|
|
32
|
+
const apiBase = config.apiBase ?? "/api/hud";
|
|
33
|
+
const canSeed = can("inject.seed") && seeds.length > 0;
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setMockCookie(readHudMockCookie());
|
|
36
|
+
}, []);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!canSeed)
|
|
39
|
+
return;
|
|
40
|
+
let cancelled = false;
|
|
41
|
+
getBridgeInfo(apiBase).then((info) => {
|
|
42
|
+
if (!cancelled)
|
|
43
|
+
setBridge(info);
|
|
44
|
+
});
|
|
45
|
+
return () => {
|
|
46
|
+
cancelled = true;
|
|
47
|
+
};
|
|
48
|
+
}, [apiBase, canSeed]);
|
|
49
|
+
const mockOn = mockCookie === "1";
|
|
50
|
+
function toggleMock(next) {
|
|
51
|
+
setHudMockCookie(next);
|
|
52
|
+
setMockCookie(next ? "1" : undefined);
|
|
53
|
+
window.location.reload();
|
|
54
|
+
}
|
|
55
|
+
async function runSeed(seed) {
|
|
56
|
+
if (!bridge)
|
|
57
|
+
return;
|
|
58
|
+
const runId = `${seed.id}-${Date.now()}`;
|
|
59
|
+
setRuns((prev) => [{ id: runId, label: seed.label, status: "running" }, ...prev]);
|
|
60
|
+
try {
|
|
61
|
+
const res = await bridgeFetch(bridge, "/seed", {
|
|
62
|
+
method: "POST",
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
app: config.appSlug,
|
|
65
|
+
fn: seed.fn,
|
|
66
|
+
args: seed.args,
|
|
67
|
+
local: seed.local
|
|
68
|
+
})
|
|
69
|
+
});
|
|
70
|
+
const body = await res.json();
|
|
71
|
+
setRuns((prev) => prev.map((run) => run.id === runId ? { ...run, status: body.ok ? "done" : "failed", detail: body.output ?? body.error } : run));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
setRuns((prev) => prev.map((run) => run.id === runId ? {
|
|
74
|
+
...run,
|
|
75
|
+
status: "failed",
|
|
76
|
+
detail: error instanceof Error ? error.message : "seed failed"
|
|
77
|
+
} : run));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
81
|
+
className: "space-y-4",
|
|
82
|
+
children: [
|
|
83
|
+
can("mock.toggle") && /* @__PURE__ */ jsx(PanelSection, {
|
|
84
|
+
icon: FlaskConical,
|
|
85
|
+
title: "Mock Convex",
|
|
86
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
87
|
+
className: "flex items-center justify-between px-2",
|
|
88
|
+
children: [
|
|
89
|
+
/* @__PURE__ */ jsxs("div", {
|
|
90
|
+
children: [
|
|
91
|
+
/* @__PURE__ */ jsx("div", {
|
|
92
|
+
className: "text-xs font-medium",
|
|
93
|
+
children: "Mock backend"
|
|
94
|
+
}),
|
|
95
|
+
/* @__PURE__ */ jsx("p", {
|
|
96
|
+
className: "text-[10px] text-muted-foreground/60",
|
|
97
|
+
children: "Swaps the Convex client for a dummy — reload applies (server re-reads the cookie)."
|
|
98
|
+
})
|
|
99
|
+
]
|
|
100
|
+
}),
|
|
101
|
+
/* @__PURE__ */ jsxs("div", {
|
|
102
|
+
className: "flex items-center gap-2",
|
|
103
|
+
children: [
|
|
104
|
+
mockOn && /* @__PURE__ */ jsx(Badge, {
|
|
105
|
+
variant: "outline",
|
|
106
|
+
className: "h-4 px-1.5 text-[9px] text-amber-500",
|
|
107
|
+
children: "mock"
|
|
108
|
+
}),
|
|
109
|
+
/* @__PURE__ */ jsx(Switch, {
|
|
110
|
+
checked: mockOn,
|
|
111
|
+
onCheckedChange: toggleMock
|
|
112
|
+
})
|
|
113
|
+
]
|
|
114
|
+
})
|
|
115
|
+
]
|
|
116
|
+
})
|
|
117
|
+
}),
|
|
118
|
+
canSeed && /* @__PURE__ */ jsxs(PanelSection, {
|
|
119
|
+
icon: Database,
|
|
120
|
+
title: "Seed data",
|
|
121
|
+
children: [
|
|
122
|
+
bridge === undefined && /* @__PURE__ */ jsx(LoadingSpinner, {}),
|
|
123
|
+
bridge === null && /* @__PURE__ */ jsx("p", {
|
|
124
|
+
className: "px-2 text-[10px] text-amber-500",
|
|
125
|
+
children: "bridge not reachable — run `bun hud:bridge` in the repo to seed from here."
|
|
126
|
+
}),
|
|
127
|
+
bridge != null && /* @__PURE__ */ jsx("div", {
|
|
128
|
+
className: "space-y-1 px-2",
|
|
129
|
+
children: seeds.map((seed) => /* @__PURE__ */ jsxs("div", {
|
|
130
|
+
className: "flex items-center gap-2",
|
|
131
|
+
children: [
|
|
132
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
133
|
+
size: "sm",
|
|
134
|
+
variant: "outline",
|
|
135
|
+
className: "h-6 text-[11px]",
|
|
136
|
+
onClick: () => runSeed(seed),
|
|
137
|
+
children: [
|
|
138
|
+
/* @__PURE__ */ jsx(RefreshCw, {
|
|
139
|
+
className: "mr-1.5 h-3 w-3"
|
|
140
|
+
}),
|
|
141
|
+
seed.label
|
|
142
|
+
]
|
|
143
|
+
}),
|
|
144
|
+
seed.description && /* @__PURE__ */ jsx("span", {
|
|
145
|
+
className: "min-w-0 flex-1 truncate text-[10px] text-muted-foreground/60",
|
|
146
|
+
children: seed.description
|
|
147
|
+
})
|
|
148
|
+
]
|
|
149
|
+
}, seed.id))
|
|
150
|
+
})
|
|
151
|
+
]
|
|
152
|
+
}),
|
|
153
|
+
runs.length > 0 && /* @__PURE__ */ jsx(PanelSection, {
|
|
154
|
+
title: "Seed runs",
|
|
155
|
+
children: runs.map((run) => /* @__PURE__ */ jsxs("div", {
|
|
156
|
+
className: "flex items-center gap-2 px-2 py-1 font-mono text-[10.5px]",
|
|
157
|
+
children: [
|
|
158
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
159
|
+
variant: "outline",
|
|
160
|
+
className: `h-4 px-1.5 text-[9px] ${run.status === "done" ? "text-emerald-500" : run.status === "failed" ? "text-red-500" : "text-amber-500"}`,
|
|
161
|
+
children: run.status
|
|
162
|
+
}),
|
|
163
|
+
/* @__PURE__ */ jsx("span", {
|
|
164
|
+
className: "min-w-0 flex-1 truncate",
|
|
165
|
+
children: run.label
|
|
166
|
+
}),
|
|
167
|
+
run.detail && /* @__PURE__ */ jsx("span", {
|
|
168
|
+
className: "max-w-[220px] truncate text-muted-foreground/60",
|
|
169
|
+
title: run.detail,
|
|
170
|
+
children: run.detail
|
|
171
|
+
})
|
|
172
|
+
]
|
|
173
|
+
}, run.id))
|
|
174
|
+
})
|
|
175
|
+
]
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { MockSeedPanel };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useHud
|
|
3
|
+
} from "./chunk-wspz08q7.js";
|
|
4
|
+
import {
|
|
5
|
+
PanelSection
|
|
6
|
+
} from "./chunk-kyeb6qeq.js";
|
|
7
|
+
import {
|
|
8
|
+
Badge,
|
|
9
|
+
Button
|
|
10
|
+
} from "./chunk-xztw0xyt.js";
|
|
11
|
+
|
|
12
|
+
// panels/variants-panel.tsx
|
|
13
|
+
import { ArrowUpRight, Beaker, Wand2 } from "lucide-react";
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
16
|
+
"use client";
|
|
17
|
+
var STATUS_CLASS = {
|
|
18
|
+
active: "text-emerald-500",
|
|
19
|
+
decided: "text-muted-foreground"
|
|
20
|
+
};
|
|
21
|
+
function VariantsPanel({ adapter }) {
|
|
22
|
+
const { can } = useHud();
|
|
23
|
+
const { categories, reviewHref, categoryHref, openPreview, generate } = adapter;
|
|
24
|
+
const [path, setPath] = useState("");
|
|
25
|
+
const [brief, setBrief] = useState("");
|
|
26
|
+
const [result, setResult] = useState(null);
|
|
27
|
+
const [sending, setSending] = useState(false);
|
|
28
|
+
const grouped = new Map;
|
|
29
|
+
for (const category of categories) {
|
|
30
|
+
const key = category.domain ?? "shared";
|
|
31
|
+
grouped.set(key, [...grouped.get(key) ?? [], category]);
|
|
32
|
+
}
|
|
33
|
+
async function submitGenerate() {
|
|
34
|
+
if (!generate || !path.trim())
|
|
35
|
+
return;
|
|
36
|
+
setSending(true);
|
|
37
|
+
setResult(null);
|
|
38
|
+
try {
|
|
39
|
+
setResult(await generate({ path: path.trim(), brief: brief.trim(), count: 3 }));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
setResult({ ok: false, error: error instanceof Error ? error.message : "generate failed" });
|
|
42
|
+
} finally {
|
|
43
|
+
setSending(false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
47
|
+
className: "space-y-4",
|
|
48
|
+
children: [
|
|
49
|
+
/* @__PURE__ */ jsxs(PanelSection, {
|
|
50
|
+
icon: Beaker,
|
|
51
|
+
title: `Categories (${categories.length})`,
|
|
52
|
+
children: [
|
|
53
|
+
categories.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
54
|
+
className: "px-2 text-[10px] text-muted-foreground/60",
|
|
55
|
+
children: "No preview categories registered for this app."
|
|
56
|
+
}),
|
|
57
|
+
/* @__PURE__ */ jsx("div", {
|
|
58
|
+
className: "space-y-2 px-2",
|
|
59
|
+
children: Array.from(grouped.entries()).map(([domain, list]) => /* @__PURE__ */ jsxs("div", {
|
|
60
|
+
children: [
|
|
61
|
+
/* @__PURE__ */ jsx("div", {
|
|
62
|
+
className: "text-[9px] uppercase tracking-wider text-muted-foreground/50",
|
|
63
|
+
children: domain
|
|
64
|
+
}),
|
|
65
|
+
list.map((category) => /* @__PURE__ */ jsxs("div", {
|
|
66
|
+
className: "flex items-center gap-2 py-1 text-[11px]",
|
|
67
|
+
children: [
|
|
68
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
69
|
+
variant: "outline",
|
|
70
|
+
className: `h-4 px-1.5 text-[9px] ${STATUS_CLASS[category.status] ?? "text-amber-500"}`,
|
|
71
|
+
children: category.status
|
|
72
|
+
}),
|
|
73
|
+
/* @__PURE__ */ jsx("span", {
|
|
74
|
+
className: "min-w-0 flex-1 truncate",
|
|
75
|
+
children: category.name
|
|
76
|
+
}),
|
|
77
|
+
/* @__PURE__ */ jsxs("span", {
|
|
78
|
+
className: "text-muted-foreground/60",
|
|
79
|
+
children: [
|
|
80
|
+
category.alternativeCount,
|
|
81
|
+
" alt",
|
|
82
|
+
category.alternativeCount === 1 ? "" : "s"
|
|
83
|
+
]
|
|
84
|
+
}),
|
|
85
|
+
openPreview ? /* @__PURE__ */ jsx(Button, {
|
|
86
|
+
variant: "ghost",
|
|
87
|
+
size: "sm",
|
|
88
|
+
className: "h-5 px-1.5 text-[10px]",
|
|
89
|
+
onClick: () => openPreview(category.id),
|
|
90
|
+
children: "open"
|
|
91
|
+
}) : categoryHref ? /* @__PURE__ */ jsxs("a", {
|
|
92
|
+
href: categoryHref(category.id),
|
|
93
|
+
className: "inline-flex items-center gap-0.5 text-[10px] text-primary hover:underline",
|
|
94
|
+
children: [
|
|
95
|
+
"open",
|
|
96
|
+
/* @__PURE__ */ jsx(ArrowUpRight, {
|
|
97
|
+
className: "h-2.5 w-2.5"
|
|
98
|
+
})
|
|
99
|
+
]
|
|
100
|
+
}) : null
|
|
101
|
+
]
|
|
102
|
+
}, category.id))
|
|
103
|
+
]
|
|
104
|
+
}, domain))
|
|
105
|
+
}),
|
|
106
|
+
/* @__PURE__ */ jsx("div", {
|
|
107
|
+
className: "px-2 pt-1",
|
|
108
|
+
children: /* @__PURE__ */ jsxs("a", {
|
|
109
|
+
href: reviewHref,
|
|
110
|
+
className: "inline-flex items-center gap-1 text-[10px] text-primary hover:underline",
|
|
111
|
+
children: [
|
|
112
|
+
"Full review dashboard (vote, showcase, decisions)",
|
|
113
|
+
/* @__PURE__ */ jsx(ArrowUpRight, {
|
|
114
|
+
className: "h-3 w-3"
|
|
115
|
+
})
|
|
116
|
+
]
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
]
|
|
120
|
+
}),
|
|
121
|
+
generate && can("variants.generate") && /* @__PURE__ */ jsx(PanelSection, {
|
|
122
|
+
icon: Wand2,
|
|
123
|
+
title: "Generate variants",
|
|
124
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
125
|
+
className: "space-y-2 px-2",
|
|
126
|
+
children: [
|
|
127
|
+
/* @__PURE__ */ jsx("input", {
|
|
128
|
+
value: path,
|
|
129
|
+
onChange: (e) => setPath(e.target.value),
|
|
130
|
+
placeholder: "apps/console/src/components/… (repo-relative)",
|
|
131
|
+
className: "w-full rounded-md border bg-muted/20 p-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-ring"
|
|
132
|
+
}),
|
|
133
|
+
/* @__PURE__ */ jsx("textarea", {
|
|
134
|
+
value: brief,
|
|
135
|
+
onChange: (e) => setBrief(e.target.value),
|
|
136
|
+
placeholder: "Brief, e.g.: three denser table treatments, keep the glass language",
|
|
137
|
+
className: "min-h-[44px] w-full rounded-md border bg-muted/20 p-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-ring"
|
|
138
|
+
}),
|
|
139
|
+
/* @__PURE__ */ jsxs("div", {
|
|
140
|
+
className: "flex items-center gap-2",
|
|
141
|
+
children: [
|
|
142
|
+
/* @__PURE__ */ jsx(Button, {
|
|
143
|
+
size: "sm",
|
|
144
|
+
className: "h-6 text-[11px]",
|
|
145
|
+
disabled: sending || !path.trim(),
|
|
146
|
+
onClick: submitGenerate,
|
|
147
|
+
children: "Queue 3 variants"
|
|
148
|
+
}),
|
|
149
|
+
/* @__PURE__ */ jsx("span", {
|
|
150
|
+
className: "text-[10px] text-muted-foreground/60",
|
|
151
|
+
children: "Writes a .claude/queue marker — the next session runs /generate-variants."
|
|
152
|
+
})
|
|
153
|
+
]
|
|
154
|
+
}),
|
|
155
|
+
result && /* @__PURE__ */ jsx("p", {
|
|
156
|
+
className: `font-mono text-[10px] ${result.ok ? "text-emerald-500" : "text-red-500"}`,
|
|
157
|
+
children: result.ok ? `queued → ${result.markerPath}` : result.error
|
|
158
|
+
})
|
|
159
|
+
]
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export { VariantsPanel };
|