@fonderie/react-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.tsx
40
41
  var import_react_config_admin_screens = require("@fonderie/react-config-admin-screens");
41
42
  var import_react_courier_admin_screens = require("@fonderie/react-courier-admin-screens");
42
- var import_react5 = require("react");
43
+ var import_react7 = require("react");
43
44
 
44
45
  // src/styles.ts
45
46
  var t = {
@@ -61,6 +62,31 @@ 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
  };
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
+ };
64
90
  var styles = {
65
91
  shell: {
66
92
  display: "flex",
@@ -69,155 +95,578 @@ 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}`
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"
80
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",
131
276
  fontWeight: 600,
132
- color: t.muted
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,
316
+ fontWeight: 600,
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
  };
169
430
 
431
+ // src/icons.ts
432
+ var ICONS = {
433
+ attention: ["M22 12h-4l-3 9L9 3l-3 9H2"],
434
+ modules: [
435
+ "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",
436
+ "M3.3 7 12 12l8.7-5",
437
+ "M12 22V12"
438
+ ],
439
+ environment: [
440
+ "M4 21v-7",
441
+ "M4 10V3",
442
+ "M12 21v-9",
443
+ "M12 8V3",
444
+ "M20 21v-5",
445
+ "M20 12V3",
446
+ "M1 14h6",
447
+ "M9 8h6",
448
+ "M17 16h6"
449
+ ],
450
+ doctor: [
451
+ "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"
452
+ ],
453
+ routes: [
454
+ "M6 3v12",
455
+ "M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
456
+ "M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
457
+ "M18 9a9 9 0 0 1-9 9"
458
+ ],
459
+ users: [
460
+ "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",
461
+ "M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z",
462
+ "M22 21v-2a4 4 0 0 0-3-3.87",
463
+ "M16 3.13a4 4 0 0 1 0 7.75"
464
+ ],
465
+ catalog: [
466
+ "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",
467
+ "M7.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"
468
+ ],
469
+ subscriber: ["M2 7h20v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z", "M2 11h20", "M6 15h4"],
470
+ settings: [
471
+ "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",
472
+ "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"
473
+ ],
474
+ templates: [
475
+ "M22 7l-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7",
476
+ "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"
477
+ ],
478
+ audit: ["M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z", "M9 12l2 2 4-4"],
479
+ log: ["M12 8v4l3 3", "M3.05 11a9 9 0 1 1 .5 4m-.5 5v-5h5"],
480
+ tokens: [
481
+ "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",
482
+ "M16.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"
483
+ ],
484
+ migrations: [
485
+ "M12 22c4.97 0 9-1.79 9-4V6",
486
+ "M3 6v12c0 2.21 4.03 4 9 4",
487
+ "M12 10c4.97 0 9-1.79 9-4s-4.03-4-9-4-9 1.79-9 4 4.03 4 9 4z",
488
+ "M3 12c0 2.21 4.03 4 9 4s9-1.79 9-4"
489
+ ],
490
+ menu: ["M4 6h16", "M4 12h16", "M4 18h16"],
491
+ close: ["M18 6 6 18", "m6 6 12 12"],
492
+ refresh: [
493
+ "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",
494
+ "M21 3v5h-5",
495
+ "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",
496
+ "M8 16H3v5"
497
+ ],
498
+ check: ["M20 6 9 17l-5-5"],
499
+ alert: [
500
+ "M12 9v4",
501
+ "M12 17h.01",
502
+ "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"
503
+ ],
504
+ search: ["M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z", "m21 21-4.3-4.3"],
505
+ back: ["m12 19-7-7 7-7", "M19 12H5"],
506
+ logout: ["M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4", "m16 17 5-5-5-5", "M21 12H9"],
507
+ lock: [
508
+ "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",
509
+ "M7 11V7a5 5 0 0 1 10 0v4"
510
+ ],
511
+ plus: ["M12 5v14", "M5 12h14"],
512
+ mark: [
513
+ "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",
514
+ "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",
515
+ "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",
516
+ "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"
517
+ ]
518
+ };
519
+
520
+ // src/ui.tsx
521
+ var import_jsx_runtime = require("react/jsx-runtime");
522
+ function Icon({
523
+ name,
524
+ size = 16,
525
+ style
526
+ }) {
527
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
528
+ "svg",
529
+ {
530
+ width: size,
531
+ height: size,
532
+ viewBox: "0 0 24 24",
533
+ fill: "none",
534
+ stroke: "currentColor",
535
+ strokeWidth: 2,
536
+ strokeLinecap: "round",
537
+ strokeLinejoin: "round",
538
+ "aria-hidden": "true",
539
+ style: { flexShrink: 0, ...style },
540
+ children: ICONS[name].map((d) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d }, d))
541
+ }
542
+ );
543
+ }
544
+ function PageHeader({
545
+ title,
546
+ lead,
547
+ actions
548
+ }) {
549
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.header, children: [
550
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { minWidth: 0 }, children: [
551
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { style: styles.title, children: title }),
552
+ lead ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.lead, children: lead }) : null
553
+ ] }),
554
+ actions ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.actions, children: actions }) : null
555
+ ] });
556
+ }
557
+ function Pill({
558
+ tone,
559
+ children,
560
+ dot = true
561
+ }) {
562
+ const color = toneColor[tone];
563
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { ...styles.pill, color, background: tint(color, tone === "neutral" ? 10 : 13) }, children: [
564
+ dot ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.dot }) : null,
565
+ children
566
+ ] });
567
+ }
568
+ function Stat({
569
+ label,
570
+ value,
571
+ hint,
572
+ tone
573
+ }) {
574
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.card, children: [
575
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.statLabel, children: label }),
576
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...styles.statValue, ...tone ? { color: toneColor[tone] } : {} }, children: value }),
577
+ hint ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.statHint, children: hint }) : null
578
+ ] });
579
+ }
580
+ function Empty({
581
+ icon = "check",
582
+ title,
583
+ children
584
+ }) {
585
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.empty, children: [
586
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { name: icon, size: 22 }),
587
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.emptyTitle, children: title }),
588
+ children ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children }) : null
589
+ ] });
590
+ }
591
+ function RefreshButton({
592
+ onClick,
593
+ busy,
594
+ label = "Refresh"
595
+ }) {
596
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", style: styles.button, onClick, disabled: busy, children: [
597
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { name: "refresh", size: 14 }),
598
+ busy ? "Working\u2026" : label
599
+ ] });
600
+ }
601
+ function MethodChip({ method }) {
602
+ const tone = method === "GET" ? "ok" : method === "DELETE" ? "bad" : method === "POST" ? "info" : "warn";
603
+ const color = toneColor[tone];
604
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
605
+ "span",
606
+ {
607
+ style: {
608
+ ...styles.mono,
609
+ fontSize: 11,
610
+ fontWeight: 700,
611
+ color,
612
+ background: tint(color, 12),
613
+ borderRadius: 4,
614
+ padding: "2px 6px",
615
+ display: "inline-block",
616
+ minWidth: 44,
617
+ textAlign: "center"
618
+ },
619
+ children: method
620
+ }
621
+ );
622
+ }
623
+
170
624
  // src/screens/AdminLogScreen.tsx
171
625
  var import_react_admin = require("@fonderie/react-admin");
172
- var import_jsx_runtime = require("react/jsx-runtime");
626
+ var import_jsx_runtime2 = require("react/jsx-runtime");
173
627
  function AdminLogScreen({ client, pageSize = 50 }) {
174
628
  const { entries, hasMore, isLoading, error, refresh, loadMore } = (0, import_react_admin.useAdminLog)(client, {
175
629
  limit: pageSize
176
630
  });
177
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.container, children: [
178
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.toolbar, children: [
179
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { style: { ...styles.title, marginBottom: 0 }, children: "Admin log" }),
180
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
181
- "button",
182
- {
183
- type: "button",
184
- style: styles.button,
185
- onClick: () => void refresh(),
186
- disabled: isLoading,
187
- children: "Refresh"
188
- }
189
- )
190
- ] }),
191
- error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, role: "alert", children: error.status === 404 ? "The admin log is off \u2014 give AdminModule a store." : error.explanation }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
192
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { style: styles.table, children: [
193
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
194
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: styles.th, children: "When" }),
195
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: styles.th, children: "Actor" }),
196
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: styles.th, children: "Request" }),
197
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: styles.th, children: "Status" }),
198
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: styles.th, children: "Module" })
631
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.container, children: [
632
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
633
+ PageHeader,
634
+ {
635
+ title: "Admin log",
636
+ lead: "Every request made through this surface, newest first \u2014 refused ones included.",
637
+ actions: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RefreshButton, { onClick: () => void refresh(), busy: isLoading })
638
+ }
639
+ ),
640
+ error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles.error, role: "alert", children: error.status === 404 ? "The admin log is off \u2014 give AdminModule a store." : error.explanation }) : entries.length === 0 && !isLoading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Empty, { icon: "log", title: "No requests yet" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
641
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { style: styles.table, children: [
642
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
643
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: styles.th, children: "When" }),
644
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: styles.th, children: "Actor" }),
645
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: styles.th, children: "Request" }),
646
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: styles.th, children: "Status" }),
647
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: styles.th, children: "Module" })
199
648
  ] }) }),
200
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: entries.map((e) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
201
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: new Date(e.at).toLocaleString() }),
202
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: styles.td, children: e.actor }),
203
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: { ...styles.td, ...styles.mono }, children: [
204
- e.method,
649
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: entries.map((e) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
650
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: { ...styles.td, ...styles.muted, whiteSpace: "nowrap" }, children: new Date(e.at).toLocaleString() }),
651
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: styles.td, children: e.actor }),
652
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("td", { style: styles.td, children: [
653
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MethodChip, { method: e.method }),
205
654
  " ",
206
- e.path
655
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { ...styles.mono, wordBreak: "break-all" }, children: e.path })
207
656
  ] }),
208
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: styles.td, children: [
209
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: e.status >= 400 ? styles.bad : styles.ok, children: e.status }),
657
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("td", { style: { ...styles.td, whiteSpace: "nowrap" }, children: [
658
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Pill, { tone: e.status >= 500 ? "bad" : e.status >= 400 ? "warn" : "ok", children: e.status }),
210
659
  " ",
211
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: styles.muted, children: [
660
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: styles.muted, children: [
212
661
  e.durationMs,
213
662
  " ms"
214
663
  ] })
215
664
  ] }),
216
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: e.module })
665
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: e.module })
217
666
  ] }, e.id)) })
218
667
  ] }),
219
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : null,
220
- hasMore && !isLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
668
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : null,
669
+ hasMore && !isLoading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
221
670
  "button",
222
671
  {
223
672
  type: "button",
@@ -232,271 +681,737 @@ function AdminLogScreen({ client, pageSize = 50 }) {
232
681
 
233
682
  // src/screens/AttentionScreen.tsx
234
683
  var import_react_admin2 = require("@fonderie/react-admin");
235
- var import_jsx_runtime2 = require("react/jsx-runtime");
684
+ var import_jsx_runtime3 = require("react/jsx-runtime");
236
685
  function AttentionScreen({ client }) {
237
686
  const { attention, isLoading, error, refresh } = (0, import_react_admin2.useAttention)(client);
238
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.container, children: [
239
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.toolbar, children: [
240
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { style: { ...styles.title, marginBottom: 0 }, children: "Attention" }),
241
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
242
- "button",
687
+ const { manifest } = (0, import_react_admin2.useManifest)(client);
688
+ const errors = attention?.items.filter((i) => i.severity === "error") ?? [];
689
+ const advice = attention?.items.filter((i) => i.severity !== "error") ?? [];
690
+ const ready = manifest?.modules.filter((m) => m.readiness.ok).length ?? 0;
691
+ const total = manifest?.modules.length ?? 0;
692
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles.container, children: [
693
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
694
+ PageHeader,
695
+ {
696
+ title: "Attention",
697
+ lead: attention ? `What needs you on this deployment \xB7 checked ${new Date(attention.generatedAt).toLocaleTimeString()}` : "What needs you on this deployment",
698
+ actions: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RefreshButton, { onClick: () => void refresh(), busy: isLoading })
699
+ }
700
+ ),
701
+ error ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
702
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles.grid, children: [
703
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
704
+ Stat,
243
705
  {
244
- type: "button",
245
- style: styles.button,
246
- onClick: () => void refresh(),
247
- disabled: isLoading,
248
- children: "Refresh"
706
+ label: "Needs action",
707
+ value: attention ? errors.length : "\u2014",
708
+ tone: errors.length ? "bad" : attention ? "ok" : void 0,
709
+ hint: "errors to fix"
710
+ }
711
+ ),
712
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
713
+ Stat,
714
+ {
715
+ label: "Advice",
716
+ value: attention ? advice.length : "\u2014",
717
+ tone: advice.length ? "warn" : void 0,
718
+ hint: "worth a look"
719
+ }
720
+ ),
721
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
722
+ Stat,
723
+ {
724
+ label: "Modules ready",
725
+ value: manifest ? `${ready}/${total}` : "\u2014",
726
+ tone: manifest && ready < total ? "warn" : manifest ? "ok" : void 0,
727
+ hint: manifest ? `admin ${manifest.admin.version}` : void 0
728
+ }
729
+ ),
730
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
731
+ Stat,
732
+ {
733
+ label: "Routes",
734
+ value: manifest ? manifest.routes.length : "\u2014",
735
+ hint: manifest ? manifest.env : void 0
249
736
  }
250
737
  )
251
738
  ] }),
252
- isLoading && !attention ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles.status, children: "Running the checks\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : attention?.items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { style: styles.ok, children: [
253
- "Nothing needs you. Checked ",
739
+ isLoading && !attention ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles.status, children: "Running the checks\u2026" }) : attention && attention.items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Empty, { title: "Nothing needs you", children: [
740
+ "Every check passes. Checked ",
254
741
  new Date(attention.generatedAt).toLocaleString(),
255
742
  "."
256
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { style: styles.list, children: attention?.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { style: styles.row, children: [
257
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: item.severity === "error" ? styles.bad : styles.advice, children: item.severity === "error" ? "error" : "advice" }),
258
- " ",
259
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles.mono, children: item.source }),
260
- " \u2014 ",
261
- item.message
262
- ] }, `${item.source}:${item.message}`)) })
743
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { style: styles.list, children: [...errors, ...advice].map((item) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
744
+ "li",
745
+ {
746
+ style: { ...styles.row, display: "flex", gap: 12, alignItems: "flex-start" },
747
+ children: [
748
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
749
+ Icon,
750
+ {
751
+ name: "alert",
752
+ size: 16,
753
+ style: {
754
+ marginTop: 3,
755
+ color: item.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-warning,#f5a623)"
756
+ }
757
+ }
758
+ ),
759
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { minWidth: 0, flex: 1 }, children: [
760
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
761
+ "div",
762
+ {
763
+ style: {
764
+ display: "flex",
765
+ gap: 8,
766
+ alignItems: "center",
767
+ flexWrap: "wrap",
768
+ marginBottom: 2
769
+ },
770
+ children: [
771
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Pill, { tone: item.severity === "error" ? "bad" : "warn", children: item.severity === "error" ? "error" : "advice" }),
772
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: styles.mono, children: item.source })
773
+ ]
774
+ }
775
+ ),
776
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { lineHeight: 1.55 }, children: item.message })
777
+ ] })
778
+ ]
779
+ },
780
+ `${item.source}:${item.message}`
781
+ )) })
263
782
  ] });
264
783
  }
265
784
 
266
785
  // src/screens/EnvironmentScreen.tsx
267
786
  var import_react_admin3 = require("@fonderie/react-admin");
268
- var import_jsx_runtime3 = require("react/jsx-runtime");
787
+ var import_jsx_runtime4 = require("react/jsx-runtime");
269
788
  function EnvironmentScreen({ client }) {
270
789
  const { report, isLoading, error } = (0, import_react_admin3.useAdminEnvironment)(client);
271
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles.container, children: [
272
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { style: styles.title, children: "Configuration" }),
273
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : report ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
274
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: styles.subtitle, children: "Readiness" }),
275
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { style: styles.list, children: report.modules.map((m) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { style: styles.row, children: [
276
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: styles.mono, children: m.name }),
277
- " ",
278
- m.problems.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: styles.ok, children: "ok" }) : m.problems.map((p) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
790
+ const missing = report?.env.filter((e) => !e.set).length ?? 0;
791
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: styles.container, children: [
792
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
793
+ PageHeader,
794
+ {
795
+ title: "Environment",
796
+ lead: "Whether each module is configured and each variable the app reads is set. Values are never shown.",
797
+ actions: report && report.env.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Pill, { tone: missing ? "bad" : "ok", children: missing ? `${missing} missing` : "all set" }) : null
798
+ }
799
+ ),
800
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : report ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
801
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { style: { ...styles.subtitle, marginTop: 0 }, children: "Variables" }),
802
+ report.env.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: styles.muted, children: "No variables declared \u2014 pass `env` to AdminModule." }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
803
+ "div",
804
+ {
805
+ style: {
806
+ ...styles.grid,
807
+ gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))",
808
+ gap: 8
809
+ },
810
+ children: report.env.map((e) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
811
+ "div",
812
+ {
813
+ style: {
814
+ ...styles.card,
815
+ padding: "10px 12px",
816
+ display: "flex",
817
+ alignItems: "center",
818
+ justifyContent: "space-between",
819
+ gap: 8
820
+ },
821
+ children: [
822
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { ...styles.mono, wordBreak: "break-all" }, children: e.name }),
823
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Pill, { tone: e.set ? "ok" : "bad", children: e.set ? "set" : "missing" })
824
+ ]
825
+ },
826
+ e.name
827
+ ))
828
+ }
829
+ ),
830
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { style: styles.subtitle, children: "Module readiness" }),
831
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { style: styles.list, children: report.modules.map((m) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("li", { style: styles.row, children: [
832
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
833
+ "div",
834
+ {
835
+ style: {
836
+ display: "flex",
837
+ justifyContent: "space-between",
838
+ gap: 12,
839
+ alignItems: "center"
840
+ },
841
+ children: [
842
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { ...styles.mono, fontWeight: 600 }, children: m.name }),
843
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
844
+ Pill,
845
+ {
846
+ tone: m.problems.length === 0 ? "ok" : m.problems.some((p) => p.severity === "error") ? "bad" : "warn",
847
+ children: m.problems.length === 0 ? "ready" : `${m.problems.length} problem${m.problems.length === 1 ? "" : "s"}`
848
+ }
849
+ )
850
+ ]
851
+ }
852
+ ),
853
+ m.problems.map((p) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
279
854
  "div",
280
855
  {
281
- style: p.severity === "error" ? styles.bad : styles.advice,
856
+ style: {
857
+ marginTop: 6,
858
+ fontSize: 13,
859
+ color: p.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-text-muted,#5c5c5c)"
860
+ },
282
861
  children: p.message
283
862
  },
284
863
  p.message
285
864
  ))
286
- ] }, m.name)) }),
287
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: styles.subtitle, children: "Environment" }),
288
- report.env.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles.muted, children: "No variables declared \u2014 pass `env` to AdminModule." }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { style: styles.list, children: report.env.map((e) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { style: styles.row, children: [
289
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: styles.mono, children: e.name }),
290
- " ",
291
- e.set ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: styles.ok, children: "set" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: styles.bad, children: "missing" })
292
- ] }, e.name)) })
865
+ ] }, m.name)) })
293
866
  ] }) : null
294
867
  ] });
295
868
  }
296
869
 
297
870
  // src/screens/DoctorScreen.tsx
298
871
  var import_react_admin4 = require("@fonderie/react-admin");
299
- var import_jsx_runtime4 = require("react/jsx-runtime");
872
+ var import_jsx_runtime5 = require("react/jsx-runtime");
300
873
  function DoctorScreen({ client }) {
301
874
  const { report, isLoading, error, refresh } = (0, import_react_admin4.useDoctor)(client);
302
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: styles.container, children: [
303
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: styles.toolbar, children: [
304
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h1", { style: { ...styles.title, marginBottom: 0 }, children: "Doctor" }),
305
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
306
- "button",
307
- {
308
- type: "button",
309
- style: styles.button,
310
- onClick: () => void refresh(),
311
- disabled: isLoading,
312
- children: "Run again"
313
- }
314
- ),
315
- report && !isLoading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: report.ok ? styles.ok : styles.bad, children: report.ok ? "all checks pass" : "a check failed" }) : null
316
- ] }),
317
- isLoading && !report ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: styles.status, children: "Running the checks\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { style: styles.list, children: report?.checks.map((c) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("li", { style: styles.row, children: [
318
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: c.skipped ? styles.muted : c.ok ? styles.ok : styles.bad, children: c.skipped ? "skipped" : c.ok ? "ok" : "failed" }),
319
- " ",
320
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: styles.mono, children: c.name }),
321
- " ",
322
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: styles.muted, children: [
323
- c.module,
324
- " \xB7 ",
325
- c.durationMs,
326
- " ms"
327
- ] }),
328
- c.skipped ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: styles.muted, children: c.skipped }) : null,
329
- c.findings.map((f) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: c.ok ? styles.advice : styles.bad, children: f }, f))
330
- ] }, c.name)) })
875
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: styles.container, children: [
876
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
877
+ PageHeader,
878
+ {
879
+ title: "Doctor",
880
+ lead: "Every reconciliation check across the installed modules, run on demand.",
881
+ actions: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
882
+ report && !isLoading ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Pill, { tone: report.ok ? "ok" : "bad", children: report.ok ? "all checks pass" : "a check failed" }) : null,
883
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RefreshButton, { onClick: () => void refresh(), busy: isLoading, label: "Run again" })
884
+ ] })
885
+ }
886
+ ),
887
+ isLoading && !report ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: styles.status, children: "Running the checks\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("table", { style: styles.table, children: [
888
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("tr", { children: [
889
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { style: { ...styles.th, width: 110 }, children: "Result" }),
890
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { style: styles.th, children: "Check" }),
891
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { style: { ...styles.th, textAlign: "right", width: 90 }, children: "Time" })
892
+ ] }) }),
893
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("tbody", { children: report?.checks.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("tr", { children: [
894
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { style: styles.td, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
895
+ Pill,
896
+ {
897
+ tone: c.skipped ? "neutral" : c.ok ? c.findings.length ? "warn" : "ok" : "bad",
898
+ children: c.skipped ? "skipped" : c.ok ? c.findings.length ? "advice" : "ok" : "failed"
899
+ }
900
+ ) }),
901
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("td", { style: styles.td, children: [
902
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
903
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { ...styles.mono, fontWeight: 600 }, children: c.name }),
904
+ " ",
905
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: styles.muted, children: c.module })
906
+ ] }),
907
+ c.skipped ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...styles.muted, marginTop: 4 }, children: c.skipped }) : null,
908
+ c.findings.map((f) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
909
+ "div",
910
+ {
911
+ style: {
912
+ marginTop: 4,
913
+ fontSize: 13,
914
+ lineHeight: 1.55,
915
+ color: c.ok ? "var(--fonderie-text,#171717)" : "var(--fonderie-danger,#e00)"
916
+ },
917
+ children: f
918
+ },
919
+ f
920
+ ))
921
+ ] }),
922
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
923
+ "td",
924
+ {
925
+ style: {
926
+ ...styles.td,
927
+ ...styles.muted,
928
+ textAlign: "right",
929
+ whiteSpace: "nowrap"
930
+ },
931
+ children: [
932
+ c.durationMs,
933
+ " ms"
934
+ ]
935
+ }
936
+ )
937
+ ] }, c.name)) })
938
+ ] })
331
939
  ] });
332
940
  }
333
941
 
334
942
  // src/screens/ModulesScreen.tsx
335
943
  var import_react_admin5 = require("@fonderie/react-admin");
336
- var import_jsx_runtime5 = require("react/jsx-runtime");
944
+ var import_jsx_runtime6 = require("react/jsx-runtime");
337
945
  function ModulesScreen({ client }) {
338
946
  const { manifest, isLoading, error } = (0, import_react_admin5.useManifest)(client);
339
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: styles.container, children: [
340
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h1", { style: styles.title, children: "Modules" }),
341
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : manifest ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
342
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { style: styles.muted, children: [
343
- manifest.env,
344
- " \xB7 admin ",
345
- manifest.admin.version,
346
- " \xB7 admin log",
347
- " ",
348
- manifest.admin.log ? "on" : "off",
349
- " \xB7 ",
350
- manifest.routes.length,
351
- " routes"
352
- ] }),
353
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("table", { style: styles.table, children: [
354
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("tr", { children: [
355
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { style: styles.th, children: "Module" }),
356
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { style: styles.th, children: "Version" }),
357
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { style: styles.th, children: "Readiness" }),
358
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { style: styles.th, children: "Describes admin" })
359
- ] }) }),
360
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("tbody", { children: manifest.modules.map((m) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("tr", { children: [
361
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: m.name }),
362
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { style: styles.td, children: m.version ?? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: styles.muted, children: "not reported" }) }),
363
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("td", { style: styles.td, children: [
364
- m.readiness.ok ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: styles.ok, children: "ok" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: styles.bad, children: "error" }),
365
- m.readiness.problems.map((p) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
366
- "div",
367
- {
368
- style: p.severity === "error" ? styles.bad : styles.advice,
369
- children: p.message
947
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: styles.container, children: [
948
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
949
+ PageHeader,
950
+ {
951
+ title: "Modules",
952
+ lead: manifest ? `${manifest.env} \xB7 admin ${manifest.admin.version} \xB7 admin log ${manifest.admin.log ? "on" : "off"} \xB7 ${manifest.routes.length} routes` : "Every installed module, its version and whether it is ready."
953
+ }
954
+ ),
955
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : manifest ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("table", { style: styles.table, children: [
956
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("tr", { children: [
957
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("th", { style: styles.th, children: "Module" }),
958
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("th", { style: styles.th, children: "Version" }),
959
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("th", { style: styles.th, children: "Readiness" }),
960
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("th", { style: styles.th, children: "Admin" })
961
+ ] }) }),
962
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("tbody", { children: manifest.modules.map((m) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("tr", { children: [
963
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("td", { style: { ...styles.td, ...styles.mono, fontWeight: 600 }, children: m.name }),
964
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: m.version ?? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: styles.muted, children: "not reported" }) }),
965
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("td", { style: styles.td, children: [
966
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Pill, { tone: m.readiness.ok ? "ok" : "bad", children: m.readiness.ok ? "ready" : "error" }),
967
+ m.readiness.problems.map((p) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
968
+ "div",
969
+ {
970
+ style: {
971
+ marginTop: 6,
972
+ fontSize: 13,
973
+ color: p.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-text-muted,#5c5c5c)"
370
974
  },
371
- p.message
372
- ))
373
- ] }),
374
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { style: styles.td, children: m.describesAdmin ? "yes" : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: styles.muted, children: "no" }) })
375
- ] }, m.name)) })
376
- ] })
975
+ children: p.message
976
+ },
977
+ p.message
978
+ ))
979
+ ] }),
980
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("td", { style: styles.td, children: m.describesAdmin ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: styles.badge, children: "describes admin" }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: styles.muted, children: "\u2014" }) })
981
+ ] }, m.name)) })
377
982
  ] }) : null
378
983
  ] });
379
984
  }
380
985
 
381
986
  // src/screens/RoutesScreen.tsx
382
987
  var import_react_admin6 = require("@fonderie/react-admin");
383
- var import_jsx_runtime6 = require("react/jsx-runtime");
988
+ var import_react = require("react");
989
+ var import_jsx_runtime7 = require("react/jsx-runtime");
384
990
  function RoutesScreen({ client }) {
385
991
  const { report, isLoading, error } = (0, import_react_admin6.useAdminRoutes)(client);
386
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: styles.container, children: [
387
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { style: styles.title, children: "Routes" }),
388
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("table", { style: styles.table, children: [
389
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("tr", { children: [
390
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("th", { style: styles.th, children: "Method" }),
391
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("th", { style: styles.th, children: "Path" }),
392
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("th", { style: styles.th, children: "Guard" }),
393
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("th", { style: styles.th, children: "Module" })
992
+ const [q, setQ] = (0, import_react.useState)("");
993
+ const needle = q.trim().toLowerCase();
994
+ const routes = (report?.routes ?? []).filter(
995
+ (r) => !needle || `${r.method} ${r.path} ${r.module ?? "application"} ${r.guard}`.toLowerCase().includes(needle)
996
+ );
997
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: styles.container, children: [
998
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
999
+ PageHeader,
1000
+ {
1001
+ title: "Routes",
1002
+ lead: report ? `${report.routes.length} routes exposed by this deployment, with the guard in front of each.` : "Every exposed route, with its guard.",
1003
+ actions: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1004
+ "input",
1005
+ {
1006
+ type: "search",
1007
+ value: q,
1008
+ onChange: (e) => setQ(e.target.value),
1009
+ placeholder: "Filter by path, method, module\u2026",
1010
+ style: { ...styles.input, width: 280 },
1011
+ "aria-label": "Filter routes"
1012
+ }
1013
+ )
1014
+ }
1015
+ ),
1016
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("table", { style: styles.table, children: [
1017
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("tr", { children: [
1018
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("th", { style: { ...styles.th, width: 80 }, children: "Method" }),
1019
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("th", { style: styles.th, children: "Path" }),
1020
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("th", { style: styles.th, children: "Guard" }),
1021
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("th", { style: styles.th, children: "Module" })
1022
+ ] }) }),
1023
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("tbody", { children: [
1024
+ routes.map((r) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("tr", { children: [
1025
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("td", { style: styles.td, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(MethodChip, { method: r.method }) }),
1026
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("td", { style: { ...styles.td, ...styles.mono, wordBreak: "break-all" }, children: r.path }),
1027
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("td", { style: styles.td, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1028
+ Pill,
1029
+ {
1030
+ tone: r.guard === "admin" ? "ok" : r.guard === "probe" ? "neutral" : "info",
1031
+ dot: false,
1032
+ children: r.guard
1033
+ }
1034
+ ) }),
1035
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: r.module ?? "application" })
1036
+ ] }, `${r.method} ${r.path}`)),
1037
+ routes.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("tr", { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("td", { style: { ...styles.td, ...styles.muted }, colSpan: 4, children: [
1038
+ "No route matches \u201C",
1039
+ q,
1040
+ "\u201D."
1041
+ ] }) }) : null
1042
+ ] })
1043
+ ] })
1044
+ ] });
1045
+ }
1046
+
1047
+ // src/screens/MigrationsScreen.tsx
1048
+ var import_react_admin7 = require("@fonderie/react-admin");
1049
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1050
+ function why(m, everApplied) {
1051
+ if (m.pending.length === 0) return null;
1052
+ if (m.blockedBy) return `Apply "${m.blockedBy}" first \u2014 it runs before this one and is behind.`;
1053
+ if (everApplied && m.pending.some((p) => p.impact === "destructive"))
1054
+ return "Contains a migration that deletes data. No down-migration brings it back \u2014 apply this one through CI or `npm run migrate`.";
1055
+ return null;
1056
+ }
1057
+ function MigrationsScreen({ client }) {
1058
+ const { report, isLoading, error, refresh, apply } = (0, import_react_admin7.useAdminMigrations)(client);
1059
+ const behind = report?.modules.filter((m) => m.pending.length > 0) ?? [];
1060
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: styles.container, children: [
1061
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1062
+ PageHeader,
1063
+ {
1064
+ title: "Migrations",
1065
+ lead: "Schema changes each module ships, and whether this database has them.",
1066
+ actions: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(RefreshButton, { onClick: () => void refresh(), busy: isLoading })
1067
+ }
1068
+ ),
1069
+ error ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: styles.error, role: "alert", children: error.status === 403 ? "Applying migrations needs a token with the write scope." : error.explanation }) : null,
1070
+ report && !report.everApplied ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: styles.notice, children: "This database has never been migrated \u2014 treating it as a first install, so nothing is held back." }) : null,
1071
+ report && behind.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Empty, { icon: "migrations", title: "Every module is up to date", children: "No pending migrations." }) : null,
1072
+ behind.map((m) => {
1073
+ const blocked = why(m, report?.everApplied ?? false);
1074
+ const files = m.pending.map((p) => p.file);
1075
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("section", { style: { ...styles.card, marginBottom: 16 }, children: [
1076
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("h2", { style: { ...styles.subtitle, marginTop: 0 }, children: [
1077
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.mono, children: m.name }),
1078
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(Pill, { tone: "warn", children: [
1079
+ m.pending.length,
1080
+ " pending"
1081
+ ] })
1082
+ ] }),
1083
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ul", { style: { margin: "0 0 12px", paddingLeft: 18, lineHeight: 1.9 }, children: m.pending.map((p) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("li", { children: [
1084
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("code", { style: styles.code, children: p.file }),
1085
+ " ",
1086
+ p.impact === "destructive" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Pill, { tone: "bad", children: "destructive" }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Pill, { tone: "neutral", children: "additive" }),
1087
+ p.destructive.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ul", { children: p.destructive.map((s) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("code", { children: s.slice(0, 120) }) }, s)) }) : null
1088
+ ] }, p.file)) }),
1089
+ blocked ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: { ...styles.notice, marginBottom: 0 }, children: blocked }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1090
+ "button",
1091
+ {
1092
+ type: "button",
1093
+ style: styles.buttonPrimary,
1094
+ disabled: isLoading || !m.appliable,
1095
+ onClick: () => {
1096
+ if (window.confirm(
1097
+ `Apply ${m.pending.length} migration(s) to "${m.name}"? This changes the database schema.`
1098
+ ))
1099
+ void apply(m.name, files).catch(() => {
1100
+ });
1101
+ },
1102
+ children: [
1103
+ "Apply ",
1104
+ m.pending.length,
1105
+ " migration",
1106
+ m.pending.length === 1 ? "" : "s"
1107
+ ]
1108
+ }
1109
+ )
1110
+ ] }, m.name);
1111
+ })
1112
+ ] });
1113
+ }
1114
+
1115
+ // src/screens/OperatorsScreen.tsx
1116
+ var import_react_admin8 = require("@fonderie/react-admin");
1117
+ var import_react2 = require("react");
1118
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1119
+ var LEVELS = [
1120
+ { label: "Read only", scopes: ["read"], hint: "look around, change nothing" },
1121
+ { label: "Editor", scopes: ["read", "write"], hint: "change config, templates, users" },
1122
+ {
1123
+ label: "Owner",
1124
+ scopes: ["read", "write", "secrets"],
1125
+ hint: "secrets, tokens and operators too"
1126
+ }
1127
+ ];
1128
+ var levelOf = (scopes) => scopes.includes("secrets") ? "Owner" : scopes.includes("write") ? "Editor" : "Read only";
1129
+ function Minted({ link, onClose }) {
1130
+ const [copied, setCopied] = (0, import_react2.useState)(false);
1131
+ const url = `${window.location.origin}${link.url}`;
1132
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1133
+ "div",
1134
+ {
1135
+ style: {
1136
+ ...styles.notice,
1137
+ background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
1138
+ borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1139
+ },
1140
+ children: [
1141
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("strong", { children: [
1142
+ "Send this link to ",
1143
+ link.email,
1144
+ ". It is shown once."
1145
+ ] }),
1146
+ " It works once and expires",
1147
+ " ",
1148
+ new Date(link.expiresAt).toLocaleString(),
1149
+ ".",
1150
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { display: "flex", gap: 8, marginTop: 8, alignItems: "center" }, children: [
1151
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { style: { ...styles.code, flex: 1, padding: "7px 10px", wordBreak: "break-all" }, children: url }),
1152
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1153
+ "button",
1154
+ {
1155
+ type: "button",
1156
+ style: styles.button,
1157
+ onClick: () => {
1158
+ void navigator.clipboard?.writeText(url);
1159
+ setCopied(true);
1160
+ },
1161
+ children: copied ? "Copied" : "Copy"
1162
+ }
1163
+ ),
1164
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", style: styles.buttonGhost, onClick: onClose, "aria-label": "Dismiss", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Icon, { name: "close", size: 14 }) })
1165
+ ] })
1166
+ ]
1167
+ }
1168
+ );
1169
+ }
1170
+ function OperatorsScreen({ client, me }) {
1171
+ const { report, isLoading, error, invite, recover, update, revokeLink } = (0, import_react_admin8.useAdminOperators)(client);
1172
+ const [email, setEmail] = (0, import_react2.useState)("");
1173
+ const [level, setLevel] = (0, import_react2.useState)("Editor");
1174
+ const [minted, setMinted] = (0, import_react2.useState)(null);
1175
+ const scopesFor = (label) => LEVELS.find((l) => l.label === label)?.scopes ?? ["read"];
1176
+ const status = (o) => o.disabledAt ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Pill, { tone: "neutral", children: "disabled" }) : o.locked ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Pill, { tone: "warn", children: "locked" }) : !o.enrolled ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Pill, { tone: "warn", children: "setting up" }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Pill, { tone: "ok", children: "active" });
1177
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: styles.container, children: [
1178
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1179
+ PageHeader,
1180
+ {
1181
+ title: "Operators",
1182
+ lead: "The people who can sign in to this console. Each one uses a password and an authenticator app; there is no sign-up."
1183
+ }
1184
+ ),
1185
+ error ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: styles.error, role: "alert", children: error.status === 403 && error.reason === "FORBIDDEN" ? "Managing operators needs the Owner level." : error.explanation }) : null,
1186
+ minted ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Minted, { link: minted, onClose: () => setMinted(null) }) : null,
1187
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1188
+ "form",
1189
+ {
1190
+ style: {
1191
+ ...styles.card,
1192
+ display: "flex",
1193
+ gap: 8,
1194
+ alignItems: "center",
1195
+ flexWrap: "wrap",
1196
+ marginBottom: 20
1197
+ },
1198
+ onSubmit: (e) => {
1199
+ e.preventDefault();
1200
+ void invite({ email: email.trim(), scopes: scopesFor(level) }).then((l) => {
1201
+ setMinted(l);
1202
+ setEmail("");
1203
+ }).catch(() => {
1204
+ });
1205
+ },
1206
+ children: [
1207
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Icon, { name: "users", size: 16 }),
1208
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { style: { fontSize: 13.5, marginRight: 4 }, children: "Invite" }),
1209
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1210
+ "input",
1211
+ {
1212
+ type: "email",
1213
+ required: true,
1214
+ value: email,
1215
+ onChange: (e) => setEmail(e.target.value),
1216
+ placeholder: "teammate@company.com",
1217
+ style: { ...styles.input, flex: 1, minWidth: 220 },
1218
+ "aria-label": "Email to invite"
1219
+ }
1220
+ ),
1221
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1222
+ "select",
1223
+ {
1224
+ value: level,
1225
+ onChange: (e) => setLevel(e.target.value),
1226
+ style: styles.input,
1227
+ "aria-label": "Access level",
1228
+ children: LEVELS.map((l) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("option", { value: l.label, children: [
1229
+ l.label,
1230
+ " \u2014 ",
1231
+ l.hint
1232
+ ] }, l.label))
1233
+ }
1234
+ ),
1235
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("button", { type: "submit", style: styles.buttonPrimary, disabled: !email.trim(), children: [
1236
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Icon, { name: "plus", size: 14 }),
1237
+ "Create invite link"
1238
+ ] })
1239
+ ]
1240
+ }
1241
+ ),
1242
+ isLoading && !report ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : report && report.operators.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Empty, { icon: "users", title: "No operators yet" }) : report ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("table", { style: styles.table, children: [
1243
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
1244
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Operator" }),
1245
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Access" }),
1246
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Status" }),
1247
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Last sign-in" }),
1248
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th })
394
1249
  ] }) }),
395
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("tbody", { children: report?.routes.map((r) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("tr", { children: [
396
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: r.method }),
397
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: r.path }),
398
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("td", { style: styles.td, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: styles.badge, children: r.guard }) }),
399
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: r.module ?? "application" })
400
- ] }, `${r.method} ${r.path}`)) })
401
- ] })
402
- ] });
403
- }
404
-
405
- // src/screens/MigrationsScreen.tsx
406
- var import_react_admin7 = require("@fonderie/react-admin");
407
- var import_jsx_runtime7 = require("react/jsx-runtime");
408
- function why(m, everApplied) {
409
- if (m.pending.length === 0) return null;
410
- if (m.blockedBy) return `Apply "${m.blockedBy}" first \u2014 it runs before this one and is behind.`;
411
- if (everApplied && m.pending.some((p) => p.impact === "destructive"))
412
- return "Contains a migration that deletes data. No down-migration brings it back \u2014 apply this one through CI or `npm run migrate`.";
413
- return null;
414
- }
415
- function MigrationsScreen({ client }) {
416
- const { report, isLoading, error, refresh, apply } = (0, import_react_admin7.useAdminMigrations)(client);
417
- const behind = report?.modules.filter((m) => m.pending.length > 0) ?? [];
418
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: styles.container, children: [
419
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h1", { style: styles.title, children: "Migrations" }),
420
- error ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { style: styles.error, role: "alert", children: error.status === 403 ? "Applying migrations needs a token with the write scope." : error.explanation }) : null,
421
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { children: [
422
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", style: styles.button, onClick: () => void refresh(), disabled: isLoading, children: isLoading ? "Checking\u2026" : "Refresh" }),
423
- " ",
424
- report && !report.everApplied ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: styles.advice, children: "This database has never been migrated \u2014 treating it as a first install, so nothing is held back." }) : null
425
- ] }),
426
- report && behind.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { style: styles.ok, children: "Every module is up to date." }) : null,
427
- behind.map((m) => {
428
- const blocked = why(m, report?.everApplied ?? false);
429
- const files = m.pending.map((p) => p.file);
430
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { style: { marginBottom: 24 }, children: [
431
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("h2", { style: styles.subtitle, children: [
432
- m.name,
433
- " \u2014 ",
434
- m.pending.length,
435
- " pending"
1250
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("tbody", { children: report.operators.map((o) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
1251
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("td", { style: styles.td, children: [
1252
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { fontWeight: 600 }, children: o.name || o.email }),
1253
+ o.name ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: styles.muted, children: o.email }) : null
436
1254
  ] }),
437
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { children: m.pending.map((p) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("li", { children: [
438
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("code", { children: p.file }),
439
- " ",
440
- p.impact === "destructive" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: styles.bad, children: "destructive" }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: styles.advice, children: "additive" }),
441
- p.destructive.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { children: p.destructive.map((s) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("code", { children: s.slice(0, 120) }) }, s)) }) : null
442
- ] }, p.file)) }),
443
- blocked ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { style: styles.advice, children: blocked }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
444
- "button",
1255
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1256
+ "select",
445
1257
  {
446
- type: "button",
447
- style: styles.button,
448
- disabled: isLoading || !m.appliable,
449
- onClick: () => {
450
- if (window.confirm(
451
- `Apply ${m.pending.length} migration(s) to "${m.name}"? This changes the database schema.`
452
- ))
453
- void apply(m.name, files).catch(() => {
454
- });
455
- },
456
- children: [
457
- "Apply ",
458
- m.pending.length,
459
- " migration",
460
- m.pending.length === 1 ? "" : "s"
461
- ]
1258
+ value: levelOf(o.scopes),
1259
+ onChange: (e) => void update(o.id, { scopes: scopesFor(e.target.value) }).catch(() => {
1260
+ }),
1261
+ style: { ...styles.input, height: 28, fontSize: 12.5 },
1262
+ disabled: o.email === me,
1263
+ "aria-label": `Access level for ${o.email}`,
1264
+ children: LEVELS.map((l) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: l.label, children: l.label }, l.label))
462
1265
  }
463
- )
464
- ] }, m.name);
465
- })
1266
+ ) }),
1267
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("td", { style: styles.td, children: [
1268
+ status(o),
1269
+ o.enrolled && o.backupCodesLeft <= 2 && !o.disabledAt ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { ...styles.muted, marginTop: 4 }, children: [
1270
+ o.backupCodesLeft,
1271
+ " backup code(s) left"
1272
+ ] }) : null
1273
+ ] }),
1274
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: { ...styles.td, ...styles.muted, whiteSpace: "nowrap" }, children: o.lastLoginAt ? new Date(o.lastLoginAt).toLocaleString() : "never" }),
1275
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: { ...styles.td, textAlign: "right", whiteSpace: "nowrap" }, children: o.email === me ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.muted, children: "you" }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
1276
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1277
+ "button",
1278
+ {
1279
+ type: "button",
1280
+ style: { ...styles.button, height: 28, marginRight: 6 },
1281
+ onClick: () => {
1282
+ if (window.confirm(
1283
+ `Create a recovery link for ${o.email}? It signs them out everywhere; the link sets a new password and a new authenticator.`
1284
+ ))
1285
+ void recover(o.id).then(setMinted).catch(() => {
1286
+ });
1287
+ },
1288
+ children: "Recovery link"
1289
+ }
1290
+ ),
1291
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1292
+ "button",
1293
+ {
1294
+ type: "button",
1295
+ style: {
1296
+ ...o.disabledAt ? styles.button : styles.buttonDanger,
1297
+ height: 28
1298
+ },
1299
+ onClick: () => {
1300
+ if (o.disabledAt || window.confirm(`Disable ${o.email}? They are signed out immediately.`))
1301
+ void update(o.id, { disabled: !o.disabledAt }).catch(() => {
1302
+ });
1303
+ },
1304
+ children: o.disabledAt ? "Enable" : "Disable"
1305
+ }
1306
+ )
1307
+ ] }) })
1308
+ ] }, o.id)) })
1309
+ ] }) : null,
1310
+ report && report.links.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
1311
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h2", { style: styles.subtitle, children: "Pending links" }),
1312
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { style: styles.list, children: report.links.map((l) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1313
+ "li",
1314
+ {
1315
+ style: { ...styles.row, display: "flex", gap: 12, alignItems: "center" },
1316
+ children: [
1317
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Pill, { tone: l.kind === "invite" ? "info" : "warn", dot: false, children: l.kind }),
1318
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: { flex: 1 }, children: [
1319
+ l.email,
1320
+ l.kind === "invite" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: styles.muted, children: [
1321
+ " \xB7 ",
1322
+ levelOf(l.scopes)
1323
+ ] }) : null
1324
+ ] }),
1325
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: styles.muted, children: [
1326
+ "expires ",
1327
+ new Date(l.expiresAt).toLocaleString()
1328
+ ] }),
1329
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1330
+ "button",
1331
+ {
1332
+ type: "button",
1333
+ style: { ...styles.buttonDanger, height: 28 },
1334
+ onClick: () => void revokeLink(l.id).catch(() => {
1335
+ }),
1336
+ children: "Revoke"
1337
+ }
1338
+ )
1339
+ ]
1340
+ },
1341
+ l.id
1342
+ )) })
1343
+ ] }) : null
466
1344
  ] });
467
1345
  }
468
1346
 
469
1347
  // src/screens/TokensScreen.tsx
470
- var import_react_admin8 = require("@fonderie/react-admin");
471
- var import_react = require("react");
472
- var import_jsx_runtime8 = require("react/jsx-runtime");
1348
+ var import_react_admin9 = require("@fonderie/react-admin");
1349
+ var import_react3 = require("react");
1350
+ var import_jsx_runtime10 = require("react/jsx-runtime");
473
1351
  var ALL = ["read", "write", "secrets"];
474
1352
  function TokensScreen({ client }) {
475
- const { report, isLoading, error, issue, revoke } = (0, import_react_admin8.useAdminTokens)(client);
476
- const [name, setName] = (0, import_react.useState)("");
477
- const [scopes, setScopes] = (0, import_react.useState)(["read"]);
478
- const [days, setDays] = (0, import_react.useState)("");
479
- const [minted, setMinted] = (0, import_react.useState)(null);
1353
+ const { report, isLoading, error, issue, revoke } = (0, import_react_admin9.useAdminTokens)(client);
1354
+ const [name, setName] = (0, import_react3.useState)("");
1355
+ const [scopes, setScopes] = (0, import_react3.useState)(["read"]);
1356
+ const [days, setDays] = (0, import_react3.useState)("");
1357
+ const [minted, setMinted] = (0, import_react3.useState)(null);
480
1358
  const toggle = (s) => setScopes((prev) => prev.includes(s) ? prev.filter((x) => x !== s) : [...prev, s]);
481
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: styles.container, children: [
482
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h1", { style: styles.title, children: "Access" }),
483
- error ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: styles.error, role: "alert", children: error.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.explanation }) : null,
484
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h2", { style: styles.subtitle, children: "Root token" }),
485
- report ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { children: [
486
- report.admin.ok ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.ok, children: "strong" }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.bad, children: "weak" }),
487
- report.admin.problems.map((p) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: styles.bad, children: p.message }, p.message))
1359
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: styles.container, children: [
1360
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1361
+ PageHeader,
1362
+ {
1363
+ title: "Tokens",
1364
+ lead: "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."
1365
+ }
1366
+ ),
1367
+ error ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: styles.error, role: "alert", children: error.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.explanation }) : null,
1368
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h2", { style: { ...styles.subtitle, marginTop: 0 }, children: "Root token" }),
1369
+ report ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { ...styles.card, display: "flex", gap: 12, alignItems: "flex-start" }, children: [
1370
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Icon, { name: "lock", size: 18, style: { marginTop: 2 } }),
1371
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { children: [
1372
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Pill, { tone: report.admin.ok ? "ok" : "bad", children: report.admin.ok ? "strong" : "weak" }),
1373
+ report.admin.problems.map((p) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1374
+ "div",
1375
+ {
1376
+ style: { marginTop: 6, color: "var(--fonderie-danger,#e00)", fontSize: 13 },
1377
+ children: p.message
1378
+ },
1379
+ p.message
1380
+ ))
1381
+ ] })
488
1382
  ] }) : null,
489
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h2", { style: styles.subtitle, children: "Issued tokens" }),
490
- report?.issued === null ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: styles.muted, children: "Issuing is off \u2014 give AdminModule a store and run its migrations." }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
491
- minted ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { style: styles.ok, children: [
492
- "Copy this now \u2014 it is never shown again.",
493
- " ",
494
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.mono, children: minted.token }),
495
- " (",
496
- minted.name,
497
- ")"
498
- ] }) : null,
499
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1383
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h2", { style: styles.subtitle, children: "Issued tokens" }),
1384
+ report?.issued === null ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: styles.muted, children: "Issuing is off \u2014 give AdminModule a store and run its migrations." }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1385
+ minted ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1386
+ "div",
1387
+ {
1388
+ style: {
1389
+ ...styles.notice,
1390
+ background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
1391
+ borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1392
+ },
1393
+ children: [
1394
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: "Copy this now \u2014 it is never shown again." }),
1395
+ " (",
1396
+ minted.name,
1397
+ ")",
1398
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1399
+ "div",
1400
+ {
1401
+ style: {
1402
+ ...styles.code,
1403
+ display: "block",
1404
+ marginTop: 8,
1405
+ padding: "8px 10px",
1406
+ wordBreak: "break-all"
1407
+ },
1408
+ children: minted.token
1409
+ }
1410
+ )
1411
+ ]
1412
+ }
1413
+ ) : null,
1414
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
500
1415
  "form",
501
1416
  {
502
1417
  style: styles.toolbar,
@@ -515,70 +1430,90 @@ function TokensScreen({ client }) {
515
1430
  });
516
1431
  },
517
1432
  children: [
518
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1433
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
519
1434
  "input",
520
1435
  {
521
1436
  value: name,
522
1437
  onChange: (e) => setName(e.target.value),
523
1438
  placeholder: "name",
524
- style: { ...styles.button, cursor: "text" },
1439
+ style: styles.input,
525
1440
  "aria-label": "Name"
526
1441
  }
527
1442
  ),
528
- ALL.map((s) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { ...styles.badge, cursor: "pointer" }, children: [
529
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("input", { type: "checkbox", checked: scopes.includes(s), onChange: () => toggle(s) }),
530
- " ",
1443
+ ALL.map((s) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1444
+ "label",
1445
+ {
1446
+ style: {
1447
+ ...styles.badge,
1448
+ cursor: "pointer",
1449
+ height: 32,
1450
+ boxSizing: "border-box",
1451
+ padding: "0 10px",
1452
+ fontSize: 13
1453
+ },
1454
+ children: [
1455
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("input", { type: "checkbox", checked: scopes.includes(s), onChange: () => toggle(s) }),
1456
+ " ",
1457
+ s
1458
+ ]
1459
+ },
531
1460
  s
532
- ] }, s)),
533
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1461
+ )),
1462
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
534
1463
  "input",
535
1464
  {
536
1465
  value: days,
537
1466
  onChange: (e) => setDays(e.target.value),
538
1467
  placeholder: "days (optional)",
539
- style: { ...styles.button, cursor: "text", width: 140 },
1468
+ style: { ...styles.input, width: 140 },
540
1469
  "aria-label": "Days"
541
1470
  }
542
1471
  ),
543
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1472
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
544
1473
  "button",
545
1474
  {
546
1475
  type: "submit",
547
- style: styles.button,
1476
+ style: styles.buttonPrimary,
548
1477
  disabled: !name.trim() || scopes.length === 0 || isLoading,
549
- children: "Issue"
1478
+ children: [
1479
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Icon, { name: "plus", size: 14 }),
1480
+ "Issue token"
1481
+ ]
550
1482
  }
551
1483
  )
552
1484
  ]
553
1485
  }
554
1486
  ),
555
- report?.issued && report.issued.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("table", { style: styles.table, children: [
556
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("tr", { children: [
557
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { style: styles.th, children: "Name" }),
558
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { style: styles.th, children: "Scopes" }),
559
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { style: styles.th, children: "Created" }),
560
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { style: styles.th, children: "Expires" }),
561
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { style: styles.th, children: "Last used" }),
562
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { style: styles.th })
1487
+ report?.issued && report.issued.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("table", { style: styles.table, children: [
1488
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("tr", { children: [
1489
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Name" }),
1490
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Scopes" }),
1491
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Created" }),
1492
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Expires" }),
1493
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Last used" }),
1494
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th })
563
1495
  ] }) }),
564
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("tbody", { children: report.issued.map((t2) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("tr", { children: [
565
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("td", { style: styles.td, children: [
1496
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("tbody", { children: report.issued.map((t2) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("tr", { children: [
1497
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("td", { style: styles.td, children: [
566
1498
  t2.name,
567
- t2.revokedAt ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.badge, children: " revoked" }) : null
1499
+ t2.revokedAt ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1500
+ " ",
1501
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Pill, { tone: "neutral", children: "revoked" })
1502
+ ] }) : null
568
1503
  ] }),
569
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: t2.scopes.join(", ") }),
570
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("td", { style: { ...styles.td, ...styles.muted }, children: [
1504
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: t2.scopes.join(", ") }),
1505
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("td", { style: { ...styles.td, ...styles.muted }, children: [
571
1506
  new Date(t2.createdAt).toLocaleDateString(),
572
1507
  " \xB7 ",
573
1508
  t2.createdBy
574
1509
  ] }),
575
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: t2.expiresAt ? new Date(t2.expiresAt).toLocaleDateString() : "never" }),
576
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: t2.lastUsedAt ? new Date(t2.lastUsedAt).toLocaleString() : "never" }),
577
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { style: styles.td, children: t2.revokedAt ? null : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1510
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: t2.expiresAt ? new Date(t2.expiresAt).toLocaleDateString() : "never" }),
1511
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: t2.lastUsedAt ? new Date(t2.lastUsedAt).toLocaleString() : "never" }),
1512
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: styles.td, children: t2.revokedAt ? null : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
578
1513
  "button",
579
1514
  {
580
1515
  type: "button",
581
- style: styles.button,
1516
+ style: styles.buttonDanger,
582
1517
  onClick: () => {
583
1518
  if (window.confirm(
584
1519
  `Revoke "${t2.name}"? Anything using it stops working immediately.`
@@ -590,41 +1525,47 @@ function TokensScreen({ client }) {
590
1525
  }
591
1526
  ) })
592
1527
  ] }, t2.id)) })
593
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: styles.muted, children: "None issued." })
1528
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Empty, { icon: "tokens", title: "No scoped tokens yet", children: "Issue a read-only token for a dashboard or a teammate instead of sharing the root." })
594
1529
  ] }),
595
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h2", { style: styles.subtitle, children: "Legacy per-brick tokens" }),
596
- report && report.legacy.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: styles.muted, children: "None \u2014 every brick's admin surface goes through this token." }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ul", { style: styles.list, children: report?.legacy.map((l) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("li", { style: styles.row, children: [
597
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.mono, children: l.module }),
1530
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h2", { style: styles.subtitle, children: "Legacy per-brick tokens" }),
1531
+ report && report.legacy.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: styles.muted, children: "None \u2014 every brick's admin surface goes through this token." }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { style: styles.list, children: report?.legacy.map((l) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("li", { style: styles.row, children: [
1532
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: styles.mono, children: l.module }),
598
1533
  " ",
599
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: styles.muted, children: "still registers its standalone routes with its own token (deprecated)" })
1534
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: styles.muted, children: "still registers its standalone routes with its own token (deprecated)" })
600
1535
  ] }, l.module)) })
601
1536
  ] });
602
1537
  }
603
1538
 
604
1539
  // src/screens/UsersScreen.tsx
605
1540
  var import_client = require("@fonderie/client");
606
- var import_react_admin9 = require("@fonderie/react-admin");
607
- var import_react2 = require("react");
608
- var import_jsx_runtime9 = require("react/jsx-runtime");
1541
+ var import_react_admin10 = require("@fonderie/react-admin");
1542
+ var import_react4 = require("react");
1543
+ var import_jsx_runtime11 = require("react/jsx-runtime");
609
1544
  function UsersScreen({ client, pageSize = 50 }) {
610
- const [input, setInput] = (0, import_react2.useState)("");
611
- const [selected, setSelected] = (0, import_react2.useState)(null);
612
- const list = (0, import_react_admin9.useAdminUsers)(client, { limit: pageSize });
613
- const { user, isLoading, error, suspend, unsuspend, revokeSessions } = (0, import_react_admin9.useAdminUser)(
1545
+ const [input, setInput] = (0, import_react4.useState)("");
1546
+ const [selected, setSelected] = (0, import_react4.useState)(null);
1547
+ const list = (0, import_react_admin10.useAdminUsers)(client, { limit: pageSize });
1548
+ const { user, isLoading, error, suspend, unsuspend, revokeSessions } = (0, import_react_admin10.useAdminUser)(
614
1549
  client,
615
1550
  selected ?? {}
616
1551
  );
617
1552
  const userId = user?.id ?? null;
618
- const sessions = (0, import_react_admin9.useAdminUserSessions)(client, userId);
619
- const history = (0, import_react_admin9.useAdminLoginHistory)(client, userId, { limit: 20 });
620
- const yesNo = (v) => v ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.ok, children: "yes" }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.muted, children: "no" });
1553
+ const sessions = (0, import_react_admin10.useAdminUserSessions)(client, userId);
1554
+ const history = (0, import_react_admin10.useAdminLoginHistory)(client, userId, { limit: 20 });
1555
+ const yesNo = (v) => v ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Pill, { tone: "ok", children: "yes" }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Pill, { tone: "neutral", children: "no" });
621
1556
  const clear = () => {
622
1557
  setSelected(null);
623
1558
  setInput("");
624
1559
  };
625
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: styles.container, children: [
626
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h1", { style: styles.title, children: "Users" }),
627
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1560
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: styles.container, children: [
1561
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1562
+ PageHeader,
1563
+ {
1564
+ title: "Users",
1565
+ lead: "Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
1566
+ }
1567
+ ),
1568
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
628
1569
  "form",
629
1570
  {
630
1571
  style: styles.toolbar,
@@ -634,19 +1575,25 @@ function UsersScreen({ client, pageSize = 50 }) {
634
1575
  setSelected(email ? { email } : null);
635
1576
  },
636
1577
  children: [
637
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1578
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
638
1579
  "input",
639
1580
  {
640
1581
  type: "email",
641
1582
  value: input,
642
1583
  onChange: (e) => setInput(e.target.value),
643
1584
  placeholder: "email address",
644
- style: { ...styles.button, cursor: "text", minWidth: 280 },
1585
+ style: { ...styles.input, minWidth: 280 },
645
1586
  "aria-label": "Email"
646
1587
  }
647
1588
  ),
648
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "submit", style: styles.button, disabled: isLoading, children: "Look up" }),
649
- selected ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", style: styles.button, onClick: clear, children: "\u2190 All users" }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1589
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("button", { type: "submit", style: styles.buttonPrimary, disabled: isLoading, children: [
1590
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Icon, { name: "search", size: 14 }),
1591
+ "Look up"
1592
+ ] }),
1593
+ selected ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("button", { type: "button", style: styles.buttonGhost, onClick: clear, children: [
1594
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Icon, { name: "back", size: 14 }),
1595
+ "All users"
1596
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
650
1597
  "button",
651
1598
  {
652
1599
  type: "button",
@@ -659,37 +1606,37 @@ function UsersScreen({ client, pageSize = 50 }) {
659
1606
  ]
660
1607
  }
661
1608
  ),
662
- error ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: styles.error, role: "alert", children: error.status === 404 ? "No user with that email." : error.explanation }) : null,
663
- !selected ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
664
- list.error ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: styles.error, role: "alert", children: list.error.explanation }) : null,
665
- list.users.length === 0 && !list.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: styles.muted, children: "No users yet." }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("table", { style: styles.table, children: [
666
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
667
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Email" }),
668
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Name" }),
669
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Created" }),
670
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("th", { style: styles.th, children: "Status" })
1609
+ error ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.error, role: "alert", children: error.status === 404 ? "No user with that email." : error.explanation }) : null,
1610
+ !selected ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
1611
+ list.error ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.error, role: "alert", children: list.error.explanation }) : null,
1612
+ list.users.length === 0 && !list.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Empty, { icon: "users", title: "No users yet", children: "Sign-ups appear here as they happen." }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("table", { style: styles.table, children: [
1613
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1614
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { style: styles.th, children: "Email" }),
1615
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { style: styles.th, children: "Name" }),
1616
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { style: styles.th, children: "Created" }),
1617
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { style: styles.th, children: "Status" })
671
1618
  ] }) }),
672
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("tbody", { children: list.users.map((u) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
673
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1619
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("tbody", { children: list.users.map((u) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1620
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
674
1621
  "button",
675
1622
  {
676
1623
  type: "button",
677
- style: { ...styles.navItem, padding: 0 },
1624
+ style: styles.link,
678
1625
  onClick: () => setSelected({ id: u.id }),
679
1626
  children: u.email
680
1627
  }
681
1628
  ) }),
682
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: `${u.firstName ?? ""} ${u.lastName ?? ""}`.trim() || /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.muted, children: "\u2014" }) }),
683
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: new Date(u.createdAt).toLocaleDateString() }),
684
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("td", { style: styles.td, children: [
685
- u.suspended ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.badge, children: "suspended" }) : null,
686
- u.deletedAt ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.badge, children: "deleted" }) : null,
687
- !u.suspended && !u.deletedAt ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.muted, children: "active" }) : null
1629
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: `${u.firstName ?? ""} ${u.lastName ?? ""}`.trim() || /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: styles.muted, children: "\u2014" }) }),
1630
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: new Date(u.createdAt).toLocaleDateString() }),
1631
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("td", { style: styles.td, children: [
1632
+ u.suspended ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Pill, { tone: "warn", children: "suspended" }) : null,
1633
+ u.deletedAt ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Pill, { tone: "neutral", children: "deleted" }) : null,
1634
+ !u.suspended && !u.deletedAt ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Pill, { tone: "ok", children: "active" }) : null
688
1635
  ] })
689
1636
  ] }, u.id)) })
690
1637
  ] }),
691
- list.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : null,
692
- list.hasMore && !list.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1638
+ list.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : null,
1639
+ list.hasMore && !list.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
693
1640
  "button",
694
1641
  {
695
1642
  type: "button",
@@ -699,48 +1646,48 @@ function UsersScreen({ client, pageSize = 50 }) {
699
1646
  }
700
1647
  ) : null
701
1648
  ] }) : null,
702
- user ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
703
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("h2", { style: styles.subtitle, children: [
1649
+ user ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
1650
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("h2", { style: styles.subtitle, children: [
704
1651
  user.firstName || user.lastName ? `${user.firstName} ${user.lastName}`.trim() : user.email,
705
1652
  " ",
706
- user.suspended ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.badge, children: "suspended" }) : null,
707
- user.deletedAt ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.badge, children: "deleted" }) : null
1653
+ user.suspended ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Pill, { tone: "warn", children: "suspended" }) : null,
1654
+ user.deletedAt ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Pill, { tone: "neutral", children: "deleted" }) : null
708
1655
  ] }),
709
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("table", { style: styles.table, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tbody", { children: [
710
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
711
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: "id" }),
712
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: user.id })
1656
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("table", { style: styles.table, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tbody", { children: [
1657
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1658
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: "id" }),
1659
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: user.id })
713
1660
  ] }),
714
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
715
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: "email" }),
716
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("td", { style: styles.td, children: [
1661
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1662
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: "email" }),
1663
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("td", { style: styles.td, children: [
717
1664
  user.email,
718
1665
  " \xB7 verified ",
719
1666
  yesNo(user.isEmailVerified)
720
1667
  ] })
721
1668
  ] }),
722
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
723
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: "MFA" }),
724
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: yesNo(user.mfaEnabled) })
1669
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1670
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: "MFA" }),
1671
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: yesNo(user.mfaEnabled) })
725
1672
  ] }),
726
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
727
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: "provider" }),
728
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("td", { style: styles.td, children: [
1673
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1674
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: "provider" }),
1675
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("td", { style: styles.td, children: [
729
1676
  user.provider || "password",
730
1677
  user.hasPassword ? "" : " \xB7 no password set"
731
1678
  ] })
732
1679
  ] }),
733
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
734
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: "last login" }),
735
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: user.lastLogin ? new Date(user.lastLogin).toLocaleString() : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.muted, children: "never" }) })
1680
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1681
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: "last login" }),
1682
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: user.lastLogin ? new Date(user.lastLogin).toLocaleString() : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: styles.muted, children: "never" }) })
736
1683
  ] }),
737
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("tr", { children: [
738
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: "created" }),
739
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("td", { style: styles.td, children: new Date(user.createdAt).toLocaleString() })
1684
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1685
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: "created" }),
1686
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: new Date(user.createdAt).toLocaleString() })
740
1687
  ] })
741
1688
  ] }) }),
742
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { ...styles.toolbar, marginTop: 12 }, children: [
743
- user.suspended ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1689
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { ...styles.toolbar, marginTop: 12 }, children: [
1690
+ user.suspended ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
744
1691
  "button",
745
1692
  {
746
1693
  type: "button",
@@ -749,17 +1696,17 @@ function UsersScreen({ client, pageSize = 50 }) {
749
1696
  disabled: isLoading,
750
1697
  children: "Unsuspend"
751
1698
  }
752
- ) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1699
+ ) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
753
1700
  "button",
754
1701
  {
755
1702
  type: "button",
756
- style: styles.button,
1703
+ style: styles.buttonDanger,
757
1704
  onClick: () => void suspend(),
758
1705
  disabled: isLoading,
759
1706
  children: "Suspend"
760
1707
  }
761
1708
  ),
762
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1709
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
763
1710
  "button",
764
1711
  {
765
1712
  type: "button",
@@ -770,38 +1717,38 @@ function UsersScreen({ client, pageSize = 50 }) {
770
1717
  }
771
1718
  )
772
1719
  ] }),
773
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h2", { style: styles.subtitle, children: "Live sessions" }),
774
- sessions.sessions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: styles.muted, children: "None." }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { style: styles.list, children: sessions.sessions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("li", { style: styles.row, children: [
775
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.mono, children: s.ipAddress ?? "\u2014" }),
1720
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h2", { style: styles.subtitle, children: "Live sessions" }),
1721
+ sessions.sessions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.muted, children: "None." }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ul", { style: styles.list, children: sessions.sessions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("li", { style: styles.row, children: [
1722
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: styles.mono, children: s.ipAddress ?? "\u2014" }),
776
1723
  " ",
777
- (0, import_client.describeLocation)(s.location) ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: styles.muted, children: [
1724
+ (0, import_client.describeLocation)(s.location) ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { style: styles.muted, children: [
778
1725
  (0, import_client.describeLocation)(s.location),
779
1726
  " "
780
1727
  ] }) : null,
781
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.muted, children: s.userAgent ?? "" }),
1728
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: styles.muted, children: s.userAgent ?? "" }),
782
1729
  " ",
783
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: styles.muted, children: [
1730
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { style: styles.muted, children: [
784
1731
  "since ",
785
1732
  new Date(s.createdAt).toLocaleString()
786
1733
  ] })
787
1734
  ] }, s.id)) }),
788
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h2", { style: styles.subtitle, children: "Recent sign-ins" }),
789
- history.events.length === 0 && !history.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: styles.muted, children: "None recorded." }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { style: styles.list, children: history.events.map((e) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("li", { style: styles.row, children: [
790
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: e.outcome === "success" ? styles.ok : styles.bad, children: e.outcome }),
1735
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h2", { style: styles.subtitle, children: "Recent sign-ins" }),
1736
+ history.events.length === 0 && !history.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.muted, children: "None recorded." }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ul", { style: styles.list, children: history.events.map((e) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("li", { style: styles.row, children: [
1737
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Pill, { tone: e.outcome === "success" ? "ok" : "bad", children: e.outcome }),
791
1738
  " ",
792
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.muted, children: e.method }),
1739
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: styles.muted, children: e.method }),
793
1740
  " ",
794
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.mono, children: e.ipAddress ?? "\u2014" }),
1741
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: styles.mono, children: e.ipAddress ?? "\u2014" }),
795
1742
  " ",
796
- (0, import_client.describeLocation)(e.location) ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.muted, children: (0, import_client.describeLocation)(e.location) }) : null,
797
- e.location?.proxy || e.location?.hosting ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: styles.bad, children: [
1743
+ (0, import_client.describeLocation)(e.location) ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: styles.muted, children: (0, import_client.describeLocation)(e.location) }) : null,
1744
+ e.location?.proxy || e.location?.hosting ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
798
1745
  " ",
799
- e.location.proxy ? "proxy/VPN" : "hosting"
1746
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Pill, { tone: "warn", children: e.location.proxy ? "proxy/VPN" : "hosting" })
800
1747
  ] }) : null,
801
1748
  " ",
802
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: styles.muted, children: new Date(e.createdAt).toLocaleString() })
1749
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: styles.muted, children: new Date(e.createdAt).toLocaleString() })
803
1750
  ] }, e.id)) }),
804
- history.hasMore && !history.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1751
+ history.hasMore && !history.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
805
1752
  "button",
806
1753
  {
807
1754
  type: "button",
@@ -815,60 +1762,68 @@ function UsersScreen({ client, pageSize = 50 }) {
815
1762
  }
816
1763
 
817
1764
  // src/screens/CatalogScreen.tsx
818
- var import_react_admin10 = require("@fonderie/react-admin");
819
- var import_jsx_runtime10 = require("react/jsx-runtime");
1765
+ var import_react_admin11 = require("@fonderie/react-admin");
1766
+ var import_jsx_runtime12 = require("react/jsx-runtime");
820
1767
  function CatalogScreen({ client }) {
821
- const { catalog, isLoading, error, refresh, deletePlan } = (0, import_react_admin10.useAdminCatalog)(client);
1768
+ const { catalog, isLoading, error, refresh, deletePlan } = (0, import_react_admin11.useAdminCatalog)(client);
822
1769
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
823
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: styles.container, children: [
824
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: styles.toolbar, children: [
825
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h1", { style: { ...styles.title, marginBottom: 0 }, children: "Catalog" }),
826
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
827
- "button",
1770
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: styles.container, children: [
1771
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1772
+ PageHeader,
1773
+ {
1774
+ title: "Catalog",
1775
+ lead: "What you sell: plans as configured in code, and as stored in the database, side by side.",
1776
+ actions: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(RefreshButton, { onClick: () => void refresh(), busy: isLoading })
1777
+ }
1778
+ ),
1779
+ error ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
1780
+ isLoading && !catalog ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : null,
1781
+ catalog ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
1782
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h2", { style: { ...styles.subtitle, marginTop: 0 }, children: "Configured (code)" }),
1783
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1784
+ "pre",
828
1785
  {
829
- type: "button",
830
- style: styles.button,
831
- onClick: () => void refresh(),
832
- disabled: isLoading,
833
- children: "Refresh"
1786
+ style: {
1787
+ ...styles.card,
1788
+ ...styles.mono,
1789
+ margin: 0,
1790
+ overflowX: "auto",
1791
+ maxHeight: 360,
1792
+ lineHeight: 1.6
1793
+ },
1794
+ children: JSON.stringify(catalog.configured, null, 2)
834
1795
  }
835
- )
836
- ] }),
837
- error ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
838
- isLoading && !catalog ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : null,
839
- catalog ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
840
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h2", { style: styles.subtitle, children: "Configured (code)" }),
841
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("pre", { style: { ...styles.mono, background: "var(--fonderie-surface-alt,#fafafa)", padding: 12, overflowX: "auto" }, children: JSON.stringify(catalog.configured, null, 2) }),
842
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h2", { style: styles.subtitle, children: "Stored (database)" }),
843
- catalog.stored.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: styles.muted, children: "No stored plans." }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("table", { style: styles.table, children: [
844
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("tr", { children: [
845
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Plan" }),
846
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Tier" }),
847
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Seats" }),
848
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Monthly" }),
849
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Yearly" }),
850
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th, children: "Trial" }),
851
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("th", { style: styles.th })
1796
+ ),
1797
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h2", { style: styles.subtitle, children: "Stored (database)" }),
1798
+ catalog.stored.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Empty, { icon: "catalog", title: "No stored plans", children: "Plans are configured in code; nothing has been written to the database." }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("table", { style: styles.table, children: [
1799
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("tr", { children: [
1800
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("th", { style: styles.th, children: "Plan" }),
1801
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("th", { style: styles.th, children: "Tier" }),
1802
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("th", { style: styles.th, children: "Seats" }),
1803
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("th", { style: styles.th, children: "Monthly" }),
1804
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("th", { style: styles.th, children: "Yearly" }),
1805
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("th", { style: styles.th, children: "Trial" }),
1806
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("th", { style: styles.th })
852
1807
  ] }) }),
853
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("tbody", { children: catalog.stored.map((p) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("tr", { children: [
854
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("td", { style: styles.td, children: [
855
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: p.name }),
856
- p.description ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: styles.muted, children: p.description }) : null
1808
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("tbody", { children: catalog.stored.map((p) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("tr", { children: [
1809
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("td", { style: styles.td, children: [
1810
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("strong", { children: p.name }),
1811
+ p.description ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: styles.muted, children: p.description }) : null
857
1812
  ] }),
858
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: styles.td, children: p.tier }),
859
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: styles.td, children: p.seats ?? "\u221E" }),
860
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("td", { style: styles.td, children: [
1813
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("td", { style: styles.td, children: p.tier }),
1814
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("td", { style: styles.td, children: p.seats ?? "\u221E" }),
1815
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("td", { style: styles.td, children: [
861
1816
  money(p.pricing?.monthly),
862
1817
  " ",
863
1818
  p.pricing?.currency ?? ""
864
1819
  ] }),
865
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: styles.td, children: money(p.pricing?.yearly) }),
866
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: styles.td, children: p.trialDays ? `${p.trialDays} d` : "\u2014" }),
867
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("td", { style: styles.td, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1820
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("td", { style: styles.td, children: money(p.pricing?.yearly) }),
1821
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("td", { style: styles.td, children: p.trialDays ? `${p.trialDays} d` : "\u2014" }),
1822
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("td", { style: styles.td, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
868
1823
  "button",
869
1824
  {
870
1825
  type: "button",
871
- style: styles.button,
1826
+ style: styles.buttonDanger,
872
1827
  onClick: () => {
873
1828
  if (window.confirm(`Delete stored plan "${p.name}"?`))
874
1829
  void deletePlan(p.id).catch(() => {
@@ -884,27 +1839,33 @@ function CatalogScreen({ client }) {
884
1839
  }
885
1840
 
886
1841
  // src/screens/SubscriberScreen.tsx
887
- var import_react_admin11 = require("@fonderie/react-admin");
888
- var import_react3 = require("react");
889
- var import_jsx_runtime11 = require("react/jsx-runtime");
1842
+ var import_react_admin12 = require("@fonderie/react-admin");
1843
+ var import_react5 = require("react");
1844
+ var import_jsx_runtime13 = require("react/jsx-runtime");
890
1845
  function SubscriberScreen({ client, pageSize = 50 }) {
891
- const [type, setType] = (0, import_react3.useState)("user");
892
- const [idInput, setIdInput] = (0, import_react3.useState)("");
893
- const [subscriber, setSubscriber] = (0, import_react3.useState)(null);
894
- const list = (0, import_react_admin11.useAdminSubscribers)(client, { limit: pageSize });
895
- const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = (0, import_react_admin11.useAdminSubscriber)(client, subscriber, { limit: 20 });
896
- const [amount, setAmount] = (0, import_react3.useState)("");
897
- const [note, setNote] = (0, import_react3.useState)("");
898
- const [granted, setGranted] = (0, import_react3.useState)(null);
1846
+ const [type, setType] = (0, import_react5.useState)("user");
1847
+ const [idInput, setIdInput] = (0, import_react5.useState)("");
1848
+ const [subscriber, setSubscriber] = (0, import_react5.useState)(null);
1849
+ const list = (0, import_react_admin12.useAdminSubscribers)(client, { limit: pageSize });
1850
+ const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = (0, import_react_admin12.useAdminSubscriber)(client, subscriber, { limit: 20 });
1851
+ const [amount, setAmount] = (0, import_react5.useState)("");
1852
+ const [note, setNote] = (0, import_react5.useState)("");
1853
+ const [granted, setGranted] = (0, import_react5.useState)(null);
899
1854
  const open = (t2, id) => {
900
1855
  setGranted(null);
901
1856
  setType(t2);
902
1857
  setIdInput(id);
903
1858
  setSubscriber({ type: t2, id });
904
1859
  };
905
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: styles.container, children: [
906
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h1", { style: styles.title, children: "Subscriber" }),
907
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1860
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: styles.container, children: [
1861
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1862
+ PageHeader,
1863
+ {
1864
+ title: "Subscribers",
1865
+ lead: "Who is subscribed. Open one for their plan, wallet and ledger, and to grant credit."
1866
+ }
1867
+ ),
1868
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
908
1869
  "form",
909
1870
  {
910
1871
  style: styles.toolbar,
@@ -914,31 +1875,34 @@ function SubscriberScreen({ client, pageSize = 50 }) {
914
1875
  setSubscriber(idInput.trim() ? { type, id: idInput.trim() } : null);
915
1876
  },
916
1877
  children: [
917
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1878
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
918
1879
  "select",
919
1880
  {
920
1881
  value: type,
921
1882
  onChange: (e) => setType(e.target.value),
922
- style: styles.button,
1883
+ style: styles.input,
923
1884
  "aria-label": "Type",
924
1885
  children: [
925
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("option", { value: "user", children: "user" }),
926
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("option", { value: "workspace", children: "workspace" })
1886
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("option", { value: "user", children: "user" }),
1887
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("option", { value: "workspace", children: "workspace" })
927
1888
  ]
928
1889
  }
929
1890
  ),
930
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1891
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
931
1892
  "input",
932
1893
  {
933
1894
  value: idInput,
934
1895
  onChange: (e) => setIdInput(e.target.value),
935
1896
  placeholder: "subscriber id",
936
- style: { ...styles.button, cursor: "text", minWidth: 280 },
1897
+ style: { ...styles.input, minWidth: 280 },
937
1898
  "aria-label": "Subscriber id"
938
1899
  }
939
1900
  ),
940
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "submit", style: styles.button, disabled: isLoading, children: "Look up" }),
941
- subscriber ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1901
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("button", { type: "submit", style: styles.buttonPrimary, disabled: isLoading, children: [
1902
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Icon, { name: "search", size: 14 }),
1903
+ "Look up"
1904
+ ] }),
1905
+ subscriber ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
942
1906
  "button",
943
1907
  {
944
1908
  type: "button",
@@ -948,9 +1912,12 @@ function SubscriberScreen({ client, pageSize = 50 }) {
948
1912
  setIdInput("");
949
1913
  setGranted(null);
950
1914
  },
951
- children: "\u2190 All subscribers"
1915
+ children: [
1916
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Icon, { name: "back", size: 14 }),
1917
+ "All subscribers"
1918
+ ]
952
1919
  }
953
- ) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1920
+ ) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
954
1921
  "button",
955
1922
  {
956
1923
  type: "button",
@@ -963,22 +1930,22 @@ function SubscriberScreen({ client, pageSize = 50 }) {
963
1930
  ]
964
1931
  }
965
1932
  ),
966
- error ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
967
- !subscriber ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
968
- list.error ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.error, role: "alert", children: list.error.explanation }) : null,
969
- list.subscriptions.length === 0 && !list.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.muted, children: "No subscribers yet." }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("table", { style: styles.table, children: [
970
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
971
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { style: styles.th, children: "Subscriber" }),
972
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { style: styles.th, children: "Plan" }),
973
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { style: styles.th, children: "Status" }),
974
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { style: styles.th, children: "Renews" })
1933
+ error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
1934
+ !subscriber ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
1935
+ list.error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { style: styles.error, role: "alert", children: list.error.explanation }) : null,
1936
+ list.subscriptions.length === 0 && !list.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Empty, { icon: "subscriber", title: "No subscribers yet", children: "Subscriptions appear here once someone checks out." }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("table", { style: styles.table, children: [
1937
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("tr", { children: [
1938
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("th", { style: styles.th, children: "Subscriber" }),
1939
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("th", { style: styles.th, children: "Plan" }),
1940
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("th", { style: styles.th, children: "Status" }),
1941
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("th", { style: styles.th, children: "Renews" })
975
1942
  ] }) }),
976
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("tbody", { children: list.subscriptions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
977
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1943
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("tbody", { children: list.subscriptions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("tr", { children: [
1944
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("td", { style: styles.td, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
978
1945
  "button",
979
1946
  {
980
1947
  type: "button",
981
- style: { ...styles.navItem, padding: 0, ...styles.mono },
1948
+ style: { ...styles.link, ...styles.mono },
982
1949
  onClick: () => open(s.subscriberType, s.subscriberId),
983
1950
  children: [
984
1951
  s.subscriberType,
@@ -987,20 +1954,29 @@ function SubscriberScreen({ client, pageSize = 50 }) {
987
1954
  ]
988
1955
  }
989
1956
  ) }),
990
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("td", { style: styles.td, children: [
991
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("strong", { children: s.plan }),
1957
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("td", { style: styles.td, children: [
1958
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("strong", { children: s.plan }),
992
1959
  " \xB7 ",
993
1960
  s.interval
994
1961
  ] }),
995
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("td", { style: styles.td, children: [
996
- s.status,
997
- s.cancelAtPeriodEnd ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: styles.badge, children: "cancels" }) : null
1962
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("td", { style: styles.td, children: [
1963
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1964
+ Pill,
1965
+ {
1966
+ tone: s.status === "active" ? "ok" : s.status === "trialing" ? "info" : s.status === "past_due" ? "warn" : "neutral",
1967
+ children: s.status
1968
+ }
1969
+ ),
1970
+ s.cancelAtPeriodEnd ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
1971
+ " ",
1972
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Pill, { tone: "warn", dot: false, children: "cancels" })
1973
+ ] }) : null
998
1974
  ] }),
999
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: styles.muted, children: "\u2014" }) })
1975
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("td", { style: styles.td, children: s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { style: styles.muted, children: "\u2014" }) })
1000
1976
  ] }, s.id)) })
1001
1977
  ] }),
1002
- list.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : null,
1003
- list.hasMore && !list.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1978
+ list.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : null,
1979
+ list.hasMore && !list.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1004
1980
  "button",
1005
1981
  {
1006
1982
  type: "button",
@@ -1010,27 +1986,27 @@ function SubscriberScreen({ client, pageSize = 50 }) {
1010
1986
  }
1011
1987
  ) : null
1012
1988
  ] }) : null,
1013
- subscriber && !isLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
1014
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h2", { style: styles.subtitle, children: "Subscription" }),
1015
- subscription ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("table", { style: styles.table, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tbody", { children: [
1016
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1017
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: "plan" }),
1018
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("td", { style: styles.td, children: [
1019
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("strong", { children: subscription.plan }),
1989
+ subscriber && !isLoading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
1990
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("h2", { style: styles.subtitle, children: "Subscription" }),
1991
+ subscription ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("table", { style: styles.table, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("tbody", { children: [
1992
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("tr", { children: [
1993
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("td", { style: styles.td, children: "plan" }),
1994
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("td", { style: styles.td, children: [
1995
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("strong", { children: subscription.plan }),
1020
1996
  " \xB7 ",
1021
1997
  subscription.interval
1022
1998
  ] })
1023
1999
  ] }),
1024
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1025
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: "status" }),
1026
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("td", { style: styles.td, children: [
2000
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("tr", { children: [
2001
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("td", { style: styles.td, children: "status" }),
2002
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("td", { style: styles.td, children: [
1027
2003
  subscription.status,
1028
2004
  subscription.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""
1029
2005
  ] })
1030
2006
  ] }),
1031
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1032
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: "period" }),
1033
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("td", { style: styles.td, children: [
2007
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("tr", { children: [
2008
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("td", { style: styles.td, children: "period" }),
2009
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("td", { style: styles.td, children: [
1034
2010
  subscription.currentPeriodStart ? new Date(subscription.currentPeriodStart).toLocaleDateString() : "\u2014",
1035
2011
  " ",
1036
2012
  "\u2192",
@@ -1038,28 +2014,49 @@ function SubscriberScreen({ client, pageSize = 50 }) {
1038
2014
  subscription.currentPeriodEnd ? new Date(subscription.currentPeriodEnd).toLocaleDateString() : "\u2014"
1039
2015
  ] })
1040
2016
  ] }),
1041
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1042
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: "provider" }),
1043
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: subscription.providerSubscriptionId ?? "\u2014" })
2017
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("tr", { children: [
2018
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("td", { style: styles.td, children: "provider" }),
2019
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: subscription.providerSubscriptionId ?? "\u2014" })
1044
2020
  ] })
1045
- ] }) }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.muted, children: "No subscription." }),
1046
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h2", { style: styles.subtitle, children: "Wallet" }),
1047
- wallet ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
1048
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { children: [
1049
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("strong", { children: wallet.balance }),
1050
- " ",
1051
- wallet.currency,
1052
- " ",
1053
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { style: styles.muted, children: [
1054
- "(granted ",
1055
- wallet.granted ?? "0",
1056
- " \xB7 purchased ",
1057
- wallet.purchased ?? "0",
1058
- ") \xB7 minor units, precision ",
1059
- wallet.precision
1060
- ] })
1061
- ] }),
1062
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2021
+ ] }) }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { style: styles.muted, children: "No subscription." }),
2022
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("h2", { style: styles.subtitle, children: "Wallet" }),
2023
+ wallet ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
2024
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2025
+ "p",
2026
+ {
2027
+ style: {
2028
+ ...styles.card,
2029
+ display: "flex",
2030
+ alignItems: "baseline",
2031
+ gap: 8,
2032
+ flexWrap: "wrap"
2033
+ },
2034
+ children: [
2035
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2036
+ "strong",
2037
+ {
2038
+ style: {
2039
+ fontSize: 22,
2040
+ letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
2041
+ },
2042
+ children: wallet.balance
2043
+ }
2044
+ ),
2045
+ " ",
2046
+ wallet.currency,
2047
+ " ",
2048
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { style: styles.muted, children: [
2049
+ "(granted ",
2050
+ wallet.granted ?? "0",
2051
+ " \xB7 purchased ",
2052
+ wallet.purchased ?? "0",
2053
+ ") \xB7 minor units, precision ",
2054
+ wallet.precision
2055
+ ] })
2056
+ ]
2057
+ }
2058
+ ),
2059
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1063
2060
  "form",
1064
2061
  {
1065
2062
  style: styles.toolbar,
@@ -1078,47 +2075,58 @@ function SubscriberScreen({ client, pageSize = 50 }) {
1078
2075
  });
1079
2076
  },
1080
2077
  children: [
1081
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2078
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1082
2079
  "input",
1083
2080
  {
1084
2081
  value: amount,
1085
2082
  onChange: (e) => setAmount(e.target.value),
1086
2083
  placeholder: "amount (minor units)",
1087
- style: { ...styles.button, cursor: "text" },
2084
+ style: styles.input,
1088
2085
  "aria-label": "Amount"
1089
2086
  }
1090
2087
  ),
1091
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2088
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1092
2089
  "input",
1093
2090
  {
1094
2091
  value: note,
1095
2092
  onChange: (e) => setNote(e.target.value),
1096
2093
  placeholder: "reason (optional)",
1097
- style: { ...styles.button, cursor: "text", minWidth: 220 },
2094
+ style: { ...styles.input, minWidth: 220 },
1098
2095
  "aria-label": "Reason"
1099
2096
  }
1100
2097
  ),
1101
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "submit", style: styles.button, disabled: !/^\d+$/.test(amount.trim()), children: "Grant" }),
1102
- granted ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: styles.ok, children: granted }) : null
2098
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2099
+ "button",
2100
+ {
2101
+ type: "submit",
2102
+ style: styles.buttonPrimary,
2103
+ disabled: !/^\d+$/.test(amount.trim()),
2104
+ children: [
2105
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Icon, { name: "plus", size: 14 }),
2106
+ "Grant"
2107
+ ]
2108
+ }
2109
+ ),
2110
+ granted ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { style: styles.ok, children: granted }) : null
1103
2111
  ]
1104
2112
  }
1105
2113
  ),
1106
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h2", { style: styles.subtitle, children: "Ledger" }),
1107
- ledger.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.muted, children: "Nothing moved yet." }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("table", { style: styles.table, children: [
1108
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1109
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { style: styles.th, children: "When" }),
1110
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { style: styles.th, children: "Kind" }),
1111
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { style: styles.th, children: "Amount" }),
1112
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { style: styles.th, children: "Note" })
2114
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("h2", { style: styles.subtitle, children: "Ledger" }),
2115
+ ledger.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Empty, { icon: "subscriber", title: "Nothing moved yet" }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("table", { style: styles.table, children: [
2116
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("tr", { children: [
2117
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("th", { style: styles.th, children: "When" }),
2118
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("th", { style: styles.th, children: "Kind" }),
2119
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("th", { style: styles.th, children: "Amount" }),
2120
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("th", { style: styles.th, children: "Note" })
1113
2121
  ] }) }),
1114
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("tbody", { children: ledger.map((t2) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { children: [
1115
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: new Date(t2.createdAt).toLocaleString() }),
1116
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: styles.td, children: t2.type }),
1117
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: t2.amount }),
1118
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: t2.description ?? "" })
2122
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("tbody", { children: ledger.map((t2) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("tr", { children: [
2123
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: new Date(t2.createdAt).toLocaleString() }),
2124
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("td", { style: styles.td, children: t2.type }),
2125
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: t2.amount }),
2126
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: t2.description ?? "" })
1119
2127
  ] }, t2.id)) })
1120
2128
  ] }),
1121
- hasMoreLedger ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2129
+ hasMoreLedger ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1122
2130
  "button",
1123
2131
  {
1124
2132
  type: "button",
@@ -1127,37 +2135,43 @@ function SubscriberScreen({ client, pageSize = 50 }) {
1127
2135
  children: "Load more"
1128
2136
  }
1129
2137
  ) : null
1130
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: styles.muted, children: "No wallet \u2014 billing has no wallet configuration." })
2138
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { style: styles.muted, children: "No wallet \u2014 billing has no wallet configuration." })
1131
2139
  ] }) : null
1132
2140
  ] });
1133
2141
  }
1134
2142
 
1135
2143
  // src/screens/AuditScreen.tsx
1136
- var import_react_admin12 = require("@fonderie/react-admin");
1137
- var import_react4 = require("react");
1138
- var import_jsx_runtime12 = require("react/jsx-runtime");
2144
+ var import_react_admin13 = require("@fonderie/react-admin");
2145
+ var import_react6 = require("react");
2146
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1139
2147
  function AuditScreen({ client, pageSize = 50 }) {
1140
- const [draft, setDraft] = (0, import_react4.useState)({ workspaceId: "", type: "", actorId: "" });
1141
- const [filter, setFilter] = (0, import_react4.useState)(
2148
+ const [draft, setDraft] = (0, import_react6.useState)({ workspaceId: "", type: "", actorId: "" });
2149
+ const [filter, setFilter] = (0, import_react6.useState)(
1142
2150
  {}
1143
2151
  );
1144
- const { events, hasMore, isLoading, error, refresh, loadMore } = (0, import_react_admin12.useAdminAudit)(client, {
2152
+ const { events, hasMore, isLoading, error, refresh, loadMore } = (0, import_react_admin13.useAdminAudit)(client, {
1145
2153
  ...filter,
1146
2154
  limit: pageSize
1147
2155
  });
1148
- const field = (key, placeholder) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2156
+ const field = (key, placeholder) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1149
2157
  "input",
1150
2158
  {
1151
2159
  value: draft[key],
1152
2160
  onChange: (e) => setDraft({ ...draft, [key]: e.target.value }),
1153
2161
  placeholder,
1154
- style: { ...styles.button, cursor: "text" },
2162
+ style: styles.input,
1155
2163
  "aria-label": placeholder
1156
2164
  }
1157
2165
  );
1158
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: styles.container, children: [
1159
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h1", { style: styles.title, children: "Audit" }),
1160
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2166
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: styles.container, children: [
2167
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2168
+ PageHeader,
2169
+ {
2170
+ title: "Audit",
2171
+ lead: "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
2172
+ }
2173
+ ),
2174
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1161
2175
  "form",
1162
2176
  {
1163
2177
  style: styles.toolbar,
@@ -1173,8 +2187,11 @@ function AuditScreen({ client, pageSize = 50 }) {
1173
2187
  field("workspaceId", "workspace id (all if empty)"),
1174
2188
  field("type", "event type"),
1175
2189
  field("actorId", "actor id"),
1176
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "submit", style: styles.button, disabled: isLoading, children: "Filter" }),
1177
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2190
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("button", { type: "submit", style: styles.buttonPrimary, disabled: isLoading, children: [
2191
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Icon, { name: "search", size: 14 }),
2192
+ "Filter"
2193
+ ] }),
2194
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1178
2195
  "button",
1179
2196
  {
1180
2197
  type: "button",
@@ -1187,25 +2204,25 @@ function AuditScreen({ client, pageSize = 50 }) {
1187
2204
  ]
1188
2205
  }
1189
2206
  ),
1190
- error ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
1191
- events.length === 0 && !isLoading ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: styles.muted, children: "Nothing recorded for this filter." }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("table", { style: styles.table, children: [
1192
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("tr", { children: [
1193
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("th", { style: styles.th, children: "When" }),
1194
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("th", { style: styles.th, children: "Type" }),
1195
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("th", { style: styles.th, children: "Workspace" }),
1196
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("th", { style: styles.th, children: "Actor" }),
1197
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("th", { style: styles.th, children: "Request" })
2207
+ error ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
2208
+ events.length === 0 && !isLoading ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Empty, { icon: "audit", title: "Nothing recorded for this filter" }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("table", { style: styles.table, children: [
2209
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("tr", { children: [
2210
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("th", { style: styles.th, children: "When" }),
2211
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("th", { style: styles.th, children: "Type" }),
2212
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("th", { style: styles.th, children: "Workspace" }),
2213
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("th", { style: styles.th, children: "Actor" }),
2214
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("th", { style: styles.th, children: "Request" })
1198
2215
  ] }) }),
1199
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("tbody", { children: events.map((e) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("tr", { children: [
1200
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: new Date(e.createdAt).toLocaleString() }),
1201
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: e.type }),
1202
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: String(e.payload["workspaceId"] ?? "\u2014") }),
1203
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: e.actorId ?? "\u2014" }),
1204
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: e.requestId ?? "" })
2216
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("tbody", { children: events.map((e) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("tr", { children: [
2217
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: new Date(e.createdAt).toLocaleString() }),
2218
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: e.type }),
2219
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: String(e.payload["workspaceId"] ?? "\u2014") }),
2220
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("td", { style: { ...styles.td, ...styles.mono }, children: e.actorId ?? "\u2014" }),
2221
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("td", { style: { ...styles.td, ...styles.muted }, children: e.requestId ?? "" })
1205
2222
  ] }, e.id)) })
1206
2223
  ] }),
1207
- isLoading ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : null,
1208
- hasMore && !isLoading ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2224
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { style: styles.status, children: "Loading\u2026" }) : null,
2225
+ hasMore && !isLoading ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1209
2226
  "button",
1210
2227
  {
1211
2228
  type: "button",
@@ -1218,7 +2235,36 @@ function AuditScreen({ client, pageSize = 50 }) {
1218
2235
  }
1219
2236
 
1220
2237
  // src/screens/AdminShell.tsx
1221
- var import_jsx_runtime13 = require("react/jsx-runtime");
2238
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2239
+ var ICON = {
2240
+ attention: "attention",
2241
+ modules: "modules",
2242
+ environment: "environment",
2243
+ doctor: "doctor",
2244
+ routes: "routes",
2245
+ users: "users",
2246
+ catalog: "catalog",
2247
+ subscriber: "subscriber",
2248
+ settings: "settings",
2249
+ templates: "templates",
2250
+ audit: "audit",
2251
+ log: "log",
2252
+ tokens: "tokens",
2253
+ operators: "users",
2254
+ migrations: "migrations"
2255
+ };
2256
+ var NARROW = "(max-width: 820px)";
2257
+ function useNarrow() {
2258
+ const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
2259
+ const [narrow, setNarrow] = (0, import_react7.useState)(() => query?.matches ?? false);
2260
+ (0, import_react7.useEffect)(() => {
2261
+ if (!query) return;
2262
+ const on = (e) => setNarrow(e.matches);
2263
+ query.addEventListener("change", on);
2264
+ return () => query.removeEventListener("change", on);
2265
+ }, [query]);
2266
+ return narrow;
2267
+ }
1222
2268
  var NAV = [
1223
2269
  { group: "Today", items: [{ page: "attention", label: "Attention" }] },
1224
2270
  {
@@ -1245,7 +2291,8 @@ var NAV = [
1245
2291
  items: [
1246
2292
  { page: "audit", label: "Audit", needs: "audit" },
1247
2293
  { page: "log", label: "Admin log" },
1248
- { page: "tokens", label: "Access" },
2294
+ { page: "operators", label: "Operators", needs: "operators" },
2295
+ { page: "tokens", label: "Tokens" },
1249
2296
  { page: "migrations", label: "Migrations" }
1250
2297
  ]
1251
2298
  }
@@ -1259,64 +2306,79 @@ function AdminShell({
1259
2306
  auditClient,
1260
2307
  environment,
1261
2308
  page,
1262
- onNavigate
2309
+ onNavigate,
2310
+ appName = "Admin",
2311
+ envLabel,
2312
+ footer,
2313
+ operators = false,
2314
+ currentOperator
1263
2315
  }) {
1264
- const [own, setOwn] = (0, import_react5.useState)("attention");
2316
+ const narrow = useNarrow();
2317
+ const [drawer, setDrawer] = (0, import_react7.useState)(false);
2318
+ const [own, setOwn] = (0, import_react7.useState)("attention");
1265
2319
  const current = page ?? own;
1266
2320
  const go = (p) => {
1267
2321
  if (onNavigate) onNavigate(p);
1268
2322
  if (page === void 0) setOwn(p);
2323
+ setDrawer(false);
1269
2324
  };
1270
- const [editing, setEditing] = (0, import_react5.useState)(null);
2325
+ const [editing, setEditing] = (0, import_react7.useState)(null);
1271
2326
  const has = {
1272
2327
  config: Boolean(configClient),
1273
2328
  courier: Boolean(courierClient),
1274
2329
  auth: Boolean(authClient),
1275
2330
  billing: Boolean(billingClient),
1276
- audit: Boolean(auditClient)
2331
+ audit: Boolean(auditClient),
2332
+ operators
1277
2333
  };
1278
2334
  let body;
1279
2335
  switch (current) {
1280
2336
  case "attention":
1281
- body = /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(AttentionScreen, { client });
2337
+ body = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(AttentionScreen, { client });
1282
2338
  break;
1283
2339
  case "modules":
1284
- body = /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ModulesScreen, { client });
2340
+ body = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ModulesScreen, { client });
1285
2341
  break;
1286
2342
  case "doctor":
1287
- body = /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(DoctorScreen, { client });
2343
+ body = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DoctorScreen, { client });
1288
2344
  break;
1289
2345
  case "environment":
1290
- body = /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(EnvironmentScreen, { client });
2346
+ body = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(EnvironmentScreen, { client });
1291
2347
  break;
1292
2348
  case "routes":
1293
- body = /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(RoutesScreen, { client });
2349
+ body = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(RoutesScreen, { client });
1294
2350
  break;
1295
2351
  case "tokens":
1296
- body = /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(TokensScreen, { client });
2352
+ body = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(TokensScreen, { client });
2353
+ break;
2354
+ case "operators":
2355
+ body = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(OperatorsScreen, { client, me: currentOperator });
1297
2356
  break;
1298
2357
  case "migrations":
1299
- body = /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MigrationsScreen, { client });
2358
+ body = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(MigrationsScreen, { client });
1300
2359
  break;
1301
2360
  case "log":
1302
- body = /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(AdminLogScreen, { client });
2361
+ body = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(AdminLogScreen, { client });
1303
2362
  break;
1304
2363
  case "catalog":
1305
- body = billingClient ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CatalogScreen, { client: billingClient }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { style: styles.status, children: "Pass a BillingAdminClient to see the catalog here." });
2364
+ body = billingClient ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CatalogScreen, { client: billingClient }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a BillingAdminClient to see the catalog here." });
1306
2365
  break;
1307
2366
  case "subscriber":
1308
- body = billingClient ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(SubscriberScreen, { client: billingClient }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { style: styles.status, children: "Pass a BillingAdminClient to look up subscribers here." });
2367
+ body = billingClient ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SubscriberScreen, { client: billingClient }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a BillingAdminClient to look up subscribers here." });
1309
2368
  break;
1310
2369
  case "audit":
1311
- body = auditClient ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(AuditScreen, { client: auditClient }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { style: styles.status, children: "Pass an AuditAdminClient to see the audit trail here." });
2370
+ body = auditClient ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(AuditScreen, { client: auditClient }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass an AuditAdminClient to see the audit trail here." });
1312
2371
  break;
1313
2372
  case "users":
1314
- body = authClient ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(UsersScreen, { client: authClient }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { style: styles.status, children: "Pass an AuthAdminClient to look up users here." });
2373
+ body = authClient ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(UsersScreen, { client: authClient }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass an AuthAdminClient to look up users here." });
1315
2374
  break;
1316
2375
  case "settings":
1317
- body = !configClient ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { style: styles.status, children: "Pass a ConfigAdminClient to manage config and secrets here." }) : editing && editing.kind !== "template" ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
1318
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", style: styles.button, onClick: () => setEditing(null), children: "\u2190 Back" }),
1319
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2376
+ body = !configClient ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a ConfigAdminClient to manage config and secrets here." }) : editing && editing.kind !== "template" ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
2377
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { padding: "24px 40px 0" }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("button", { type: "button", style: styles.buttonGhost, onClick: () => setEditing(null), children: [
2378
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "back", size: 14 }),
2379
+ "Config & secrets"
2380
+ ] }) }),
2381
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1320
2382
  import_react_config_admin_screens.ConfigEditorScreen,
1321
2383
  {
1322
2384
  client: configClient,
@@ -1326,7 +2388,7 @@ function AdminShell({
1326
2388
  onSaved: () => setEditing(null)
1327
2389
  }
1328
2390
  )
1329
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2391
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1330
2392
  import_react_config_admin_screens.ConfigListScreen,
1331
2393
  {
1332
2394
  client: configClient,
@@ -1339,9 +2401,12 @@ function AdminShell({
1339
2401
  );
1340
2402
  break;
1341
2403
  case "templates":
1342
- body = !courierClient ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { style: styles.status, children: "Pass a CourierAdminClient to manage templates here." }) : editing?.kind === "template" ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
1343
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", style: styles.button, onClick: () => setEditing(null), children: "\u2190 Back" }),
1344
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2404
+ body = !courierClient ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a CourierAdminClient to manage templates here." }) : editing?.kind === "template" ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
2405
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { padding: "24px 40px 0" }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("button", { type: "button", style: styles.buttonGhost, onClick: () => setEditing(null), children: [
2406
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "back", size: 14 }),
2407
+ "Templates"
2408
+ ] }) }),
2409
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1345
2410
  import_react_courier_admin_screens.TemplateEditorScreen,
1346
2411
  {
1347
2412
  client: courierClient,
@@ -1349,7 +2414,7 @@ function AdminShell({
1349
2414
  onSaved: () => setEditing(null)
1350
2415
  }
1351
2416
  )
1352
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2417
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1353
2418
  import_react_courier_admin_screens.TemplateListScreen,
1354
2419
  {
1355
2420
  client: courierClient,
@@ -1358,29 +2423,105 @@ function AdminShell({
1358
2423
  );
1359
2424
  break;
1360
2425
  }
1361
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: styles.shell, children: [
1362
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("nav", { style: styles.nav, "aria-label": "Admin", children: NAV.map((g) => {
1363
- const items = g.items.filter((i) => !i.needs || has[i.needs]);
1364
- if (items.length === 0) return null;
1365
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { children: [
1366
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { style: styles.navGroup, children: g.group }),
1367
- items.map((i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2426
+ const production = envLabel ? /^prod/i.test(envLabel) : false;
2427
+ const brand = /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: styles.navBrand, children: [
2428
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: styles.navMark, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "mark", size: 15 }) }),
2429
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { minWidth: 0, flex: 1 }, children: [
2430
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: styles.navBrandTitle, children: appName }),
2431
+ envLabel ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2432
+ "span",
2433
+ {
2434
+ style: {
2435
+ ...styles.badge,
2436
+ marginTop: 3,
2437
+ padding: "0 7px",
2438
+ fontSize: 11,
2439
+ ...production ? {
2440
+ color: "var(--fonderie-danger,#e00)",
2441
+ borderColor: "color-mix(in srgb, var(--fonderie-danger,#e00) 35%, transparent)"
2442
+ } : {}
2443
+ },
2444
+ children: envLabel
2445
+ }
2446
+ ) : null
2447
+ ] }),
2448
+ narrow ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2449
+ "button",
2450
+ {
2451
+ type: "button",
2452
+ style: styles.buttonGhost,
2453
+ "aria-label": "Close menu",
2454
+ onClick: () => setDrawer(false),
2455
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "close" })
2456
+ }
2457
+ ) : null
2458
+ ] });
2459
+ const nav = /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
2460
+ "nav",
2461
+ {
2462
+ style: {
2463
+ ...styles.nav,
2464
+ ...narrow ? { width: 280, maxWidth: "85vw", boxShadow: "0 10px 40px rgba(0,0,0,.25)" } : {}
2465
+ },
2466
+ "aria-label": "Admin",
2467
+ children: [
2468
+ brand,
2469
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: styles.navScroll, children: NAV.map((g) => {
2470
+ const items = g.items.filter((i) => !i.needs || has[i.needs]);
2471
+ if (items.length === 0) return null;
2472
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { children: [
2473
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: styles.navGroup, children: g.group }),
2474
+ items.map((i) => {
2475
+ const active = current === i.page;
2476
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
2477
+ "button",
2478
+ {
2479
+ type: "button",
2480
+ className: "fonderie-admin-nav-item",
2481
+ style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
2482
+ "aria-current": active ? "page" : void 0,
2483
+ onClick: () => {
2484
+ setEditing(null);
2485
+ go(i.page);
2486
+ },
2487
+ children: [
2488
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: ICON[i.page], style: active ? styles.navIconActive : void 0 }),
2489
+ i.label
2490
+ ]
2491
+ },
2492
+ i.page
2493
+ );
2494
+ })
2495
+ ] }, g.group);
2496
+ }) }),
2497
+ footer ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: styles.navFooter, children: footer }) : null
2498
+ ]
2499
+ }
2500
+ );
2501
+ const title = NAV.flatMap((g) => g.items).find((i) => i.page === current)?.label ?? "";
2502
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { ...styles.shell, ...narrow ? { flexDirection: "column" } : {} }, children: [
2503
+ narrow ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
2504
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: styles.topbar, children: [
2505
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1368
2506
  "button",
1369
2507
  {
1370
2508
  type: "button",
1371
- style: { ...styles.navItem, ...current === i.page ? styles.navItemActive : {} },
1372
- "aria-current": current === i.page ? "page" : void 0,
1373
- onClick: () => {
1374
- setEditing(null);
1375
- go(i.page);
1376
- },
1377
- children: i.label
1378
- },
1379
- i.page
1380
- ))
1381
- ] }, g.group);
1382
- }) }),
1383
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("main", { style: styles.main, children: body })
2509
+ style: styles.buttonGhost,
2510
+ "aria-label": "Open menu",
2511
+ "aria-expanded": drawer,
2512
+ onClick: () => setDrawer(true),
2513
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { name: "menu" })
2514
+ }
2515
+ ),
2516
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: { fontWeight: 600, fontSize: 14 }, children: title }),
2517
+ envLabel ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: { ...styles.badge, marginLeft: "auto" }, children: envLabel }) : null
2518
+ ] }),
2519
+ drawer ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: styles.drawer, children: [
2520
+ nav,
2521
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: styles.scrim, onClick: () => setDrawer(false), "aria-hidden": "true" })
2522
+ ] }) : null
2523
+ ] }) : nav,
2524
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("main", { className: "fonderie-admin-main", style: styles.main, children: body })
1384
2525
  ] });
1385
2526
  }
1386
2527
  // Annotate the CommonJS export names for ESM import in node:
@@ -1394,6 +2535,7 @@ function AdminShell({
1394
2535
  EnvironmentScreen,
1395
2536
  MigrationsScreen,
1396
2537
  ModulesScreen,
2538
+ OperatorsScreen,
1397
2539
  RoutesScreen,
1398
2540
  SubscriberScreen,
1399
2541
  TokensScreen,