@fonderie/vue-admin-screens 1.0.2 → 1.2.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/dist/index.cjs CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  EnvironmentScreen: () => EnvironmentScreen,
30
30
  MigrationsScreen: () => MigrationsScreen,
31
31
  ModulesScreen: () => ModulesScreen,
32
+ OperatorsScreen: () => OperatorsScreen,
32
33
  RoutesScreen: () => RoutesScreen,
33
34
  SubscriberScreen: () => SubscriberScreen,
34
35
  TokensScreen: () => TokensScreen,
@@ -39,7 +40,7 @@ module.exports = __toCommonJS(index_exports);
39
40
  // src/screens/AdminShell.ts
40
41
  var import_vue_config_admin_screens = require("@fonderie/vue-config-admin-screens");
41
42
  var import_vue_courier_admin_screens = require("@fonderie/vue-courier-admin-screens");
42
- var import_vue14 = require("vue");
43
+ var import_vue16 = require("vue");
43
44
 
44
45
  // src/styles.ts
45
46
  var t = {
@@ -61,7 +62,32 @@ var t = {
61
62
  trackingDisplay: "var(--fonderie-tracking-display,-0.05em)",
62
63
  shadowCard: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))"
63
64
  };
64
- var styles = {
65
+ var tint = (token, pct) => `color-mix(in srgb, ${token} ${pct}%, transparent)`;
66
+ var toneColor = {
67
+ ok: t.accentStrong,
68
+ bad: t.danger,
69
+ // Amber on white fails contrast; mixing in the text colour darkens it in
70
+ // light mode and lightens it in dark, from the same two tokens.
71
+ warn: `color-mix(in srgb, ${t.warning} 70%, ${t.text})`,
72
+ neutral: t.muted,
73
+ info: t.text
74
+ };
75
+ var control = {
76
+ display: "inline-flex",
77
+ alignItems: "center",
78
+ justifyContent: "center",
79
+ gap: 6,
80
+ height: 32,
81
+ boxSizing: "border-box",
82
+ borderRadius: 6,
83
+ padding: "0 12px",
84
+ fontSize: 13,
85
+ fontWeight: 500,
86
+ fontFamily: "inherit",
87
+ lineHeight: 1,
88
+ whiteSpace: "nowrap"
89
+ };
90
+ var raw = {
65
91
  shell: {
66
92
  display: "flex",
67
93
  minHeight: "100vh",
@@ -69,140 +95,571 @@ var styles = {
69
95
  color: t.text,
70
96
  background: t.bg
71
97
  },
72
- // The sidebar sits on the raised surface against the page canvas, which is
73
- // what separates the two without a heavy rule between them.
98
+ // Sticky, full height, scrolls on its own: the nav never scrolls away with
99
+ // a long table, and the footer (session controls) stays in reach.
74
100
  nav: {
75
- width: 220,
76
- borderRight: `1px solid ${t.border}`,
77
- padding: 16,
101
+ width: 248,
78
102
  flexShrink: 0,
79
- background: t.surface
103
+ boxSizing: "border-box",
104
+ borderRight: `1px solid ${t.border}`,
105
+ background: t.surface,
106
+ position: "sticky",
107
+ top: 0,
108
+ height: "100vh",
109
+ display: "flex",
110
+ flexDirection: "column"
111
+ },
112
+ navBrand: {
113
+ display: "flex",
114
+ alignItems: "center",
115
+ gap: 10,
116
+ padding: "18px 16px 14px",
117
+ borderBottom: `1px solid ${t.borderLight}`
80
118
  },
119
+ navMark: {
120
+ width: 28,
121
+ height: 28,
122
+ borderRadius: 7,
123
+ background: t.text,
124
+ color: t.surface,
125
+ display: "grid",
126
+ placeItems: "center",
127
+ flexShrink: 0
128
+ },
129
+ navBrandTitle: {
130
+ fontSize: 14,
131
+ fontWeight: 600,
132
+ letterSpacing: t.trackingDisplay,
133
+ lineHeight: 1.2,
134
+ overflow: "hidden",
135
+ textOverflow: "ellipsis",
136
+ whiteSpace: "nowrap"
137
+ },
138
+ navScroll: { flex: 1, overflowY: "auto", padding: "8px 10px 16px" },
81
139
  navGroup: {
82
140
  fontSize: 11,
83
141
  textTransform: "uppercase",
84
142
  letterSpacing: 0.6,
85
143
  color: t.muted,
86
- margin: "16px 0 6px",
144
+ margin: "16px 8px 4px",
87
145
  fontWeight: 600
88
146
  },
89
147
  navItem: {
90
- display: "block",
148
+ display: "flex",
149
+ alignItems: "center",
150
+ gap: 10,
91
151
  width: "100%",
92
152
  textAlign: "left",
93
153
  background: "none",
94
154
  border: "none",
95
- borderRadius: t.radius,
96
- padding: "6px 8px",
155
+ borderRadius: 6,
156
+ padding: "7px 8px",
97
157
  cursor: "pointer",
98
- fontSize: 14,
99
- color: t.text,
100
- fontFamily: "inherit"
158
+ fontSize: 13.5,
159
+ fontWeight: 500,
160
+ color: t.muted,
161
+ fontFamily: "inherit",
162
+ position: "relative"
163
+ },
164
+ navItemActive: { background: tint(t.accent, 12), color: t.text, fontWeight: 600 },
165
+ navIconActive: { color: t.accentStrong },
166
+ navFooter: {
167
+ borderTop: `1px solid ${t.borderLight}`,
168
+ padding: 12,
169
+ display: "flex",
170
+ flexDirection: "column",
171
+ gap: 10
172
+ },
173
+ // Phone layout: a top bar with the menu button replaces the sidebar, which
174
+ // opens as a drawer over the content.
175
+ topbar: {
176
+ position: "sticky",
177
+ top: 0,
178
+ zIndex: 20,
179
+ display: "flex",
180
+ alignItems: "center",
181
+ gap: 10,
182
+ padding: "10px 16px",
183
+ background: t.surface,
184
+ borderBottom: `1px solid ${t.border}`
185
+ },
186
+ drawer: {
187
+ position: "fixed",
188
+ inset: 0,
189
+ zIndex: 30,
190
+ display: "flex"
191
+ },
192
+ scrim: { flex: 1, background: "rgba(0,0,0,.35)" },
193
+ main: { flex: 1, minWidth: 0 },
194
+ container: { padding: "32px 40px 64px", maxWidth: 1160, boxSizing: "border-box" },
195
+ // ── page header ──
196
+ header: {
197
+ display: "flex",
198
+ alignItems: "flex-start",
199
+ justifyContent: "space-between",
200
+ gap: 16,
201
+ marginBottom: 24,
202
+ flexWrap: "wrap"
101
203
  },
102
- navItemActive: { background: t.surfaceAlt, fontWeight: 600, color: t.accentStrong },
103
- main: { flex: 1, padding: 24, maxWidth: 960 },
104
- container: { padding: 24, maxWidth: 960 },
105
204
  title: {
106
- fontSize: 20,
205
+ fontSize: 22,
107
206
  fontWeight: 600,
108
- marginTop: 0,
109
- marginBottom: 12,
110
- letterSpacing: t.trackingDisplay
207
+ margin: 0,
208
+ letterSpacing: t.trackingDisplay,
209
+ lineHeight: 1.25
111
210
  },
211
+ lead: { color: t.muted, fontSize: 13.5, margin: "4px 0 0", lineHeight: 1.5 },
212
+ actions: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" },
112
213
  subtitle: {
113
214
  fontSize: 15,
114
215
  fontWeight: 600,
115
- marginTop: 24,
116
- marginBottom: 8,
117
- letterSpacing: t.trackingDisplay
216
+ marginTop: 32,
217
+ marginBottom: 10,
218
+ letterSpacing: t.trackingDisplay,
219
+ display: "flex",
220
+ alignItems: "center",
221
+ gap: 8
222
+ },
223
+ // ── feedback ──
224
+ status: { padding: "24px 0", color: t.muted, fontSize: 13.5 },
225
+ error: {
226
+ color: t.danger,
227
+ background: tint(t.danger, 8),
228
+ border: `1px solid ${tint(t.danger, 25)}`,
229
+ borderRadius: t.radiusLg,
230
+ padding: "10px 14px",
231
+ marginBottom: 16,
232
+ fontSize: 13.5
233
+ },
234
+ notice: {
235
+ color: t.text,
236
+ background: tint(t.warning, 10),
237
+ border: `1px solid ${tint(t.warning, 35)}`,
238
+ borderRadius: t.radiusLg,
239
+ padding: "10px 14px",
240
+ marginBottom: 16,
241
+ fontSize: 13.5
118
242
  },
119
- status: { padding: 12, color: t.muted },
120
- error: { color: t.danger, marginBottom: 12, fontSize: 14 },
121
243
  ok: { color: t.accentStrong, fontWeight: 600 },
122
244
  bad: { color: t.danger, fontWeight: 600 },
123
245
  advice: { color: t.warning },
124
246
  muted: { color: t.muted, fontSize: 13 },
125
- mono: { fontFamily: t.mono, fontSize: 13 },
126
- table: { width: "100%", borderCollapse: "collapse", fontSize: 14 },
247
+ mono: { fontFamily: t.mono, fontSize: 12.5 },
248
+ code: {
249
+ fontFamily: t.mono,
250
+ fontSize: 12,
251
+ background: t.surfaceAlt,
252
+ border: `1px solid ${t.borderLight}`,
253
+ borderRadius: 4,
254
+ padding: "1px 5px"
255
+ },
256
+ // ── surfaces ──
257
+ // Tables and lists are cards themselves, so every screen gets the framed,
258
+ // rounded look without wrapping each one. The last row's rule is removed in
259
+ // the shell stylesheet (a pseudo-selector); embedded without the shell it
260
+ // simply doubles against the frame.
261
+ table: {
262
+ width: "100%",
263
+ borderCollapse: "separate",
264
+ borderSpacing: 0,
265
+ fontSize: 13.5,
266
+ background: t.surface,
267
+ border: `1px solid ${t.border}`,
268
+ borderRadius: t.radiusLg,
269
+ overflow: "hidden",
270
+ boxShadow: t.shadowCard
271
+ },
127
272
  th: {
128
273
  textAlign: "left",
129
274
  borderBottom: `1px solid ${t.border}`,
130
- padding: "6px 8px",
275
+ padding: "9px 14px",
276
+ fontWeight: 600,
277
+ fontSize: 11.5,
278
+ textTransform: "uppercase",
279
+ letterSpacing: 0.4,
280
+ color: t.muted,
281
+ background: t.surfaceAlt,
282
+ whiteSpace: "nowrap"
283
+ },
284
+ td: {
285
+ borderBottom: `1px solid ${t.borderLight}`,
286
+ padding: "11px 14px",
287
+ verticalAlign: "top"
288
+ },
289
+ list: {
290
+ listStyle: "none",
291
+ padding: 0,
292
+ margin: 0,
293
+ background: t.surface,
294
+ border: `1px solid ${t.border}`,
295
+ borderRadius: t.radiusLg,
296
+ overflow: "hidden",
297
+ boxShadow: t.shadowCard
298
+ },
299
+ row: { borderBottom: `1px solid ${t.borderLight}`, padding: "12px 16px" },
300
+ card: {
301
+ background: t.surface,
302
+ border: `1px solid ${t.border}`,
303
+ borderRadius: t.radiusLg,
304
+ padding: 16,
305
+ boxShadow: t.shadowCard
306
+ },
307
+ grid: {
308
+ display: "grid",
309
+ gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))",
310
+ gap: 12,
311
+ marginBottom: 24
312
+ },
313
+ statLabel: { fontSize: 12, color: t.muted, fontWeight: 500 },
314
+ statValue: {
315
+ fontSize: 26,
131
316
  fontWeight: 600,
132
- color: t.muted
317
+ letterSpacing: t.trackingDisplay,
318
+ lineHeight: 1.2,
319
+ marginTop: 6
320
+ },
321
+ statHint: { fontSize: 12, color: t.muted, marginTop: 2 },
322
+ empty: {
323
+ background: t.surface,
324
+ border: `1px dashed ${t.border}`,
325
+ borderRadius: t.radiusLg,
326
+ padding: "40px 24px",
327
+ textAlign: "center",
328
+ color: t.muted,
329
+ fontSize: 13.5
330
+ },
331
+ emptyTitle: { color: t.text, fontWeight: 600, fontSize: 14.5, margin: "10px 0 4px" },
332
+ dl: { display: "grid", gridTemplateColumns: "160px 1fr", margin: 0 },
333
+ dt: {
334
+ color: t.muted,
335
+ fontSize: 13,
336
+ padding: "10px 16px",
337
+ borderBottom: `1px solid ${t.borderLight}`
338
+ },
339
+ dd: {
340
+ margin: 0,
341
+ padding: "10px 16px",
342
+ borderBottom: `1px solid ${t.borderLight}`,
343
+ fontSize: 13.5,
344
+ minWidth: 0
133
345
  },
134
- td: { borderBottom: `1px solid ${t.borderLight}`, padding: "6px 8px", verticalAlign: "top" },
135
- list: { listStyle: "none", padding: 0, margin: 0 },
136
- row: { borderBottom: `1px solid ${t.borderLight}`, padding: "8px 0" },
137
346
  badge: {
138
- display: "inline-block",
347
+ display: "inline-flex",
348
+ alignItems: "center",
349
+ gap: 5,
139
350
  borderRadius: 999,
140
- padding: "1px 8px",
141
- fontSize: 12,
351
+ padding: "2px 8px",
352
+ fontSize: 11.5,
353
+ fontWeight: 500,
354
+ lineHeight: 1.5,
142
355
  background: t.surfaceAlt,
143
356
  border: `1px solid ${t.border}`,
144
- color: t.muted
357
+ color: t.muted,
358
+ whiteSpace: "nowrap"
359
+ },
360
+ pill: {
361
+ display: "inline-flex",
362
+ alignItems: "center",
363
+ gap: 6,
364
+ borderRadius: 999,
365
+ padding: "2px 9px 2px 8px",
366
+ fontSize: 12,
367
+ fontWeight: 600,
368
+ lineHeight: 1.5,
369
+ whiteSpace: "nowrap",
370
+ verticalAlign: "baseline"
145
371
  },
372
+ dot: { width: 6, height: 6, borderRadius: 999, flexShrink: 0, background: "currentColor" },
373
+ // ── controls ──
146
374
  button: {
375
+ ...control,
147
376
  background: t.surface,
148
377
  border: `1px solid ${t.border}`,
149
- borderRadius: t.radius,
150
- padding: "4px 10px",
151
- cursor: "pointer",
152
- fontSize: 13,
153
378
  color: t.text,
154
- fontFamily: "inherit",
379
+ cursor: "pointer",
155
380
  boxShadow: t.shadowCard
156
381
  },
157
- toolbar: { display: "flex", gap: 8, alignItems: "center", marginBottom: 12 },
158
- // Surfaces the screens compose themselves, kept here so a card looks the
159
- // same on every page rather than being re-invented per screen.
160
- card: {
382
+ buttonPrimary: {
383
+ ...control,
384
+ background: t.text,
385
+ border: `1px solid ${t.text}`,
386
+ color: t.surface,
387
+ cursor: "pointer"
388
+ },
389
+ buttonDanger: {
390
+ ...control,
161
391
  background: t.surface,
392
+ border: `1px solid ${tint(t.danger, 40)}`,
393
+ color: t.danger,
394
+ cursor: "pointer"
395
+ },
396
+ buttonGhost: {
397
+ ...control,
398
+ background: "none",
399
+ border: "1px solid transparent",
400
+ color: t.muted,
401
+ cursor: "pointer",
402
+ padding: "0 8px"
403
+ },
404
+ link: {
405
+ background: "none",
406
+ border: "none",
407
+ padding: 0,
408
+ color: t.text,
409
+ fontFamily: "inherit",
410
+ fontSize: "inherit",
411
+ fontWeight: 500,
412
+ cursor: "pointer",
413
+ textAlign: "left"
414
+ },
415
+ input: {
416
+ height: 32,
417
+ boxSizing: "border-box",
162
418
  border: `1px solid ${t.border}`,
163
- borderRadius: t.radiusLg,
164
- padding: 16,
165
- boxShadow: t.shadowCard
419
+ borderRadius: 6,
420
+ padding: "0 10px",
421
+ fontSize: 13.5,
422
+ fontFamily: "inherit",
423
+ background: t.surface,
424
+ color: t.text,
425
+ minWidth: 0
166
426
  },
427
+ toolbar: { display: "flex", gap: 8, alignItems: "center", marginBottom: 16, flexWrap: "wrap" },
167
428
  accent: { color: t.accent }
168
429
  };
430
+ var UNITLESS = /* @__PURE__ */ new Set([
431
+ "fontWeight",
432
+ "lineHeight",
433
+ "opacity",
434
+ "zIndex",
435
+ "flex",
436
+ "flexGrow",
437
+ "flexShrink",
438
+ "order"
439
+ ]);
440
+ function withUnits(sheet) {
441
+ const out = {};
442
+ for (const [name, rule] of Object.entries(sheet)) {
443
+ const fixed = {};
444
+ for (const [prop, value] of Object.entries(rule)) {
445
+ fixed[prop] = typeof value === "number" && !UNITLESS.has(prop) ? `${value}px` : value;
446
+ }
447
+ out[name] = fixed;
448
+ }
449
+ return out;
450
+ }
451
+ var styles = withUnits(raw);
452
+
453
+ // src/ui.ts
454
+ var import_vue = require("vue");
455
+
456
+ // src/icons.ts
457
+ var ICONS = {
458
+ attention: ["M22 12h-4l-3 9L9 3l-3 9H2"],
459
+ modules: [
460
+ "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z",
461
+ "M3.3 7 12 12l8.7-5",
462
+ "M12 22V12"
463
+ ],
464
+ environment: [
465
+ "M4 21v-7",
466
+ "M4 10V3",
467
+ "M12 21v-9",
468
+ "M12 8V3",
469
+ "M20 21v-5",
470
+ "M20 12V3",
471
+ "M1 14h6",
472
+ "M9 8h6",
473
+ "M17 16h6"
474
+ ],
475
+ doctor: [
476
+ "M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"
477
+ ],
478
+ routes: [
479
+ "M6 3v12",
480
+ "M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
481
+ "M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
482
+ "M18 9a9 9 0 0 1-9 9"
483
+ ],
484
+ users: [
485
+ "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",
486
+ "M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z",
487
+ "M22 21v-2a4 4 0 0 0-3-3.87",
488
+ "M16 3.13a4 4 0 0 1 0 7.75"
489
+ ],
490
+ catalog: [
491
+ "M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z",
492
+ "M7.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"
493
+ ],
494
+ subscriber: ["M2 7h20v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z", "M2 11h20", "M6 15h4"],
495
+ settings: [
496
+ "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",
497
+ "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"
498
+ ],
499
+ templates: [
500
+ "M22 7l-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7",
501
+ "M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"
502
+ ],
503
+ audit: ["M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z", "M9 12l2 2 4-4"],
504
+ log: ["M12 8v4l3 3", "M3.05 11a9 9 0 1 1 .5 4m-.5 5v-5h5"],
505
+ tokens: [
506
+ "M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z",
507
+ "M16.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"
508
+ ],
509
+ migrations: [
510
+ "M12 22c4.97 0 9-1.79 9-4V6",
511
+ "M3 6v12c0 2.21 4.03 4 9 4",
512
+ "M12 10c4.97 0 9-1.79 9-4s-4.03-4-9-4-9 1.79-9 4 4.03 4 9 4z",
513
+ "M3 12c0 2.21 4.03 4 9 4s9-1.79 9-4"
514
+ ],
515
+ menu: ["M4 6h16", "M4 12h16", "M4 18h16"],
516
+ close: ["M18 6 6 18", "m6 6 12 12"],
517
+ refresh: [
518
+ "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",
519
+ "M21 3v5h-5",
520
+ "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",
521
+ "M8 16H3v5"
522
+ ],
523
+ check: ["M20 6 9 17l-5-5"],
524
+ alert: [
525
+ "M12 9v4",
526
+ "M12 17h.01",
527
+ "M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
528
+ ],
529
+ search: ["M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z", "m21 21-4.3-4.3"],
530
+ back: ["m12 19-7-7 7-7", "M19 12H5"],
531
+ logout: ["M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4", "m16 17 5-5-5-5", "M21 12H9"],
532
+ lock: [
533
+ "M5 11h14a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2z",
534
+ "M7 11V7a5 5 0 0 1 10 0v4"
535
+ ],
536
+ plus: ["M12 5v14", "M5 12h14"],
537
+ mark: [
538
+ "M4 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z",
539
+ "M15 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z",
540
+ "M15 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z",
541
+ "M4 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z"
542
+ ]
543
+ };
544
+
545
+ // src/ui.ts
546
+ function icon(name, size = 16, style) {
547
+ return (0, import_vue.h)(
548
+ "svg",
549
+ {
550
+ width: size,
551
+ height: size,
552
+ viewBox: "0 0 24 24",
553
+ fill: "none",
554
+ stroke: "currentColor",
555
+ "stroke-width": 2,
556
+ "stroke-linecap": "round",
557
+ "stroke-linejoin": "round",
558
+ "aria-hidden": "true",
559
+ style: { flexShrink: 0, ...style }
560
+ },
561
+ ICONS[name].map((d) => (0, import_vue.h)("path", { key: d, d }))
562
+ );
563
+ }
564
+ function pageHeader(title, lead, actions) {
565
+ return (0, import_vue.h)("div", { style: styles.header }, [
566
+ (0, import_vue.h)("div", { style: { minWidth: 0 } }, [
567
+ (0, import_vue.h)("h1", { style: styles.title }, title),
568
+ lead ? (0, import_vue.h)("p", { style: styles.lead }, lead) : null
569
+ ]),
570
+ actions?.filter(Boolean).length ? (0, import_vue.h)("div", { style: styles.actions }, actions) : null
571
+ ]);
572
+ }
573
+ function pill(tone, text, dot = true) {
574
+ const color = toneColor[tone];
575
+ return (0, import_vue.h)(
576
+ "span",
577
+ { style: { ...styles.pill, color, background: tint(color, tone === "neutral" ? 10 : 13) } },
578
+ [dot ? (0, import_vue.h)("span", { style: styles.dot }) : null, text]
579
+ );
580
+ }
581
+ function stat(label, value, hint, tone) {
582
+ return (0, import_vue.h)("div", { style: styles.card }, [
583
+ (0, import_vue.h)("div", { style: styles.statLabel }, label),
584
+ (0, import_vue.h)(
585
+ "div",
586
+ { style: { ...styles.statValue, ...tone ? { color: toneColor[tone] } : {} } },
587
+ value
588
+ ),
589
+ hint ? (0, import_vue.h)("div", { style: styles.statHint }, hint) : null
590
+ ]);
591
+ }
592
+ function empty(title, body, iconName = "check") {
593
+ return (0, import_vue.h)("div", { style: styles.empty }, [
594
+ icon(iconName, 22),
595
+ (0, import_vue.h)("div", { style: styles.emptyTitle }, title),
596
+ body ? (0, import_vue.h)("div", body) : null
597
+ ]);
598
+ }
599
+ function methodChip(method) {
600
+ const tone = method === "GET" ? "ok" : method === "DELETE" ? "bad" : method === "POST" ? "info" : "warn";
601
+ const color = toneColor[tone];
602
+ return (0, import_vue.h)(
603
+ "span",
604
+ {
605
+ style: {
606
+ ...styles.mono,
607
+ fontSize: "11px",
608
+ fontWeight: 700,
609
+ color,
610
+ background: tint(color, 12),
611
+ borderRadius: "4px",
612
+ padding: "2px 6px",
613
+ display: "inline-block",
614
+ minWidth: "44px",
615
+ textAlign: "center"
616
+ }
617
+ },
618
+ method
619
+ );
620
+ }
169
621
 
170
622
  // src/screens/AdminLogScreen.ts
171
623
  var import_vue_admin = require("@fonderie/vue-admin");
172
- var import_vue2 = require("vue");
624
+ var import_vue3 = require("vue");
173
625
 
174
626
  // src/screens/common.ts
175
- var import_vue = require("vue");
627
+ var import_vue2 = require("vue");
176
628
  function page(title, state, content, toolbar = [], options = {}) {
177
- const body = state.isLoading.value ? (0, import_vue.h)("p", { style: styles.status }, options.loadingText ?? "Loading\u2026") : state.error.value ? (0, import_vue.h)(
629
+ const body = state.isLoading.value ? (0, import_vue2.h)("p", { style: styles.status }, options.loadingText ?? "Loading\u2026") : state.error.value ? (0, import_vue2.h)(
178
630
  "p",
179
631
  { style: styles.error, role: "alert" },
180
632
  (options.errorText ?? ((e) => e.explanation))(state.error.value)
181
633
  ) : content();
182
- return (0, import_vue.h)("div", { style: styles.container }, [
183
- (0, import_vue.h)("div", { style: styles.toolbar }, [
184
- (0, import_vue.h)("h1", { style: { ...styles.title, marginBottom: 0 } }, title),
185
- ...toolbar
186
- ]),
187
- body
188
- ]);
634
+ return (0, import_vue2.h)("div", { style: styles.container }, [pageHeader(title, options.lead, toolbar), body]);
189
635
  }
190
636
  function table(headers, rows) {
191
- return (0, import_vue.h)("table", { style: styles.table }, [
192
- (0, import_vue.h)("thead", [
193
- (0, import_vue.h)(
637
+ return (0, import_vue2.h)("table", { style: styles.table }, [
638
+ (0, import_vue2.h)("thead", [
639
+ (0, import_vue2.h)(
194
640
  "tr",
195
- headers.map((t2) => (0, import_vue.h)("th", { style: styles.th }, t2))
641
+ headers.map(
642
+ (t2) => typeof t2 === "string" ? (0, import_vue2.h)("th", { style: styles.th }, t2) : (0, import_vue2.h)("th", { style: { ...styles.th, ...t2.style } }, t2.label)
643
+ )
196
644
  )
197
645
  ]),
198
- (0, import_vue.h)("tbody", rows)
646
+ (0, import_vue2.h)("tbody", rows)
199
647
  ]);
200
648
  }
201
- var td = (children, extra = {}) => (0, import_vue.h)("td", { style: { ...styles.td, ...extra } }, children);
202
- var refreshButton = (label, disabled, onClick) => (0, import_vue.h)("button", { type: "button", style: styles.button, disabled, onClick }, label);
649
+ var td = (children, extra = {}) => (0, import_vue2.h)("td", { style: { ...styles.td, ...extra } }, children);
650
+ var refreshButton = (label, disabled, onClick) => (0, import_vue2.h)("button", { type: "button", style: styles.button, disabled, onClick }, [
651
+ icon("refresh", 14),
652
+ disabled ? "Working\u2026" : label
653
+ ]);
654
+ var loadMoreButton = (onClick) => (0, import_vue2.h)(
655
+ "button",
656
+ { type: "button", style: { ...styles.button, marginTop: "12px" }, onClick },
657
+ "Load more"
658
+ );
659
+ var actionButton = (label, disabled, onClick, style = styles.button) => (0, import_vue2.h)("button", { type: "button", style, disabled, onClick }, label);
203
660
 
204
661
  // src/screens/AdminLogScreen.ts
205
- var AdminLogScreen = (0, import_vue2.defineComponent)({
662
+ var AdminLogScreen = (0, import_vue3.defineComponent)({
206
663
  name: "FonderieAdminLogScreen",
207
664
  props: {
208
665
  client: { type: Object, required: true },
@@ -212,46 +669,48 @@ var AdminLogScreen = (0, import_vue2.defineComponent)({
212
669
  const { entries, hasMore, isLoading, error, refresh, loadMore } = (0, import_vue_admin.useAdminLog)(props.client, {
213
670
  limit: props.pageSize
214
671
  });
215
- return () => (0, import_vue2.h)("div", { style: styles.container }, [
216
- (0, import_vue2.h)("div", { style: styles.toolbar }, [
217
- (0, import_vue2.h)("h1", { style: { ...styles.title, marginBottom: 0 } }, "Admin log"),
218
- refreshButton("Refresh", isLoading.value, () => void refresh())
219
- ]),
220
- error.value ? (0, import_vue2.h)(
672
+ return () => (0, import_vue3.h)("div", { style: styles.container }, [
673
+ pageHeader(
674
+ "Admin log",
675
+ "Every request made through this surface, newest first \u2014 refused ones included.",
676
+ [refreshButton("Refresh", isLoading.value, () => void refresh())]
677
+ ),
678
+ error.value ? (0, import_vue3.h)(
221
679
  "p",
222
680
  { style: styles.error, role: "alert" },
223
681
  error.value.status === 404 ? "The admin log is off \u2014 give AdminModule a store." : error.value.explanation
224
- ) : [
682
+ ) : entries.value.length === 0 && !isLoading.value ? empty("No requests yet", void 0, "log") : [
225
683
  table(
226
684
  ["When", "Actor", "Request", "Status", "Module"],
227
685
  entries.value.map(
228
- (e) => (0, import_vue2.h)("tr", { key: e.id }, [
229
- td(new Date(e.at).toLocaleString(), styles.muted),
686
+ (e) => (0, import_vue3.h)("tr", { key: e.id }, [
687
+ td(new Date(e.at).toLocaleString(), {
688
+ ...styles.muted,
689
+ whiteSpace: "nowrap"
690
+ }),
230
691
  td(e.actor),
231
- td(`${e.method} ${e.path}`, styles.mono),
232
692
  td([
233
- (0, import_vue2.h)(
234
- "span",
235
- { style: e.status >= 400 ? styles.bad : styles.ok },
236
- String(e.status)
237
- ),
693
+ methodChip(e.method),
238
694
  " ",
239
- (0, import_vue2.h)("span", { style: styles.muted }, `${e.durationMs} ms`)
695
+ (0, import_vue3.h)("span", { style: { ...styles.mono, wordBreak: "break-all" } }, e.path)
240
696
  ]),
697
+ td(
698
+ [
699
+ pill(
700
+ e.status >= 500 ? "bad" : e.status >= 400 ? "warn" : "ok",
701
+ String(e.status)
702
+ ),
703
+ " ",
704
+ (0, import_vue3.h)("span", { style: styles.muted }, `${e.durationMs} ms`)
705
+ ],
706
+ { whiteSpace: "nowrap" }
707
+ ),
241
708
  td(e.module, styles.muted)
242
709
  ])
243
710
  )
244
711
  ),
245
- isLoading.value ? (0, import_vue2.h)("p", { style: styles.status }, "Loading\u2026") : null,
246
- hasMore.value && !isLoading.value ? (0, import_vue2.h)(
247
- "button",
248
- {
249
- type: "button",
250
- style: { ...styles.button, marginTop: "12px" },
251
- onClick: () => void loadMore()
252
- },
253
- "Load more"
254
- ) : null
712
+ isLoading.value ? (0, import_vue3.h)("p", { style: styles.status }, "Loading\u2026") : null,
713
+ hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
255
714
  ]
256
715
  ]);
257
716
  }
@@ -259,224 +718,414 @@ var AdminLogScreen = (0, import_vue2.defineComponent)({
259
718
 
260
719
  // src/screens/AttentionScreen.ts
261
720
  var import_vue_admin2 = require("@fonderie/vue-admin");
262
- var import_vue3 = require("vue");
263
- var AttentionScreen = (0, import_vue3.defineComponent)({
721
+ var import_vue4 = require("vue");
722
+ var AttentionScreen = (0, import_vue4.defineComponent)({
264
723
  name: "FonderieAttentionScreen",
265
724
  props: { client: { type: Object, required: true } },
266
725
  setup(props) {
267
726
  const { attention, isLoading, error, refresh } = (0, import_vue_admin2.useAttention)(props.client);
268
- return () => page(
269
- "Attention",
270
- { isLoading, error },
271
- () => {
272
- const a = attention.value;
273
- if (!a) return null;
274
- if (a.items.length === 0)
275
- return (0, import_vue3.h)(
276
- "p",
277
- { style: styles.ok },
278
- `Nothing needs you. Checked ${new Date(a.generatedAt).toLocaleString()}.`
279
- );
280
- return (0, import_vue3.h)(
281
- "ul",
282
- { style: styles.list },
283
- a.items.map(
284
- (item) => (0, import_vue3.h)("li", { key: `${item.source}:${item.message}`, style: styles.row }, [
285
- (0, import_vue3.h)(
286
- "span",
287
- { style: item.severity === "error" ? styles.bad : styles.advice },
288
- item.severity
289
- ),
290
- " ",
291
- (0, import_vue3.h)("span", { style: styles.mono }, item.source),
292
- ` \u2014 ${item.message}`
293
- ])
294
- )
295
- );
296
- },
297
- [refreshButton("Refresh", isLoading.value, () => void refresh())],
298
- { loadingText: "Running the checks\u2026" }
299
- );
727
+ const { manifest } = (0, import_vue_admin2.useManifest)(props.client);
728
+ return () => {
729
+ const a = attention.value;
730
+ const m = manifest.value;
731
+ const errors = a?.items.filter((i) => i.severity === "error") ?? [];
732
+ const advice = a?.items.filter((i) => i.severity !== "error") ?? [];
733
+ const ready = m?.modules.filter((x) => x.readiness.ok).length ?? 0;
734
+ const total = m?.modules.length ?? 0;
735
+ const tiles = (0, import_vue4.h)("div", { style: styles.grid }, [
736
+ stat(
737
+ "Needs action",
738
+ a ? errors.length : "\u2014",
739
+ "errors to fix",
740
+ errors.length ? "bad" : a ? "ok" : void 0
741
+ ),
742
+ stat("Advice", a ? advice.length : "\u2014", "worth a look", advice.length ? "warn" : void 0),
743
+ stat(
744
+ "Modules ready",
745
+ m ? `${ready}/${total}` : "\u2014",
746
+ m ? `admin ${m.admin.version}` : void 0,
747
+ m && ready < total ? "warn" : m ? "ok" : void 0
748
+ ),
749
+ stat("Routes", m ? m.routes.length : "\u2014", m ? m.env : void 0)
750
+ ]);
751
+ return page(
752
+ "Attention",
753
+ // Tiles show while the checks run; only the list waits.
754
+ { isLoading: { value: false }, error },
755
+ () => {
756
+ if (isLoading.value && !a)
757
+ return [tiles, (0, import_vue4.h)("p", { style: styles.status }, "Running the checks\u2026")];
758
+ if (!a) return tiles;
759
+ if (a.items.length === 0)
760
+ return [
761
+ tiles,
762
+ empty(
763
+ "Nothing needs you",
764
+ `Every check passes. Checked ${new Date(a.generatedAt).toLocaleString()}.`
765
+ )
766
+ ];
767
+ return [
768
+ tiles,
769
+ (0, import_vue4.h)(
770
+ "ul",
771
+ { style: styles.list },
772
+ [...errors, ...advice].map(
773
+ (item) => (0, import_vue4.h)(
774
+ "li",
775
+ {
776
+ key: `${item.source}:${item.message}`,
777
+ style: {
778
+ ...styles.row,
779
+ display: "flex",
780
+ gap: "12px",
781
+ alignItems: "flex-start"
782
+ }
783
+ },
784
+ [
785
+ icon("alert", 16, {
786
+ marginTop: "3px",
787
+ color: item.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-warning,#f5a623)"
788
+ }),
789
+ (0, import_vue4.h)("div", { style: { minWidth: 0, flex: 1 } }, [
790
+ (0, import_vue4.h)(
791
+ "div",
792
+ {
793
+ style: {
794
+ display: "flex",
795
+ gap: "8px",
796
+ alignItems: "center",
797
+ flexWrap: "wrap",
798
+ marginBottom: "2px"
799
+ }
800
+ },
801
+ [
802
+ pill(
803
+ item.severity === "error" ? "bad" : "warn",
804
+ item.severity === "error" ? "error" : "advice"
805
+ ),
806
+ (0, import_vue4.h)("span", { style: styles.mono }, item.source)
807
+ ]
808
+ ),
809
+ (0, import_vue4.h)("div", { style: { lineHeight: 1.55 } }, item.message)
810
+ ])
811
+ ]
812
+ )
813
+ )
814
+ )
815
+ ];
816
+ },
817
+ [refreshButton("Refresh", isLoading.value, () => void refresh())],
818
+ {
819
+ lead: a ? `What needs you on this deployment \xB7 checked ${new Date(a.generatedAt).toLocaleTimeString()}` : "What needs you on this deployment"
820
+ }
821
+ );
822
+ };
300
823
  }
301
824
  });
302
825
 
303
826
  // src/screens/EnvironmentScreen.ts
304
827
  var import_vue_admin3 = require("@fonderie/vue-admin");
305
- var import_vue4 = require("vue");
306
- var EnvironmentScreen = (0, import_vue4.defineComponent)({
828
+ var import_vue5 = require("vue");
829
+ var EnvironmentScreen = (0, import_vue5.defineComponent)({
307
830
  name: "FonderieEnvironmentScreen",
308
831
  props: { client: { type: Object, required: true } },
309
832
  setup(props) {
310
833
  const { report, isLoading, error } = (0, import_vue_admin3.useAdminEnvironment)(props.client);
311
- return () => page("Configuration", { isLoading, error }, () => {
834
+ return () => {
312
835
  const r = report.value;
313
- if (!r) return null;
314
- return [
315
- (0, import_vue4.h)("h2", { style: styles.subtitle }, "Readiness"),
316
- (0, import_vue4.h)(
317
- "ul",
318
- { style: styles.list },
319
- r.modules.map(
320
- (m) => (0, import_vue4.h)("li", { key: m.name, style: styles.row }, [
321
- (0, import_vue4.h)("span", { style: styles.mono }, m.name),
322
- " ",
323
- ...m.problems.length === 0 ? [(0, import_vue4.h)("span", { style: styles.ok }, "ok")] : m.problems.map(
324
- (p) => (0, import_vue4.h)(
836
+ const missing = r?.env.filter((e) => !e.set).length ?? 0;
837
+ return page(
838
+ "Environment",
839
+ { isLoading, error },
840
+ () => {
841
+ if (!r) return null;
842
+ return [
843
+ (0, import_vue5.h)("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Variables"),
844
+ r.env.length === 0 ? (0, import_vue5.h)(
845
+ "p",
846
+ { style: styles.muted },
847
+ "No variables declared \u2014 pass `env` to AdminModule."
848
+ ) : (0, import_vue5.h)(
849
+ "div",
850
+ {
851
+ style: {
852
+ ...styles.grid,
853
+ gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))",
854
+ gap: "8px"
855
+ }
856
+ },
857
+ r.env.map(
858
+ (e) => (0, import_vue5.h)(
325
859
  "div",
326
860
  {
327
- key: p.message,
328
- style: p.severity === "error" ? styles.bad : styles.advice
861
+ key: e.name,
862
+ style: {
863
+ ...styles.card,
864
+ padding: "10px 12px",
865
+ display: "flex",
866
+ alignItems: "center",
867
+ justifyContent: "space-between",
868
+ gap: "8px"
869
+ }
329
870
  },
330
- p.message
871
+ [
872
+ (0, import_vue5.h)("span", { style: { ...styles.mono, wordBreak: "break-all" } }, e.name),
873
+ pill(e.set ? "ok" : "bad", e.set ? "set" : "missing")
874
+ ]
331
875
  )
332
876
  )
333
- ])
334
- )
335
- ),
336
- (0, import_vue4.h)("h2", { style: styles.subtitle }, "Environment"),
337
- r.env.length === 0 ? (0, import_vue4.h)("p", { style: styles.muted }, "No variables declared \u2014 pass `env` to AdminModule.") : (0, import_vue4.h)(
338
- "ul",
339
- { style: styles.list },
340
- r.env.map(
341
- (e) => (0, import_vue4.h)("li", { key: e.name, style: styles.row }, [
342
- (0, import_vue4.h)("span", { style: styles.mono }, e.name),
343
- " ",
344
- (0, import_vue4.h)("span", { style: e.set ? styles.ok : styles.bad }, e.set ? "set" : "missing")
345
- ])
346
- )
347
- )
348
- ];
349
- });
877
+ ),
878
+ (0, import_vue5.h)("h2", { style: styles.subtitle }, "Module readiness"),
879
+ (0, import_vue5.h)(
880
+ "ul",
881
+ { style: styles.list },
882
+ r.modules.map(
883
+ (m) => (0, import_vue5.h)("li", { key: m.name, style: styles.row }, [
884
+ (0, import_vue5.h)(
885
+ "div",
886
+ {
887
+ style: {
888
+ display: "flex",
889
+ justifyContent: "space-between",
890
+ gap: "12px",
891
+ alignItems: "center"
892
+ }
893
+ },
894
+ [
895
+ (0, import_vue5.h)("span", { style: { ...styles.mono, fontWeight: 600 } }, m.name),
896
+ pill(
897
+ m.problems.length === 0 ? "ok" : m.problems.some((p) => p.severity === "error") ? "bad" : "warn",
898
+ m.problems.length === 0 ? "ready" : `${m.problems.length} problem${m.problems.length === 1 ? "" : "s"}`
899
+ )
900
+ ]
901
+ ),
902
+ ...m.problems.map(
903
+ (p) => (0, import_vue5.h)(
904
+ "div",
905
+ {
906
+ key: p.message,
907
+ style: {
908
+ marginTop: "6px",
909
+ fontSize: "13px",
910
+ color: p.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-text-muted,#5c5c5c)"
911
+ }
912
+ },
913
+ p.message
914
+ )
915
+ )
916
+ ])
917
+ )
918
+ )
919
+ ];
920
+ },
921
+ [
922
+ r && r.env.length > 0 ? pill(missing ? "bad" : "ok", missing ? `${missing} missing` : "all set") : null
923
+ ],
924
+ {
925
+ lead: "Whether each module is configured and each variable the app reads is set. Values are never shown."
926
+ }
927
+ );
928
+ };
350
929
  }
351
930
  });
352
931
 
353
932
  // src/screens/DoctorScreen.ts
354
933
  var import_vue_admin4 = require("@fonderie/vue-admin");
355
- var import_vue5 = require("vue");
356
- var DoctorScreen = (0, import_vue5.defineComponent)({
934
+ var import_vue6 = require("vue");
935
+ var DoctorScreen = (0, import_vue6.defineComponent)({
357
936
  name: "FonderieDoctorScreen",
358
937
  props: { client: { type: Object, required: true } },
359
938
  setup(props) {
360
939
  const { report, isLoading, error, refresh } = (0, import_vue_admin4.useDoctor)(props.client);
361
940
  return () => page(
362
941
  "Doctor",
363
- { isLoading, error },
942
+ { isLoading: { value: isLoading.value && !report.value }, error },
364
943
  () => {
365
944
  const r = report.value;
366
945
  if (!r) return null;
367
- return (0, import_vue5.h)(
368
- "ul",
369
- { style: styles.list },
946
+ return table(
947
+ [
948
+ { label: "Result", style: { width: "110px" } },
949
+ "Check",
950
+ { label: "Time", style: { textAlign: "right", width: "90px" } }
951
+ ],
370
952
  r.checks.map(
371
- (c) => (0, import_vue5.h)("li", { key: c.name, style: styles.row }, [
372
- (0, import_vue5.h)(
373
- "span",
374
- { style: c.skipped ? styles.muted : c.ok ? styles.ok : styles.bad },
375
- c.skipped ? "skipped" : c.ok ? "ok" : "failed"
953
+ (c) => (0, import_vue6.h)("tr", { key: c.name }, [
954
+ td(
955
+ pill(
956
+ c.skipped ? "neutral" : c.ok ? c.findings.length ? "warn" : "ok" : "bad",
957
+ c.skipped ? "skipped" : c.ok ? c.findings.length ? "advice" : "ok" : "failed"
958
+ )
376
959
  ),
377
- " ",
378
- (0, import_vue5.h)("span", { style: styles.mono }, c.name),
379
- " ",
380
- (0, import_vue5.h)("span", { style: styles.muted }, `${c.module} \xB7 ${c.durationMs} ms`),
381
- c.skipped ? (0, import_vue5.h)("div", { style: styles.muted }, c.skipped) : null,
382
- ...c.findings.map(
383
- (f) => (0, import_vue5.h)("div", { key: f, style: c.ok ? styles.advice : styles.bad }, f)
384
- )
960
+ td([
961
+ (0, import_vue6.h)("div", [
962
+ (0, import_vue6.h)("span", { style: { ...styles.mono, fontWeight: 600 } }, c.name),
963
+ " ",
964
+ (0, import_vue6.h)("span", { style: styles.muted }, c.module)
965
+ ]),
966
+ c.skipped ? (0, import_vue6.h)("div", { style: { ...styles.muted, marginTop: "4px" } }, c.skipped) : null,
967
+ ...c.findings.map(
968
+ (f) => (0, import_vue6.h)(
969
+ "div",
970
+ {
971
+ key: f,
972
+ style: {
973
+ marginTop: "4px",
974
+ fontSize: "13px",
975
+ lineHeight: 1.55,
976
+ color: c.ok ? "var(--fonderie-text,#171717)" : "var(--fonderie-danger,#e00)"
977
+ }
978
+ },
979
+ f
980
+ )
981
+ )
982
+ ]),
983
+ td(`${c.durationMs} ms`, {
984
+ ...styles.muted,
985
+ textAlign: "right",
986
+ whiteSpace: "nowrap"
987
+ })
385
988
  ])
386
989
  )
387
990
  );
388
991
  },
389
992
  [
390
- refreshButton("Run again", isLoading.value, () => void refresh()),
391
- report.value && !isLoading.value ? (0, import_vue5.h)(
392
- "span",
393
- { style: report.value.ok ? styles.ok : styles.bad },
993
+ report.value && !isLoading.value ? pill(
994
+ report.value.ok ? "ok" : "bad",
394
995
  report.value.ok ? "all checks pass" : "a check failed"
395
- ) : (0, import_vue5.h)("span")
996
+ ) : null,
997
+ refreshButton("Run again", isLoading.value, () => void refresh())
396
998
  ],
397
- { loadingText: "Running the checks\u2026" }
999
+ {
1000
+ loadingText: "Running the checks\u2026",
1001
+ lead: "Every reconciliation check across the installed modules, run on demand."
1002
+ }
398
1003
  );
399
1004
  }
400
1005
  });
401
1006
 
402
1007
  // src/screens/ModulesScreen.ts
403
1008
  var import_vue_admin5 = require("@fonderie/vue-admin");
404
- var import_vue6 = require("vue");
405
- var ModulesScreen = (0, import_vue6.defineComponent)({
1009
+ var import_vue7 = require("vue");
1010
+ var ModulesScreen = (0, import_vue7.defineComponent)({
406
1011
  name: "FonderieModulesScreen",
407
1012
  props: { client: { type: Object, required: true } },
408
1013
  setup(props) {
409
1014
  const { manifest, isLoading, error } = (0, import_vue_admin5.useManifest)(props.client);
410
- return () => page("Modules", { isLoading, error }, () => {
1015
+ return () => {
411
1016
  const m = manifest.value;
412
- if (!m) return null;
413
- return [
414
- (0, import_vue6.h)(
415
- "p",
416
- { style: styles.muted },
417
- `${m.env} \xB7 admin ${m.admin.version} \xB7 admin log ${m.admin.log ? "on" : "off"} \xB7 ${m.routes.length} routes`
418
- ),
419
- table(
420
- ["Module", "Version", "Readiness", "Describes admin"],
421
- m.modules.map(
422
- (mod) => (0, import_vue6.h)("tr", { key: mod.name }, [
423
- td(mod.name, styles.mono),
424
- td(mod.version ?? (0, import_vue6.h)("span", { style: styles.muted }, "not reported")),
425
- td([
426
- (0, import_vue6.h)(
427
- "span",
428
- { style: mod.readiness.ok ? styles.ok : styles.bad },
429
- mod.readiness.ok ? "ok" : "error"
430
- ),
431
- ...mod.readiness.problems.map(
432
- (p) => (0, import_vue6.h)(
433
- "div",
434
- {
435
- key: p.message,
436
- style: p.severity === "error" ? styles.bad : styles.advice
437
- },
438
- p.message
1017
+ return page(
1018
+ "Modules",
1019
+ { isLoading, error },
1020
+ () => {
1021
+ if (!m) return null;
1022
+ return table(
1023
+ ["Module", "Version", "Readiness", "Admin"],
1024
+ m.modules.map(
1025
+ (mod) => (0, import_vue7.h)("tr", { key: mod.name }, [
1026
+ td(mod.name, { ...styles.mono, fontWeight: 600 }),
1027
+ td(mod.version ?? (0, import_vue7.h)("span", { style: styles.muted }, "not reported"), styles.mono),
1028
+ td([
1029
+ pill(mod.readiness.ok ? "ok" : "bad", mod.readiness.ok ? "ready" : "error"),
1030
+ ...mod.readiness.problems.map(
1031
+ (p) => (0, import_vue7.h)(
1032
+ "div",
1033
+ {
1034
+ key: p.message,
1035
+ style: {
1036
+ marginTop: "6px",
1037
+ fontSize: "13px",
1038
+ color: p.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-text-muted,#5c5c5c)"
1039
+ }
1040
+ },
1041
+ p.message
1042
+ )
439
1043
  )
1044
+ ]),
1045
+ td(
1046
+ mod.describesAdmin ? (0, import_vue7.h)("span", { style: styles.badge }, "describes admin") : (0, import_vue7.h)("span", { style: styles.muted }, "\u2014")
440
1047
  )
441
- ]),
442
- td(mod.describesAdmin ? "yes" : (0, import_vue6.h)("span", { style: styles.muted }, "no"))
443
- ])
444
- )
445
- )
446
- ];
447
- });
1048
+ ])
1049
+ )
1050
+ );
1051
+ },
1052
+ [],
1053
+ {
1054
+ lead: m ? `${m.env} \xB7 admin ${m.admin.version} \xB7 admin log ${m.admin.log ? "on" : "off"} \xB7 ${m.routes.length} routes` : "Every installed module, its version and whether it is ready."
1055
+ }
1056
+ );
1057
+ };
448
1058
  }
449
1059
  });
450
1060
 
451
1061
  // src/screens/RoutesScreen.ts
452
1062
  var import_vue_admin6 = require("@fonderie/vue-admin");
453
- var import_vue7 = require("vue");
454
- var RoutesScreen = (0, import_vue7.defineComponent)({
1063
+ var import_vue8 = require("vue");
1064
+ var RoutesScreen = (0, import_vue8.defineComponent)({
455
1065
  name: "FonderieRoutesScreen",
456
1066
  props: { client: { type: Object, required: true } },
457
1067
  setup(props) {
458
1068
  const { report, isLoading, error } = (0, import_vue_admin6.useAdminRoutes)(props.client);
459
- return () => page(
460
- "Routes",
461
- { isLoading, error },
462
- () => table(
463
- ["Method", "Path", "Guard", "Module"],
464
- (report.value?.routes ?? []).map(
465
- (r) => (0, import_vue7.h)("tr", { key: `${r.method} ${r.path}` }, [
466
- td(r.method, styles.mono),
467
- td(r.path, styles.mono),
468
- td((0, import_vue7.h)("span", { style: styles.badge }, r.guard)),
469
- td(r.module ?? "application", styles.muted)
470
- ])
471
- )
472
- )
473
- );
1069
+ const q = (0, import_vue8.ref)("");
1070
+ return () => {
1071
+ const needle = q.value.trim().toLowerCase();
1072
+ const routes = (report.value?.routes ?? []).filter(
1073
+ (r) => !needle || `${r.method} ${r.path} ${r.module ?? "application"} ${r.guard}`.toLowerCase().includes(needle)
1074
+ );
1075
+ return page(
1076
+ "Routes",
1077
+ { isLoading, error },
1078
+ () => table(
1079
+ [{ label: "Method", style: { width: "80px" } }, "Path", "Guard", "Module"],
1080
+ [
1081
+ ...routes.map(
1082
+ (r) => (0, import_vue8.h)("tr", { key: `${r.method} ${r.path}` }, [
1083
+ td(methodChip(r.method)),
1084
+ td(r.path, { ...styles.mono, wordBreak: "break-all" }),
1085
+ td(
1086
+ pill(
1087
+ r.guard === "admin" ? "ok" : r.guard === "probe" ? "neutral" : "info",
1088
+ r.guard,
1089
+ false
1090
+ )
1091
+ ),
1092
+ td(r.module ?? "application", styles.muted)
1093
+ ])
1094
+ ),
1095
+ ...routes.length === 0 ? [
1096
+ (0, import_vue8.h)("tr", [
1097
+ (0, import_vue8.h)(
1098
+ "td",
1099
+ { style: { ...styles.td, ...styles.muted }, colspan: 4 },
1100
+ `No route matches \u201C${q.value}\u201D.`
1101
+ )
1102
+ ])
1103
+ ] : []
1104
+ ]
1105
+ ),
1106
+ [
1107
+ (0, import_vue8.h)("input", {
1108
+ type: "search",
1109
+ value: q.value,
1110
+ placeholder: "Filter by path, method, module\u2026",
1111
+ style: { ...styles.input, width: "280px" },
1112
+ "aria-label": "Filter routes",
1113
+ onInput: (e) => {
1114
+ q.value = e.target.value;
1115
+ }
1116
+ })
1117
+ ],
1118
+ {
1119
+ lead: report.value ? `${report.value.routes.length} routes exposed by this deployment, with the guard in front of each.` : "Every exposed route, with its guard."
1120
+ }
1121
+ );
1122
+ };
474
1123
  }
475
1124
  });
476
1125
 
477
1126
  // src/screens/MigrationsScreen.ts
478
1127
  var import_vue_admin7 = require("@fonderie/vue-admin");
479
- var import_vue8 = require("vue");
1128
+ var import_vue9 = require("vue");
480
1129
  function why(m, everApplied) {
481
1130
  if (m.pending.length === 0) return null;
482
1131
  if (m.blockedBy) return `Apply "${m.blockedBy}" first \u2014 it runs before this one and is behind.`;
@@ -484,7 +1133,7 @@ function why(m, everApplied) {
484
1133
  return "Contains a migration that deletes data. No down-migration brings it back \u2014 apply this one through CI or `npm run migrate`.";
485
1134
  return null;
486
1135
  }
487
- var MigrationsScreen = (0, import_vue8.defineComponent)({
1136
+ var MigrationsScreen = (0, import_vue9.defineComponent)({
488
1137
  name: "FonderieMigrationsScreen",
489
1138
  props: { client: { type: Object, required: true } },
490
1139
  setup(props) {
@@ -492,115 +1141,433 @@ var MigrationsScreen = (0, import_vue8.defineComponent)({
492
1141
  const section = (m, everApplied) => {
493
1142
  const blocked = why(m, everApplied);
494
1143
  const files = m.pending.map((p) => p.file);
495
- return (0, import_vue8.h)("section", { style: { marginBottom: "24px" } }, [
496
- (0, import_vue8.h)("h2", { style: styles.subtitle }, `${m.name} \u2014 ${m.pending.length} pending`),
497
- (0, import_vue8.h)(
1144
+ return (0, import_vue9.h)("section", { style: { ...styles.card, marginBottom: "16px" } }, [
1145
+ (0, import_vue9.h)("h2", { style: { ...styles.subtitle, marginTop: 0 } }, [
1146
+ (0, import_vue9.h)("span", { style: styles.mono }, m.name),
1147
+ pill("warn", `${m.pending.length} pending`)
1148
+ ]),
1149
+ (0, import_vue9.h)(
498
1150
  "ul",
1151
+ { style: { margin: "0 0 12px", paddingLeft: "18px", lineHeight: 1.9 } },
499
1152
  m.pending.map(
500
- (p) => (0, import_vue8.h)("li", [
501
- (0, import_vue8.h)("code", p.file),
1153
+ (p) => (0, import_vue9.h)("li", [
1154
+ (0, import_vue9.h)("code", { style: styles.code }, p.file),
502
1155
  " ",
503
- (0, import_vue8.h)(
504
- "span",
505
- { style: p.impact === "destructive" ? styles.bad : styles.advice },
506
- p.impact
507
- ),
508
- p.destructive.length > 0 ? (0, import_vue8.h)(
1156
+ p.impact === "destructive" ? pill("bad", "destructive") : pill("neutral", "additive"),
1157
+ p.destructive.length > 0 ? (0, import_vue9.h)(
509
1158
  "ul",
510
- p.destructive.map((s) => (0, import_vue8.h)("li", [(0, import_vue8.h)("code", s.slice(0, 120))]))
1159
+ p.destructive.map((s) => (0, import_vue9.h)("li", [(0, import_vue9.h)("code", s.slice(0, 120))]))
511
1160
  ) : null
512
1161
  ])
513
1162
  )
514
- ),
515
- blocked ? (0, import_vue8.h)("p", { style: styles.advice }, blocked) : (0, import_vue8.h)(
516
- "button",
517
- {
518
- type: "button",
519
- style: styles.button,
520
- disabled: isLoading.value || !m.appliable,
521
- onClick: () => {
522
- if (window.confirm(
523
- `Apply ${m.pending.length} migration(s) to "${m.name}"? This changes the database schema.`
524
- ))
525
- void apply(m.name, files).catch(() => {
526
- });
527
- }
528
- },
529
- `Apply ${m.pending.length} migration${m.pending.length === 1 ? "" : "s"}`
530
- )
1163
+ ),
1164
+ blocked ? (0, import_vue9.h)("p", { style: { ...styles.notice, marginBottom: 0 } }, blocked) : (0, import_vue9.h)(
1165
+ "button",
1166
+ {
1167
+ type: "button",
1168
+ style: styles.buttonPrimary,
1169
+ disabled: isLoading.value || !m.appliable,
1170
+ onClick: () => {
1171
+ if (window.confirm(
1172
+ `Apply ${m.pending.length} migration(s) to "${m.name}"? This changes the database schema.`
1173
+ ))
1174
+ void apply(m.name, files).catch(() => {
1175
+ });
1176
+ }
1177
+ },
1178
+ `Apply ${m.pending.length} migration${m.pending.length === 1 ? "" : "s"}`
1179
+ )
1180
+ ]);
1181
+ };
1182
+ return () => page(
1183
+ "Migrations",
1184
+ { isLoading, error },
1185
+ () => {
1186
+ const r = report.value;
1187
+ if (!r) return null;
1188
+ const behind = r.modules.filter((m) => m.pending.length > 0);
1189
+ if (behind.length === 0)
1190
+ return empty("Every module is up to date", "No pending migrations.", "migrations");
1191
+ return [
1192
+ r.everApplied ? null : (0, import_vue9.h)(
1193
+ "p",
1194
+ { style: styles.notice },
1195
+ "This database has never been migrated \u2014 treating it as a first install, so nothing is held back."
1196
+ ),
1197
+ ...behind.map((m) => section(m, r.everApplied))
1198
+ ].filter(Boolean);
1199
+ },
1200
+ [refreshButton("Refresh", isLoading.value, () => void refresh())],
1201
+ {
1202
+ lead: "Schema changes each module ships, and whether this database has them.",
1203
+ errorText: (e) => e.status === 403 ? "Applying migrations needs a token with the write scope." : e.explanation
1204
+ }
1205
+ );
1206
+ }
1207
+ });
1208
+
1209
+ // src/screens/OperatorsScreen.ts
1210
+ var import_vue_admin8 = require("@fonderie/vue-admin");
1211
+ var import_vue10 = require("vue");
1212
+ var LEVELS = [
1213
+ { label: "Read only", scopes: ["read"], hint: "look around, change nothing" },
1214
+ { label: "Editor", scopes: ["read", "write"], hint: "change config, templates, users" },
1215
+ {
1216
+ label: "Owner",
1217
+ scopes: ["read", "write", "secrets"],
1218
+ hint: "secrets, tokens and operators too"
1219
+ }
1220
+ ];
1221
+ var levelOf = (scopes) => scopes.includes("secrets") ? "Owner" : scopes.includes("write") ? "Editor" : "Read only";
1222
+ var scopesFor = (label) => LEVELS.find((l) => l.label === label)?.scopes ?? ["read"];
1223
+ var small = { height: "28px" };
1224
+ var OperatorsScreen = (0, import_vue10.defineComponent)({
1225
+ name: "FonderieOperatorsScreen",
1226
+ props: {
1227
+ client: { type: Object, required: true },
1228
+ // The signed-in operator's email: their own row offers no disable or
1229
+ // recovery (the server refuses both for yourself anyway).
1230
+ me: { type: String, default: void 0 }
1231
+ },
1232
+ setup(props) {
1233
+ const { report, isLoading, error, invite, recover, update, revokeLink } = (0, import_vue_admin8.useAdminOperators)(
1234
+ props.client
1235
+ );
1236
+ const email = (0, import_vue10.ref)("");
1237
+ const level = (0, import_vue10.ref)("Editor");
1238
+ const minted = (0, import_vue10.ref)(null);
1239
+ const copied = (0, import_vue10.ref)(false);
1240
+ const status = (o) => o.disabledAt ? pill("neutral", "disabled") : o.locked ? pill("warn", "locked") : !o.enrolled ? pill("warn", "setting up") : pill("ok", "active");
1241
+ const mintedNotice = (link) => {
1242
+ const url = `${window.location.origin}${link.url}`;
1243
+ return (0, import_vue10.h)(
1244
+ "div",
1245
+ {
1246
+ style: {
1247
+ ...styles.notice,
1248
+ background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
1249
+ borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1250
+ }
1251
+ },
1252
+ [
1253
+ (0, import_vue10.h)("strong", `Send this link to ${link.email}. It is shown once.`),
1254
+ ` It works once and expires ${new Date(link.expiresAt).toLocaleString()}.`,
1255
+ (0, import_vue10.h)(
1256
+ "div",
1257
+ { style: { display: "flex", gap: "8px", marginTop: "8px", alignItems: "center" } },
1258
+ [
1259
+ (0, import_vue10.h)(
1260
+ "code",
1261
+ { style: { ...styles.code, flex: 1, padding: "7px 10px", wordBreak: "break-all" } },
1262
+ url
1263
+ ),
1264
+ (0, import_vue10.h)(
1265
+ "button",
1266
+ {
1267
+ type: "button",
1268
+ style: styles.button,
1269
+ onClick: () => {
1270
+ void navigator.clipboard?.writeText(url);
1271
+ copied.value = true;
1272
+ }
1273
+ },
1274
+ copied.value ? "Copied" : "Copy"
1275
+ ),
1276
+ (0, import_vue10.h)(
1277
+ "button",
1278
+ {
1279
+ type: "button",
1280
+ style: styles.buttonGhost,
1281
+ "aria-label": "Dismiss",
1282
+ onClick: () => {
1283
+ minted.value = null;
1284
+ copied.value = false;
1285
+ }
1286
+ },
1287
+ [icon("close", 14)]
1288
+ )
1289
+ ]
1290
+ )
1291
+ ]
1292
+ );
1293
+ };
1294
+ const setMinted = (l) => {
1295
+ copied.value = false;
1296
+ minted.value = l;
1297
+ };
1298
+ return () => {
1299
+ const r = report.value;
1300
+ const e = error.value;
1301
+ return (0, import_vue10.h)("div", { style: styles.container }, [
1302
+ pageHeader(
1303
+ "Operators",
1304
+ "The people who can sign in to this console. Each one uses a password and an authenticator app; there is no sign-up."
1305
+ ),
1306
+ e ? (0, import_vue10.h)(
1307
+ "p",
1308
+ { style: styles.error, role: "alert" },
1309
+ e.status === 403 && e.reason === "FORBIDDEN" ? "Managing operators needs the Owner level." : e.explanation
1310
+ ) : null,
1311
+ minted.value ? mintedNotice(minted.value) : null,
1312
+ (0, import_vue10.h)(
1313
+ "form",
1314
+ {
1315
+ style: {
1316
+ ...styles.card,
1317
+ display: "flex",
1318
+ gap: "8px",
1319
+ alignItems: "center",
1320
+ flexWrap: "wrap",
1321
+ marginBottom: "20px"
1322
+ },
1323
+ onSubmit: (ev) => {
1324
+ ev.preventDefault();
1325
+ void invite({ email: email.value.trim(), scopes: scopesFor(level.value) }).then((l) => {
1326
+ setMinted(l);
1327
+ email.value = "";
1328
+ }).catch(() => {
1329
+ });
1330
+ }
1331
+ },
1332
+ [
1333
+ icon("users", 16),
1334
+ (0, import_vue10.h)("strong", { style: { fontSize: "13.5px", marginRight: "4px" } }, "Invite"),
1335
+ (0, import_vue10.h)("input", {
1336
+ type: "email",
1337
+ required: true,
1338
+ value: email.value,
1339
+ onInput: (ev) => email.value = ev.target.value,
1340
+ placeholder: "teammate@company.com",
1341
+ style: { ...styles.input, flex: 1, minWidth: "220px" },
1342
+ "aria-label": "Email to invite"
1343
+ }),
1344
+ (0, import_vue10.h)(
1345
+ "select",
1346
+ {
1347
+ value: level.value,
1348
+ onChange: (ev) => level.value = ev.target.value,
1349
+ style: styles.input,
1350
+ "aria-label": "Access level"
1351
+ },
1352
+ LEVELS.map(
1353
+ (l) => (0, import_vue10.h)("option", { key: l.label, value: l.label }, `${l.label} \u2014 ${l.hint}`)
1354
+ )
1355
+ ),
1356
+ (0, import_vue10.h)(
1357
+ "button",
1358
+ { type: "submit", style: styles.buttonPrimary, disabled: !email.value.trim() },
1359
+ [icon("plus", 14), "Create invite link"]
1360
+ )
1361
+ ]
1362
+ ),
1363
+ isLoading.value && !r ? (0, import_vue10.h)("p", { style: styles.status }, "Loading\u2026") : r && r.operators.length === 0 ? empty("No operators yet", void 0, "users") : r ? table(
1364
+ ["Operator", "Access", "Status", "Last sign-in", ""],
1365
+ r.operators.map((o) => {
1366
+ const mine = o.email === props.me;
1367
+ return (0, import_vue10.h)("tr", { key: o.id }, [
1368
+ td([
1369
+ (0, import_vue10.h)("div", { style: { fontWeight: 600 } }, o.name || o.email),
1370
+ o.name ? (0, import_vue10.h)("div", { style: styles.muted }, o.email) : null
1371
+ ]),
1372
+ td(
1373
+ (0, import_vue10.h)(
1374
+ "select",
1375
+ {
1376
+ value: levelOf(o.scopes),
1377
+ disabled: mine,
1378
+ onChange: (ev) => void update(o.id, {
1379
+ scopes: scopesFor(ev.target.value)
1380
+ }).catch(() => {
1381
+ }),
1382
+ style: { ...styles.input, ...small, fontSize: "12.5px" },
1383
+ "aria-label": `Access level for ${o.email}`
1384
+ },
1385
+ LEVELS.map((l) => (0, import_vue10.h)("option", { key: l.label, value: l.label }, l.label))
1386
+ )
1387
+ ),
1388
+ td([
1389
+ status(o),
1390
+ o.enrolled && o.backupCodesLeft <= 2 && !o.disabledAt ? (0, import_vue10.h)(
1391
+ "div",
1392
+ { style: { ...styles.muted, marginTop: "4px" } },
1393
+ `${o.backupCodesLeft} backup code(s) left`
1394
+ ) : null
1395
+ ]),
1396
+ td(o.lastLoginAt ? new Date(o.lastLoginAt).toLocaleString() : "never", {
1397
+ ...styles.muted,
1398
+ whiteSpace: "nowrap"
1399
+ }),
1400
+ td(
1401
+ mine ? (0, import_vue10.h)("span", { style: styles.muted }, "you") : [
1402
+ (0, import_vue10.h)(
1403
+ "button",
1404
+ {
1405
+ type: "button",
1406
+ style: { ...styles.button, ...small, marginRight: "6px" },
1407
+ onClick: () => {
1408
+ if (window.confirm(
1409
+ `Create a recovery link for ${o.email}? It signs them out everywhere; the link sets a new password and a new authenticator.`
1410
+ ))
1411
+ void recover(o.id).then(setMinted).catch(() => {
1412
+ });
1413
+ }
1414
+ },
1415
+ "Recovery link"
1416
+ ),
1417
+ (0, import_vue10.h)(
1418
+ "button",
1419
+ {
1420
+ type: "button",
1421
+ style: {
1422
+ ...o.disabledAt ? styles.button : styles.buttonDanger,
1423
+ ...small
1424
+ },
1425
+ onClick: () => {
1426
+ if (o.disabledAt || window.confirm(
1427
+ `Disable ${o.email}? They are signed out immediately.`
1428
+ ))
1429
+ void update(o.id, { disabled: !o.disabledAt }).catch(
1430
+ () => {
1431
+ }
1432
+ );
1433
+ }
1434
+ },
1435
+ o.disabledAt ? "Enable" : "Disable"
1436
+ )
1437
+ ],
1438
+ { textAlign: "right", whiteSpace: "nowrap" }
1439
+ )
1440
+ ]);
1441
+ })
1442
+ ) : null,
1443
+ r && r.links.length > 0 ? [
1444
+ (0, import_vue10.h)("h2", { style: styles.subtitle }, "Pending links"),
1445
+ (0, import_vue10.h)(
1446
+ "ul",
1447
+ { style: styles.list },
1448
+ r.links.map(
1449
+ (l) => (0, import_vue10.h)(
1450
+ "li",
1451
+ {
1452
+ key: l.id,
1453
+ style: { ...styles.row, display: "flex", gap: "12px", alignItems: "center" }
1454
+ },
1455
+ [
1456
+ pill(l.kind === "invite" ? "info" : "warn", l.kind, false),
1457
+ (0, import_vue10.h)("span", { style: { flex: 1 } }, [
1458
+ l.email,
1459
+ l.kind === "invite" ? (0, import_vue10.h)("span", { style: styles.muted }, ` \xB7 ${levelOf(l.scopes)}`) : null
1460
+ ]),
1461
+ (0, import_vue10.h)(
1462
+ "span",
1463
+ { style: styles.muted },
1464
+ `expires ${new Date(l.expiresAt).toLocaleString()}`
1465
+ ),
1466
+ (0, import_vue10.h)(
1467
+ "button",
1468
+ {
1469
+ type: "button",
1470
+ style: { ...styles.buttonDanger, ...small },
1471
+ onClick: () => void revokeLink(l.id).catch(() => {
1472
+ })
1473
+ },
1474
+ "Revoke"
1475
+ )
1476
+ ]
1477
+ )
1478
+ )
1479
+ )
1480
+ ] : null
531
1481
  ]);
532
1482
  };
533
- return () => page(
534
- "Migrations",
535
- { isLoading, error },
536
- () => {
537
- const r = report.value;
538
- if (!r) return null;
539
- const behind = r.modules.filter((m) => m.pending.length > 0);
540
- if (behind.length === 0) return (0, import_vue8.h)("p", { style: styles.ok }, "Every module is up to date.");
541
- return [
542
- r.everApplied ? null : (0, import_vue8.h)(
543
- "p",
544
- { style: styles.advice },
545
- "This database has never been migrated \u2014 treating it as a first install, so nothing is held back."
546
- ),
547
- ...behind.map((m) => section(m, r.everApplied))
548
- ].filter(Boolean);
549
- },
550
- [refreshButton("Refresh", isLoading.value, () => void refresh())],
551
- {
552
- errorText: (e) => e.status === 403 ? "Applying migrations needs a token with the write scope." : e.explanation
553
- }
554
- );
555
1483
  }
556
1484
  });
557
1485
 
558
1486
  // src/screens/TokensScreen.ts
559
- var import_vue_admin8 = require("@fonderie/vue-admin");
560
- var import_vue9 = require("vue");
1487
+ var import_vue_admin9 = require("@fonderie/vue-admin");
1488
+ var import_vue11 = require("vue");
561
1489
  var ALL = ["read", "write", "secrets"];
562
- var TokensScreen = (0, import_vue9.defineComponent)({
1490
+ var TokensScreen = (0, import_vue11.defineComponent)({
563
1491
  name: "FonderieTokensScreen",
564
1492
  props: { client: { type: Object, required: true } },
565
1493
  setup(props) {
566
- const { report, isLoading, error, issue, revoke } = (0, import_vue_admin8.useAdminTokens)(props.client);
567
- const name = (0, import_vue9.ref)("");
568
- const scopes = (0, import_vue9.ref)(["read"]);
569
- const days = (0, import_vue9.ref)("");
570
- const minted = (0, import_vue9.ref)(null);
1494
+ const { report, isLoading, error, issue, revoke } = (0, import_vue_admin9.useAdminTokens)(props.client);
1495
+ const name = (0, import_vue11.ref)("");
1496
+ const scopes = (0, import_vue11.ref)(["read"]);
1497
+ const days = (0, import_vue11.ref)("");
1498
+ const minted = (0, import_vue11.ref)(null);
571
1499
  const toggle = (s) => scopes.value = scopes.value.includes(s) ? scopes.value.filter((x) => x !== s) : [...scopes.value, s];
572
1500
  return () => {
573
1501
  const r = report.value;
574
- return (0, import_vue9.h)("div", { style: styles.container }, [
575
- (0, import_vue9.h)("h1", { style: styles.title }, "Access"),
576
- error.value ? (0, import_vue9.h)(
1502
+ return (0, import_vue11.h)("div", { style: styles.container }, [
1503
+ pageHeader(
1504
+ "Tokens",
1505
+ "Credentials for machines: the root token's strength, scoped tokens for the CLI and CI, and any legacy per-module tokens. People sign in as operators."
1506
+ ),
1507
+ error.value ? (0, import_vue11.h)(
577
1508
  "p",
578
1509
  { style: styles.error, role: "alert" },
579
1510
  error.value.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.value.explanation
580
1511
  ) : null,
581
- (0, import_vue9.h)("h2", { style: styles.subtitle }, "Root token"),
582
- r ? (0, import_vue9.h)("p", [
583
- (0, import_vue9.h)(
584
- "span",
585
- { style: r.admin.ok ? styles.ok : styles.bad },
586
- r.admin.ok ? "strong" : "weak"
587
- ),
588
- ...r.admin.problems.map(
589
- (p) => (0, import_vue9.h)("div", { key: p.message, style: styles.bad }, p.message)
590
- )
591
- ]) : null,
592
- (0, import_vue9.h)("h2", { style: styles.subtitle }, "Issued tokens"),
593
- r?.issued === null ? (0, import_vue9.h)(
1512
+ (0, import_vue11.h)("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Root token"),
1513
+ r ? (0, import_vue11.h)(
1514
+ "div",
1515
+ { style: { ...styles.card, display: "flex", gap: "12px", alignItems: "flex-start" } },
1516
+ [
1517
+ icon("lock", 18, { marginTop: "2px" }),
1518
+ (0, import_vue11.h)("div", [
1519
+ pill(r.admin.ok ? "ok" : "bad", r.admin.ok ? "strong" : "weak"),
1520
+ ...r.admin.problems.map(
1521
+ (p) => (0, import_vue11.h)(
1522
+ "div",
1523
+ {
1524
+ key: p.message,
1525
+ style: {
1526
+ marginTop: "6px",
1527
+ color: "var(--fonderie-danger,#e00)",
1528
+ fontSize: "13px"
1529
+ }
1530
+ },
1531
+ p.message
1532
+ )
1533
+ )
1534
+ ])
1535
+ ]
1536
+ ) : null,
1537
+ (0, import_vue11.h)("h2", { style: styles.subtitle }, "Issued tokens"),
1538
+ r?.issued === null ? (0, import_vue11.h)(
594
1539
  "p",
595
1540
  { style: styles.muted },
596
1541
  "Issuing is off \u2014 give AdminModule a store and run its migrations."
597
1542
  ) : [
598
- minted.value ? (0, import_vue9.h)("p", { style: styles.ok }, [
599
- "Copy this now \u2014 it is never shown again. ",
600
- (0, import_vue9.h)("span", { style: styles.mono }, minted.value.token),
601
- ` (${minted.value.name})`
602
- ]) : null,
603
- (0, import_vue9.h)(
1543
+ minted.value ? (0, import_vue11.h)(
1544
+ "div",
1545
+ {
1546
+ style: {
1547
+ ...styles.notice,
1548
+ background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
1549
+ borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1550
+ }
1551
+ },
1552
+ [
1553
+ (0, import_vue11.h)("strong", "Copy this now \u2014 it is never shown again."),
1554
+ ` (${minted.value.name})`,
1555
+ (0, import_vue11.h)(
1556
+ "div",
1557
+ {
1558
+ style: {
1559
+ ...styles.code,
1560
+ display: "block",
1561
+ marginTop: "8px",
1562
+ padding: "8px 10px",
1563
+ wordBreak: "break-all"
1564
+ }
1565
+ },
1566
+ minted.value.token
1567
+ )
1568
+ ]
1569
+ ) : null,
1570
+ (0, import_vue11.h)(
604
1571
  "form",
605
1572
  {
606
1573
  style: styles.toolbar,
@@ -620,49 +1587,60 @@ var TokensScreen = (0, import_vue9.defineComponent)({
620
1587
  }
621
1588
  },
622
1589
  [
623
- (0, import_vue9.h)("input", {
1590
+ (0, import_vue11.h)("input", {
624
1591
  value: name.value,
625
1592
  onInput: (e) => name.value = e.target.value,
626
1593
  placeholder: "name",
627
- style: { ...styles.button, cursor: "text" },
1594
+ style: styles.input,
628
1595
  "aria-label": "Name"
629
1596
  }),
630
1597
  ...ALL.map(
631
- (s) => (0, import_vue9.h)("label", { key: s, style: { ...styles.badge, cursor: "pointer" } }, [
632
- (0, import_vue9.h)("input", {
633
- type: "checkbox",
634
- checked: scopes.value.includes(s),
635
- onChange: () => toggle(s)
636
- }),
637
- ` ${s}`
638
- ])
1598
+ (s) => (0, import_vue11.h)(
1599
+ "label",
1600
+ {
1601
+ key: s,
1602
+ style: {
1603
+ ...styles.badge,
1604
+ cursor: "pointer",
1605
+ height: "32px",
1606
+ boxSizing: "border-box",
1607
+ padding: "0 10px",
1608
+ fontSize: "13px"
1609
+ }
1610
+ },
1611
+ [
1612
+ (0, import_vue11.h)("input", {
1613
+ type: "checkbox",
1614
+ checked: scopes.value.includes(s),
1615
+ onChange: () => toggle(s)
1616
+ }),
1617
+ ` ${s}`
1618
+ ]
1619
+ )
639
1620
  ),
640
- (0, import_vue9.h)("input", {
1621
+ (0, import_vue11.h)("input", {
641
1622
  value: days.value,
642
1623
  onInput: (e) => days.value = e.target.value,
643
1624
  placeholder: "days (optional)",
644
- style: { ...styles.button, cursor: "text", width: "140px" },
1625
+ style: { ...styles.input, width: "140px" },
645
1626
  "aria-label": "Days"
646
1627
  }),
647
- (0, import_vue9.h)(
1628
+ (0, import_vue11.h)(
648
1629
  "button",
649
1630
  {
650
1631
  type: "submit",
651
- style: styles.button,
1632
+ style: styles.buttonPrimary,
652
1633
  disabled: !name.value.trim() || scopes.value.length === 0 || isLoading.value
653
1634
  },
654
- "Issue"
1635
+ [icon("plus", 14), "Issue token"]
655
1636
  )
656
1637
  ]
657
1638
  ),
658
1639
  r?.issued && r.issued.length > 0 ? table(
659
1640
  ["Name", "Scopes", "Created", "Expires", "Last used", ""],
660
1641
  r.issued.map(
661
- (t2) => (0, import_vue9.h)("tr", { key: t2.id }, [
662
- td([
663
- t2.name,
664
- t2.revokedAt ? (0, import_vue9.h)("span", { style: styles.badge }, " revoked") : ""
665
- ]),
1642
+ (t2) => (0, import_vue11.h)("tr", { key: t2.id }, [
1643
+ td([t2.name, t2.revokedAt ? [" ", pill("neutral", "revoked")] : ""]),
666
1644
  td(t2.scopes.join(", "), styles.mono),
667
1645
  td(
668
1646
  `${new Date(t2.createdAt).toLocaleDateString()} \xB7 ${t2.createdBy}`,
@@ -677,11 +1655,11 @@ var TokensScreen = (0, import_vue9.defineComponent)({
677
1655
  styles.muted
678
1656
  ),
679
1657
  td(
680
- t2.revokedAt ? "" : (0, import_vue9.h)(
1658
+ t2.revokedAt ? "" : (0, import_vue11.h)(
681
1659
  "button",
682
1660
  {
683
1661
  type: "button",
684
- style: styles.button,
1662
+ style: styles.buttonDanger,
685
1663
  onClick: () => {
686
1664
  if (window.confirm(
687
1665
  `Revoke "${t2.name}"? Anything using it stops working immediately.`
@@ -695,21 +1673,25 @@ var TokensScreen = (0, import_vue9.defineComponent)({
695
1673
  )
696
1674
  ])
697
1675
  )
698
- ) : (0, import_vue9.h)("p", { style: styles.muted }, "None issued.")
1676
+ ) : empty(
1677
+ "No scoped tokens yet",
1678
+ "Issue a read-only token for a dashboard or a teammate instead of sharing the root.",
1679
+ "tokens"
1680
+ )
699
1681
  ],
700
- (0, import_vue9.h)("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
701
- r && r.legacy.length === 0 ? (0, import_vue9.h)(
1682
+ (0, import_vue11.h)("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
1683
+ r && r.legacy.length === 0 ? (0, import_vue11.h)(
702
1684
  "p",
703
1685
  { style: styles.muted },
704
1686
  "None \u2014 every brick's admin surface goes through this token."
705
- ) : (0, import_vue9.h)(
1687
+ ) : (0, import_vue11.h)(
706
1688
  "ul",
707
1689
  { style: styles.list },
708
1690
  (r?.legacy ?? []).map(
709
- (l) => (0, import_vue9.h)("li", { key: l.module, style: styles.row }, [
710
- (0, import_vue9.h)("span", { style: styles.mono }, l.module),
1691
+ (l) => (0, import_vue11.h)("li", { key: l.module, style: styles.row }, [
1692
+ (0, import_vue11.h)("span", { style: styles.mono }, l.module),
711
1693
  " ",
712
- (0, import_vue9.h)(
1694
+ (0, import_vue11.h)(
713
1695
  "span",
714
1696
  { style: styles.muted },
715
1697
  "still registers its standalone routes with its own token (deprecated)"
@@ -724,29 +1706,29 @@ var TokensScreen = (0, import_vue9.defineComponent)({
724
1706
 
725
1707
  // src/screens/UsersScreen.ts
726
1708
  var import_client = require("@fonderie/client");
727
- var import_vue_admin9 = require("@fonderie/vue-admin");
728
- var import_vue10 = require("vue");
729
- var UsersScreen = (0, import_vue10.defineComponent)({
1709
+ var import_vue_admin10 = require("@fonderie/vue-admin");
1710
+ var import_vue12 = require("vue");
1711
+ var UsersScreen = (0, import_vue12.defineComponent)({
730
1712
  name: "FonderieUsersScreen",
731
1713
  props: {
732
1714
  client: { type: Object, required: true },
733
1715
  pageSize: { type: Number, default: 50 }
734
1716
  },
735
1717
  setup(props) {
736
- const input = (0, import_vue10.ref)("");
737
- const email = (0, import_vue10.ref)("");
738
- const selectedId = (0, import_vue10.ref)("");
739
- const list = (0, import_vue_admin9.useAdminUsers)(props.client, { limit: props.pageSize });
740
- const { user, isLoading, error, suspend, unsuspend, revokeSessions } = (0, import_vue_admin9.useAdminUser)(
1718
+ const input = (0, import_vue12.ref)("");
1719
+ const email = (0, import_vue12.ref)("");
1720
+ const selectedId = (0, import_vue12.ref)("");
1721
+ const list = (0, import_vue_admin10.useAdminUsers)(props.client, { limit: props.pageSize });
1722
+ const { user, isLoading, error, suspend, unsuspend, revokeSessions } = (0, import_vue_admin10.useAdminUser)(
741
1723
  props.client,
742
1724
  { email, id: selectedId }
743
1725
  );
744
- const userId = (0, import_vue10.computed)(() => user.value?.id ?? null);
745
- const sessions = (0, import_vue_admin9.useAdminUserSessions)(props.client, userId);
746
- const history = (0, import_vue_admin9.useAdminLoginHistory)(props.client, userId, { limit: 20 });
747
- const showList = (0, import_vue10.computed)(() => !email.value && !selectedId.value);
748
- const yesNo = (v) => (0, import_vue10.h)("span", { style: v ? styles.ok : styles.muted }, v ? "yes" : "no");
749
- const row = (k, v) => (0, import_vue10.h)("tr", [td(k), td(v)]);
1726
+ const userId = (0, import_vue12.computed)(() => user.value?.id ?? null);
1727
+ const sessions = (0, import_vue_admin10.useAdminUserSessions)(props.client, userId);
1728
+ const history = (0, import_vue_admin10.useAdminLoginHistory)(props.client, userId, { limit: 20 });
1729
+ const showList = (0, import_vue12.computed)(() => !email.value && !selectedId.value);
1730
+ const yesNo = (v) => v ? pill("ok", "yes") : pill("neutral", "no");
1731
+ const row = (k, v) => (0, import_vue12.h)("tr", [td(k), td(v)]);
750
1732
  const clear = () => {
751
1733
  email.value = "";
752
1734
  selectedId.value = "";
@@ -754,9 +1736,12 @@ var UsersScreen = (0, import_vue10.defineComponent)({
754
1736
  };
755
1737
  return () => {
756
1738
  const u = user.value;
757
- return (0, import_vue10.h)("div", { style: styles.container }, [
758
- (0, import_vue10.h)("h1", { style: styles.title }, "Users"),
759
- (0, import_vue10.h)(
1739
+ return (0, import_vue12.h)("div", { style: styles.container }, [
1740
+ pageHeader(
1741
+ "Users",
1742
+ "Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
1743
+ ),
1744
+ (0, import_vue12.h)(
760
1745
  "form",
761
1746
  {
762
1747
  style: styles.toolbar,
@@ -767,75 +1752,67 @@ var UsersScreen = (0, import_vue10.defineComponent)({
767
1752
  }
768
1753
  },
769
1754
  [
770
- (0, import_vue10.h)("input", {
1755
+ (0, import_vue12.h)("input", {
771
1756
  type: "email",
772
1757
  value: input.value,
773
1758
  onInput: (e) => input.value = e.target.value,
774
1759
  placeholder: "email address",
775
- style: { ...styles.button, cursor: "text", minWidth: "280px" },
1760
+ style: { ...styles.input, minWidth: "280px" },
776
1761
  "aria-label": "Email"
777
1762
  }),
778
- (0, import_vue10.h)(
1763
+ (0, import_vue12.h)(
779
1764
  "button",
780
- { type: "submit", style: styles.button, disabled: isLoading.value },
781
- "Look up"
1765
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1766
+ [icon("search", 14), "Look up"]
782
1767
  ),
783
- showList.value ? refreshButton("Refresh", list.isLoading.value, () => void list.refresh()) : refreshButton("\u2190 All users", false, clear)
1768
+ showList.value ? refreshButton("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost)
784
1769
  ]
785
1770
  ),
786
- showList.value ? (0, import_vue10.h)("div", [
787
- list.error.value ? (0, import_vue10.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
788
- list.users.value.length === 0 && !list.isLoading.value ? (0, import_vue10.h)("p", { style: styles.muted }, "No users yet.") : table(
1771
+ showList.value ? (0, import_vue12.h)("div", [
1772
+ list.error.value ? (0, import_vue12.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1773
+ list.users.value.length === 0 && !list.isLoading.value ? empty("No users yet", "Sign-ups appear here as they happen.", "users") : table(
789
1774
  ["Email", "Name", "Created", "Status"],
790
1775
  list.users.value.map(
791
- (u2) => (0, import_vue10.h)("tr", { key: u2.id }, [
1776
+ (u2) => (0, import_vue12.h)("tr", { key: u2.id }, [
792
1777
  td(
793
- (0, import_vue10.h)(
1778
+ (0, import_vue12.h)(
794
1779
  "button",
795
1780
  {
796
1781
  type: "button",
797
- style: { ...styles.navItem, padding: 0 },
1782
+ style: styles.link,
798
1783
  onClick: () => selectedId.value = u2.id
799
1784
  },
800
1785
  u2.email
801
1786
  )
802
1787
  ),
803
1788
  td(
804
- `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || (0, import_vue10.h)("span", { style: styles.muted }, "\u2014")
1789
+ `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || (0, import_vue12.h)("span", { style: styles.muted }, "\u2014")
805
1790
  ),
806
1791
  td(new Date(u2.createdAt).toLocaleDateString()),
807
1792
  td(
808
- u2.suspended ? (0, import_vue10.h)("span", { style: styles.badge }, "suspended") : u2.deletedAt ? (0, import_vue10.h)("span", { style: styles.badge }, "deleted") : (0, import_vue10.h)("span", { style: styles.muted }, "active")
1793
+ u2.suspended ? pill("warn", "suspended") : u2.deletedAt ? pill("neutral", "deleted") : pill("ok", "active")
809
1794
  )
810
1795
  ])
811
1796
  )
812
1797
  ),
813
- list.isLoading.value ? (0, import_vue10.h)("p", { style: styles.status }, "Loading\u2026") : null,
814
- list.hasMore.value && !list.isLoading.value ? (0, import_vue10.h)(
815
- "button",
816
- {
817
- type: "button",
818
- style: { ...styles.button, marginTop: "8px" },
819
- onClick: () => void list.loadMore()
820
- },
821
- "Load more"
822
- ) : null
1798
+ list.isLoading.value ? (0, import_vue12.h)("p", { style: styles.status }, "Loading\u2026") : null,
1799
+ list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
823
1800
  ]) : null,
824
- error.value ? (0, import_vue10.h)(
1801
+ error.value ? (0, import_vue12.h)(
825
1802
  "p",
826
1803
  { style: styles.error, role: "alert" },
827
1804
  error.value.status === 404 ? "No user with that email." : error.value.explanation
828
1805
  ) : null,
829
1806
  u ? [
830
- (0, import_vue10.h)("h2", { style: styles.subtitle }, [
1807
+ (0, import_vue12.h)("h2", { style: styles.subtitle }, [
831
1808
  u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
832
1809
  " ",
833
- u.suspended ? (0, import_vue10.h)("span", { style: styles.badge }, "suspended") : null,
834
- u.deletedAt ? (0, import_vue10.h)("span", { style: styles.badge }, "deleted") : null
1810
+ u.suspended ? pill("warn", "suspended") : null,
1811
+ u.deletedAt ? pill("neutral", "deleted") : null
835
1812
  ]),
836
- (0, import_vue10.h)("table", { style: styles.table }, [
837
- (0, import_vue10.h)("tbody", [
838
- row("id", (0, import_vue10.h)("span", { style: styles.mono }, u.id)),
1813
+ (0, import_vue12.h)("table", { style: styles.table }, [
1814
+ (0, import_vue12.h)("tbody", [
1815
+ row("id", (0, import_vue12.h)("span", { style: styles.mono }, u.id)),
839
1816
  row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
840
1817
  row("MFA", yesNo(u.mfaEnabled)),
841
1818
  row(
@@ -844,32 +1821,37 @@ var UsersScreen = (0, import_vue10.defineComponent)({
844
1821
  ),
845
1822
  row(
846
1823
  "last login",
847
- u.lastLogin ? new Date(u.lastLogin).toLocaleString() : (0, import_vue10.h)("span", { style: styles.muted }, "never")
1824
+ u.lastLogin ? new Date(u.lastLogin).toLocaleString() : (0, import_vue12.h)("span", { style: styles.muted }, "never")
848
1825
  ),
849
1826
  row("created", new Date(u.createdAt).toLocaleString())
850
1827
  ])
851
1828
  ]),
852
- (0, import_vue10.h)("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
853
- u.suspended ? refreshButton("Unsuspend", isLoading.value, () => void unsuspend()) : refreshButton("Suspend", isLoading.value, () => void suspend()),
854
- refreshButton(
1829
+ (0, import_vue12.h)("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
1830
+ u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
1831
+ "Suspend",
1832
+ isLoading.value,
1833
+ () => void suspend(),
1834
+ styles.buttonDanger
1835
+ ),
1836
+ actionButton(
855
1837
  "Sign out everywhere",
856
1838
  isLoading.value,
857
1839
  () => void revokeSessions().then(() => sessions.refresh())
858
1840
  )
859
1841
  ]),
860
- (0, import_vue10.h)("h2", { style: styles.subtitle }, "Live sessions"),
861
- sessions.sessions.value.length === 0 ? (0, import_vue10.h)("p", { style: styles.muted }, "None.") : (0, import_vue10.h)(
1842
+ (0, import_vue12.h)("h2", { style: styles.subtitle }, "Live sessions"),
1843
+ sessions.sessions.value.length === 0 ? (0, import_vue12.h)("p", { style: styles.muted }, "None.") : (0, import_vue12.h)(
862
1844
  "ul",
863
1845
  { style: styles.list },
864
1846
  sessions.sessions.value.map(
865
- (s) => (0, import_vue10.h)("li", { key: s.id, style: styles.row }, [
866
- (0, import_vue10.h)("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
1847
+ (s) => (0, import_vue12.h)("li", { key: s.id, style: styles.row }, [
1848
+ (0, import_vue12.h)("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
867
1849
  " ",
868
- (0, import_vue10.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(s.location) ?? ""),
1850
+ (0, import_vue12.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(s.location) ?? ""),
869
1851
  " ",
870
- (0, import_vue10.h)("span", { style: styles.muted }, s.userAgent ?? ""),
1852
+ (0, import_vue12.h)("span", { style: styles.muted }, s.userAgent ?? ""),
871
1853
  " ",
872
- (0, import_vue10.h)(
1854
+ (0, import_vue12.h)(
873
1855
  "span",
874
1856
  { style: styles.muted },
875
1857
  `since ${new Date(s.createdAt).toLocaleString()}`
@@ -877,38 +1859,26 @@ var UsersScreen = (0, import_vue10.defineComponent)({
877
1859
  ])
878
1860
  )
879
1861
  ),
880
- (0, import_vue10.h)("h2", { style: styles.subtitle }, "Recent sign-ins"),
881
- history.events.value.length === 0 && !history.isLoading.value ? (0, import_vue10.h)("p", { style: styles.muted }, "None recorded.") : (0, import_vue10.h)(
1862
+ (0, import_vue12.h)("h2", { style: styles.subtitle }, "Recent sign-ins"),
1863
+ history.events.value.length === 0 && !history.isLoading.value ? (0, import_vue12.h)("p", { style: styles.muted }, "None recorded.") : (0, import_vue12.h)(
882
1864
  "ul",
883
1865
  { style: styles.list },
884
1866
  history.events.value.map(
885
- (e) => (0, import_vue10.h)("li", { key: e.id, style: styles.row }, [
886
- (0, import_vue10.h)(
887
- "span",
888
- { style: e.outcome === "success" ? styles.ok : styles.bad },
889
- e.outcome
890
- ),
1867
+ (e) => (0, import_vue12.h)("li", { key: e.id, style: styles.row }, [
1868
+ pill(e.outcome === "success" ? "ok" : "bad", e.outcome),
891
1869
  " ",
892
- (0, import_vue10.h)("span", { style: styles.muted }, e.method),
1870
+ (0, import_vue12.h)("span", { style: styles.muted }, e.method),
893
1871
  " ",
894
- (0, import_vue10.h)("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
1872
+ (0, import_vue12.h)("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
895
1873
  " ",
896
- (0, import_vue10.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(e.location) ?? ""),
897
- e.location?.proxy || e.location?.hosting ? (0, import_vue10.h)("span", { style: styles.bad }, e.location.proxy ? " proxy/VPN" : " hosting") : "",
1874
+ (0, import_vue12.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(e.location) ?? ""),
1875
+ e.location?.proxy || e.location?.hosting ? [" ", pill("warn", e.location.proxy ? "proxy/VPN" : "hosting")] : "",
898
1876
  " ",
899
- (0, import_vue10.h)("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
1877
+ (0, import_vue12.h)("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
900
1878
  ])
901
1879
  )
902
1880
  ),
903
- history.hasMore.value && !history.isLoading.value ? (0, import_vue10.h)(
904
- "button",
905
- {
906
- type: "button",
907
- style: { ...styles.button, marginTop: "8px" },
908
- onClick: () => void history.loadMore()
909
- },
910
- "Load more"
911
- ) : null
1881
+ history.hasMore.value && !history.isLoading.value ? loadMoreButton(() => void history.loadMore()) : null
912
1882
  ] : null
913
1883
  ]);
914
1884
  };
@@ -916,45 +1886,52 @@ var UsersScreen = (0, import_vue10.defineComponent)({
916
1886
  });
917
1887
 
918
1888
  // src/screens/CatalogScreen.ts
919
- var import_vue_admin10 = require("@fonderie/vue-admin");
920
- var import_vue11 = require("vue");
921
- var CatalogScreen = (0, import_vue11.defineComponent)({
1889
+ var import_vue_admin11 = require("@fonderie/vue-admin");
1890
+ var import_vue13 = require("vue");
1891
+ var CatalogScreen = (0, import_vue13.defineComponent)({
922
1892
  name: "FonderieCatalogScreen",
923
1893
  props: { client: { type: Object, required: true } },
924
1894
  setup(props) {
925
- const { catalog, isLoading, error, refresh, deletePlan } = (0, import_vue_admin10.useAdminCatalog)(props.client);
1895
+ const { catalog, isLoading, error, refresh, deletePlan } = (0, import_vue_admin11.useAdminCatalog)(props.client);
926
1896
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
927
1897
  return () => {
928
1898
  const c = catalog.value;
929
- return (0, import_vue11.h)("div", { style: styles.container }, [
930
- (0, import_vue11.h)("div", { style: styles.toolbar }, [
931
- (0, import_vue11.h)("h1", { style: { ...styles.title, marginBottom: 0 } }, "Catalog"),
932
- refreshButton("Refresh", isLoading.value, () => void refresh())
933
- ]),
934
- error.value ? (0, import_vue11.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
935
- isLoading.value && !c ? (0, import_vue11.h)("p", { style: styles.status }, "Loading\u2026") : null,
1899
+ return (0, import_vue13.h)("div", { style: styles.container }, [
1900
+ pageHeader(
1901
+ "Catalog",
1902
+ "What you sell: plans as configured in code, and as stored in the database, side by side.",
1903
+ [refreshButton("Refresh", isLoading.value, () => void refresh())]
1904
+ ),
1905
+ error.value ? (0, import_vue13.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1906
+ isLoading.value && !c ? (0, import_vue13.h)("p", { style: styles.status }, "Loading\u2026") : null,
936
1907
  c ? [
937
- (0, import_vue11.h)("h2", { style: styles.subtitle }, "Configured (code)"),
938
- (0, import_vue11.h)(
1908
+ (0, import_vue13.h)("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
1909
+ (0, import_vue13.h)(
939
1910
  "pre",
940
1911
  {
941
1912
  style: {
1913
+ ...styles.card,
942
1914
  ...styles.mono,
943
- background: "var(--fonderie-surface-alt,#fafafa)",
944
- padding: "12px",
945
- overflowX: "auto"
1915
+ margin: 0,
1916
+ overflowX: "auto",
1917
+ maxHeight: "360px",
1918
+ lineHeight: 1.6
946
1919
  }
947
1920
  },
948
1921
  JSON.stringify(c.configured, null, 2)
949
1922
  ),
950
- (0, import_vue11.h)("h2", { style: styles.subtitle }, "Stored (database)"),
951
- c.stored.length === 0 ? (0, import_vue11.h)("p", { style: styles.muted }, "No stored plans.") : table(
1923
+ (0, import_vue13.h)("h2", { style: styles.subtitle }, "Stored (database)"),
1924
+ c.stored.length === 0 ? empty(
1925
+ "No stored plans",
1926
+ "Plans are configured in code; nothing has been written to the database.",
1927
+ "catalog"
1928
+ ) : table(
952
1929
  ["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
953
1930
  c.stored.map(
954
- (p) => (0, import_vue11.h)("tr", { key: p.id }, [
1931
+ (p) => (0, import_vue13.h)("tr", { key: p.id }, [
955
1932
  td([
956
- (0, import_vue11.h)("strong", p.name),
957
- p.description ? (0, import_vue11.h)("div", { style: styles.muted }, p.description) : ""
1933
+ (0, import_vue13.h)("strong", p.name),
1934
+ p.description ? (0, import_vue13.h)("div", { style: styles.muted }, p.description) : ""
958
1935
  ]),
959
1936
  td(String(p.tier)),
960
1937
  td(p.seats == null ? "\u221E" : String(p.seats)),
@@ -962,11 +1939,11 @@ var CatalogScreen = (0, import_vue11.defineComponent)({
962
1939
  td(money(p.pricing?.yearly)),
963
1940
  td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
964
1941
  td(
965
- (0, import_vue11.h)(
1942
+ (0, import_vue13.h)(
966
1943
  "button",
967
1944
  {
968
1945
  type: "button",
969
- style: styles.button,
1946
+ style: styles.buttonDanger,
970
1947
  onClick: () => {
971
1948
  if (window.confirm(`Delete stored plan "${p.name}"?`))
972
1949
  void deletePlan(p.id).catch(() => {
@@ -986,42 +1963,45 @@ var CatalogScreen = (0, import_vue11.defineComponent)({
986
1963
  });
987
1964
 
988
1965
  // src/screens/SubscriberScreen.ts
989
- var import_vue_admin11 = require("@fonderie/vue-admin");
990
- var import_vue12 = require("vue");
991
- var SubscriberScreen = (0, import_vue12.defineComponent)({
1966
+ var import_vue_admin12 = require("@fonderie/vue-admin");
1967
+ var import_vue14 = require("vue");
1968
+ var SubscriberScreen = (0, import_vue14.defineComponent)({
992
1969
  name: "FonderieSubscriberScreen",
993
1970
  props: {
994
1971
  client: { type: Object, required: true },
995
1972
  pageSize: { type: Number, default: 50 }
996
1973
  },
997
1974
  setup(props) {
998
- const type = (0, import_vue12.ref)("user");
999
- const idInput = (0, import_vue12.ref)("");
1000
- const subscriber = (0, import_vue12.ref)(null);
1001
- const list = (0, import_vue_admin11.useAdminSubscribers)(props.client, { limit: props.pageSize });
1002
- const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = (0, import_vue_admin11.useAdminSubscriber)(props.client, subscriber, { limit: 20 });
1975
+ const type = (0, import_vue14.ref)("user");
1976
+ const idInput = (0, import_vue14.ref)("");
1977
+ const subscriber = (0, import_vue14.ref)(null);
1978
+ const list = (0, import_vue_admin12.useAdminSubscribers)(props.client, { limit: props.pageSize });
1979
+ const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = (0, import_vue_admin12.useAdminSubscriber)(props.client, subscriber, { limit: 20 });
1003
1980
  const open = (t2, id) => {
1004
1981
  granted.value = null;
1005
1982
  type.value = t2;
1006
1983
  idInput.value = id;
1007
1984
  subscriber.value = { type: t2, id };
1008
1985
  };
1009
- const amount = (0, import_vue12.ref)("");
1010
- const note = (0, import_vue12.ref)("");
1011
- const granted = (0, import_vue12.ref)(null);
1012
- const input = (model, placeholder, extra = {}) => (0, import_vue12.h)("input", {
1986
+ const amount = (0, import_vue14.ref)("");
1987
+ const note = (0, import_vue14.ref)("");
1988
+ const granted = (0, import_vue14.ref)(null);
1989
+ const input = (model, placeholder, extra = {}) => (0, import_vue14.h)("input", {
1013
1990
  value: model.value,
1014
1991
  onInput: (e) => model.value = e.target.value,
1015
1992
  placeholder,
1016
- style: { ...styles.button, cursor: "text", ...extra },
1993
+ style: { ...styles.input, ...extra },
1017
1994
  "aria-label": placeholder
1018
1995
  });
1019
1996
  return () => {
1020
1997
  const s = subscription.value;
1021
1998
  const w = wallet.value;
1022
- return (0, import_vue12.h)("div", { style: styles.container }, [
1023
- (0, import_vue12.h)("h1", { style: styles.title }, "Subscriber"),
1024
- (0, import_vue12.h)(
1999
+ return (0, import_vue14.h)("div", { style: styles.container }, [
2000
+ pageHeader(
2001
+ "Subscribers",
2002
+ "Who is subscribed. Open one for their plan, wallet and ledger, and to grant credit."
2003
+ ),
2004
+ (0, import_vue14.h)(
1025
2005
  "form",
1026
2006
  {
1027
2007
  style: styles.toolbar,
@@ -1032,26 +2012,26 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1032
2012
  }
1033
2013
  },
1034
2014
  [
1035
- (0, import_vue12.h)(
2015
+ (0, import_vue14.h)(
1036
2016
  "select",
1037
2017
  {
1038
2018
  value: type.value,
1039
2019
  onChange: (e) => type.value = e.target.value,
1040
- style: styles.button,
2020
+ style: styles.input,
1041
2021
  "aria-label": "Type"
1042
2022
  },
1043
2023
  [
1044
- (0, import_vue12.h)("option", { value: "user" }, "user"),
1045
- (0, import_vue12.h)("option", { value: "workspace" }, "workspace")
2024
+ (0, import_vue14.h)("option", { value: "user" }, "user"),
2025
+ (0, import_vue14.h)("option", { value: "workspace" }, "workspace")
1046
2026
  ]
1047
2027
  ),
1048
2028
  input(idInput, "subscriber id", { minWidth: "280px" }),
1049
- (0, import_vue12.h)(
2029
+ (0, import_vue14.h)(
1050
2030
  "button",
1051
- { type: "submit", style: styles.button, disabled: isLoading.value },
1052
- "Look up"
2031
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
2032
+ [icon("search", 14), "Look up"]
1053
2033
  ),
1054
- subscriber.value ? (0, import_vue12.h)(
2034
+ subscriber.value ? (0, import_vue14.h)(
1055
2035
  "button",
1056
2036
  {
1057
2037
  type: "button",
@@ -1062,8 +2042,8 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1062
2042
  granted.value = null;
1063
2043
  }
1064
2044
  },
1065
- "\u2190 All subscribers"
1066
- ) : (0, import_vue12.h)(
2045
+ [icon("back", 14), "All subscribers"]
2046
+ ) : (0, import_vue14.h)(
1067
2047
  "button",
1068
2048
  {
1069
2049
  type: "button",
@@ -1075,35 +2055,43 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1075
2055
  )
1076
2056
  ]
1077
2057
  ),
1078
- !subscriber.value ? (0, import_vue12.h)("div", [
1079
- list.error.value ? (0, import_vue12.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1080
- list.subscriptions.value.length === 0 && !list.isLoading.value ? (0, import_vue12.h)("p", { style: styles.muted }, "No subscribers yet.") : table(
2058
+ !subscriber.value ? (0, import_vue14.h)("div", [
2059
+ list.error.value ? (0, import_vue14.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
2060
+ list.subscriptions.value.length === 0 && !list.isLoading.value ? empty(
2061
+ "No subscribers yet",
2062
+ "Subscriptions appear here once someone checks out.",
2063
+ "subscriber"
2064
+ ) : table(
1081
2065
  ["Subscriber", "Plan", "Status", "Renews"],
1082
2066
  list.subscriptions.value.map(
1083
- (sub) => (0, import_vue12.h)("tr", { key: sub.id }, [
2067
+ (sub) => (0, import_vue14.h)("tr", { key: sub.id }, [
1084
2068
  td(
1085
- (0, import_vue12.h)(
2069
+ (0, import_vue14.h)(
1086
2070
  "button",
1087
2071
  {
1088
2072
  type: "button",
1089
- style: { ...styles.navItem, padding: 0, ...styles.mono },
2073
+ style: { ...styles.link, ...styles.mono },
1090
2074
  onClick: () => open(sub.subscriberType, sub.subscriberId)
1091
2075
  },
1092
2076
  `${sub.subscriberType}/${sub.subscriberId}`
1093
2077
  )
1094
2078
  ),
1095
2079
  td(`${sub.plan} \xB7 ${sub.interval}`),
2080
+ td([
2081
+ pill(
2082
+ sub.status === "active" ? "ok" : sub.status === "trialing" ? "info" : sub.status === "past_due" ? "warn" : "neutral",
2083
+ sub.status
2084
+ ),
2085
+ sub.cancelAtPeriodEnd ? [" ", pill("warn", "cancels", false)] : null
2086
+ ]),
1096
2087
  td(
1097
- sub.cancelAtPeriodEnd ? [sub.status, " ", (0, import_vue12.h)("span", { style: styles.badge }, "cancels")] : sub.status
1098
- ),
1099
- td(
1100
- sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : (0, import_vue12.h)("span", { style: styles.muted }, "\u2014")
2088
+ sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : (0, import_vue14.h)("span", { style: styles.muted }, "\u2014")
1101
2089
  )
1102
2090
  ])
1103
2091
  )
1104
2092
  ),
1105
- list.isLoading.value ? (0, import_vue12.h)("p", { style: styles.status }, "Loading\u2026") : null,
1106
- list.hasMore.value && !list.isLoading.value ? (0, import_vue12.h)(
2093
+ list.isLoading.value ? (0, import_vue14.h)("p", { style: styles.status }, "Loading\u2026") : null,
2094
+ list.hasMore.value && !list.isLoading.value ? (0, import_vue14.h)(
1107
2095
  "button",
1108
2096
  {
1109
2097
  type: "button",
@@ -1113,37 +2101,58 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1113
2101
  "Load more"
1114
2102
  ) : null
1115
2103
  ]) : null,
1116
- error.value ? (0, import_vue12.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2104
+ error.value ? (0, import_vue14.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1117
2105
  subscriber.value && !isLoading.value ? [
1118
- (0, import_vue12.h)("h2", { style: styles.subtitle }, "Subscription"),
1119
- s ? (0, import_vue12.h)("table", { style: styles.table }, [
1120
- (0, import_vue12.h)("tbody", [
1121
- (0, import_vue12.h)("tr", [td("plan"), td([(0, import_vue12.h)("strong", s.plan), ` \xB7 ${s.interval}`])]),
1122
- (0, import_vue12.h)("tr", [
2106
+ (0, import_vue14.h)("h2", { style: styles.subtitle }, "Subscription"),
2107
+ s ? (0, import_vue14.h)("table", { style: styles.table }, [
2108
+ (0, import_vue14.h)("tbody", [
2109
+ (0, import_vue14.h)("tr", [td("plan"), td([(0, import_vue14.h)("strong", s.plan), ` \xB7 ${s.interval}`])]),
2110
+ (0, import_vue14.h)("tr", [
1123
2111
  td("status"),
1124
2112
  td(`${s.status}${s.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""}`)
1125
2113
  ]),
1126
- (0, import_vue12.h)("tr", [
2114
+ (0, import_vue14.h)("tr", [
1127
2115
  td("period"),
1128
2116
  td(
1129
2117
  `${s.currentPeriodStart ? new Date(s.currentPeriodStart).toLocaleDateString() : "\u2014"} \u2192 ${s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : "\u2014"}`
1130
2118
  )
1131
2119
  ]),
1132
- (0, import_vue12.h)("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
2120
+ (0, import_vue14.h)("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
1133
2121
  ])
1134
- ]) : (0, import_vue12.h)("p", { style: styles.muted }, "No subscription."),
1135
- (0, import_vue12.h)("h2", { style: styles.subtitle }, "Wallet"),
2122
+ ]) : (0, import_vue14.h)("p", { style: styles.muted }, "No subscription."),
2123
+ (0, import_vue14.h)("h2", { style: styles.subtitle }, "Wallet"),
1136
2124
  w ? [
1137
- (0, import_vue12.h)("p", [
1138
- (0, import_vue12.h)("strong", w.balance),
1139
- ` ${w.currency} `,
1140
- (0, import_vue12.h)(
1141
- "span",
1142
- { style: styles.muted },
1143
- `(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
1144
- )
1145
- ]),
1146
- (0, import_vue12.h)(
2125
+ (0, import_vue14.h)(
2126
+ "p",
2127
+ {
2128
+ style: {
2129
+ ...styles.card,
2130
+ display: "flex",
2131
+ alignItems: "baseline",
2132
+ gap: "8px",
2133
+ flexWrap: "wrap"
2134
+ }
2135
+ },
2136
+ [
2137
+ (0, import_vue14.h)(
2138
+ "strong",
2139
+ {
2140
+ style: {
2141
+ fontSize: "22px",
2142
+ letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
2143
+ }
2144
+ },
2145
+ w.balance
2146
+ ),
2147
+ ` ${w.currency} `,
2148
+ (0, import_vue14.h)(
2149
+ "span",
2150
+ { style: styles.muted },
2151
+ `(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
2152
+ )
2153
+ ]
2154
+ ),
2155
+ (0, import_vue14.h)(
1147
2156
  "form",
1148
2157
  {
1149
2158
  style: styles.toolbar,
@@ -1168,23 +2177,23 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1168
2177
  [
1169
2178
  input(amount, "amount (minor units)"),
1170
2179
  input(note, "reason (optional)", { minWidth: "220px" }),
1171
- (0, import_vue12.h)(
2180
+ (0, import_vue14.h)(
1172
2181
  "button",
1173
2182
  {
1174
2183
  type: "submit",
1175
- style: styles.button,
2184
+ style: styles.buttonPrimary,
1176
2185
  disabled: !/^\d+$/.test(amount.value.trim())
1177
2186
  },
1178
- "Grant"
2187
+ [icon("plus", 14), "Grant"]
1179
2188
  ),
1180
- granted.value ? (0, import_vue12.h)("span", { style: styles.ok }, granted.value) : null
2189
+ granted.value ? (0, import_vue14.h)("span", { style: styles.ok }, granted.value) : null
1181
2190
  ]
1182
2191
  ),
1183
- (0, import_vue12.h)("h2", { style: styles.subtitle }, "Ledger"),
1184
- ledger.value.length === 0 ? (0, import_vue12.h)("p", { style: styles.muted }, "Nothing moved yet.") : table(
2192
+ (0, import_vue14.h)("h2", { style: styles.subtitle }, "Ledger"),
2193
+ ledger.value.length === 0 ? empty("Nothing moved yet", void 0, "subscriber") : table(
1185
2194
  ["When", "Kind", "Amount", "Note"],
1186
2195
  ledger.value.map(
1187
- (t2) => (0, import_vue12.h)("tr", { key: t2.id }, [
2196
+ (t2) => (0, import_vue14.h)("tr", { key: t2.id }, [
1188
2197
  td(new Date(t2.createdAt).toLocaleString(), styles.muted),
1189
2198
  td(t2.type),
1190
2199
  td(t2.amount, styles.mono),
@@ -1192,7 +2201,7 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1192
2201
  ])
1193
2202
  )
1194
2203
  ),
1195
- hasMoreLedger.value ? (0, import_vue12.h)(
2204
+ hasMoreLedger.value ? (0, import_vue14.h)(
1196
2205
  "button",
1197
2206
  {
1198
2207
  type: "button",
@@ -1201,7 +2210,7 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1201
2210
  },
1202
2211
  "Load more"
1203
2212
  ) : null
1204
- ] : (0, import_vue12.h)(
2213
+ ] : (0, import_vue14.h)(
1205
2214
  "p",
1206
2215
  { style: styles.muted },
1207
2216
  "No wallet \u2014 billing has no wallet configuration."
@@ -1213,31 +2222,34 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1213
2222
  });
1214
2223
 
1215
2224
  // src/screens/AuditScreen.ts
1216
- var import_vue_admin12 = require("@fonderie/vue-admin");
1217
- var import_vue13 = require("vue");
1218
- var AuditScreen = (0, import_vue13.defineComponent)({
2225
+ var import_vue_admin13 = require("@fonderie/vue-admin");
2226
+ var import_vue15 = require("vue");
2227
+ var AuditScreen = (0, import_vue15.defineComponent)({
1219
2228
  name: "FonderieAuditScreen",
1220
2229
  props: {
1221
2230
  client: { type: Object, required: true },
1222
2231
  pageSize: { type: Number, default: 50 }
1223
2232
  },
1224
2233
  setup(props) {
1225
- const draft = (0, import_vue13.ref)({ workspaceId: "", type: "", actorId: "" });
1226
- const query = (0, import_vue13.ref)({ limit: props.pageSize });
1227
- const { events, hasMore, isLoading, error, refresh, loadMore } = (0, import_vue_admin12.useAdminAudit)(
2234
+ const draft = (0, import_vue15.ref)({ workspaceId: "", type: "", actorId: "" });
2235
+ const query = (0, import_vue15.ref)({ limit: props.pageSize });
2236
+ const { events, hasMore, isLoading, error, refresh, loadMore } = (0, import_vue_admin13.useAdminAudit)(
1228
2237
  props.client,
1229
2238
  query
1230
2239
  );
1231
- const field = (key, placeholder) => (0, import_vue13.h)("input", {
2240
+ const field = (key, placeholder) => (0, import_vue15.h)("input", {
1232
2241
  value: draft.value[key],
1233
2242
  onInput: (e) => draft.value = { ...draft.value, [key]: e.target.value },
1234
2243
  placeholder,
1235
- style: { ...styles.button, cursor: "text" },
2244
+ style: styles.input,
1236
2245
  "aria-label": placeholder
1237
2246
  });
1238
- return () => (0, import_vue13.h)("div", { style: styles.container }, [
1239
- (0, import_vue13.h)("h1", { style: styles.title }, "Audit"),
1240
- (0, import_vue13.h)(
2247
+ return () => (0, import_vue15.h)("div", { style: styles.container }, [
2248
+ pageHeader(
2249
+ "Audit",
2250
+ "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
2251
+ ),
2252
+ (0, import_vue15.h)(
1241
2253
  "form",
1242
2254
  {
1243
2255
  style: styles.toolbar,
@@ -1256,19 +2268,19 @@ var AuditScreen = (0, import_vue13.defineComponent)({
1256
2268
  field("workspaceId", "workspace id (all if empty)"),
1257
2269
  field("type", "event type"),
1258
2270
  field("actorId", "actor id"),
1259
- (0, import_vue13.h)(
2271
+ (0, import_vue15.h)(
1260
2272
  "button",
1261
- { type: "submit", style: styles.button, disabled: isLoading.value },
1262
- "Filter"
2273
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
2274
+ [icon("search", 14), "Filter"]
1263
2275
  ),
1264
2276
  refreshButton("Refresh", isLoading.value, () => void refresh())
1265
2277
  ]
1266
2278
  ),
1267
- error.value ? (0, import_vue13.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1268
- events.value.length === 0 && !isLoading.value ? (0, import_vue13.h)("p", { style: styles.muted }, "Nothing recorded for this filter.") : table(
2279
+ error.value ? (0, import_vue15.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2280
+ events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
1269
2281
  ["When", "Type", "Workspace", "Actor", "Request"],
1270
2282
  events.value.map(
1271
- (e) => (0, import_vue13.h)("tr", { key: e.id }, [
2283
+ (e) => (0, import_vue15.h)("tr", { key: e.id }, [
1272
2284
  td(new Date(e.createdAt).toLocaleString(), styles.muted),
1273
2285
  td(e.type, styles.mono),
1274
2286
  td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
@@ -1277,16 +2289,8 @@ var AuditScreen = (0, import_vue13.defineComponent)({
1277
2289
  ])
1278
2290
  )
1279
2291
  ),
1280
- isLoading.value ? (0, import_vue13.h)("p", { style: styles.status }, "Loading\u2026") : null,
1281
- hasMore.value && !isLoading.value ? (0, import_vue13.h)(
1282
- "button",
1283
- {
1284
- type: "button",
1285
- style: { ...styles.button, marginTop: "8px" },
1286
- onClick: () => void loadMore()
1287
- },
1288
- "Load more"
1289
- ) : null
2292
+ isLoading.value ? (0, import_vue15.h)("p", { style: styles.status }, "Loading\u2026") : null,
2293
+ hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
1290
2294
  ]);
1291
2295
  }
1292
2296
  });
@@ -1318,12 +2322,31 @@ var NAV = [
1318
2322
  items: [
1319
2323
  { page: "audit", label: "Audit", needs: "audit" },
1320
2324
  { page: "log", label: "Admin log" },
1321
- { page: "tokens", label: "Access" },
2325
+ { page: "operators", label: "Operators", needs: "operators" },
2326
+ { page: "tokens", label: "Tokens" },
1322
2327
  { page: "migrations", label: "Migrations" }
1323
2328
  ]
1324
2329
  }
1325
2330
  ];
1326
- var AdminShell = (0, import_vue14.defineComponent)({
2331
+ var ICON = {
2332
+ attention: "attention",
2333
+ modules: "modules",
2334
+ environment: "environment",
2335
+ doctor: "doctor",
2336
+ routes: "routes",
2337
+ users: "users",
2338
+ catalog: "catalog",
2339
+ subscriber: "subscriber",
2340
+ settings: "settings",
2341
+ templates: "templates",
2342
+ audit: "audit",
2343
+ log: "log",
2344
+ tokens: "tokens",
2345
+ operators: "users",
2346
+ migrations: "migrations"
2347
+ };
2348
+ var NARROW = "(max-width: 820px)";
2349
+ var AdminShell = (0, import_vue16.defineComponent)({
1327
2350
  name: "FonderieAdminShell",
1328
2351
  props: {
1329
2352
  client: { type: Object, required: true },
@@ -1340,75 +2363,85 @@ var AdminShell = (0, import_vue14.defineComponent)({
1340
2363
  environment: { type: String, default: void 0 },
1341
2364
  // Controlled navigation: pass `page` and listen to `navigate` to own the
1342
2365
  // URL. Omit `page` and the shell keeps it itself.
1343
- page: { type: String, default: void 0 }
2366
+ page: { type: String, default: void 0 },
2367
+ // Shown at the top of the sidebar. Defaults to "Admin".
2368
+ appName: { type: String, default: "Admin" },
2369
+ // A badge beside the name — the deployment's environment ("production").
2370
+ // Production is tinted so an operator always knows where their clicks land.
2371
+ envLabel: { type: String, default: void 0 },
2372
+ // The deployment has operator accounts (@fonderie/admin with a store):
2373
+ // shows the Operators page.
2374
+ operators: { type: Boolean, default: false },
2375
+ // The signed-in operator's email, when a person (not a token) is signed in.
2376
+ currentOperator: { type: String, default: void 0 }
1344
2377
  },
2378
+ // `footer` slot: pinned to the bottom of the sidebar — session controls
2379
+ // (theme, sign out). The React shell takes the same thing as a `footer` prop.
1345
2380
  emits: { navigate: (_page) => true },
1346
- setup(props, { emit }) {
1347
- const own = (0, import_vue14.ref)("attention");
1348
- const current = (0, import_vue14.computed)(() => props.page ?? own.value);
1349
- const editing = (0, import_vue14.ref)(null);
2381
+ setup(props, { emit, slots }) {
2382
+ const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
2383
+ const narrow = (0, import_vue16.ref)(query?.matches ?? false);
2384
+ const drawer = (0, import_vue16.ref)(false);
2385
+ const onMedia = (e) => narrow.value = e.matches;
2386
+ (0, import_vue16.onMounted)(() => query?.addEventListener("change", onMedia));
2387
+ (0, import_vue16.onBeforeUnmount)(() => query?.removeEventListener("change", onMedia));
2388
+ const own = (0, import_vue16.ref)("attention");
2389
+ const current = (0, import_vue16.computed)(() => props.page ?? own.value);
2390
+ const editing = (0, import_vue16.ref)(null);
1350
2391
  const go = (p) => {
1351
2392
  editing.value = null;
1352
2393
  emit("navigate", p);
1353
2394
  if (props.page === void 0) own.value = p;
2395
+ drawer.value = false;
1354
2396
  };
1355
- const back = () => (0, import_vue14.h)(
1356
- "button",
1357
- { type: "button", style: styles.button, onClick: () => editing.value = null },
1358
- "\u2190 Back"
1359
- );
2397
+ const back = (label) => (0, import_vue16.h)("div", { style: { padding: "24px 40px 0" } }, [
2398
+ (0, import_vue16.h)(
2399
+ "button",
2400
+ { type: "button", style: styles.buttonGhost, onClick: () => editing.value = null },
2401
+ [icon("back", 14), label]
2402
+ )
2403
+ ]);
2404
+ const missing = (text) => (0, import_vue16.h)("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
1360
2405
  const body = () => {
1361
2406
  const c = props.client;
1362
2407
  switch (current.value) {
1363
2408
  case "attention":
1364
- return (0, import_vue14.h)(AttentionScreen, { client: c });
2409
+ return (0, import_vue16.h)(AttentionScreen, { client: c });
1365
2410
  case "modules":
1366
- return (0, import_vue14.h)(ModulesScreen, { client: c });
2411
+ return (0, import_vue16.h)(ModulesScreen, { client: c });
1367
2412
  case "doctor":
1368
- return (0, import_vue14.h)(DoctorScreen, { client: c });
2413
+ return (0, import_vue16.h)(DoctorScreen, { client: c });
1369
2414
  case "environment":
1370
- return (0, import_vue14.h)(EnvironmentScreen, { client: c });
2415
+ return (0, import_vue16.h)(EnvironmentScreen, { client: c });
1371
2416
  case "routes":
1372
- return (0, import_vue14.h)(RoutesScreen, { client: c });
2417
+ return (0, import_vue16.h)(RoutesScreen, { client: c });
1373
2418
  case "tokens":
1374
- return (0, import_vue14.h)(TokensScreen, { client: c });
2419
+ return (0, import_vue16.h)(TokensScreen, { client: c });
2420
+ case "operators":
2421
+ return (0, import_vue16.h)(OperatorsScreen, {
2422
+ client: c,
2423
+ ...props.currentOperator ? { me: props.currentOperator } : {}
2424
+ });
1375
2425
  case "migrations":
1376
- return (0, import_vue14.h)(MigrationsScreen, { client: c });
2426
+ return (0, import_vue16.h)(MigrationsScreen, { client: c });
1377
2427
  case "log":
1378
- return (0, import_vue14.h)(AdminLogScreen, { client: c });
2428
+ return (0, import_vue16.h)(AdminLogScreen, { client: c });
1379
2429
  case "catalog":
1380
- return props.billingClient ? (0, import_vue14.h)(CatalogScreen, { client: props.billingClient }) : (0, import_vue14.h)(
1381
- "p",
1382
- { style: styles.status },
1383
- "Pass a BillingAdminClient to see the catalog here."
1384
- );
2430
+ return props.billingClient ? (0, import_vue16.h)(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
1385
2431
  case "subscriber":
1386
- return props.billingClient ? (0, import_vue14.h)(SubscriberScreen, { client: props.billingClient }) : (0, import_vue14.h)(
1387
- "p",
1388
- { style: styles.status },
1389
- "Pass a BillingAdminClient to look up subscribers here."
1390
- );
2432
+ return props.billingClient ? (0, import_vue16.h)(SubscriberScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to look up subscribers here.");
1391
2433
  case "audit":
1392
- return props.auditClient ? (0, import_vue14.h)(AuditScreen, { client: props.auditClient }) : (0, import_vue14.h)(
1393
- "p",
1394
- { style: styles.status },
1395
- "Pass an AuditAdminClient to see the audit trail here."
1396
- );
2434
+ return props.auditClient ? (0, import_vue16.h)(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
1397
2435
  case "users":
1398
- return props.authClient ? (0, import_vue14.h)(UsersScreen, { client: props.authClient }) : (0, import_vue14.h)("p", { style: styles.status }, "Pass an AuthAdminClient to look up users here.");
2436
+ return props.authClient ? (0, import_vue16.h)(UsersScreen, { client: props.authClient }) : missing("Pass an AuthAdminClient to look up users here.");
1399
2437
  case "settings": {
1400
2438
  const cc = props.configClient;
1401
- if (!cc)
1402
- return (0, import_vue14.h)(
1403
- "p",
1404
- { style: styles.status },
1405
- "Pass a ConfigAdminClient to manage config and secrets here."
1406
- );
2439
+ if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
1407
2440
  const e = editing.value;
1408
2441
  if (e && e.kind !== "template")
1409
2442
  return [
1410
- back(),
1411
- (0, import_vue14.h)(import_vue_config_admin_screens.ConfigEditorScreen, {
2443
+ back("Config & secrets"),
2444
+ (0, import_vue16.h)(import_vue_config_admin_screens.ConfigEditorScreen, {
1412
2445
  client: cc,
1413
2446
  kind: e.kind,
1414
2447
  configKey: e.key,
@@ -1416,7 +2449,7 @@ var AdminShell = (0, import_vue14.defineComponent)({
1416
2449
  onSaved: () => editing.value = null
1417
2450
  })
1418
2451
  ];
1419
- return (0, import_vue14.h)(import_vue_config_admin_screens.ConfigListScreen, {
2452
+ return (0, import_vue16.h)(import_vue_config_admin_screens.ConfigListScreen, {
1420
2453
  client: cc,
1421
2454
  ...props.environment !== void 0 ? { environment: props.environment } : {},
1422
2455
  "onSelect-config": (key) => editing.value = { kind: "config", key },
@@ -1428,61 +2461,135 @@ var AdminShell = (0, import_vue14.defineComponent)({
1428
2461
  }
1429
2462
  case "templates": {
1430
2463
  const kc = props.courierClient;
1431
- if (!kc)
1432
- return (0, import_vue14.h)(
1433
- "p",
1434
- { style: styles.status },
1435
- "Pass a CourierAdminClient to manage templates here."
1436
- );
2464
+ if (!kc) return missing("Pass a CourierAdminClient to manage templates here.");
1437
2465
  const e = editing.value;
1438
2466
  if (e?.kind === "template")
1439
2467
  return [
1440
- back(),
1441
- (0, import_vue14.h)(import_vue_courier_admin_screens.TemplateEditorScreen, {
2468
+ back("Templates"),
2469
+ (0, import_vue16.h)(import_vue_courier_admin_screens.TemplateEditorScreen, {
1442
2470
  client: kc,
1443
2471
  type: e.type,
1444
2472
  onSaved: () => editing.value = null
1445
2473
  })
1446
2474
  ];
1447
- return (0, import_vue14.h)(import_vue_courier_admin_screens.TemplateListScreen, {
2475
+ return (0, import_vue16.h)(import_vue_courier_admin_screens.TemplateListScreen, {
1448
2476
  client: kc,
1449
2477
  "onSelect-template": (t2) => editing.value = { kind: "template", type: t2.type }
1450
2478
  });
1451
2479
  }
1452
2480
  }
1453
2481
  };
1454
- return () => (0, import_vue14.h)("div", { style: styles.shell }, [
1455
- (0, import_vue14.h)(
1456
- "nav",
1457
- { style: styles.nav, "aria-label": "Admin" },
1458
- NAV.map((g) => {
1459
- const items = g.items.filter(
1460
- (i) => !i.needs || (i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : i.needs === "billing" ? props.billingClient : props.auditClient)
1461
- );
1462
- if (items.length === 0) return null;
1463
- return (0, import_vue14.h)("div", { key: g.group }, [
1464
- (0, import_vue14.h)("div", { style: styles.navGroup }, g.group),
1465
- ...items.map(
1466
- (i) => (0, import_vue14.h)(
2482
+ const visible = (i) => !i.needs || (i.needs === "operators" ? props.operators : i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : i.needs === "billing" ? props.billingClient : props.auditClient);
2483
+ const brand = () => {
2484
+ const production = props.envLabel ? /^prod/i.test(props.envLabel) : false;
2485
+ return (0, import_vue16.h)("div", { style: styles.navBrand }, [
2486
+ (0, import_vue16.h)("div", { style: styles.navMark }, [icon("mark", 15)]),
2487
+ (0, import_vue16.h)("div", { style: { minWidth: 0, flex: 1 } }, [
2488
+ (0, import_vue16.h)("div", { style: styles.navBrandTitle }, props.appName),
2489
+ props.envLabel ? (0, import_vue16.h)(
2490
+ "span",
2491
+ {
2492
+ style: {
2493
+ ...styles.badge,
2494
+ marginTop: "3px",
2495
+ padding: "0 7px",
2496
+ fontSize: "11px",
2497
+ ...production ? {
2498
+ color: "var(--fonderie-danger,#e00)",
2499
+ borderColor: "color-mix(in srgb, var(--fonderie-danger,#e00) 35%, transparent)"
2500
+ } : {}
2501
+ }
2502
+ },
2503
+ props.envLabel
2504
+ ) : null
2505
+ ]),
2506
+ narrow.value ? (0, import_vue16.h)(
2507
+ "button",
2508
+ {
2509
+ type: "button",
2510
+ style: styles.buttonGhost,
2511
+ "aria-label": "Close menu",
2512
+ onClick: () => drawer.value = false
2513
+ },
2514
+ [icon("close")]
2515
+ ) : null
2516
+ ]);
2517
+ };
2518
+ const nav = () => (0, import_vue16.h)(
2519
+ "nav",
2520
+ {
2521
+ style: {
2522
+ ...styles.nav,
2523
+ ...narrow.value ? { width: "280px", maxWidth: "85vw", boxShadow: "0 10px 40px rgba(0,0,0,.25)" } : {}
2524
+ },
2525
+ "aria-label": "Admin"
2526
+ },
2527
+ [
2528
+ brand(),
2529
+ (0, import_vue16.h)(
2530
+ "div",
2531
+ { style: styles.navScroll },
2532
+ NAV.map((g) => {
2533
+ const items = g.items.filter(visible);
2534
+ if (items.length === 0) return null;
2535
+ return (0, import_vue16.h)("div", { key: g.group }, [
2536
+ (0, import_vue16.h)("div", { style: styles.navGroup }, g.group),
2537
+ ...items.map((i) => {
2538
+ const active = current.value === i.page;
2539
+ return (0, import_vue16.h)(
2540
+ "button",
2541
+ {
2542
+ key: i.page,
2543
+ type: "button",
2544
+ class: "fonderie-admin-nav-item",
2545
+ style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
2546
+ "aria-current": active ? "page" : void 0,
2547
+ onClick: () => go(i.page)
2548
+ },
2549
+ [icon(ICON[i.page], 16, active ? styles.navIconActive : void 0), i.label]
2550
+ );
2551
+ })
2552
+ ]);
2553
+ })
2554
+ ),
2555
+ slots["footer"] ? (0, import_vue16.h)("div", { style: styles.navFooter }, slots["footer"]()) : null
2556
+ ]
2557
+ );
2558
+ return () => {
2559
+ const title = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label ?? "";
2560
+ return (0, import_vue16.h)(
2561
+ "div",
2562
+ { style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
2563
+ [
2564
+ ...narrow.value ? [
2565
+ (0, import_vue16.h)("div", { style: styles.topbar }, [
2566
+ (0, import_vue16.h)(
1467
2567
  "button",
1468
2568
  {
1469
- key: i.page,
1470
2569
  type: "button",
1471
- style: {
1472
- ...styles.navItem,
1473
- ...current.value === i.page ? styles.navItemActive : {}
1474
- },
1475
- "aria-current": current.value === i.page ? "page" : void 0,
1476
- onClick: () => go(i.page)
2570
+ style: styles.buttonGhost,
2571
+ "aria-label": "Open menu",
2572
+ "aria-expanded": drawer.value,
2573
+ onClick: () => drawer.value = true
1477
2574
  },
1478
- i.label
1479
- )
1480
- )
1481
- ]);
1482
- })
1483
- ),
1484
- (0, import_vue14.h)("main", { style: styles.main }, body())
1485
- ]);
2575
+ [icon("menu")]
2576
+ ),
2577
+ (0, import_vue16.h)("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2578
+ props.envLabel ? (0, import_vue16.h)("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2579
+ ]),
2580
+ drawer.value ? (0, import_vue16.h)("div", { style: styles.drawer }, [
2581
+ nav(),
2582
+ (0, import_vue16.h)("div", {
2583
+ style: styles.scrim,
2584
+ "aria-hidden": "true",
2585
+ onClick: () => drawer.value = false
2586
+ })
2587
+ ]) : null
2588
+ ] : [nav()],
2589
+ (0, import_vue16.h)("main", { class: "fonderie-admin-main", style: styles.main }, body())
2590
+ ]
2591
+ );
2592
+ };
1486
2593
  }
1487
2594
  });
1488
2595
  // Annotate the CommonJS export names for ESM import in node:
@@ -1496,6 +2603,7 @@ var AdminShell = (0, import_vue14.defineComponent)({
1496
2603
  EnvironmentScreen,
1497
2604
  MigrationsScreen,
1498
2605
  ModulesScreen,
2606
+ OperatorsScreen,
1499
2607
  RoutesScreen,
1500
2608
  SubscriberScreen,
1501
2609
  TokensScreen,