@omg-dev/admin 0.4.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1603 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/styles.ts
4
+ const STYLE_ID = "vibes-admin-styles";
5
+ function ensureStyles() {
6
+ if (typeof document === "undefined") return;
7
+ if (document.getElementById("vibes-admin-styles")) return;
8
+ const el = document.createElement("style");
9
+ el.id = STYLE_ID;
10
+ el.textContent = CSS;
11
+ document.head.appendChild(el);
12
+ }
13
+ const CSS = `
14
+ .vadm {
15
+ --vadm-bg: var(--background, #ffffff);
16
+ --vadm-fg: var(--foreground, #18181b);
17
+ --vadm-muted: var(--muted-foreground, #71717a);
18
+ --vadm-border: var(--border, #e4e4e7);
19
+ --vadm-accent: var(--primary, #2563eb);
20
+ --vadm-accent-fg: var(--primary-foreground, #ffffff);
21
+ --vadm-card: var(--card, #fafafa);
22
+ --vadm-danger: #dc2626;
23
+ color: var(--vadm-fg);
24
+ font-family: inherit;
25
+ font-size: 14px;
26
+ line-height: 1.5;
27
+ }
28
+ .vadm, .vadm * { box-sizing: border-box; }
29
+ @media (prefers-color-scheme: dark) {
30
+ .vadm {
31
+ --vadm-bg: var(--background, #0a0a0a);
32
+ --vadm-fg: var(--foreground, #f4f4f5);
33
+ --vadm-muted: var(--muted-foreground, #a1a1aa);
34
+ --vadm-border: var(--border, #27272a);
35
+ --vadm-card: var(--card, #18181b);
36
+ --vadm-danger: #f87171;
37
+ }
38
+ }
39
+
40
+ .vadm-tabs {
41
+ display: flex; gap: 3px; border: 1px solid var(--vadm-border);
42
+ background: color-mix(in srgb, var(--vadm-muted) 9%, transparent);
43
+ border-radius: 8px; padding: 3px; margin-bottom: 16px; overflow-x: auto;
44
+ scrollbar-width: none;
45
+ }
46
+ .vadm-tabs::-webkit-scrollbar { display: none; }
47
+ .vadm-tab {
48
+ appearance: none; background: none; border: none; cursor: pointer;
49
+ padding: 7px 12px; color: var(--vadm-muted); font: inherit; font-weight: 600;
50
+ border-radius: 6px; white-space: nowrap; flex: none;
51
+ }
52
+ .vadm-tab[data-active="true"] {
53
+ color: var(--vadm-fg); background: var(--vadm-bg);
54
+ box-shadow: 0 1px 2px color-mix(in srgb, var(--vadm-fg) 10%, transparent);
55
+ }
56
+
57
+ .vadm-row { display: flex; align-items: center; gap: 12px; min-width: 0; }
58
+ .vadm-grow { flex: 1; min-width: 0; }
59
+ .vadm-spread { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-width: 0; }
60
+ .vadm-muted { color: var(--vadm-muted); }
61
+ .vadm-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
62
+ .vadm-h { font-weight: 600; font-size: 15px; margin: 0 0 4px; }
63
+ .vadm-sub { color: var(--vadm-muted); font-size: 13px; margin: 0 0 16px; }
64
+
65
+ .vadm-card {
66
+ border: 1px solid var(--vadm-border); border-radius: 8px;
67
+ background: var(--vadm-card); padding: 14px 16px; margin-bottom: 10px;
68
+ }
69
+ .vadm-card[data-off="true"] { opacity: 0.6; }
70
+
71
+ .vadm-btn {
72
+ appearance: none; cursor: pointer; font: inherit; font-weight: 500;
73
+ border: 1px solid var(--vadm-border); border-radius: 8px;
74
+ background: var(--vadm-bg); color: var(--vadm-fg); padding: 6px 12px;
75
+ min-height: 34px; white-space: nowrap;
76
+ }
77
+ .vadm-btn:hover { border-color: var(--vadm-accent); }
78
+ .vadm-btn[data-primary="true"] { background: var(--vadm-accent); color: var(--vadm-accent-fg); border-color: var(--vadm-accent); }
79
+ .vadm-btn[data-danger="true"] { color: var(--vadm-danger); border-color: var(--vadm-danger); }
80
+ .vadm-btn:disabled { opacity: 0.5; cursor: default; }
81
+ .vadm-btn-sm { padding: 3px 8px; font-size: 12px; border-radius: 6px; }
82
+
83
+ .vadm-input, .vadm-select, .vadm-textarea {
84
+ font: inherit; color: var(--vadm-fg); background: var(--vadm-bg);
85
+ border: 1px solid var(--vadm-border); border-radius: 8px; padding: 7px 10px; width: 100%;
86
+ min-height: 36px;
87
+ }
88
+ .vadm-input:focus, .vadm-select:focus, .vadm-textarea:focus { outline: none; border-color: var(--vadm-accent); }
89
+ .vadm-textarea { resize: vertical; min-height: 56px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
90
+
91
+ .vadm-field { margin-bottom: 10px; }
92
+ .vadm-label { display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px; color: var(--vadm-muted); }
93
+
94
+ /* iOS-style switch */
95
+ .vadm-switch { position: relative; display: inline-block; width: 38px; height: 22px; flex: none; }
96
+ .vadm-switch input { opacity: 0; width: 0; height: 0; }
97
+ .vadm-slider {
98
+ position: absolute; inset: 0; cursor: pointer; border-radius: 999px;
99
+ background: var(--vadm-border); transition: background 0.15s;
100
+ }
101
+ .vadm-slider::before {
102
+ content: ""; position: absolute; height: 16px; width: 16px; left: 3px; top: 3px;
103
+ background: #fff; border-radius: 50%; transition: transform 0.15s;
104
+ }
105
+ .vadm-switch input:checked + .vadm-slider { background: var(--vadm-accent); }
106
+ .vadm-switch input:checked + .vadm-slider::before { transform: translateX(16px); }
107
+
108
+ .vadm-badge {
109
+ display: inline-block; font-size: 11px; font-weight: 500; padding: 1px 7px;
110
+ border-radius: 999px; border: 1px solid var(--vadm-border); color: var(--vadm-muted);
111
+ }
112
+ .vadm-badge[data-on="true"] { color: var(--vadm-accent); border-color: var(--vadm-accent); }
113
+
114
+ .vadm-table { width: 100%; border-collapse: collapse; font-size: 13px; }
115
+ .vadm-table th { text-align: left; color: var(--vadm-muted); font-weight: 500; padding: 6px 8px; border-bottom: 1px solid var(--vadm-border); }
116
+ .vadm-table td { padding: 6px 8px; border-bottom: 1px solid var(--vadm-border); }
117
+
118
+ .vadm-empty { text-align: center; color: var(--vadm-muted); padding: 32px 0; }
119
+ .vadm-err { color: var(--vadm-danger); background: color-mix(in srgb, var(--vadm-danger) 10%, transparent); border: 1px solid var(--vadm-danger); border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; }
120
+ .vadm-detail { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--vadm-border); }
121
+
122
+ .vadm-panel-head { align-items: flex-start; margin-bottom: 12px; }
123
+ .vadm-search { display: flex; align-items: center; gap: 8px; width: min(100%, 420px); }
124
+ .vadm-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 360px); gap: 14px; align-items: start; }
125
+ .vadm-list { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
126
+ .vadm-user-row {
127
+ appearance: none; width: 100%; border: 1px solid var(--vadm-border); border-radius: 8px;
128
+ background: var(--vadm-bg); color: var(--vadm-fg); padding: 10px;
129
+ display: flex; align-items: center; gap: 10px; text-align: left; font: inherit;
130
+ }
131
+ button.vadm-user-row { cursor: pointer; }
132
+ .vadm-user-row[data-active="true"] {
133
+ border-color: color-mix(in srgb, var(--vadm-accent) 55%, var(--vadm-border));
134
+ background: color-mix(in srgb, var(--vadm-accent) 8%, var(--vadm-bg));
135
+ }
136
+ .vadm-avatar {
137
+ width: 38px; height: 38px; border-radius: 50%; flex: none;
138
+ display: inline-flex; align-items: center; justify-content: center;
139
+ background: color-mix(in srgb, var(--vadm-accent) 13%, var(--vadm-bg));
140
+ color: var(--vadm-accent); font-weight: 700; font-size: 12px; overflow: hidden;
141
+ }
142
+ .vadm-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
143
+ .vadm-user-name { display: block; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
144
+ .vadm-user-stats { color: var(--vadm-muted); font-size: 12px; display: flex; flex-direction: column; align-items: flex-end; gap: 1px; }
145
+ .vadm-inspector { position: sticky; top: 12px; margin-bottom: 0; }
146
+ .vadm-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin: 14px 0; }
147
+ .vadm-metrics div {
148
+ border: 1px solid var(--vadm-border); border-radius: 8px; padding: 8px; background: var(--vadm-bg);
149
+ }
150
+ .vadm-metrics strong { display: block; font-size: 18px; line-height: 1.1; }
151
+ .vadm-metrics span { display: block; color: var(--vadm-muted); font-size: 11px; margin-top: 2px; }
152
+ .vadm-kv { display: grid; gap: 8px; }
153
+ .vadm-kv > div { display: grid; grid-template-columns: 84px minmax(0, 1fr); gap: 10px; align-items: baseline; }
154
+ .vadm-kv span { color: var(--vadm-muted); font-size: 12px; }
155
+ .vadm-kv strong { font-size: 13px; font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
156
+ .vadm-kv code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; overflow-wrap: anywhere; }
157
+ .vadm-skel {
158
+ display: block; height: 12px; border-radius: 999px;
159
+ background: color-mix(in srgb, var(--vadm-muted) 18%, transparent);
160
+ animation: vadmPulse 1.4s ease-in-out infinite;
161
+ }
162
+ .vadm-skel-avatar { width: 38px; height: 38px; border-radius: 50%; flex: none; }
163
+ @keyframes vadmPulse {
164
+ 0%, 100% { opacity: 0.55; }
165
+ 50% { opacity: 1; }
166
+ }
167
+
168
+ @media (max-width: 760px) {
169
+ .vadm { font-size: 13px; }
170
+ .vadm-tabs { margin-left: -2px; margin-right: -2px; }
171
+ .vadm-row, .vadm-spread { flex-wrap: wrap; }
172
+ .vadm-panel-head { display: block; }
173
+ .vadm-search { width: 100%; margin-top: 10px; }
174
+ .vadm-search .vadm-input { min-width: 0; }
175
+ .vadm-split { display: flex; flex-direction: column; }
176
+ .vadm-inspector { position: static; width: 100% !important; max-width: 100%; }
177
+ .vadm-user-row { align-items: flex-start; flex-wrap: wrap; }
178
+ .vadm-user-stats { flex-direction: row; align-items: center; gap: 8px; width: 100%; padding-left: 48px; margin-top: -4px; }
179
+ .vadm-table { display: block; overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; }
180
+ .vadm-detail.vadm-card { width: 100% !important; max-width: 100%; }
181
+ }
182
+
183
+ @media (max-width: 480px) {
184
+ .vadm-card { padding: 12px; }
185
+ .vadm-search { flex-wrap: wrap; }
186
+ .vadm-search .vadm-input { flex: 1 0 100%; }
187
+ .vadm-btn { min-height: 36px; }
188
+ .vadm-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
189
+ .vadm-kv > div { grid-template-columns: 1fr; gap: 1px; }
190
+ }
191
+ `;
192
+ //#endregion
193
+ //#region src/client.ts
194
+ var AdminError = class extends Error {
195
+ status;
196
+ data;
197
+ constructor(message, status, data) {
198
+ super(message);
199
+ this.name = "AdminError";
200
+ this.status = status;
201
+ this.data = data;
202
+ }
203
+ };
204
+ var AdminClient = class {
205
+ apiBase;
206
+ getToken;
207
+ constructor(config) {
208
+ this.apiBase = config.apiBase.replace(/\/$/, "");
209
+ this.getToken = config.getToken;
210
+ }
211
+ async rpc(module, name, args) {
212
+ const token = await this.getToken();
213
+ const headers = { "content-type": "application/json" };
214
+ if (token) headers.authorization = `Bearer ${token}`;
215
+ const res = await fetch(`${this.apiBase}/api/${module}/${name}`, {
216
+ method: "POST",
217
+ headers,
218
+ body: JSON.stringify(args ?? {})
219
+ });
220
+ if (!res.ok) {
221
+ let message = `${module}.${name} failed (${res.status})`;
222
+ let payload = null;
223
+ try {
224
+ payload = await res.json();
225
+ if (payload && typeof payload === "object" && "error" in payload) message = String(payload.error);
226
+ } catch {}
227
+ throw new AdminError(message, res.status, payload);
228
+ }
229
+ if (res.status === 204) return void 0;
230
+ const text = await res.text();
231
+ return text ? JSON.parse(text) : void 0;
232
+ }
233
+ listFlags() {
234
+ return this.rpc("flags", "listFlags");
235
+ }
236
+ upsertFlag(flag) {
237
+ return this.rpc("flags", "upsert", flag);
238
+ }
239
+ setFlagArchived(key, archived) {
240
+ return this.rpc("flags", "setArchived", {
241
+ key,
242
+ archived
243
+ });
244
+ }
245
+ removeFlag(key) {
246
+ return this.rpc("flags", "removeFlag", { key });
247
+ }
248
+ listOverrides(flagKey) {
249
+ return this.rpc("flags", "listOverrides", { flagKey });
250
+ }
251
+ setOverride(o) {
252
+ return this.rpc("flags", "setOverride", o);
253
+ }
254
+ removeOverride(flagKey, userId) {
255
+ return this.rpc("flags", "removeOverride", {
256
+ flagKey,
257
+ userId
258
+ });
259
+ }
260
+ findUsers(query) {
261
+ return this.rpc("users", "findUsers", { query });
262
+ }
263
+ listUsers(args) {
264
+ return this.rpc("users", "listUsers", args ?? {});
265
+ }
266
+ planVersions() {
267
+ return this.rpc("adminBilling", "planVersions");
268
+ }
269
+ models() {
270
+ return this.rpc("adminBilling", "models");
271
+ }
272
+ userBalance(userId) {
273
+ return this.rpc("adminBilling", "userBalance", { userId });
274
+ }
275
+ listReports(args) {
276
+ return this.rpc("bugReports", "listReports", args ?? {});
277
+ }
278
+ getReport(id) {
279
+ return this.rpc("bugReports", "getReport", { id });
280
+ }
281
+ setReportStatus(id, status, assignee) {
282
+ return this.rpc("bugReports", "setStatus", {
283
+ id,
284
+ status,
285
+ assignee
286
+ });
287
+ }
288
+ addReportNote(id, note) {
289
+ return this.rpc("bugReports", "addNote", {
290
+ id,
291
+ note
292
+ });
293
+ }
294
+ setReportCluster(id, clusterKey) {
295
+ return this.rpc("bugReports", "setCluster", {
296
+ id,
297
+ clusterKey
298
+ });
299
+ }
300
+ listClusters() {
301
+ return this.rpc("bugReports", "listClusters", {});
302
+ }
303
+ removeReport(id) {
304
+ return this.rpc("bugReports", "removeReport", { id });
305
+ }
306
+ };
307
+ //#endregion
308
+ //#region src/react.tsx
309
+ function Switch({ checked, onChange, disabled }) {
310
+ return /* @__PURE__ */ jsxs("label", {
311
+ className: "vadm-switch",
312
+ children: [/* @__PURE__ */ jsx("input", {
313
+ type: "checkbox",
314
+ checked,
315
+ disabled,
316
+ onChange: (e) => onChange(e.target.checked)
317
+ }), /* @__PURE__ */ jsx("span", { className: "vadm-slider" })]
318
+ });
319
+ }
320
+ function ErrorBanner({ error }) {
321
+ if (!error) return null;
322
+ return /* @__PURE__ */ jsx("div", {
323
+ className: "vadm-err",
324
+ children: error instanceof Error ? error.message : String(error)
325
+ });
326
+ }
327
+ function initialsFor(user) {
328
+ const source = (user.name || user.email || "?").trim();
329
+ const parts = source.split(/\s+/).filter(Boolean);
330
+ if (parts.length >= 2) return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
331
+ return source.slice(0, 2).toUpperCase();
332
+ }
333
+ function UserAvatar({ user }) {
334
+ return /* @__PURE__ */ jsx("span", {
335
+ className: "vadm-avatar",
336
+ children: user.image ? /* @__PURE__ */ jsx("img", {
337
+ src: user.image,
338
+ alt: ""
339
+ }) : initialsFor(user)
340
+ });
341
+ }
342
+ function UserListSkeleton() {
343
+ return /* @__PURE__ */ jsx("div", {
344
+ className: "vadm-list",
345
+ "aria-label": "Loading users",
346
+ children: Array.from({ length: 5 }).map((_, i) => /* @__PURE__ */ jsxs("div", {
347
+ className: "vadm-user-row",
348
+ "aria-hidden": "true",
349
+ children: [
350
+ /* @__PURE__ */ jsx("span", { className: "vadm-skel vadm-skel-avatar" }),
351
+ /* @__PURE__ */ jsxs("span", {
352
+ className: "vadm-grow",
353
+ children: [/* @__PURE__ */ jsx("span", {
354
+ className: "vadm-skel",
355
+ style: { width: "42%" }
356
+ }), /* @__PURE__ */ jsx("span", {
357
+ className: "vadm-skel",
358
+ style: {
359
+ width: "68%",
360
+ marginTop: 8
361
+ }
362
+ })]
363
+ }),
364
+ /* @__PURE__ */ jsx("span", {
365
+ className: "vadm-skel",
366
+ style: { width: 72 }
367
+ })
368
+ ]
369
+ }, i))
370
+ });
371
+ }
372
+ const EMPTY_DRAFT = {
373
+ key: "",
374
+ description: "",
375
+ valueType: "bool",
376
+ enabled: false,
377
+ defaultValue: "",
378
+ rolloutPercent: "",
379
+ planRules: ""
380
+ };
381
+ function FlagsPanel({ client }) {
382
+ const [flags, setFlags] = useState(null);
383
+ const [error, setError] = useState(null);
384
+ const [busy, setBusy] = useState(false);
385
+ const [editing, setEditing] = useState(null);
386
+ const [draft, setDraft] = useState(EMPTY_DRAFT);
387
+ const [expanded, setExpanded] = useState(null);
388
+ const load = useCallback(async () => {
389
+ try {
390
+ setFlags(await client.listFlags());
391
+ setError(null);
392
+ } catch (e) {
393
+ setError(e);
394
+ }
395
+ }, [client]);
396
+ useEffect(() => {
397
+ ensureStyles();
398
+ load();
399
+ }, [load]);
400
+ function openNew() {
401
+ setDraft(EMPTY_DRAFT);
402
+ setEditing("__new__");
403
+ }
404
+ function openEdit(f) {
405
+ setDraft({
406
+ key: f.key,
407
+ description: f.description,
408
+ valueType: f.valueType,
409
+ enabled: f.enabled,
410
+ defaultValue: f.valueType === "json" ? JSON.stringify(f.defaultValue ?? null, null, 2) : "",
411
+ rolloutPercent: f.rolloutPercent == null ? "" : String(f.rolloutPercent),
412
+ planRules: f.planRules ? JSON.stringify(f.planRules, null, 2) : ""
413
+ });
414
+ setEditing(f.key);
415
+ }
416
+ async function save() {
417
+ setBusy(true);
418
+ try {
419
+ const payload = {
420
+ key: draft.key.trim(),
421
+ description: draft.description.trim(),
422
+ valueType: draft.valueType,
423
+ enabled: draft.enabled,
424
+ rolloutPercent: draft.rolloutPercent.trim() === "" ? null : Number(draft.rolloutPercent)
425
+ };
426
+ if (draft.valueType === "json" && draft.defaultValue.trim()) payload.defaultValue = JSON.parse(draft.defaultValue);
427
+ payload.planRules = draft.planRules.trim() ? JSON.parse(draft.planRules) : null;
428
+ await client.upsertFlag(payload);
429
+ setEditing(null);
430
+ await load();
431
+ } catch (e) {
432
+ setError(e);
433
+ } finally {
434
+ setBusy(false);
435
+ }
436
+ }
437
+ async function toggleEnabled(f, enabled) {
438
+ setBusy(true);
439
+ try {
440
+ await client.upsertFlag({
441
+ key: f.key,
442
+ valueType: f.valueType,
443
+ description: f.description,
444
+ enabled,
445
+ rolloutPercent: f.rolloutPercent,
446
+ planRules: f.planRules,
447
+ defaultValue: f.valueType === "json" ? f.defaultValue : void 0
448
+ });
449
+ await load();
450
+ } catch (e) {
451
+ setError(e);
452
+ } finally {
453
+ setBusy(false);
454
+ }
455
+ }
456
+ async function archive(f) {
457
+ setBusy(true);
458
+ try {
459
+ await client.setFlagArchived(f.key, !f.archivedAt);
460
+ await load();
461
+ } catch (e) {
462
+ setError(e);
463
+ } finally {
464
+ setBusy(false);
465
+ }
466
+ }
467
+ if (editing) {
468
+ const isNew = editing === "__new__";
469
+ return /* @__PURE__ */ jsxs("div", { children: [
470
+ /* @__PURE__ */ jsx("p", {
471
+ className: "vadm-h",
472
+ children: isNew ? "New flag" : `Edit ${draft.key}`
473
+ }),
474
+ /* @__PURE__ */ jsx(ErrorBanner, { error }),
475
+ /* @__PURE__ */ jsxs("div", {
476
+ className: "vadm-field",
477
+ children: [/* @__PURE__ */ jsx("label", {
478
+ className: "vadm-label",
479
+ children: "Key"
480
+ }), /* @__PURE__ */ jsx("input", {
481
+ className: "vadm-input vadm-mono",
482
+ placeholder: "model.claude-opus-4-8",
483
+ value: draft.key,
484
+ disabled: !isNew,
485
+ onChange: (e) => setDraft({
486
+ ...draft,
487
+ key: e.target.value
488
+ })
489
+ })]
490
+ }),
491
+ /* @__PURE__ */ jsxs("div", {
492
+ className: "vadm-field",
493
+ children: [/* @__PURE__ */ jsx("label", {
494
+ className: "vadm-label",
495
+ children: "Description"
496
+ }), /* @__PURE__ */ jsx("input", {
497
+ className: "vadm-input",
498
+ value: draft.description,
499
+ onChange: (e) => setDraft({
500
+ ...draft,
501
+ description: e.target.value
502
+ })
503
+ })]
504
+ }),
505
+ /* @__PURE__ */ jsxs("div", {
506
+ className: "vadm-row",
507
+ style: { marginBottom: 10 },
508
+ children: [
509
+ /* @__PURE__ */ jsxs("div", {
510
+ className: "vadm-grow",
511
+ children: [/* @__PURE__ */ jsx("label", {
512
+ className: "vadm-label",
513
+ children: "Type"
514
+ }), /* @__PURE__ */ jsxs("select", {
515
+ className: "vadm-select",
516
+ value: draft.valueType,
517
+ disabled: !isNew,
518
+ onChange: (e) => setDraft({
519
+ ...draft,
520
+ valueType: e.target.value
521
+ }),
522
+ children: [/* @__PURE__ */ jsx("option", {
523
+ value: "bool",
524
+ children: "boolean"
525
+ }), /* @__PURE__ */ jsx("option", {
526
+ value: "json",
527
+ children: "json"
528
+ })]
529
+ })]
530
+ }),
531
+ /* @__PURE__ */ jsxs("div", {
532
+ className: "vadm-grow",
533
+ children: [/* @__PURE__ */ jsx("label", {
534
+ className: "vadm-label",
535
+ children: "Rollout %"
536
+ }), /* @__PURE__ */ jsx("input", {
537
+ className: "vadm-input",
538
+ placeholder: "(none)",
539
+ inputMode: "numeric",
540
+ value: draft.rolloutPercent,
541
+ onChange: (e) => setDraft({
542
+ ...draft,
543
+ rolloutPercent: e.target.value
544
+ })
545
+ })]
546
+ }),
547
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
548
+ className: "vadm-label",
549
+ children: "Default on"
550
+ }), /* @__PURE__ */ jsx(Switch, {
551
+ checked: draft.enabled,
552
+ onChange: (v) => setDraft({
553
+ ...draft,
554
+ enabled: v
555
+ })
556
+ })] })
557
+ ]
558
+ }),
559
+ draft.valueType === "json" && /* @__PURE__ */ jsxs("div", {
560
+ className: "vadm-field",
561
+ children: [/* @__PURE__ */ jsx("label", {
562
+ className: "vadm-label",
563
+ children: "Default value (JSON)"
564
+ }), /* @__PURE__ */ jsx("textarea", {
565
+ className: "vadm-textarea",
566
+ value: draft.defaultValue,
567
+ onChange: (e) => setDraft({
568
+ ...draft,
569
+ defaultValue: e.target.value
570
+ })
571
+ })]
572
+ }),
573
+ /* @__PURE__ */ jsxs("div", {
574
+ className: "vadm-field",
575
+ children: [/* @__PURE__ */ jsxs("label", {
576
+ className: "vadm-label",
577
+ children: ["Plan rules (JSON map, optional) — e.g. ", `{"free":false,"pro":true}`]
578
+ }), /* @__PURE__ */ jsx("textarea", {
579
+ className: "vadm-textarea",
580
+ value: draft.planRules,
581
+ onChange: (e) => setDraft({
582
+ ...draft,
583
+ planRules: e.target.value
584
+ })
585
+ })]
586
+ }),
587
+ /* @__PURE__ */ jsxs("div", {
588
+ className: "vadm-row",
589
+ children: [/* @__PURE__ */ jsx("button", {
590
+ className: "vadm-btn",
591
+ "data-primary": "true",
592
+ disabled: busy || !draft.key.trim(),
593
+ onClick: save,
594
+ children: "Save"
595
+ }), /* @__PURE__ */ jsx("button", {
596
+ className: "vadm-btn",
597
+ disabled: busy,
598
+ onClick: () => {
599
+ setEditing(null);
600
+ setError(null);
601
+ },
602
+ children: "Cancel"
603
+ })]
604
+ })
605
+ ] });
606
+ }
607
+ return /* @__PURE__ */ jsxs("div", { children: [
608
+ /* @__PURE__ */ jsxs("div", {
609
+ className: "vadm-spread",
610
+ style: { marginBottom: 12 },
611
+ children: [/* @__PURE__ */ jsx("p", {
612
+ className: "vadm-sub",
613
+ style: { margin: 0 },
614
+ children: "Gate UI / API features per account, plan, or rollout %."
615
+ }), /* @__PURE__ */ jsx("button", {
616
+ className: "vadm-btn",
617
+ "data-primary": "true",
618
+ onClick: openNew,
619
+ children: "New flag"
620
+ })]
621
+ }),
622
+ /* @__PURE__ */ jsx(ErrorBanner, { error }),
623
+ flags === null ? /* @__PURE__ */ jsx("div", {
624
+ className: "vadm-empty",
625
+ children: "Loading…"
626
+ }) : flags.length === 0 ? /* @__PURE__ */ jsx("div", {
627
+ className: "vadm-empty",
628
+ children: "No flags yet."
629
+ }) : flags.map((f) => /* @__PURE__ */ jsxs("div", {
630
+ className: "vadm-card",
631
+ "data-off": f.archivedAt ? "true" : void 0,
632
+ children: [/* @__PURE__ */ jsxs("div", {
633
+ className: "vadm-spread",
634
+ children: [/* @__PURE__ */ jsxs("div", {
635
+ className: "vadm-grow",
636
+ children: [/* @__PURE__ */ jsxs("div", {
637
+ className: "vadm-row",
638
+ children: [
639
+ /* @__PURE__ */ jsx("span", {
640
+ className: "vadm-mono",
641
+ style: { fontWeight: 600 },
642
+ children: f.key
643
+ }),
644
+ /* @__PURE__ */ jsx("span", {
645
+ className: "vadm-badge",
646
+ children: f.valueType
647
+ }),
648
+ f.rolloutPercent != null && /* @__PURE__ */ jsxs("span", {
649
+ className: "vadm-badge",
650
+ children: [f.rolloutPercent, "%"]
651
+ }),
652
+ f.planRules && /* @__PURE__ */ jsx("span", {
653
+ className: "vadm-badge",
654
+ children: "plan rules"
655
+ }),
656
+ f.archivedAt && /* @__PURE__ */ jsx("span", {
657
+ className: "vadm-badge",
658
+ children: "archived"
659
+ })
660
+ ]
661
+ }), f.description && /* @__PURE__ */ jsx("div", {
662
+ className: "vadm-muted",
663
+ style: {
664
+ fontSize: 13,
665
+ marginTop: 2
666
+ },
667
+ children: f.description
668
+ })]
669
+ }), /* @__PURE__ */ jsxs("div", {
670
+ className: "vadm-row",
671
+ children: [
672
+ f.valueType === "bool" && !f.archivedAt && /* @__PURE__ */ jsx(Switch, {
673
+ checked: f.enabled,
674
+ disabled: busy,
675
+ onChange: (v) => toggleEnabled(f, v)
676
+ }),
677
+ /* @__PURE__ */ jsx("button", {
678
+ className: "vadm-btn vadm-btn-sm",
679
+ onClick: () => setExpanded(expanded === f.key ? null : f.key),
680
+ children: f.overrideCount > 0 ? `${f.overrideCount} override${f.overrideCount > 1 ? "s" : ""}` : "Overrides"
681
+ }),
682
+ /* @__PURE__ */ jsx("button", {
683
+ className: "vadm-btn vadm-btn-sm",
684
+ onClick: () => openEdit(f),
685
+ children: "Edit"
686
+ }),
687
+ /* @__PURE__ */ jsx("button", {
688
+ className: "vadm-btn vadm-btn-sm",
689
+ onClick: () => archive(f),
690
+ children: f.archivedAt ? "Restore" : "Archive"
691
+ })
692
+ ]
693
+ })]
694
+ }), expanded === f.key && /* @__PURE__ */ jsx(OverridesEditor, {
695
+ client,
696
+ flag: f,
697
+ onChange: load
698
+ })]
699
+ }, f.key))
700
+ ] });
701
+ }
702
+ function OverridesEditor({ client, flag, onChange }) {
703
+ const [rows, setRows] = useState(null);
704
+ const [error, setError] = useState(null);
705
+ const [query, setQuery] = useState("");
706
+ const [hits, setHits] = useState([]);
707
+ const [picked, setPicked] = useState(null);
708
+ const [enabled, setEnabled] = useState(true);
709
+ const [value, setValue] = useState("");
710
+ const [busy, setBusy] = useState(false);
711
+ const reload = useCallback(async () => {
712
+ try {
713
+ setRows(await client.listOverrides(flag.key));
714
+ } catch (e) {
715
+ setError(e);
716
+ }
717
+ }, [client, flag.key]);
718
+ useEffect(() => {
719
+ reload();
720
+ }, [reload]);
721
+ useEffect(() => {
722
+ if (!query.trim() || picked) {
723
+ setHits([]);
724
+ return;
725
+ }
726
+ let live = true;
727
+ const t = setTimeout(async () => {
728
+ try {
729
+ const r = await client.findUsers(query.trim());
730
+ if (live) setHits(r);
731
+ } catch {}
732
+ }, 200);
733
+ return () => {
734
+ live = false;
735
+ clearTimeout(t);
736
+ };
737
+ }, [
738
+ query,
739
+ picked,
740
+ client
741
+ ]);
742
+ async function add() {
743
+ if (!picked) return;
744
+ setBusy(true);
745
+ try {
746
+ await client.setOverride({
747
+ flagKey: flag.key,
748
+ userId: picked.id,
749
+ enabled,
750
+ value: flag.valueType === "json" && value.trim() ? JSON.parse(value) : void 0
751
+ });
752
+ setPicked(null);
753
+ setQuery("");
754
+ setValue("");
755
+ setEnabled(true);
756
+ await reload();
757
+ onChange();
758
+ } catch (e) {
759
+ setError(e);
760
+ } finally {
761
+ setBusy(false);
762
+ }
763
+ }
764
+ async function drop(userId) {
765
+ setBusy(true);
766
+ try {
767
+ await client.removeOverride(flag.key, userId);
768
+ await reload();
769
+ onChange();
770
+ } catch (e) {
771
+ setError(e);
772
+ } finally {
773
+ setBusy(false);
774
+ }
775
+ }
776
+ return /* @__PURE__ */ jsxs("div", {
777
+ className: "vadm-detail",
778
+ children: [
779
+ /* @__PURE__ */ jsx(ErrorBanner, { error }),
780
+ rows && rows.length > 0 && /* @__PURE__ */ jsxs("table", {
781
+ className: "vadm-table",
782
+ style: { marginBottom: 12 },
783
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
784
+ /* @__PURE__ */ jsx("th", { children: "Account" }),
785
+ /* @__PURE__ */ jsx("th", { children: "State" }),
786
+ flag.valueType === "json" && /* @__PURE__ */ jsx("th", { children: "Value" }),
787
+ /* @__PURE__ */ jsx("th", {})
788
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: rows.map((r) => /* @__PURE__ */ jsxs("tr", { children: [
789
+ /* @__PURE__ */ jsx("td", { children: r.userEmail ?? r.userId }),
790
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("span", {
791
+ className: "vadm-badge",
792
+ "data-on": r.enabled ? "true" : void 0,
793
+ children: r.enabled ? "on" : "off"
794
+ }) }),
795
+ flag.valueType === "json" && /* @__PURE__ */ jsx("td", {
796
+ className: "vadm-mono",
797
+ children: r.value == null ? "—" : JSON.stringify(r.value)
798
+ }),
799
+ /* @__PURE__ */ jsx("td", {
800
+ style: { textAlign: "right" },
801
+ children: /* @__PURE__ */ jsx("button", {
802
+ className: "vadm-btn vadm-btn-sm",
803
+ "data-danger": "true",
804
+ disabled: busy,
805
+ onClick: () => drop(r.userId),
806
+ children: "Remove"
807
+ })
808
+ })
809
+ ] }, r.userId)) })]
810
+ }),
811
+ /* @__PURE__ */ jsxs("div", {
812
+ className: "vadm-row",
813
+ style: { position: "relative" },
814
+ children: [
815
+ /* @__PURE__ */ jsxs("div", {
816
+ className: "vadm-grow",
817
+ style: { position: "relative" },
818
+ children: [/* @__PURE__ */ jsx("input", {
819
+ className: "vadm-input",
820
+ placeholder: "Search account by email…",
821
+ value: picked ? `${picked.email}` : query,
822
+ onChange: (e) => {
823
+ setPicked(null);
824
+ setQuery(e.target.value);
825
+ }
826
+ }), hits.length > 0 && /* @__PURE__ */ jsx("div", {
827
+ className: "vadm-card",
828
+ style: {
829
+ position: "absolute",
830
+ zIndex: 5,
831
+ left: 0,
832
+ right: 0,
833
+ marginTop: 2,
834
+ padding: 4
835
+ },
836
+ children: hits.map((h) => /* @__PURE__ */ jsx("div", {
837
+ className: "vadm-btn",
838
+ style: {
839
+ display: "block",
840
+ border: "none",
841
+ textAlign: "left",
842
+ width: "100%"
843
+ },
844
+ onClick: () => {
845
+ setPicked(h);
846
+ setHits([]);
847
+ },
848
+ children: h.email
849
+ }, h.id))
850
+ })]
851
+ }),
852
+ /* @__PURE__ */ jsx(Switch, {
853
+ checked: enabled,
854
+ onChange: setEnabled
855
+ }),
856
+ flag.valueType === "json" && /* @__PURE__ */ jsx("input", {
857
+ className: "vadm-input vadm-mono vadm-grow",
858
+ placeholder: "value JSON (optional)",
859
+ value,
860
+ onChange: (e) => setValue(e.target.value)
861
+ }),
862
+ /* @__PURE__ */ jsx("button", {
863
+ className: "vadm-btn vadm-btn-sm",
864
+ "data-primary": "true",
865
+ disabled: !picked || busy,
866
+ onClick: add,
867
+ children: "Add"
868
+ })
869
+ ]
870
+ })
871
+ ]
872
+ });
873
+ }
874
+ function PricingPanel({ client }) {
875
+ const [plans, setPlans] = useState(null);
876
+ const [models, setModels] = useState(null);
877
+ const [error, setError] = useState(null);
878
+ useEffect(() => {
879
+ ensureStyles();
880
+ (async () => {
881
+ try {
882
+ const [p, m] = await Promise.all([client.planVersions(), client.models()]);
883
+ setPlans(p);
884
+ setModels(m);
885
+ } catch (e) {
886
+ setError(e);
887
+ }
888
+ })();
889
+ }, [client]);
890
+ return /* @__PURE__ */ jsxs("div", { children: [
891
+ /* @__PURE__ */ jsx("p", {
892
+ className: "vadm-sub",
893
+ children: "Read-only — pricing is code-defined (defineBilling) and reconciled by the orchestrator."
894
+ }),
895
+ /* @__PURE__ */ jsx(ErrorBanner, { error }),
896
+ /* @__PURE__ */ jsx("p", {
897
+ className: "vadm-h",
898
+ children: "Plans"
899
+ }),
900
+ plans === null ? /* @__PURE__ */ jsx("div", {
901
+ className: "vadm-empty",
902
+ children: "Loading…"
903
+ }) : plans.length === 0 ? /* @__PURE__ */ jsx("div", {
904
+ className: "vadm-empty",
905
+ children: "No plan versions."
906
+ }) : /* @__PURE__ */ jsxs("table", {
907
+ className: "vadm-table",
908
+ style: { marginBottom: 24 },
909
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
910
+ /* @__PURE__ */ jsx("th", { children: "Plan" }),
911
+ /* @__PURE__ */ jsx("th", { children: "Ver" }),
912
+ /* @__PURE__ */ jsx("th", { children: "Status" }),
913
+ /* @__PURE__ */ jsx("th", { children: "Price" }),
914
+ /* @__PURE__ */ jsx("th", { children: "Interval" }),
915
+ /* @__PURE__ */ jsx("th", { children: "Stripe price" })
916
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: plans.map((p) => /* @__PURE__ */ jsxs("tr", { children: [
917
+ /* @__PURE__ */ jsx("td", {
918
+ style: { fontWeight: 600 },
919
+ children: p.planKey
920
+ }),
921
+ /* @__PURE__ */ jsx("td", { children: p.version }),
922
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("span", {
923
+ className: "vadm-badge",
924
+ "data-on": p.status === "active" ? "true" : void 0,
925
+ children: p.status
926
+ }) }),
927
+ /* @__PURE__ */ jsxs("td", { children: ["$", p.priceUsd.toFixed(2)] }),
928
+ /* @__PURE__ */ jsx("td", { children: p.interval }),
929
+ /* @__PURE__ */ jsx("td", {
930
+ className: "vadm-mono",
931
+ children: p.externalPriceId || "—"
932
+ })
933
+ ] }, `${p.planKey}-${p.version}`)) })]
934
+ }),
935
+ /* @__PURE__ */ jsx("p", {
936
+ className: "vadm-h",
937
+ children: "Models"
938
+ }),
939
+ models === null ? /* @__PURE__ */ jsx("div", {
940
+ className: "vadm-empty",
941
+ children: "Loading…"
942
+ }) : models.length === 0 ? /* @__PURE__ */ jsx("div", {
943
+ className: "vadm-empty",
944
+ children: "No models."
945
+ }) : /* @__PURE__ */ jsxs("table", {
946
+ className: "vadm-table",
947
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
948
+ /* @__PURE__ */ jsx("th", { children: "Model" }),
949
+ /* @__PURE__ */ jsx("th", { children: "Provider" }),
950
+ /* @__PURE__ */ jsx("th", { children: "ID" })
951
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: models.map((m) => /* @__PURE__ */ jsxs("tr", { children: [
952
+ /* @__PURE__ */ jsx("td", { children: m.name ?? m.id }),
953
+ /* @__PURE__ */ jsx("td", { children: m.provider ?? "—" }),
954
+ /* @__PURE__ */ jsx("td", {
955
+ className: "vadm-mono",
956
+ children: m.id
957
+ })
958
+ ] }, m.id)) })]
959
+ })
960
+ ] });
961
+ }
962
+ function UsersPanel({ client }) {
963
+ const [queryInput, setQueryInput] = useState("");
964
+ const [query, setQuery] = useState("");
965
+ const [users, setUsers] = useState(null);
966
+ const [total, setTotal] = useState(0);
967
+ const [error, setError] = useState(null);
968
+ const [selectedId, setSelectedId] = useState(null);
969
+ const [balance, setBalance] = useState(null);
970
+ const [balanceError, setBalanceError] = useState(null);
971
+ const load = useCallback(async () => {
972
+ try {
973
+ const res = await client.listUsers({
974
+ query,
975
+ limit: 50
976
+ });
977
+ setUsers(res.users);
978
+ setTotal(res.total);
979
+ setSelectedId((current) => current ?? res.users[0]?.id ?? null);
980
+ setError(null);
981
+ } catch (e) {
982
+ setError(e);
983
+ }
984
+ }, [client, query]);
985
+ useEffect(() => {
986
+ ensureStyles();
987
+ load();
988
+ }, [load]);
989
+ const selected = users?.find((u) => u.id === selectedId) ?? users?.[0] ?? null;
990
+ useEffect(() => {
991
+ if (!selected) {
992
+ setBalance(null);
993
+ setBalanceError(null);
994
+ return;
995
+ }
996
+ let live = true;
997
+ setBalance(null);
998
+ setBalanceError(null);
999
+ client.userBalance(selected.id).then((b) => {
1000
+ if (live) setBalance(b);
1001
+ }).catch((e) => {
1002
+ if (live) setBalanceError(e);
1003
+ });
1004
+ return () => {
1005
+ live = false;
1006
+ };
1007
+ }, [client, selected?.id]);
1008
+ function submit(e) {
1009
+ e.preventDefault();
1010
+ setQuery(queryInput.trim());
1011
+ setSelectedId(null);
1012
+ setUsers(null);
1013
+ }
1014
+ return /* @__PURE__ */ jsxs("div", { children: [
1015
+ /* @__PURE__ */ jsxs("div", {
1016
+ className: "vadm-spread vadm-panel-head",
1017
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
1018
+ className: "vadm-h",
1019
+ children: "Users"
1020
+ }), /* @__PURE__ */ jsx("p", {
1021
+ className: "vadm-sub",
1022
+ children: "Accounts, sessions, projects, flag overrides, and billing status."
1023
+ })] }), /* @__PURE__ */ jsxs("form", {
1024
+ className: "vadm-search",
1025
+ onSubmit: submit,
1026
+ children: [
1027
+ /* @__PURE__ */ jsx("input", {
1028
+ className: "vadm-input",
1029
+ placeholder: "Search users",
1030
+ value: queryInput,
1031
+ onChange: (e) => setQueryInput(e.target.value)
1032
+ }),
1033
+ /* @__PURE__ */ jsx("button", {
1034
+ className: "vadm-btn",
1035
+ type: "submit",
1036
+ children: "Search"
1037
+ }),
1038
+ query && /* @__PURE__ */ jsx("button", {
1039
+ className: "vadm-btn",
1040
+ type: "button",
1041
+ onClick: () => {
1042
+ setQuery("");
1043
+ setQueryInput("");
1044
+ setSelectedId(null);
1045
+ setUsers(null);
1046
+ },
1047
+ children: "Clear"
1048
+ })
1049
+ ]
1050
+ })]
1051
+ }),
1052
+ /* @__PURE__ */ jsx(ErrorBanner, { error }),
1053
+ users === null ? /* @__PURE__ */ jsx(UserListSkeleton, {}) : users.length === 0 ? /* @__PURE__ */ jsx("div", {
1054
+ className: "vadm-empty",
1055
+ children: "No users found."
1056
+ }) : /* @__PURE__ */ jsxs("div", {
1057
+ className: "vadm-split",
1058
+ children: [/* @__PURE__ */ jsxs("div", {
1059
+ className: "vadm-list",
1060
+ children: [/* @__PURE__ */ jsxs("div", {
1061
+ className: "vadm-muted",
1062
+ style: {
1063
+ fontSize: 12,
1064
+ marginBottom: 8
1065
+ },
1066
+ children: [
1067
+ "Showing ",
1068
+ users.length,
1069
+ " of ",
1070
+ total
1071
+ ]
1072
+ }), users.map((u) => /* @__PURE__ */ jsxs("button", {
1073
+ className: "vadm-user-row",
1074
+ "data-active": u.id === selected?.id,
1075
+ onClick: () => setSelectedId(u.id),
1076
+ children: [
1077
+ /* @__PURE__ */ jsx(UserAvatar, { user: u }),
1078
+ /* @__PURE__ */ jsxs("span", {
1079
+ className: "vadm-grow",
1080
+ children: [/* @__PURE__ */ jsx("span", {
1081
+ className: "vadm-user-name",
1082
+ children: u.name || u.email
1083
+ }), /* @__PURE__ */ jsx("span", {
1084
+ className: "vadm-muted",
1085
+ children: u.email
1086
+ })]
1087
+ }),
1088
+ /* @__PURE__ */ jsxs("span", {
1089
+ className: "vadm-user-stats",
1090
+ children: [/* @__PURE__ */ jsxs("span", { children: [u.projectCount, " apps"] }), /* @__PURE__ */ jsxs("span", { children: [u.activeSessionCount, " sessions"] })]
1091
+ })
1092
+ ]
1093
+ }, u.id))]
1094
+ }), selected && /* @__PURE__ */ jsxs("div", {
1095
+ className: "vadm-card vadm-inspector",
1096
+ children: [
1097
+ /* @__PURE__ */ jsxs("div", {
1098
+ className: "vadm-row",
1099
+ style: { alignItems: "flex-start" },
1100
+ children: [
1101
+ /* @__PURE__ */ jsx(UserAvatar, { user: selected }),
1102
+ /* @__PURE__ */ jsxs("div", {
1103
+ className: "vadm-grow",
1104
+ children: [/* @__PURE__ */ jsx("p", {
1105
+ className: "vadm-h",
1106
+ style: { marginBottom: 2 },
1107
+ children: selected.name || selected.email
1108
+ }), /* @__PURE__ */ jsx("div", {
1109
+ className: "vadm-muted",
1110
+ children: selected.email
1111
+ })]
1112
+ }),
1113
+ /* @__PURE__ */ jsx("span", {
1114
+ className: "vadm-badge",
1115
+ "data-on": selected.emailVerified ? "true" : void 0,
1116
+ children: selected.emailVerified ? "verified" : "unverified"
1117
+ })
1118
+ ]
1119
+ }),
1120
+ /* @__PURE__ */ jsxs("div", {
1121
+ className: "vadm-metrics",
1122
+ children: [
1123
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: selected.projectCount }), /* @__PURE__ */ jsx("span", { children: "apps" })] }),
1124
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: selected.ownedProjectCount }), /* @__PURE__ */ jsx("span", { children: "owned" })] }),
1125
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: selected.activeSessionCount }), /* @__PURE__ */ jsx("span", { children: "sessions" })] }),
1126
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: selected.flagOverrideCount }), /* @__PURE__ */ jsx("span", { children: "flags" })] })
1127
+ ]
1128
+ }),
1129
+ /* @__PURE__ */ jsxs("div", {
1130
+ className: "vadm-kv",
1131
+ children: [
1132
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "User ID" }), /* @__PURE__ */ jsx("code", { children: selected.id })] }),
1133
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "Joined" }), /* @__PURE__ */ jsx("strong", { children: fmtTime(selected.createdAt) })] }),
1134
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "Updated" }), /* @__PURE__ */ jsx("strong", { children: fmtTime(selected.updatedAt) })] }),
1135
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "Plan" }), /* @__PURE__ */ jsx("strong", { children: balance ? balance.plan : balanceError ? "Unavailable" : "Loading..." })] }),
1136
+ balance && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "Balance" }), /* @__PURE__ */ jsxs("strong", { children: ["$", balance.balanceUsd.toFixed(2)] })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "App credits" }), /* @__PURE__ */ jsxs("strong", { children: ["$", balance.appCreditsUsd.toFixed(2)] })] })] })
1137
+ ]
1138
+ }),
1139
+ /* @__PURE__ */ jsx(ErrorBanner, { error: balanceError })
1140
+ ]
1141
+ })]
1142
+ })
1143
+ ] });
1144
+ }
1145
+ const BUG_STATUSES = [
1146
+ "open",
1147
+ "triaged",
1148
+ "in_progress",
1149
+ "resolved",
1150
+ "wont_fix"
1151
+ ];
1152
+ const STATUS_LABEL = {
1153
+ open: "Open",
1154
+ triaged: "Triaged",
1155
+ in_progress: "In progress",
1156
+ resolved: "Resolved",
1157
+ wont_fix: "Won't fix"
1158
+ };
1159
+ const OPEN_STATUSES = new Set([
1160
+ "open",
1161
+ "triaged",
1162
+ "in_progress"
1163
+ ]);
1164
+ function fmtTime(ms) {
1165
+ try {
1166
+ return new Date(ms).toLocaleString();
1167
+ } catch {
1168
+ return String(ms);
1169
+ }
1170
+ }
1171
+ function safeHref(v) {
1172
+ if (typeof v !== "string") return void 0;
1173
+ try {
1174
+ const u = new URL(v);
1175
+ return u.protocol === "http:" || u.protocol === "https:" ? v : void 0;
1176
+ } catch {
1177
+ return;
1178
+ }
1179
+ }
1180
+ const STORAGE_HOSTS = ["t3.storage.dev"];
1181
+ function safeImageSrc(v) {
1182
+ if (typeof v !== "string") return void 0;
1183
+ try {
1184
+ const u = new URL(v);
1185
+ if (u.protocol !== "http:" && u.protocol !== "https:") return void 0;
1186
+ const host = u.hostname.toLowerCase();
1187
+ return STORAGE_HOSTS.some((h) => host === h || host.endsWith("." + h)) ? v : void 0;
1188
+ } catch {
1189
+ return;
1190
+ }
1191
+ }
1192
+ function ReportsPanel({ client }) {
1193
+ const [reports, setReports] = useState(null);
1194
+ const [counts, setCounts] = useState({});
1195
+ const [filter, setFilter] = useState("all");
1196
+ const [selectedId, setSelectedId] = useState(null);
1197
+ const [detail, setDetail] = useState(null);
1198
+ const [error, setError] = useState(null);
1199
+ const [busy, setBusy] = useState(false);
1200
+ const [note, setNote] = useState("");
1201
+ const load = useCallback(async () => {
1202
+ try {
1203
+ const res = await client.listReports(filter === "all" ? {} : { status: filter });
1204
+ setReports(res.reports);
1205
+ setCounts(res.counts);
1206
+ setError(null);
1207
+ } catch (e) {
1208
+ setError(e);
1209
+ }
1210
+ }, [client, filter]);
1211
+ useEffect(() => {
1212
+ ensureStyles();
1213
+ load();
1214
+ }, [load]);
1215
+ const openDetail = useCallback(async (id) => {
1216
+ setSelectedId(id);
1217
+ setDetail(null);
1218
+ try {
1219
+ const d = await client.getReport(id);
1220
+ setDetail(d);
1221
+ setNote(d?.adminNotes ?? "");
1222
+ } catch (e) {
1223
+ setError(e);
1224
+ }
1225
+ }, [client]);
1226
+ async function mutate(fn) {
1227
+ setBusy(true);
1228
+ try {
1229
+ await fn();
1230
+ await load();
1231
+ if (selectedId) await openDetail(selectedId);
1232
+ setError(null);
1233
+ } catch (e) {
1234
+ setError(e);
1235
+ } finally {
1236
+ setBusy(false);
1237
+ }
1238
+ }
1239
+ const totalOpen = BUG_STATUSES.filter((s) => OPEN_STATUSES.has(s)).reduce((n, s) => n + (counts[s] ?? 0), 0);
1240
+ return /* @__PURE__ */ jsxs("div", { children: [
1241
+ /* @__PURE__ */ jsxs("p", {
1242
+ className: "vadm-sub",
1243
+ children: [
1244
+ "User-submitted bug reports. Each carries the context to ",
1245
+ /* @__PURE__ */ jsx("strong", { children: "recover" }),
1246
+ " — open or fork the exact app version the reporter saw. ",
1247
+ totalOpen,
1248
+ " open."
1249
+ ]
1250
+ }),
1251
+ /* @__PURE__ */ jsx(ErrorBanner, { error }),
1252
+ /* @__PURE__ */ jsxs("div", {
1253
+ className: "vadm-row",
1254
+ style: {
1255
+ marginBottom: 12,
1256
+ gap: 6,
1257
+ flexWrap: "wrap"
1258
+ },
1259
+ children: [/* @__PURE__ */ jsx("button", {
1260
+ className: "vadm-tab",
1261
+ "data-active": filter === "all",
1262
+ onClick: () => setFilter("all"),
1263
+ children: "All"
1264
+ }), BUG_STATUSES.map((s) => /* @__PURE__ */ jsxs("button", {
1265
+ className: "vadm-tab",
1266
+ "data-active": filter === s,
1267
+ onClick: () => setFilter(s),
1268
+ children: [
1269
+ STATUS_LABEL[s],
1270
+ " ",
1271
+ counts[s] ? `(${counts[s]})` : ""
1272
+ ]
1273
+ }, s))]
1274
+ }),
1275
+ /* @__PURE__ */ jsxs("div", {
1276
+ className: "vadm-spread",
1277
+ style: {
1278
+ alignItems: "flex-start",
1279
+ gap: 16
1280
+ },
1281
+ children: [/* @__PURE__ */ jsx("div", {
1282
+ className: "vadm-grow",
1283
+ style: { minWidth: 0 },
1284
+ children: reports === null ? /* @__PURE__ */ jsx("div", {
1285
+ className: "vadm-empty",
1286
+ children: "Loading…"
1287
+ }) : reports.length === 0 ? /* @__PURE__ */ jsx("div", {
1288
+ className: "vadm-empty",
1289
+ children: "No reports."
1290
+ }) : /* @__PURE__ */ jsxs("table", {
1291
+ className: "vadm-table",
1292
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1293
+ /* @__PURE__ */ jsx("th", { children: "Title" }),
1294
+ /* @__PURE__ */ jsx("th", { children: "Sev" }),
1295
+ /* @__PURE__ */ jsx("th", { children: "Status" }),
1296
+ /* @__PURE__ */ jsx("th", { children: "Reporter" }),
1297
+ /* @__PURE__ */ jsx("th", { children: "When" })
1298
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: reports.map((r) => /* @__PURE__ */ jsxs("tr", {
1299
+ onClick: () => openDetail(r.id),
1300
+ style: {
1301
+ cursor: "pointer",
1302
+ fontWeight: r.id === selectedId ? 600 : void 0
1303
+ },
1304
+ children: [
1305
+ /* @__PURE__ */ jsx("td", {
1306
+ style: {
1307
+ maxWidth: 260,
1308
+ overflow: "hidden",
1309
+ textOverflow: "ellipsis",
1310
+ whiteSpace: "nowrap"
1311
+ },
1312
+ children: r.title
1313
+ }),
1314
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("span", {
1315
+ className: "vadm-badge",
1316
+ "data-on": r.severity === "critical" || r.severity === "high" ? "true" : void 0,
1317
+ children: r.severity
1318
+ }) }),
1319
+ /* @__PURE__ */ jsx("td", { children: STATUS_LABEL[r.status] }),
1320
+ /* @__PURE__ */ jsxs("td", {
1321
+ className: "vadm-muted",
1322
+ children: [r.reporterEmail ?? "—", r.reporterEmail && !r.reporterUserId && /* @__PURE__ */ jsx("span", {
1323
+ className: "vadm-badge",
1324
+ style: { marginLeft: 6 },
1325
+ title: "Self-reported email — not verified",
1326
+ children: "unverified"
1327
+ })]
1328
+ }),
1329
+ /* @__PURE__ */ jsx("td", {
1330
+ className: "vadm-muted",
1331
+ children: fmtTime(r.createdAt)
1332
+ })
1333
+ ]
1334
+ }, r.id)) })]
1335
+ })
1336
+ }), detail && /* @__PURE__ */ jsxs("div", {
1337
+ className: "vadm-detail vadm-card",
1338
+ style: {
1339
+ width: 360,
1340
+ flexShrink: 0
1341
+ },
1342
+ children: [
1343
+ /* @__PURE__ */ jsxs("div", {
1344
+ style: {
1345
+ border: "1px solid #f0d8a8",
1346
+ background: "#fffaf0",
1347
+ borderRadius: 8,
1348
+ padding: "8px 10px",
1349
+ margin: "0 0 12px"
1350
+ },
1351
+ children: [
1352
+ /* @__PURE__ */ jsx("div", {
1353
+ className: "vadm-muted",
1354
+ style: {
1355
+ fontSize: 11,
1356
+ marginBottom: 4,
1357
+ textTransform: "uppercase",
1358
+ letterSpacing: .4
1359
+ },
1360
+ children: "Reported text · untrusted input"
1361
+ }),
1362
+ /* @__PURE__ */ jsx("p", {
1363
+ className: "vadm-h",
1364
+ style: { margin: "0 0 4px" },
1365
+ children: detail.title
1366
+ }),
1367
+ /* @__PURE__ */ jsx("p", {
1368
+ style: {
1369
+ whiteSpace: "pre-wrap",
1370
+ wordBreak: "break-word",
1371
+ margin: 0
1372
+ },
1373
+ children: detail.body || /* @__PURE__ */ jsx("em", {
1374
+ className: "vadm-muted",
1375
+ children: "No description."
1376
+ })
1377
+ })
1378
+ ]
1379
+ }),
1380
+ /* @__PURE__ */ jsxs("div", {
1381
+ className: "vadm-field",
1382
+ children: [/* @__PURE__ */ jsx("label", {
1383
+ className: "vadm-label",
1384
+ children: "Status"
1385
+ }), /* @__PURE__ */ jsx("select", {
1386
+ className: "vadm-select",
1387
+ value: detail.status,
1388
+ disabled: busy,
1389
+ onChange: (e) => mutate(() => client.setReportStatus(detail.id, e.target.value)),
1390
+ children: BUG_STATUSES.map((s) => /* @__PURE__ */ jsx("option", {
1391
+ value: s,
1392
+ children: STATUS_LABEL[s]
1393
+ }, s))
1394
+ })]
1395
+ }),
1396
+ /* @__PURE__ */ jsxs("div", {
1397
+ className: "vadm-field",
1398
+ children: [/* @__PURE__ */ jsx("label", {
1399
+ className: "vadm-label",
1400
+ children: "Recover context"
1401
+ }), /* @__PURE__ */ jsxs("div", {
1402
+ className: "vadm-mono",
1403
+ style: {
1404
+ fontSize: 12,
1405
+ lineHeight: 1.6
1406
+ },
1407
+ children: [
1408
+ detail.slug ? /* @__PURE__ */ jsxs("div", { children: [
1409
+ "app:",
1410
+ " ",
1411
+ /* @__PURE__ */ jsx("a", {
1412
+ href: `https://omg.dev/${detail.slug}`,
1413
+ target: "_blank",
1414
+ rel: "noreferrer",
1415
+ children: detail.slug
1416
+ }),
1417
+ detail.version != null ? ` · v${detail.version}` : ""
1418
+ ] }) : /* @__PURE__ */ jsx("div", {
1419
+ className: "vadm-muted",
1420
+ children: "no app linked"
1421
+ }),
1422
+ detail.snapshotId && /* @__PURE__ */ jsxs("div", { children: ["snapshot: ", detail.snapshotId] }),
1423
+ detail.runId && /* @__PURE__ */ jsxs("div", { children: ["run: ", detail.runId] }),
1424
+ detail.pageUrl && /* @__PURE__ */ jsxs("div", {
1425
+ style: {
1426
+ overflow: "hidden",
1427
+ textOverflow: "ellipsis",
1428
+ whiteSpace: "nowrap"
1429
+ },
1430
+ children: [
1431
+ "page:",
1432
+ " ",
1433
+ safeHref(detail.pageUrl) ? /* @__PURE__ */ jsx("a", {
1434
+ href: detail.pageUrl,
1435
+ target: "_blank",
1436
+ rel: "noreferrer",
1437
+ children: detail.pageUrl
1438
+ }) : /* @__PURE__ */ jsx("span", { children: detail.pageUrl })
1439
+ ]
1440
+ })
1441
+ ]
1442
+ })]
1443
+ }),
1444
+ detail.slug && /* @__PURE__ */ jsxs("div", {
1445
+ className: "vadm-row",
1446
+ style: {
1447
+ gap: 6,
1448
+ marginBottom: 8
1449
+ },
1450
+ children: [/* @__PURE__ */ jsx("a", {
1451
+ className: "vadm-btn vadm-btn-sm",
1452
+ href: `https://omg.dev/${detail.slug}`,
1453
+ target: "_blank",
1454
+ rel: "noreferrer",
1455
+ children: "Open app"
1456
+ }), /* @__PURE__ */ jsx("a", {
1457
+ className: "vadm-btn vadm-btn-sm",
1458
+ href: `https://omg.dev/${detail.slug}/inspect/data`,
1459
+ target: "_blank",
1460
+ rel: "noreferrer",
1461
+ children: "Inspect"
1462
+ })]
1463
+ }),
1464
+ (() => {
1465
+ const ctx = detail.context;
1466
+ const url = safeImageSrc(ctx?.screenshotUrl) ?? null;
1467
+ return url ? /* @__PURE__ */ jsxs("div", {
1468
+ className: "vadm-field",
1469
+ children: [/* @__PURE__ */ jsx("label", {
1470
+ className: "vadm-label",
1471
+ children: "Screenshot"
1472
+ }), /* @__PURE__ */ jsx("a", {
1473
+ href: url,
1474
+ target: "_blank",
1475
+ rel: "noreferrer",
1476
+ children: /* @__PURE__ */ jsx("img", {
1477
+ src: url,
1478
+ alt: "screenshot",
1479
+ style: {
1480
+ width: "100%",
1481
+ maxHeight: 200,
1482
+ objectFit: "cover",
1483
+ borderRadius: 8,
1484
+ border: "1px solid #eee"
1485
+ }
1486
+ })
1487
+ })]
1488
+ }) : null;
1489
+ })(),
1490
+ detail.context != null && /* @__PURE__ */ jsxs("div", {
1491
+ className: "vadm-field",
1492
+ children: [/* @__PURE__ */ jsx("label", {
1493
+ className: "vadm-label",
1494
+ children: "Diagnostics"
1495
+ }), /* @__PURE__ */ jsx("pre", {
1496
+ className: "vadm-mono",
1497
+ style: {
1498
+ fontSize: 11,
1499
+ maxHeight: 160,
1500
+ overflow: "auto",
1501
+ margin: 0
1502
+ },
1503
+ children: JSON.stringify(detail.context, null, 2)
1504
+ })]
1505
+ }),
1506
+ /* @__PURE__ */ jsxs("div", {
1507
+ className: "vadm-field",
1508
+ children: [
1509
+ /* @__PURE__ */ jsx("label", {
1510
+ className: "vadm-label",
1511
+ children: "Triage note"
1512
+ }),
1513
+ /* @__PURE__ */ jsx("textarea", {
1514
+ className: "vadm-textarea",
1515
+ rows: 3,
1516
+ value: note,
1517
+ onChange: (e) => setNote(e.target.value),
1518
+ disabled: busy
1519
+ }),
1520
+ /* @__PURE__ */ jsx("button", {
1521
+ className: "vadm-btn vadm-btn-sm",
1522
+ disabled: busy || note === (detail.adminNotes ?? ""),
1523
+ onClick: () => mutate(() => client.addReportNote(detail.id, note)),
1524
+ style: { marginTop: 6 },
1525
+ children: "Save note"
1526
+ })
1527
+ ]
1528
+ }),
1529
+ /* @__PURE__ */ jsxs("div", {
1530
+ className: "vadm-spread",
1531
+ style: { marginTop: 12 },
1532
+ children: [/* @__PURE__ */ jsxs("span", {
1533
+ className: "vadm-muted",
1534
+ style: { fontSize: 12 },
1535
+ children: [detail.reporterEmail ?? "anonymous", detail.reporterEmail && !detail.reporterUserId && " · unverified"]
1536
+ }), /* @__PURE__ */ jsx("button", {
1537
+ className: "vadm-btn vadm-btn-sm",
1538
+ disabled: busy,
1539
+ onClick: () => {
1540
+ if (confirm("Delete this report?")) mutate(() => client.removeReport(detail.id).then(() => {
1541
+ setDetail(null);
1542
+ setSelectedId(null);
1543
+ }));
1544
+ },
1545
+ children: "Delete"
1546
+ })]
1547
+ })
1548
+ ]
1549
+ })]
1550
+ })
1551
+ ] });
1552
+ }
1553
+ const DEFAULT_PANELS = [
1554
+ {
1555
+ id: "flags",
1556
+ label: "Flags",
1557
+ render: (c) => /* @__PURE__ */ jsx(FlagsPanel, { client: c })
1558
+ },
1559
+ {
1560
+ id: "users",
1561
+ label: "Users",
1562
+ render: (c) => /* @__PURE__ */ jsx(UsersPanel, { client: c })
1563
+ },
1564
+ {
1565
+ id: "reports",
1566
+ label: "Reports",
1567
+ render: (c) => /* @__PURE__ */ jsx(ReportsPanel, { client: c })
1568
+ },
1569
+ {
1570
+ id: "pricing",
1571
+ label: "Pricing",
1572
+ render: (c) => /* @__PURE__ */ jsx(PricingPanel, { client: c })
1573
+ }
1574
+ ];
1575
+ function AdminConsole({ client, config, panels = DEFAULT_PANELS }) {
1576
+ const resolved = useMemo(() => client ?? (config ? new AdminClient(config) : null), [client, config]);
1577
+ const [active, setActive] = useState(panels[0]?.id);
1578
+ useEffect(() => {
1579
+ ensureStyles();
1580
+ }, []);
1581
+ if (!resolved) return /* @__PURE__ */ jsx("div", {
1582
+ className: "vadm",
1583
+ children: /* @__PURE__ */ jsx("div", {
1584
+ className: "vadm-err",
1585
+ children: "AdminConsole requires a `client` or `config` prop."
1586
+ })
1587
+ });
1588
+ const current = panels.find((p) => p.id === active) ?? panels[0];
1589
+ return /* @__PURE__ */ jsxs("div", {
1590
+ className: "vadm",
1591
+ children: [/* @__PURE__ */ jsx("div", {
1592
+ className: "vadm-tabs",
1593
+ children: panels.map((p) => /* @__PURE__ */ jsx("button", {
1594
+ className: "vadm-tab",
1595
+ "data-active": p.id === current?.id,
1596
+ onClick: () => setActive(p.id),
1597
+ children: p.label
1598
+ }, p.id))
1599
+ }), current?.render(resolved)]
1600
+ });
1601
+ }
1602
+ //#endregion
1603
+ export { UsersPanel as a, STYLE_ID as c, ReportsPanel as i, ensureStyles as l, FlagsPanel as n, AdminClient as o, PricingPanel as r, AdminError as s, AdminConsole as t };