@fonderie/vue-admin-screens 1.1.0 → 1.3.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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/screens/AdminShell.ts
2
2
  import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/vue-config-admin-screens";
3
3
  import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/vue-courier-admin-screens";
4
- import { computed as computed2, defineComponent as defineComponent13, h as h15, onBeforeUnmount, onMounted, ref as ref6 } from "vue";
4
+ import { computed as computed3, defineComponent as defineComponent15, h as h17, onBeforeUnmount, onMounted, ref as ref9 } from "vue";
5
5
 
6
6
  // src/styles.ts
7
7
  var t = {
@@ -557,6 +557,12 @@ function empty(title, body, iconName = "check") {
557
557
  body ? h("div", body) : null
558
558
  ]);
559
559
  }
560
+ function refreshButton(onClick, busy, label = "Refresh") {
561
+ return h("button", { type: "button", style: styles.button, onClick, disabled: busy }, [
562
+ icon("refresh", 14),
563
+ busy ? "Working\u2026" : label
564
+ ]);
565
+ }
560
566
  function methodChip(method) {
561
567
  const tone = method === "GET" ? "ok" : method === "DELETE" ? "bad" : method === "POST" ? "info" : "warn";
562
568
  const color = toneColor[tone];
@@ -608,7 +614,7 @@ function table(headers, rows) {
608
614
  ]);
609
615
  }
610
616
  var td = (children, extra = {}) => h2("td", { style: { ...styles.td, ...extra } }, children);
611
- var refreshButton = (label, disabled, onClick) => h2("button", { type: "button", style: styles.button, disabled, onClick }, [
617
+ var refreshButton2 = (label, disabled, onClick) => h2("button", { type: "button", style: styles.button, disabled, onClick }, [
612
618
  icon("refresh", 14),
613
619
  disabled ? "Working\u2026" : label
614
620
  ]);
@@ -634,7 +640,7 @@ var AdminLogScreen = defineComponent({
634
640
  pageHeader(
635
641
  "Admin log",
636
642
  "Every request made through this surface, newest first \u2014 refused ones included.",
637
- [refreshButton("Refresh", isLoading.value, () => void refresh())]
643
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())]
638
644
  ),
639
645
  error.value ? h3(
640
646
  "p",
@@ -775,7 +781,7 @@ var AttentionScreen = defineComponent2({
775
781
  )
776
782
  ];
777
783
  },
778
- [refreshButton("Refresh", isLoading.value, () => void refresh())],
784
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())],
779
785
  {
780
786
  lead: a ? `What needs you on this deployment \xB7 checked ${new Date(a.generatedAt).toLocaleTimeString()}` : "What needs you on this deployment"
781
787
  }
@@ -955,7 +961,7 @@ var DoctorScreen = defineComponent4({
955
961
  report.value.ok ? "ok" : "bad",
956
962
  report.value.ok ? "all checks pass" : "a check failed"
957
963
  ) : null,
958
- refreshButton("Run again", isLoading.value, () => void refresh())
964
+ refreshButton2("Run again", isLoading.value, () => void refresh())
959
965
  ],
960
966
  {
961
967
  loadingText: "Running the checks\u2026",
@@ -1158,7 +1164,7 @@ var MigrationsScreen = defineComponent7({
1158
1164
  ...behind.map((m) => section(m, r.everApplied))
1159
1165
  ].filter(Boolean);
1160
1166
  },
1161
- [refreshButton("Refresh", isLoading.value, () => void refresh())],
1167
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())],
1162
1168
  {
1163
1169
  lead: "Schema changes each module ships, and whether this database has them.",
1164
1170
  errorText: (e) => e.status === 403 ? "Applying migrations needs a token with the write scope." : e.explanation
@@ -1167,42 +1173,319 @@ var MigrationsScreen = defineComponent7({
1167
1173
  }
1168
1174
  });
1169
1175
 
1176
+ // src/screens/OperatorsScreen.ts
1177
+ import { useAdminOperators } from "@fonderie/vue-admin";
1178
+ import { defineComponent as defineComponent8, h as h10, ref as ref2 } from "vue";
1179
+ var LEVELS = [
1180
+ { label: "Read only", scopes: ["read"], hint: "look around, change nothing" },
1181
+ { label: "Editor", scopes: ["read", "write"], hint: "change config, templates, users" },
1182
+ {
1183
+ label: "Owner",
1184
+ scopes: ["read", "write", "secrets"],
1185
+ hint: "secrets, tokens and operators too"
1186
+ }
1187
+ ];
1188
+ var levelOf = (scopes) => scopes.includes("secrets") ? "Owner" : scopes.includes("write") ? "Editor" : "Read only";
1189
+ var scopesFor = (label) => LEVELS.find((l) => l.label === label)?.scopes ?? ["read"];
1190
+ var small = { height: "28px" };
1191
+ var OperatorsScreen = defineComponent8({
1192
+ name: "FonderieOperatorsScreen",
1193
+ props: {
1194
+ client: { type: Object, required: true },
1195
+ // The signed-in operator's email: their own row offers no disable or
1196
+ // recovery (the server refuses both for yourself anyway).
1197
+ me: { type: String, default: void 0 }
1198
+ },
1199
+ setup(props) {
1200
+ const { report, isLoading, error, invite, recover, update, revokeLink } = useAdminOperators(
1201
+ props.client
1202
+ );
1203
+ const email = ref2("");
1204
+ const level = ref2("Editor");
1205
+ const minted = ref2(null);
1206
+ const copied = ref2(false);
1207
+ const status = (o) => o.disabledAt ? pill("neutral", "disabled") : o.locked ? pill("warn", "locked") : !o.enrolled ? pill("warn", "setting up") : pill("ok", "active");
1208
+ const mintedNotice = (link) => {
1209
+ const url = `${window.location.origin}${link.url}`;
1210
+ return h10(
1211
+ "div",
1212
+ {
1213
+ style: {
1214
+ ...styles.notice,
1215
+ background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
1216
+ borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1217
+ }
1218
+ },
1219
+ [
1220
+ h10("strong", `Send this link to ${link.email}. It is shown once.`),
1221
+ ` It works once and expires ${new Date(link.expiresAt).toLocaleString()}.`,
1222
+ h10(
1223
+ "div",
1224
+ { style: { display: "flex", gap: "8px", marginTop: "8px", alignItems: "center" } },
1225
+ [
1226
+ h10(
1227
+ "code",
1228
+ { style: { ...styles.code, flex: 1, padding: "7px 10px", wordBreak: "break-all" } },
1229
+ url
1230
+ ),
1231
+ h10(
1232
+ "button",
1233
+ {
1234
+ type: "button",
1235
+ style: styles.button,
1236
+ onClick: () => {
1237
+ void navigator.clipboard?.writeText(url);
1238
+ copied.value = true;
1239
+ }
1240
+ },
1241
+ copied.value ? "Copied" : "Copy"
1242
+ ),
1243
+ h10(
1244
+ "button",
1245
+ {
1246
+ type: "button",
1247
+ style: styles.buttonGhost,
1248
+ "aria-label": "Dismiss",
1249
+ onClick: () => {
1250
+ minted.value = null;
1251
+ copied.value = false;
1252
+ }
1253
+ },
1254
+ [icon("close", 14)]
1255
+ )
1256
+ ]
1257
+ )
1258
+ ]
1259
+ );
1260
+ };
1261
+ const setMinted = (l) => {
1262
+ copied.value = false;
1263
+ minted.value = l;
1264
+ };
1265
+ return () => {
1266
+ const r = report.value;
1267
+ const e = error.value;
1268
+ return h10("div", { style: styles.container }, [
1269
+ pageHeader(
1270
+ "Operators",
1271
+ "The people who can sign in to this console. Each one uses a password and an authenticator app; there is no sign-up."
1272
+ ),
1273
+ e ? h10(
1274
+ "p",
1275
+ { style: styles.error, role: "alert" },
1276
+ e.status === 403 && e.reason === "FORBIDDEN" ? "Managing operators needs the Owner level." : e.explanation
1277
+ ) : null,
1278
+ minted.value ? mintedNotice(minted.value) : null,
1279
+ h10(
1280
+ "form",
1281
+ {
1282
+ style: {
1283
+ ...styles.card,
1284
+ display: "flex",
1285
+ gap: "8px",
1286
+ alignItems: "center",
1287
+ flexWrap: "wrap",
1288
+ marginBottom: "20px"
1289
+ },
1290
+ onSubmit: (ev) => {
1291
+ ev.preventDefault();
1292
+ void invite({ email: email.value.trim(), scopes: scopesFor(level.value) }).then((l) => {
1293
+ setMinted(l);
1294
+ email.value = "";
1295
+ }).catch(() => {
1296
+ });
1297
+ }
1298
+ },
1299
+ [
1300
+ icon("users", 16),
1301
+ h10("strong", { style: { fontSize: "13.5px", marginRight: "4px" } }, "Invite"),
1302
+ h10("input", {
1303
+ type: "email",
1304
+ required: true,
1305
+ value: email.value,
1306
+ onInput: (ev) => email.value = ev.target.value,
1307
+ placeholder: "teammate@company.com",
1308
+ style: { ...styles.input, flex: 1, minWidth: "220px" },
1309
+ "aria-label": "Email to invite"
1310
+ }),
1311
+ h10(
1312
+ "select",
1313
+ {
1314
+ value: level.value,
1315
+ onChange: (ev) => level.value = ev.target.value,
1316
+ style: styles.input,
1317
+ "aria-label": "Access level"
1318
+ },
1319
+ LEVELS.map(
1320
+ (l) => h10("option", { key: l.label, value: l.label }, `${l.label} \u2014 ${l.hint}`)
1321
+ )
1322
+ ),
1323
+ h10(
1324
+ "button",
1325
+ { type: "submit", style: styles.buttonPrimary, disabled: !email.value.trim() },
1326
+ [icon("plus", 14), "Create invite link"]
1327
+ )
1328
+ ]
1329
+ ),
1330
+ isLoading.value && !r ? h10("p", { style: styles.status }, "Loading\u2026") : r && r.operators.length === 0 ? empty("No operators yet", void 0, "users") : r ? table(
1331
+ ["Operator", "Access", "Status", "Last sign-in", ""],
1332
+ r.operators.map((o) => {
1333
+ const mine = o.email === props.me;
1334
+ return h10("tr", { key: o.id }, [
1335
+ td([
1336
+ h10("div", { style: { fontWeight: 600 } }, o.name || o.email),
1337
+ o.name ? h10("div", { style: styles.muted }, o.email) : null
1338
+ ]),
1339
+ td(
1340
+ h10(
1341
+ "select",
1342
+ {
1343
+ value: levelOf(o.scopes),
1344
+ disabled: mine,
1345
+ onChange: (ev) => void update(o.id, {
1346
+ scopes: scopesFor(ev.target.value)
1347
+ }).catch(() => {
1348
+ }),
1349
+ style: { ...styles.input, ...small, fontSize: "12.5px" },
1350
+ "aria-label": `Access level for ${o.email}`
1351
+ },
1352
+ LEVELS.map((l) => h10("option", { key: l.label, value: l.label }, l.label))
1353
+ )
1354
+ ),
1355
+ td([
1356
+ status(o),
1357
+ o.enrolled && o.backupCodesLeft <= 2 && !o.disabledAt ? h10(
1358
+ "div",
1359
+ { style: { ...styles.muted, marginTop: "4px" } },
1360
+ `${o.backupCodesLeft} backup code(s) left`
1361
+ ) : null
1362
+ ]),
1363
+ td(o.lastLoginAt ? new Date(o.lastLoginAt).toLocaleString() : "never", {
1364
+ ...styles.muted,
1365
+ whiteSpace: "nowrap"
1366
+ }),
1367
+ td(
1368
+ mine ? h10("span", { style: styles.muted }, "you") : [
1369
+ h10(
1370
+ "button",
1371
+ {
1372
+ type: "button",
1373
+ style: { ...styles.button, ...small, marginRight: "6px" },
1374
+ onClick: () => {
1375
+ if (window.confirm(
1376
+ `Create a recovery link for ${o.email}? It signs them out everywhere; the link sets a new password and a new authenticator.`
1377
+ ))
1378
+ void recover(o.id).then(setMinted).catch(() => {
1379
+ });
1380
+ }
1381
+ },
1382
+ "Recovery link"
1383
+ ),
1384
+ h10(
1385
+ "button",
1386
+ {
1387
+ type: "button",
1388
+ style: {
1389
+ ...o.disabledAt ? styles.button : styles.buttonDanger,
1390
+ ...small
1391
+ },
1392
+ onClick: () => {
1393
+ if (o.disabledAt || window.confirm(
1394
+ `Disable ${o.email}? They are signed out immediately.`
1395
+ ))
1396
+ void update(o.id, { disabled: !o.disabledAt }).catch(
1397
+ () => {
1398
+ }
1399
+ );
1400
+ }
1401
+ },
1402
+ o.disabledAt ? "Enable" : "Disable"
1403
+ )
1404
+ ],
1405
+ { textAlign: "right", whiteSpace: "nowrap" }
1406
+ )
1407
+ ]);
1408
+ })
1409
+ ) : null,
1410
+ r && r.links.length > 0 ? [
1411
+ h10("h2", { style: styles.subtitle }, "Pending links"),
1412
+ h10(
1413
+ "ul",
1414
+ { style: styles.list },
1415
+ r.links.map(
1416
+ (l) => h10(
1417
+ "li",
1418
+ {
1419
+ key: l.id,
1420
+ style: { ...styles.row, display: "flex", gap: "12px", alignItems: "center" }
1421
+ },
1422
+ [
1423
+ pill(l.kind === "invite" ? "info" : "warn", l.kind, false),
1424
+ h10("span", { style: { flex: 1 } }, [
1425
+ l.email,
1426
+ l.kind === "invite" ? h10("span", { style: styles.muted }, ` \xB7 ${levelOf(l.scopes)}`) : null
1427
+ ]),
1428
+ h10(
1429
+ "span",
1430
+ { style: styles.muted },
1431
+ `expires ${new Date(l.expiresAt).toLocaleString()}`
1432
+ ),
1433
+ h10(
1434
+ "button",
1435
+ {
1436
+ type: "button",
1437
+ style: { ...styles.buttonDanger, ...small },
1438
+ onClick: () => void revokeLink(l.id).catch(() => {
1439
+ })
1440
+ },
1441
+ "Revoke"
1442
+ )
1443
+ ]
1444
+ )
1445
+ )
1446
+ )
1447
+ ] : null
1448
+ ]);
1449
+ };
1450
+ }
1451
+ });
1452
+
1170
1453
  // src/screens/TokensScreen.ts
1171
1454
  import { useAdminTokens } from "@fonderie/vue-admin";
1172
- import { defineComponent as defineComponent8, h as h10, ref as ref2 } from "vue";
1455
+ import { defineComponent as defineComponent9, h as h11, ref as ref3 } from "vue";
1173
1456
  var ALL = ["read", "write", "secrets"];
1174
- var TokensScreen = defineComponent8({
1457
+ var TokensScreen = defineComponent9({
1175
1458
  name: "FonderieTokensScreen",
1176
1459
  props: { client: { type: Object, required: true } },
1177
1460
  setup(props) {
1178
1461
  const { report, isLoading, error, issue, revoke } = useAdminTokens(props.client);
1179
- const name = ref2("");
1180
- const scopes = ref2(["read"]);
1181
- const days = ref2("");
1182
- const minted = ref2(null);
1462
+ const name = ref3("");
1463
+ const scopes = ref3(["read"]);
1464
+ const days = ref3("");
1465
+ const minted = ref3(null);
1183
1466
  const toggle = (s) => scopes.value = scopes.value.includes(s) ? scopes.value.filter((x) => x !== s) : [...scopes.value, s];
1184
1467
  return () => {
1185
1468
  const r = report.value;
1186
- return h10("div", { style: styles.container }, [
1469
+ return h11("div", { style: styles.container }, [
1187
1470
  pageHeader(
1188
- "Access",
1189
- "Who can open this surface: the root token's strength, scoped tokens you have issued, and any legacy per-module tokens."
1471
+ "Tokens",
1472
+ "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."
1190
1473
  ),
1191
- error.value ? h10(
1474
+ error.value ? h11(
1192
1475
  "p",
1193
1476
  { style: styles.error, role: "alert" },
1194
1477
  error.value.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.value.explanation
1195
1478
  ) : null,
1196
- h10("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Root token"),
1197
- r ? h10(
1479
+ h11("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Root token"),
1480
+ r ? h11(
1198
1481
  "div",
1199
1482
  { style: { ...styles.card, display: "flex", gap: "12px", alignItems: "flex-start" } },
1200
1483
  [
1201
1484
  icon("lock", 18, { marginTop: "2px" }),
1202
- h10("div", [
1485
+ h11("div", [
1203
1486
  pill(r.admin.ok ? "ok" : "bad", r.admin.ok ? "strong" : "weak"),
1204
1487
  ...r.admin.problems.map(
1205
- (p) => h10(
1488
+ (p) => h11(
1206
1489
  "div",
1207
1490
  {
1208
1491
  key: p.message,
@@ -1218,13 +1501,13 @@ var TokensScreen = defineComponent8({
1218
1501
  ])
1219
1502
  ]
1220
1503
  ) : null,
1221
- h10("h2", { style: styles.subtitle }, "Issued tokens"),
1222
- r?.issued === null ? h10(
1504
+ h11("h2", { style: styles.subtitle }, "Issued tokens"),
1505
+ r?.issued === null ? h11(
1223
1506
  "p",
1224
1507
  { style: styles.muted },
1225
1508
  "Issuing is off \u2014 give AdminModule a store and run its migrations."
1226
1509
  ) : [
1227
- minted.value ? h10(
1510
+ minted.value ? h11(
1228
1511
  "div",
1229
1512
  {
1230
1513
  style: {
@@ -1234,9 +1517,9 @@ var TokensScreen = defineComponent8({
1234
1517
  }
1235
1518
  },
1236
1519
  [
1237
- h10("strong", "Copy this now \u2014 it is never shown again."),
1520
+ h11("strong", "Copy this now \u2014 it is never shown again."),
1238
1521
  ` (${minted.value.name})`,
1239
- h10(
1522
+ h11(
1240
1523
  "div",
1241
1524
  {
1242
1525
  style: {
@@ -1251,7 +1534,7 @@ var TokensScreen = defineComponent8({
1251
1534
  )
1252
1535
  ]
1253
1536
  ) : null,
1254
- h10(
1537
+ h11(
1255
1538
  "form",
1256
1539
  {
1257
1540
  style: styles.toolbar,
@@ -1271,7 +1554,7 @@ var TokensScreen = defineComponent8({
1271
1554
  }
1272
1555
  },
1273
1556
  [
1274
- h10("input", {
1557
+ h11("input", {
1275
1558
  value: name.value,
1276
1559
  onInput: (e) => name.value = e.target.value,
1277
1560
  placeholder: "name",
@@ -1279,7 +1562,7 @@ var TokensScreen = defineComponent8({
1279
1562
  "aria-label": "Name"
1280
1563
  }),
1281
1564
  ...ALL.map(
1282
- (s) => h10(
1565
+ (s) => h11(
1283
1566
  "label",
1284
1567
  {
1285
1568
  key: s,
@@ -1293,7 +1576,7 @@ var TokensScreen = defineComponent8({
1293
1576
  }
1294
1577
  },
1295
1578
  [
1296
- h10("input", {
1579
+ h11("input", {
1297
1580
  type: "checkbox",
1298
1581
  checked: scopes.value.includes(s),
1299
1582
  onChange: () => toggle(s)
@@ -1302,14 +1585,14 @@ var TokensScreen = defineComponent8({
1302
1585
  ]
1303
1586
  )
1304
1587
  ),
1305
- h10("input", {
1588
+ h11("input", {
1306
1589
  value: days.value,
1307
1590
  onInput: (e) => days.value = e.target.value,
1308
1591
  placeholder: "days (optional)",
1309
1592
  style: { ...styles.input, width: "140px" },
1310
1593
  "aria-label": "Days"
1311
1594
  }),
1312
- h10(
1595
+ h11(
1313
1596
  "button",
1314
1597
  {
1315
1598
  type: "submit",
@@ -1323,7 +1606,7 @@ var TokensScreen = defineComponent8({
1323
1606
  r?.issued && r.issued.length > 0 ? table(
1324
1607
  ["Name", "Scopes", "Created", "Expires", "Last used", ""],
1325
1608
  r.issued.map(
1326
- (t2) => h10("tr", { key: t2.id }, [
1609
+ (t2) => h11("tr", { key: t2.id }, [
1327
1610
  td([t2.name, t2.revokedAt ? [" ", pill("neutral", "revoked")] : ""]),
1328
1611
  td(t2.scopes.join(", "), styles.mono),
1329
1612
  td(
@@ -1339,7 +1622,7 @@ var TokensScreen = defineComponent8({
1339
1622
  styles.muted
1340
1623
  ),
1341
1624
  td(
1342
- t2.revokedAt ? "" : h10(
1625
+ t2.revokedAt ? "" : h11(
1343
1626
  "button",
1344
1627
  {
1345
1628
  type: "button",
@@ -1363,19 +1646,19 @@ var TokensScreen = defineComponent8({
1363
1646
  "tokens"
1364
1647
  )
1365
1648
  ],
1366
- h10("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
1367
- r && r.legacy.length === 0 ? h10(
1649
+ h11("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
1650
+ r && r.legacy.length === 0 ? h11(
1368
1651
  "p",
1369
1652
  { style: styles.muted },
1370
1653
  "None \u2014 every brick's admin surface goes through this token."
1371
- ) : h10(
1654
+ ) : h11(
1372
1655
  "ul",
1373
1656
  { style: styles.list },
1374
1657
  (r?.legacy ?? []).map(
1375
- (l) => h10("li", { key: l.module, style: styles.row }, [
1376
- h10("span", { style: styles.mono }, l.module),
1658
+ (l) => h11("li", { key: l.module, style: styles.row }, [
1659
+ h11("span", { style: styles.mono }, l.module),
1377
1660
  " ",
1378
- h10(
1661
+ h11(
1379
1662
  "span",
1380
1663
  { style: styles.muted },
1381
1664
  "still registers its standalone routes with its own token (deprecated)"
@@ -1396,28 +1679,233 @@ import {
1396
1679
  useAdminUserSessions,
1397
1680
  useAdminUsers
1398
1681
  } from "@fonderie/vue-admin";
1399
- import { computed, defineComponent as defineComponent9, h as h11, ref as ref3 } from "vue";
1400
- var UsersScreen = defineComponent9({
1682
+ import { computed as computed2, defineComponent as defineComponent11, h as h13, ref as ref6 } from "vue";
1683
+
1684
+ // src/billing.ts
1685
+ import { ref as ref4 } from "vue";
1686
+ function periodEnd(s) {
1687
+ if (!s.currentPeriodEnd) return null;
1688
+ const date = new Date(s.currentPeriodEnd).toLocaleDateString();
1689
+ if (s.status === "canceled")
1690
+ return { label: new Date(s.currentPeriodEnd) < /* @__PURE__ */ new Date() ? "Ended" : "Ends", date };
1691
+ if (s.cancelAtPeriodEnd) return { label: "Ends", date };
1692
+ return { label: "Renews", date };
1693
+ }
1694
+ var statusTone = (status) => status === "active" ? "ok" : status === "trialing" ? "info" : status === "past_due" || status === "unpaid" ? "warn" : "neutral";
1695
+ function useSubscriptionIndex(client) {
1696
+ const index = ref4(null);
1697
+ if (!client) return index;
1698
+ void (async () => {
1699
+ const map = /* @__PURE__ */ new Map();
1700
+ let cursor;
1701
+ for (let page2 = 0; page2 < 20; page2++) {
1702
+ const { result } = await client.listSubscriptions({
1703
+ limit: 100,
1704
+ ...cursor ? { cursor } : {}
1705
+ });
1706
+ for (const s of result.subscriptions) {
1707
+ const key = `${s.subscriberType}/${s.subscriberId}`;
1708
+ if (!map.has(key)) map.set(key, s);
1709
+ }
1710
+ if (!result.nextCursor) break;
1711
+ cursor = result.nextCursor;
1712
+ }
1713
+ index.value = map;
1714
+ })().catch(() => {
1715
+ index.value = /* @__PURE__ */ new Map();
1716
+ });
1717
+ return index;
1718
+ }
1719
+
1720
+ // src/screens/SubscriberBilling.ts
1721
+ import { useAdminSubscriber } from "@fonderie/vue-admin";
1722
+ import { computed, defineComponent as defineComponent10, h as h12, ref as ref5 } from "vue";
1723
+ var SubscriberBilling = defineComponent10({
1724
+ name: "FonderieSubscriberBilling",
1725
+ props: {
1726
+ client: { type: Object, required: true },
1727
+ subscriberType: { type: String, required: true },
1728
+ subscriberId: { type: String, required: true }
1729
+ },
1730
+ setup(props) {
1731
+ const subscriber = computed(() => ({ type: props.subscriberType, id: props.subscriberId }));
1732
+ const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(props.client, subscriber, { limit: 20 });
1733
+ const amount = ref5("");
1734
+ const note = ref5("");
1735
+ const granted = ref5(null);
1736
+ return () => {
1737
+ const s = subscription.value;
1738
+ const w = wallet.value;
1739
+ if (isLoading.value && !w && !s) return h12("p", { style: styles.status }, "Loading billing\u2026");
1740
+ const end = s ? periodEnd(s) : null;
1741
+ return h12("div", [
1742
+ error.value ? h12("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1743
+ h12("div", { style: { ...styles.grid, marginBottom: "16px" } }, [
1744
+ h12("div", { style: styles.card }, [
1745
+ h12("div", { style: styles.statLabel }, "Plan"),
1746
+ h12(
1747
+ "div",
1748
+ {
1749
+ style: {
1750
+ display: "flex",
1751
+ alignItems: "center",
1752
+ gap: "8px",
1753
+ marginTop: "6px",
1754
+ flexWrap: "wrap"
1755
+ }
1756
+ },
1757
+ [
1758
+ h12("strong", { style: { fontSize: "18px" } }, s ? s.plan : "Free"),
1759
+ ...s ? [
1760
+ h12("span", { style: styles.muted }, `\xB7 ${s.interval}`),
1761
+ pill(statusTone(s.status), s.status)
1762
+ ] : [h12("span", { style: styles.muted }, "no subscription")]
1763
+ ]
1764
+ ),
1765
+ end ? h12("div", { style: styles.statHint }, `${end.label} ${end.date}`) : null,
1766
+ s?.providerSubscriptionId ? h12(
1767
+ "div",
1768
+ { style: { ...styles.statHint, fontFamily: "var(--fonderie-mono,monospace)" } },
1769
+ s.providerSubscriptionId
1770
+ ) : null
1771
+ ]),
1772
+ h12("div", { style: styles.card }, [
1773
+ h12("div", { style: styles.statLabel }, "Credits"),
1774
+ ...w ? [
1775
+ h12("div", { style: styles.statValue }, [
1776
+ `${w.balance} `,
1777
+ h12("span", { style: { fontSize: "14px", fontWeight: 500 } }, w.currency)
1778
+ ]),
1779
+ h12(
1780
+ "div",
1781
+ { style: styles.statHint },
1782
+ `${w.purchased ?? "0"} permanent \xB7 ${w.granted ?? "0"} plan allowance${w.grantedExpiresAt ? ` (expires ${new Date(w.grantedExpiresAt).toLocaleDateString()})` : ""}`
1783
+ )
1784
+ ] : [
1785
+ h12(
1786
+ "div",
1787
+ { style: styles.statHint },
1788
+ "No wallet \u2014 billing has no wallet configuration."
1789
+ )
1790
+ ]
1791
+ ])
1792
+ ]),
1793
+ w ? h12(
1794
+ "form",
1795
+ {
1796
+ style: {
1797
+ ...styles.card,
1798
+ display: "flex",
1799
+ gap: "8px",
1800
+ alignItems: "center",
1801
+ flexWrap: "wrap",
1802
+ marginBottom: "16px"
1803
+ },
1804
+ onSubmit: (e) => {
1805
+ e.preventDefault();
1806
+ const key = `admin-grant-${props.subscriberType}-${props.subscriberId}-${Date.now()}`;
1807
+ const a = amount.value.trim();
1808
+ void grant({
1809
+ amount: a,
1810
+ ...note.value ? { description: note.value } : {},
1811
+ idempotencyKey: key
1812
+ }).then(() => {
1813
+ granted.value = `Granted ${a} ${w.currency}.`;
1814
+ amount.value = "";
1815
+ note.value = "";
1816
+ }).catch(() => {
1817
+ });
1818
+ }
1819
+ },
1820
+ [
1821
+ h12("strong", { style: { fontSize: "13.5px", marginRight: "4px" } }, "Grant credits"),
1822
+ h12("input", {
1823
+ value: amount.value,
1824
+ onInput: (e) => amount.value = e.target.value,
1825
+ placeholder: `amount (${w.currency})`,
1826
+ inputmode: "numeric",
1827
+ style: { ...styles.input, width: "150px" },
1828
+ "aria-label": "Amount"
1829
+ }),
1830
+ h12("input", {
1831
+ value: note.value,
1832
+ onInput: (e) => note.value = e.target.value,
1833
+ placeholder: "reason (optional)",
1834
+ style: { ...styles.input, flex: 1, minWidth: "180px" },
1835
+ "aria-label": "Reason"
1836
+ }),
1837
+ h12(
1838
+ "button",
1839
+ {
1840
+ type: "submit",
1841
+ style: styles.buttonPrimary,
1842
+ disabled: !/^\d+$/.test(amount.value.trim())
1843
+ },
1844
+ [icon("plus", 14), "Grant"]
1845
+ ),
1846
+ granted.value ? h12("span", { style: styles.ok }, granted.value) : null
1847
+ ]
1848
+ ) : null,
1849
+ w ? ledger.value.length === 0 ? empty("No credit movements yet", void 0, "subscriber") : h12("div", [
1850
+ table(
1851
+ ["When", "Kind", "Amount", "Note"],
1852
+ ledger.value.map(
1853
+ (t2) => h12("tr", { key: t2.id }, [
1854
+ td(new Date(t2.createdAt).toLocaleString(), {
1855
+ ...styles.muted,
1856
+ whiteSpace: "nowrap"
1857
+ }),
1858
+ td(t2.type),
1859
+ td(t2.amount, styles.mono),
1860
+ td(t2.description ?? "", styles.muted)
1861
+ ])
1862
+ )
1863
+ ),
1864
+ hasMoreLedger.value ? loadMoreButton(() => void loadMoreLedger()) : null
1865
+ ]) : null
1866
+ ]);
1867
+ };
1868
+ }
1869
+ });
1870
+
1871
+ // src/screens/UsersScreen.ts
1872
+ var UsersScreen = defineComponent11({
1401
1873
  name: "FonderieUsersScreen",
1402
1874
  props: {
1403
1875
  client: { type: Object, required: true },
1404
- pageSize: { type: Number, default: 50 }
1876
+ pageSize: { type: Number, default: 50 },
1877
+ // Given ⇒ a Plan column, and each user's plan, credits and grant form on
1878
+ // their page. Everyone has a wallet; no subscription means the free tier.
1879
+ billingClient: { type: Object, default: void 0 },
1880
+ // Open this user on arrival (e.g. from the Subscriptions list).
1881
+ openUserId: { type: String, default: void 0 }
1405
1882
  },
1406
1883
  setup(props) {
1407
- const input = ref3("");
1408
- const email = ref3("");
1409
- const selectedId = ref3("");
1884
+ const input = ref6("");
1885
+ const email = ref6("");
1886
+ const selectedId = ref6(props.openUserId ?? "");
1887
+ const plans = useSubscriptionIndex(props.billingClient);
1410
1888
  const list = useAdminUsers(props.client, { limit: props.pageSize });
1411
1889
  const { user, isLoading, error, suspend, unsuspend, revokeSessions } = useAdminUser(
1412
1890
  props.client,
1413
1891
  { email, id: selectedId }
1414
1892
  );
1415
- const userId = computed(() => user.value?.id ?? null);
1893
+ const userId = computed2(() => user.value?.id ?? null);
1416
1894
  const sessions = useAdminUserSessions(props.client, userId);
1417
1895
  const history = useAdminLoginHistory(props.client, userId, { limit: 20 });
1418
- const showList = computed(() => !email.value && !selectedId.value);
1896
+ const showList = computed2(() => !email.value && !selectedId.value);
1419
1897
  const yesNo = (v) => v ? pill("ok", "yes") : pill("neutral", "no");
1420
- const row = (k, v) => h11("tr", [td(k), td(v)]);
1898
+ const row = (k, v) => h13("tr", [td(k), td(v)]);
1899
+ const planCell = (id) => {
1900
+ if (!plans.value) return h13("span", { style: styles.muted }, "\u2026");
1901
+ const sub = plans.value.get(`user/${id}`);
1902
+ if (!sub || sub.status === "canceled") return h13("span", { style: styles.muted }, "free");
1903
+ return [
1904
+ h13("strong", sub.plan),
1905
+ " ",
1906
+ sub.status !== "active" ? pill(statusTone(sub.status), sub.status) : null
1907
+ ];
1908
+ };
1421
1909
  const clear = () => {
1422
1910
  email.value = "";
1423
1911
  selectedId.value = "";
@@ -1425,12 +1913,12 @@ var UsersScreen = defineComponent9({
1425
1913
  };
1426
1914
  return () => {
1427
1915
  const u = user.value;
1428
- return h11("div", { style: styles.container }, [
1916
+ return h13("div", { style: styles.container }, [
1429
1917
  pageHeader(
1430
1918
  "Users",
1431
1919
  "Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
1432
1920
  ),
1433
- h11(
1921
+ h13(
1434
1922
  "form",
1435
1923
  {
1436
1924
  style: styles.toolbar,
@@ -1441,7 +1929,7 @@ var UsersScreen = defineComponent9({
1441
1929
  }
1442
1930
  },
1443
1931
  [
1444
- h11("input", {
1932
+ h13("input", {
1445
1933
  type: "email",
1446
1934
  value: input.value,
1447
1935
  onInput: (e) => input.value = e.target.value,
@@ -1449,22 +1937,22 @@ var UsersScreen = defineComponent9({
1449
1937
  style: { ...styles.input, minWidth: "280px" },
1450
1938
  "aria-label": "Email"
1451
1939
  }),
1452
- h11(
1940
+ h13(
1453
1941
  "button",
1454
1942
  { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1455
1943
  [icon("search", 14), "Look up"]
1456
1944
  ),
1457
- showList.value ? refreshButton("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost)
1945
+ showList.value ? refreshButton2("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost)
1458
1946
  ]
1459
1947
  ),
1460
- showList.value ? h11("div", [
1461
- list.error.value ? h11("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1948
+ showList.value ? h13("div", [
1949
+ list.error.value ? h13("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1462
1950
  list.users.value.length === 0 && !list.isLoading.value ? empty("No users yet", "Sign-ups appear here as they happen.", "users") : table(
1463
- ["Email", "Name", "Created", "Status"],
1951
+ props.billingClient ? ["Email", "Name", "Plan", "Created", "Status"] : ["Email", "Name", "Created", "Status"],
1464
1952
  list.users.value.map(
1465
- (u2) => h11("tr", { key: u2.id }, [
1953
+ (u2) => h13("tr", { key: u2.id }, [
1466
1954
  td(
1467
- h11(
1955
+ h13(
1468
1956
  "button",
1469
1957
  {
1470
1958
  type: "button",
@@ -1475,8 +1963,9 @@ var UsersScreen = defineComponent9({
1475
1963
  )
1476
1964
  ),
1477
1965
  td(
1478
- `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || h11("span", { style: styles.muted }, "\u2014")
1966
+ `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || h13("span", { style: styles.muted }, "\u2014")
1479
1967
  ),
1968
+ props.billingClient ? td(planCell(u2.id)) : null,
1480
1969
  td(new Date(u2.createdAt).toLocaleDateString()),
1481
1970
  td(
1482
1971
  u2.suspended ? pill("warn", "suspended") : u2.deletedAt ? pill("neutral", "deleted") : pill("ok", "active")
@@ -1484,24 +1973,24 @@ var UsersScreen = defineComponent9({
1484
1973
  ])
1485
1974
  )
1486
1975
  ),
1487
- list.isLoading.value ? h11("p", { style: styles.status }, "Loading\u2026") : null,
1976
+ list.isLoading.value ? h13("p", { style: styles.status }, "Loading\u2026") : null,
1488
1977
  list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
1489
1978
  ]) : null,
1490
- error.value ? h11(
1979
+ error.value ? h13(
1491
1980
  "p",
1492
1981
  { style: styles.error, role: "alert" },
1493
1982
  error.value.status === 404 ? "No user with that email." : error.value.explanation
1494
1983
  ) : null,
1495
1984
  u ? [
1496
- h11("h2", { style: styles.subtitle }, [
1985
+ h13("h2", { style: styles.subtitle }, [
1497
1986
  u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
1498
1987
  " ",
1499
1988
  u.suspended ? pill("warn", "suspended") : null,
1500
1989
  u.deletedAt ? pill("neutral", "deleted") : null
1501
1990
  ]),
1502
- h11("table", { style: styles.table }, [
1503
- h11("tbody", [
1504
- row("id", h11("span", { style: styles.mono }, u.id)),
1991
+ h13("table", { style: styles.table }, [
1992
+ h13("tbody", [
1993
+ row("id", h13("span", { style: styles.mono }, u.id)),
1505
1994
  row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
1506
1995
  row("MFA", yesNo(u.mfaEnabled)),
1507
1996
  row(
@@ -1510,12 +1999,12 @@ var UsersScreen = defineComponent9({
1510
1999
  ),
1511
2000
  row(
1512
2001
  "last login",
1513
- u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h11("span", { style: styles.muted }, "never")
2002
+ u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h13("span", { style: styles.muted }, "never")
1514
2003
  ),
1515
2004
  row("created", new Date(u.createdAt).toLocaleString())
1516
2005
  ])
1517
2006
  ]),
1518
- h11("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
2007
+ h13("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
1519
2008
  u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
1520
2009
  "Suspend",
1521
2010
  isLoading.value,
@@ -1528,19 +2017,26 @@ var UsersScreen = defineComponent9({
1528
2017
  () => void revokeSessions().then(() => sessions.refresh())
1529
2018
  )
1530
2019
  ]),
1531
- h11("h2", { style: styles.subtitle }, "Live sessions"),
1532
- sessions.sessions.value.length === 0 ? h11("p", { style: styles.muted }, "None.") : h11(
2020
+ props.billingClient ? h13("h2", { style: styles.subtitle }, "Plan & credits") : null,
2021
+ props.billingClient ? h13(SubscriberBilling, {
2022
+ key: u.id,
2023
+ client: props.billingClient,
2024
+ subscriberType: "user",
2025
+ subscriberId: u.id
2026
+ }) : null,
2027
+ h13("h2", { style: styles.subtitle }, "Live sessions"),
2028
+ sessions.sessions.value.length === 0 ? h13("p", { style: styles.muted }, "None.") : h13(
1533
2029
  "ul",
1534
2030
  { style: styles.list },
1535
2031
  sessions.sessions.value.map(
1536
- (s) => h11("li", { key: s.id, style: styles.row }, [
1537
- h11("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
2032
+ (s) => h13("li", { key: s.id, style: styles.row }, [
2033
+ h13("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
1538
2034
  " ",
1539
- h11("span", { style: styles.muted }, describeLocation(s.location) ?? ""),
2035
+ h13("span", { style: styles.muted }, describeLocation(s.location) ?? ""),
1540
2036
  " ",
1541
- h11("span", { style: styles.muted }, s.userAgent ?? ""),
2037
+ h13("span", { style: styles.muted }, s.userAgent ?? ""),
1542
2038
  " ",
1543
- h11(
2039
+ h13(
1544
2040
  "span",
1545
2041
  { style: styles.muted },
1546
2042
  `since ${new Date(s.createdAt).toLocaleString()}`
@@ -1548,22 +2044,22 @@ var UsersScreen = defineComponent9({
1548
2044
  ])
1549
2045
  )
1550
2046
  ),
1551
- h11("h2", { style: styles.subtitle }, "Recent sign-ins"),
1552
- history.events.value.length === 0 && !history.isLoading.value ? h11("p", { style: styles.muted }, "None recorded.") : h11(
2047
+ h13("h2", { style: styles.subtitle }, "Recent sign-ins"),
2048
+ history.events.value.length === 0 && !history.isLoading.value ? h13("p", { style: styles.muted }, "None recorded.") : h13(
1553
2049
  "ul",
1554
2050
  { style: styles.list },
1555
2051
  history.events.value.map(
1556
- (e) => h11("li", { key: e.id, style: styles.row }, [
2052
+ (e) => h13("li", { key: e.id, style: styles.row }, [
1557
2053
  pill(e.outcome === "success" ? "ok" : "bad", e.outcome),
1558
2054
  " ",
1559
- h11("span", { style: styles.muted }, e.method),
2055
+ h13("span", { style: styles.muted }, e.method),
1560
2056
  " ",
1561
- h11("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
2057
+ h13("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
1562
2058
  " ",
1563
- h11("span", { style: styles.muted }, describeLocation(e.location) ?? ""),
2059
+ h13("span", { style: styles.muted }, describeLocation(e.location) ?? ""),
1564
2060
  e.location?.proxy || e.location?.hosting ? [" ", pill("warn", e.location.proxy ? "proxy/VPN" : "hosting")] : "",
1565
2061
  " ",
1566
- h11("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
2062
+ h13("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
1567
2063
  ])
1568
2064
  )
1569
2065
  ),
@@ -1576,8 +2072,8 @@ var UsersScreen = defineComponent9({
1576
2072
 
1577
2073
  // src/screens/CatalogScreen.ts
1578
2074
  import { useAdminCatalog } from "@fonderie/vue-admin";
1579
- import { defineComponent as defineComponent10, h as h12 } from "vue";
1580
- var CatalogScreen = defineComponent10({
2075
+ import { defineComponent as defineComponent12, h as h14 } from "vue";
2076
+ var CatalogScreen = defineComponent12({
1581
2077
  name: "FonderieCatalogScreen",
1582
2078
  props: { client: { type: Object, required: true } },
1583
2079
  setup(props) {
@@ -1585,17 +2081,17 @@ var CatalogScreen = defineComponent10({
1585
2081
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
1586
2082
  return () => {
1587
2083
  const c = catalog.value;
1588
- return h12("div", { style: styles.container }, [
2084
+ return h14("div", { style: styles.container }, [
1589
2085
  pageHeader(
1590
2086
  "Catalog",
1591
2087
  "What you sell: plans as configured in code, and as stored in the database, side by side.",
1592
- [refreshButton("Refresh", isLoading.value, () => void refresh())]
2088
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())]
1593
2089
  ),
1594
- error.value ? h12("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1595
- isLoading.value && !c ? h12("p", { style: styles.status }, "Loading\u2026") : null,
2090
+ error.value ? h14("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2091
+ isLoading.value && !c ? h14("p", { style: styles.status }, "Loading\u2026") : null,
1596
2092
  c ? [
1597
- h12("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
1598
- h12(
2093
+ h14("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
2094
+ h14(
1599
2095
  "pre",
1600
2096
  {
1601
2097
  style: {
@@ -1609,7 +2105,7 @@ var CatalogScreen = defineComponent10({
1609
2105
  },
1610
2106
  JSON.stringify(c.configured, null, 2)
1611
2107
  ),
1612
- h12("h2", { style: styles.subtitle }, "Stored (database)"),
2108
+ h14("h2", { style: styles.subtitle }, "Stored (database)"),
1613
2109
  c.stored.length === 0 ? empty(
1614
2110
  "No stored plans",
1615
2111
  "Plans are configured in code; nothing has been written to the database.",
@@ -1617,10 +2113,10 @@ var CatalogScreen = defineComponent10({
1617
2113
  ) : table(
1618
2114
  ["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
1619
2115
  c.stored.map(
1620
- (p) => h12("tr", { key: p.id }, [
2116
+ (p) => h14("tr", { key: p.id }, [
1621
2117
  td([
1622
- h12("strong", p.name),
1623
- p.description ? h12("div", { style: styles.muted }, p.description) : ""
2118
+ h14("strong", p.name),
2119
+ p.description ? h14("div", { style: styles.muted }, p.description) : ""
1624
2120
  ]),
1625
2121
  td(String(p.tier)),
1626
2122
  td(p.seats == null ? "\u221E" : String(p.seats)),
@@ -1628,7 +2124,7 @@ var CatalogScreen = defineComponent10({
1628
2124
  td(money(p.pricing?.yearly)),
1629
2125
  td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
1630
2126
  td(
1631
- h12(
2127
+ h14(
1632
2128
  "button",
1633
2129
  {
1634
2130
  type: "button",
@@ -1652,259 +2148,171 @@ var CatalogScreen = defineComponent10({
1652
2148
  });
1653
2149
 
1654
2150
  // src/screens/SubscriberScreen.ts
1655
- import { useAdminSubscriber, useAdminSubscribers } from "@fonderie/vue-admin";
1656
- import { defineComponent as defineComponent11, h as h13, ref as ref4 } from "vue";
1657
- var SubscriberScreen = defineComponent11({
2151
+ import { useAdminSubscribers } from "@fonderie/vue-admin";
2152
+ import { defineComponent as defineComponent13, h as h15, ref as ref7 } from "vue";
2153
+ var FILTERS = [
2154
+ { label: "All", match: () => true },
2155
+ { label: "Active", match: (s) => s === "active" },
2156
+ { label: "Trialing", match: (s) => s === "trialing" },
2157
+ { label: "Past due", match: (s) => s === "past_due" || s === "unpaid" },
2158
+ { label: "Canceled", match: (s) => s === "canceled" }
2159
+ ];
2160
+ var SubscriberScreen = defineComponent13({
1658
2161
  name: "FonderieSubscriberScreen",
1659
2162
  props: {
1660
2163
  client: { type: Object, required: true },
1661
- pageSize: { type: Number, default: 50 }
2164
+ pageSize: { type: Number, default: 50 },
2165
+ /**
2166
+ * A user subscriber opens on the Users page, where their plan and credits
2167
+ * live beside their account. Omit to open everything here.
2168
+ */
2169
+ onOpenUser: { type: Function, default: void 0 }
1662
2170
  },
1663
2171
  setup(props) {
1664
- const type = ref4("user");
1665
- const idInput = ref4("");
1666
- const subscriber = ref4(null);
1667
2172
  const list = useAdminSubscribers(props.client, { limit: props.pageSize });
1668
- const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(props.client, subscriber, { limit: 20 });
1669
- const open = (t2, id) => {
1670
- granted.value = null;
1671
- type.value = t2;
1672
- idInput.value = id;
1673
- subscriber.value = { type: t2, id };
2173
+ const filter = ref7("All");
2174
+ const open = ref7(null);
2175
+ const type = ref7("workspace");
2176
+ const idInput = ref7("");
2177
+ const openRow = (t2, id) => {
2178
+ if (t2 === "user" && props.onOpenUser) props.onOpenUser(id);
2179
+ else open.value = { type: t2, id };
1674
2180
  };
1675
- const amount = ref4("");
1676
- const note = ref4("");
1677
- const granted = ref4(null);
1678
- const input = (model, placeholder, extra = {}) => h13("input", {
1679
- value: model.value,
1680
- onInput: (e) => model.value = e.target.value,
1681
- placeholder,
1682
- style: { ...styles.input, ...extra },
1683
- "aria-label": placeholder
1684
- });
1685
2181
  return () => {
1686
- const s = subscription.value;
1687
- const w = wallet.value;
1688
- return h13("div", { style: styles.container }, [
1689
- pageHeader(
1690
- "Subscribers",
1691
- "Who is subscribed. Open one for their plan, wallet and ledger, and to grant credit."
1692
- ),
1693
- h13(
1694
- "form",
1695
- {
1696
- style: styles.toolbar,
1697
- onSubmit: (e) => {
1698
- e.preventDefault();
1699
- granted.value = null;
1700
- subscriber.value = idInput.value.trim() ? { type: type.value, id: idInput.value.trim() } : null;
1701
- }
1702
- },
1703
- [
1704
- h13(
1705
- "select",
1706
- {
1707
- value: type.value,
1708
- onChange: (e) => type.value = e.target.value,
1709
- style: styles.input,
1710
- "aria-label": "Type"
1711
- },
1712
- [
1713
- h13("option", { value: "user" }, "user"),
1714
- h13("option", { value: "workspace" }, "workspace")
1715
- ]
1716
- ),
1717
- input(idInput, "subscriber id", { minWidth: "280px" }),
1718
- h13(
2182
+ const o = open.value;
2183
+ if (o) {
2184
+ return h15("div", { style: styles.container }, [
2185
+ pageHeader(`${o.type}/${o.id}`, "Plan, credits and what moved.", [
2186
+ h15(
1719
2187
  "button",
1720
- { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1721
- [icon("search", 14), "Look up"]
1722
- ),
1723
- subscriber.value ? h13(
1724
- "button",
1725
- {
1726
- type: "button",
1727
- style: styles.button,
1728
- onClick: () => {
1729
- subscriber.value = null;
1730
- idInput.value = "";
1731
- granted.value = null;
1732
- }
1733
- },
1734
- [icon("back", 14), "All subscribers"]
1735
- ) : h13(
1736
- "button",
1737
- {
1738
- type: "button",
1739
- style: styles.button,
1740
- disabled: list.isLoading.value,
1741
- onClick: () => void list.refresh()
1742
- },
1743
- "Refresh"
2188
+ { type: "button", style: styles.buttonGhost, onClick: () => open.value = null },
2189
+ [icon("back", 14), "All subscriptions"]
1744
2190
  )
1745
- ]
2191
+ ]),
2192
+ h15(SubscriberBilling, {
2193
+ key: `${o.type}/${o.id}`,
2194
+ client: props.client,
2195
+ subscriberType: o.type,
2196
+ subscriberId: o.id
2197
+ })
2198
+ ]);
2199
+ }
2200
+ const match = FILTERS.find((f) => f.label === filter.value)?.match ?? (() => true);
2201
+ const rows = list.subscriptions.value.filter((s) => match(s.status));
2202
+ return h15("div", { style: styles.container }, [
2203
+ pageHeader(
2204
+ "Subscriptions",
2205
+ "Who pays, and who is behind. A user's plan and credits are also on their page under Users \u2014 free users appear only there.",
2206
+ [refreshButton(() => void list.refresh(), list.isLoading.value)]
1746
2207
  ),
1747
- !subscriber.value ? h13("div", [
1748
- list.error.value ? h13("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1749
- list.subscriptions.value.length === 0 && !list.isLoading.value ? empty(
1750
- "No subscribers yet",
1751
- "Subscriptions appear here once someone checks out.",
1752
- "subscriber"
1753
- ) : table(
1754
- ["Subscriber", "Plan", "Status", "Renews"],
1755
- list.subscriptions.value.map(
1756
- (sub) => h13("tr", { key: sub.id }, [
1757
- td(
1758
- h13(
1759
- "button",
1760
- {
1761
- type: "button",
1762
- style: { ...styles.link, ...styles.mono },
1763
- onClick: () => open(sub.subscriberType, sub.subscriberId)
1764
- },
1765
- `${sub.subscriberType}/${sub.subscriberId}`
1766
- )
1767
- ),
1768
- td(`${sub.plan} \xB7 ${sub.interval}`),
1769
- td([
1770
- pill(
1771
- sub.status === "active" ? "ok" : sub.status === "trialing" ? "info" : sub.status === "past_due" ? "warn" : "neutral",
1772
- sub.status
1773
- ),
1774
- sub.cancelAtPeriodEnd ? [" ", pill("warn", "cancels", false)] : null
1775
- ]),
1776
- td(
1777
- sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : h13("span", { style: styles.muted }, "\u2014")
1778
- )
1779
- ])
1780
- )
2208
+ h15("div", { style: { ...styles.toolbar, justifyContent: "space-between" } }, [
2209
+ h15(
2210
+ "fieldset",
2211
+ {
2212
+ style: {
2213
+ display: "flex",
2214
+ gap: "6px",
2215
+ flexWrap: "wrap",
2216
+ border: "none",
2217
+ margin: 0,
2218
+ padding: 0,
2219
+ minWidth: 0
2220
+ },
2221
+ "aria-label": "Filter by status"
2222
+ },
2223
+ FILTERS.map((f) => {
2224
+ const n = list.subscriptions.value.filter((s) => f.match(s.status)).length;
2225
+ const on = filter.value === f.label;
2226
+ return h15(
2227
+ "button",
2228
+ {
2229
+ key: f.label,
2230
+ type: "button",
2231
+ "aria-pressed": on,
2232
+ onClick: () => filter.value = f.label,
2233
+ style: { ...on ? styles.buttonPrimary : styles.button, height: "28px" }
2234
+ },
2235
+ [f.label, h15("span", { style: { opacity: 0.7 } }, String(n))]
2236
+ );
2237
+ })
1781
2238
  ),
1782
- list.isLoading.value ? h13("p", { style: styles.status }, "Loading\u2026") : null,
1783
- list.hasMore.value && !list.isLoading.value ? h13(
1784
- "button",
2239
+ h15(
2240
+ "form",
1785
2241
  {
1786
- type: "button",
1787
- style: { ...styles.button, marginTop: "8px" },
1788
- onClick: () => void list.loadMore()
2242
+ style: { display: "flex", gap: "6px" },
2243
+ onSubmit: (e) => {
2244
+ e.preventDefault();
2245
+ if (idInput.value.trim()) openRow(type.value, idInput.value.trim());
2246
+ }
1789
2247
  },
1790
- "Load more"
1791
- ) : null
1792
- ]) : null,
1793
- error.value ? h13("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1794
- subscriber.value && !isLoading.value ? [
1795
- h13("h2", { style: styles.subtitle }, "Subscription"),
1796
- s ? h13("table", { style: styles.table }, [
1797
- h13("tbody", [
1798
- h13("tr", [td("plan"), td([h13("strong", s.plan), ` \xB7 ${s.interval}`])]),
1799
- h13("tr", [
1800
- td("status"),
1801
- td(`${s.status}${s.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""}`)
1802
- ]),
1803
- h13("tr", [
1804
- td("period"),
1805
- td(
1806
- `${s.currentPeriodStart ? new Date(s.currentPeriodStart).toLocaleDateString() : "\u2014"} \u2192 ${s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : "\u2014"}`
1807
- )
1808
- ]),
1809
- h13("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
1810
- ])
1811
- ]) : h13("p", { style: styles.muted }, "No subscription."),
1812
- h13("h2", { style: styles.subtitle }, "Wallet"),
1813
- w ? [
1814
- h13(
1815
- "p",
1816
- {
1817
- style: {
1818
- ...styles.card,
1819
- display: "flex",
1820
- alignItems: "baseline",
1821
- gap: "8px",
1822
- flexWrap: "wrap"
1823
- }
1824
- },
1825
- [
1826
- h13(
1827
- "strong",
1828
- {
1829
- style: {
1830
- fontSize: "22px",
1831
- letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
1832
- }
1833
- },
1834
- w.balance
1835
- ),
1836
- ` ${w.currency} `,
1837
- h13(
1838
- "span",
1839
- { style: styles.muted },
1840
- `(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
1841
- )
1842
- ]
1843
- ),
1844
- h13(
1845
- "form",
1846
- {
1847
- style: styles.toolbar,
1848
- onSubmit: (e) => {
1849
- e.preventDefault();
1850
- const sub = subscriber.value;
1851
- if (!sub) return;
1852
- const key = `admin-grant-${sub.type}-${sub.id}-${Date.now()}`;
1853
- const amt = amount.value.trim();
1854
- void grant({
1855
- amount: amt,
1856
- ...note.value ? { description: note.value } : {},
1857
- idempotencyKey: key
1858
- }).then(() => {
1859
- granted.value = `Granted ${amt} ${w.currency}.`;
1860
- amount.value = "";
1861
- note.value = "";
1862
- }).catch(() => {
1863
- });
1864
- }
1865
- },
1866
- [
1867
- input(amount, "amount (minor units)"),
1868
- input(note, "reason (optional)", { minWidth: "220px" }),
1869
- h13(
2248
+ [
2249
+ h15(
2250
+ "select",
2251
+ {
2252
+ value: type.value,
2253
+ onChange: (e) => type.value = e.target.value,
2254
+ style: styles.input,
2255
+ "aria-label": "Type"
2256
+ },
2257
+ [
2258
+ h15("option", { value: "workspace" }, "workspace"),
2259
+ h15("option", { value: "user" }, "user")
2260
+ ]
2261
+ ),
2262
+ h15("input", {
2263
+ value: idInput.value,
2264
+ onInput: (e) => idInput.value = e.target.value,
2265
+ placeholder: "subscriber id",
2266
+ style: { ...styles.input, width: "220px" },
2267
+ "aria-label": "Subscriber id"
2268
+ }),
2269
+ h15(
2270
+ "button",
2271
+ { type: "submit", style: styles.button, disabled: !idInput.value.trim() },
2272
+ [icon("search", 14), "Open"]
2273
+ )
2274
+ ]
2275
+ )
2276
+ ]),
2277
+ list.error.value ? h15("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
2278
+ rows.length === 0 && !list.isLoading.value ? empty(
2279
+ filter.value === "All" ? "No subscriptions yet" : `No ${filter.value.toLowerCase()} subscriptions`,
2280
+ "Subscriptions appear here once someone checks out.",
2281
+ "subscriber"
2282
+ ) : table(
2283
+ ["Subscriber", "Plan", "Status", "Renews / ends"],
2284
+ rows.map((s) => {
2285
+ const end = periodEnd(s);
2286
+ return h15("tr", { key: s.id }, [
2287
+ td(
2288
+ h15(
1870
2289
  "button",
1871
2290
  {
1872
- type: "submit",
1873
- style: styles.buttonPrimary,
1874
- disabled: !/^\d+$/.test(amount.value.trim())
2291
+ type: "button",
2292
+ style: { ...styles.link, ...styles.mono },
2293
+ onClick: () => openRow(s.subscriberType, s.subscriberId)
1875
2294
  },
1876
- [icon("plus", 14), "Grant"]
1877
- ),
1878
- granted.value ? h13("span", { style: styles.ok }, granted.value) : null
1879
- ]
1880
- ),
1881
- h13("h2", { style: styles.subtitle }, "Ledger"),
1882
- ledger.value.length === 0 ? empty("Nothing moved yet", void 0, "subscriber") : table(
1883
- ["When", "Kind", "Amount", "Note"],
1884
- ledger.value.map(
1885
- (t2) => h13("tr", { key: t2.id }, [
1886
- td(new Date(t2.createdAt).toLocaleString(), styles.muted),
1887
- td(t2.type),
1888
- td(t2.amount, styles.mono),
1889
- td(t2.description ?? "", styles.muted)
1890
- ])
2295
+ `${s.subscriberType}/${s.subscriberId}`
2296
+ )
2297
+ ),
2298
+ td([
2299
+ h15("strong", s.plan),
2300
+ " ",
2301
+ h15("span", { style: styles.muted }, `\xB7 ${s.interval}`)
2302
+ ]),
2303
+ td([
2304
+ pill(statusTone(s.status), s.status),
2305
+ s.cancelAtPeriodEnd && s.status !== "canceled" ? h15("span", [" ", pill("warn", "cancels", false)]) : null
2306
+ ]),
2307
+ td(
2308
+ end ? [h15("span", { style: styles.muted }, end.label), ` ${end.date}`] : h15("span", { style: styles.muted }, "\u2014"),
2309
+ { whiteSpace: "nowrap" }
1891
2310
  )
1892
- ),
1893
- hasMoreLedger.value ? h13(
1894
- "button",
1895
- {
1896
- type: "button",
1897
- style: { ...styles.button, marginTop: "8px" },
1898
- onClick: () => void loadMoreLedger()
1899
- },
1900
- "Load more"
1901
- ) : null
1902
- ] : h13(
1903
- "p",
1904
- { style: styles.muted },
1905
- "No wallet \u2014 billing has no wallet configuration."
1906
- )
1907
- ] : null
2311
+ ]);
2312
+ })
2313
+ ),
2314
+ list.isLoading.value ? h15("p", { style: styles.status }, "Loading\u2026") : null,
2315
+ list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
1908
2316
  ]);
1909
2317
  };
1910
2318
  }
@@ -1912,33 +2320,33 @@ var SubscriberScreen = defineComponent11({
1912
2320
 
1913
2321
  // src/screens/AuditScreen.ts
1914
2322
  import { useAdminAudit } from "@fonderie/vue-admin";
1915
- import { defineComponent as defineComponent12, h as h14, ref as ref5 } from "vue";
1916
- var AuditScreen = defineComponent12({
2323
+ import { defineComponent as defineComponent14, h as h16, ref as ref8 } from "vue";
2324
+ var AuditScreen = defineComponent14({
1917
2325
  name: "FonderieAuditScreen",
1918
2326
  props: {
1919
2327
  client: { type: Object, required: true },
1920
2328
  pageSize: { type: Number, default: 50 }
1921
2329
  },
1922
2330
  setup(props) {
1923
- const draft = ref5({ workspaceId: "", type: "", actorId: "" });
1924
- const query = ref5({ limit: props.pageSize });
2331
+ const draft = ref8({ workspaceId: "", type: "", actorId: "" });
2332
+ const query = ref8({ limit: props.pageSize });
1925
2333
  const { events, hasMore, isLoading, error, refresh, loadMore } = useAdminAudit(
1926
2334
  props.client,
1927
2335
  query
1928
2336
  );
1929
- const field = (key, placeholder) => h14("input", {
2337
+ const field = (key, placeholder) => h16("input", {
1930
2338
  value: draft.value[key],
1931
2339
  onInput: (e) => draft.value = { ...draft.value, [key]: e.target.value },
1932
2340
  placeholder,
1933
2341
  style: styles.input,
1934
2342
  "aria-label": placeholder
1935
2343
  });
1936
- return () => h14("div", { style: styles.container }, [
2344
+ return () => h16("div", { style: styles.container }, [
1937
2345
  pageHeader(
1938
2346
  "Audit",
1939
2347
  "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
1940
2348
  ),
1941
- h14(
2349
+ h16(
1942
2350
  "form",
1943
2351
  {
1944
2352
  style: styles.toolbar,
@@ -1957,19 +2365,19 @@ var AuditScreen = defineComponent12({
1957
2365
  field("workspaceId", "workspace id (all if empty)"),
1958
2366
  field("type", "event type"),
1959
2367
  field("actorId", "actor id"),
1960
- h14(
2368
+ h16(
1961
2369
  "button",
1962
2370
  { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1963
2371
  [icon("search", 14), "Filter"]
1964
2372
  ),
1965
- refreshButton("Refresh", isLoading.value, () => void refresh())
2373
+ refreshButton2("Refresh", isLoading.value, () => void refresh())
1966
2374
  ]
1967
2375
  ),
1968
- error.value ? h14("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2376
+ error.value ? h16("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1969
2377
  events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
1970
2378
  ["When", "Type", "Workspace", "Actor", "Request"],
1971
2379
  events.value.map(
1972
- (e) => h14("tr", { key: e.id }, [
2380
+ (e) => h16("tr", { key: e.id }, [
1973
2381
  td(new Date(e.createdAt).toLocaleString(), styles.muted),
1974
2382
  td(e.type, styles.mono),
1975
2383
  td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
@@ -1978,7 +2386,7 @@ var AuditScreen = defineComponent12({
1978
2386
  ])
1979
2387
  )
1980
2388
  ),
1981
- isLoading.value ? h14("p", { style: styles.status }, "Loading\u2026") : null,
2389
+ isLoading.value ? h16("p", { style: styles.status }, "Loading\u2026") : null,
1982
2390
  hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
1983
2391
  ]);
1984
2392
  }
@@ -2001,7 +2409,7 @@ var NAV = [
2001
2409
  group: "Money",
2002
2410
  items: [
2003
2411
  { page: "catalog", label: "Catalog", needs: "billing" },
2004
- { page: "subscriber", label: "Subscriber", needs: "billing" }
2412
+ { page: "subscriber", label: "Subscriptions", needs: "billing" }
2005
2413
  ]
2006
2414
  },
2007
2415
  { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
@@ -2011,7 +2419,8 @@ var NAV = [
2011
2419
  items: [
2012
2420
  { page: "audit", label: "Audit", needs: "audit" },
2013
2421
  { page: "log", label: "Admin log" },
2014
- { page: "tokens", label: "Access" },
2422
+ { page: "operators", label: "Operators", needs: "operators" },
2423
+ { page: "tokens", label: "Tokens" },
2015
2424
  { page: "migrations", label: "Migrations" }
2016
2425
  ]
2017
2426
  }
@@ -2030,10 +2439,11 @@ var ICON = {
2030
2439
  audit: "audit",
2031
2440
  log: "log",
2032
2441
  tokens: "tokens",
2442
+ operators: "users",
2033
2443
  migrations: "migrations"
2034
2444
  };
2035
2445
  var NARROW = "(max-width: 820px)";
2036
- var AdminShell = defineComponent13({
2446
+ var AdminShell = defineComponent15({
2037
2447
  name: "FonderieAdminShell",
2038
2448
  props: {
2039
2449
  client: { type: Object, required: true },
@@ -2055,62 +2465,86 @@ var AdminShell = defineComponent13({
2055
2465
  appName: { type: String, default: "Admin" },
2056
2466
  // A badge beside the name — the deployment's environment ("production").
2057
2467
  // Production is tinted so an operator always knows where their clicks land.
2058
- envLabel: { type: String, default: void 0 }
2468
+ envLabel: { type: String, default: void 0 },
2469
+ // The deployment has operator accounts (@fonderie/admin with a store):
2470
+ // shows the Operators page.
2471
+ operators: { type: Boolean, default: false },
2472
+ // The signed-in operator's email, when a person (not a token) is signed in.
2473
+ currentOperator: { type: String, default: void 0 }
2059
2474
  },
2060
2475
  // `footer` slot: pinned to the bottom of the sidebar — session controls
2061
2476
  // (theme, sign out). The React shell takes the same thing as a `footer` prop.
2062
2477
  emits: { navigate: (_page) => true },
2063
2478
  setup(props, { emit, slots }) {
2064
2479
  const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
2065
- const narrow = ref6(query?.matches ?? false);
2066
- const drawer = ref6(false);
2480
+ const narrow = ref9(query?.matches ?? false);
2481
+ const drawer = ref9(false);
2067
2482
  const onMedia = (e) => narrow.value = e.matches;
2068
2483
  onMounted(() => query?.addEventListener("change", onMedia));
2069
2484
  onBeforeUnmount(() => query?.removeEventListener("change", onMedia));
2070
- const own = ref6("attention");
2071
- const current = computed2(() => props.page ?? own.value);
2072
- const editing = ref6(null);
2485
+ const own = ref9("attention");
2486
+ const current = computed3(() => props.page ?? own.value);
2487
+ const editing = ref9(null);
2488
+ const openUser = ref9(void 0);
2073
2489
  const go = (p) => {
2074
2490
  editing.value = null;
2075
2491
  emit("navigate", p);
2076
2492
  if (props.page === void 0) own.value = p;
2077
2493
  drawer.value = false;
2078
2494
  };
2079
- const back = (label) => h15("div", { style: { padding: "24px 40px 0" } }, [
2080
- h15(
2495
+ const back = (label) => h17("div", { style: { padding: "24px 40px 0" } }, [
2496
+ h17(
2081
2497
  "button",
2082
2498
  { type: "button", style: styles.buttonGhost, onClick: () => editing.value = null },
2083
2499
  [icon("back", 14), label]
2084
2500
  )
2085
2501
  ]);
2086
- const missing = (text) => h15("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
2502
+ const missing = (text) => h17("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
2087
2503
  const body = () => {
2088
2504
  const c = props.client;
2089
2505
  switch (current.value) {
2090
2506
  case "attention":
2091
- return h15(AttentionScreen, { client: c });
2507
+ return h17(AttentionScreen, { client: c });
2092
2508
  case "modules":
2093
- return h15(ModulesScreen, { client: c });
2509
+ return h17(ModulesScreen, { client: c });
2094
2510
  case "doctor":
2095
- return h15(DoctorScreen, { client: c });
2511
+ return h17(DoctorScreen, { client: c });
2096
2512
  case "environment":
2097
- return h15(EnvironmentScreen, { client: c });
2513
+ return h17(EnvironmentScreen, { client: c });
2098
2514
  case "routes":
2099
- return h15(RoutesScreen, { client: c });
2515
+ return h17(RoutesScreen, { client: c });
2100
2516
  case "tokens":
2101
- return h15(TokensScreen, { client: c });
2517
+ return h17(TokensScreen, { client: c });
2518
+ case "operators":
2519
+ return h17(OperatorsScreen, {
2520
+ client: c,
2521
+ ...props.currentOperator ? { me: props.currentOperator } : {}
2522
+ });
2102
2523
  case "migrations":
2103
- return h15(MigrationsScreen, { client: c });
2524
+ return h17(MigrationsScreen, { client: c });
2104
2525
  case "log":
2105
- return h15(AdminLogScreen, { client: c });
2526
+ return h17(AdminLogScreen, { client: c });
2106
2527
  case "catalog":
2107
- return props.billingClient ? h15(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
2528
+ return props.billingClient ? h17(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
2108
2529
  case "subscriber":
2109
- return props.billingClient ? h15(SubscriberScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to look up subscribers here.");
2530
+ return props.billingClient ? h17(SubscriberScreen, {
2531
+ client: props.billingClient,
2532
+ ...props.authClient ? {
2533
+ onOpenUser: (id) => {
2534
+ openUser.value = id;
2535
+ go("users");
2536
+ }
2537
+ } : {}
2538
+ }) : missing("Pass a BillingAdminClient to look up subscribers here.");
2110
2539
  case "audit":
2111
- return props.auditClient ? h15(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
2540
+ return props.auditClient ? h17(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
2112
2541
  case "users":
2113
- return props.authClient ? h15(UsersScreen, { client: props.authClient }) : missing("Pass an AuthAdminClient to look up users here.");
2542
+ return props.authClient ? h17(UsersScreen, {
2543
+ key: openUser.value ?? "list",
2544
+ client: props.authClient,
2545
+ ...props.billingClient ? { billingClient: props.billingClient } : {},
2546
+ ...openUser.value ? { openUserId: openUser.value } : {}
2547
+ }) : missing("Pass an AuthAdminClient to look up users here.");
2114
2548
  case "settings": {
2115
2549
  const cc = props.configClient;
2116
2550
  if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
@@ -2118,7 +2552,7 @@ var AdminShell = defineComponent13({
2118
2552
  if (e && e.kind !== "template")
2119
2553
  return [
2120
2554
  back("Config & secrets"),
2121
- h15(ConfigEditorScreen, {
2555
+ h17(ConfigEditorScreen, {
2122
2556
  client: cc,
2123
2557
  kind: e.kind,
2124
2558
  configKey: e.key,
@@ -2126,7 +2560,7 @@ var AdminShell = defineComponent13({
2126
2560
  onSaved: () => editing.value = null
2127
2561
  })
2128
2562
  ];
2129
- return h15(ConfigListScreen, {
2563
+ return h17(ConfigListScreen, {
2130
2564
  client: cc,
2131
2565
  ...props.environment !== void 0 ? { environment: props.environment } : {},
2132
2566
  "onSelect-config": (key) => editing.value = { kind: "config", key },
@@ -2143,27 +2577,27 @@ var AdminShell = defineComponent13({
2143
2577
  if (e?.kind === "template")
2144
2578
  return [
2145
2579
  back("Templates"),
2146
- h15(TemplateEditorScreen, {
2580
+ h17(TemplateEditorScreen, {
2147
2581
  client: kc,
2148
2582
  type: e.type,
2149
2583
  onSaved: () => editing.value = null
2150
2584
  })
2151
2585
  ];
2152
- return h15(TemplateListScreen, {
2586
+ return h17(TemplateListScreen, {
2153
2587
  client: kc,
2154
2588
  "onSelect-template": (t2) => editing.value = { kind: "template", type: t2.type }
2155
2589
  });
2156
2590
  }
2157
2591
  }
2158
2592
  };
2159
- const visible = (i) => !i.needs || (i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : i.needs === "billing" ? props.billingClient : props.auditClient);
2593
+ const visible = (i) => !i.needs || (i.needs === "operators" ? props.operators : i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : i.needs === "billing" ? props.billingClient : props.auditClient);
2160
2594
  const brand = () => {
2161
2595
  const production = props.envLabel ? /^prod/i.test(props.envLabel) : false;
2162
- return h15("div", { style: styles.navBrand }, [
2163
- h15("div", { style: styles.navMark }, [icon("mark", 15)]),
2164
- h15("div", { style: { minWidth: 0, flex: 1 } }, [
2165
- h15("div", { style: styles.navBrandTitle }, props.appName),
2166
- props.envLabel ? h15(
2596
+ return h17("div", { style: styles.navBrand }, [
2597
+ h17("div", { style: styles.navMark }, [icon("mark", 15)]),
2598
+ h17("div", { style: { minWidth: 0, flex: 1 } }, [
2599
+ h17("div", { style: styles.navBrandTitle }, props.appName),
2600
+ props.envLabel ? h17(
2167
2601
  "span",
2168
2602
  {
2169
2603
  style: {
@@ -2180,7 +2614,7 @@ var AdminShell = defineComponent13({
2180
2614
  props.envLabel
2181
2615
  ) : null
2182
2616
  ]),
2183
- narrow.value ? h15(
2617
+ narrow.value ? h17(
2184
2618
  "button",
2185
2619
  {
2186
2620
  type: "button",
@@ -2192,7 +2626,7 @@ var AdminShell = defineComponent13({
2192
2626
  ) : null
2193
2627
  ]);
2194
2628
  };
2195
- const nav = () => h15(
2629
+ const nav = () => h17(
2196
2630
  "nav",
2197
2631
  {
2198
2632
  style: {
@@ -2203,17 +2637,17 @@ var AdminShell = defineComponent13({
2203
2637
  },
2204
2638
  [
2205
2639
  brand(),
2206
- h15(
2640
+ h17(
2207
2641
  "div",
2208
2642
  { style: styles.navScroll },
2209
2643
  NAV.map((g) => {
2210
2644
  const items = g.items.filter(visible);
2211
2645
  if (items.length === 0) return null;
2212
- return h15("div", { key: g.group }, [
2213
- h15("div", { style: styles.navGroup }, g.group),
2646
+ return h17("div", { key: g.group }, [
2647
+ h17("div", { style: styles.navGroup }, g.group),
2214
2648
  ...items.map((i) => {
2215
2649
  const active = current.value === i.page;
2216
- return h15(
2650
+ return h17(
2217
2651
  "button",
2218
2652
  {
2219
2653
  key: i.page,
@@ -2221,7 +2655,10 @@ var AdminShell = defineComponent13({
2221
2655
  class: "fonderie-admin-nav-item",
2222
2656
  style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
2223
2657
  "aria-current": active ? "page" : void 0,
2224
- onClick: () => go(i.page)
2658
+ onClick: () => {
2659
+ openUser.value = void 0;
2660
+ go(i.page);
2661
+ }
2225
2662
  },
2226
2663
  [icon(ICON[i.page], 16, active ? styles.navIconActive : void 0), i.label]
2227
2664
  );
@@ -2229,18 +2666,18 @@ var AdminShell = defineComponent13({
2229
2666
  ]);
2230
2667
  })
2231
2668
  ),
2232
- slots["footer"] ? h15("div", { style: styles.navFooter }, slots["footer"]()) : null
2669
+ slots["footer"] ? h17("div", { style: styles.navFooter }, slots["footer"]()) : null
2233
2670
  ]
2234
2671
  );
2235
2672
  return () => {
2236
2673
  const title = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label ?? "";
2237
- return h15(
2674
+ return h17(
2238
2675
  "div",
2239
2676
  { style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
2240
2677
  [
2241
2678
  ...narrow.value ? [
2242
- h15("div", { style: styles.topbar }, [
2243
- h15(
2679
+ h17("div", { style: styles.topbar }, [
2680
+ h17(
2244
2681
  "button",
2245
2682
  {
2246
2683
  type: "button",
@@ -2251,19 +2688,19 @@ var AdminShell = defineComponent13({
2251
2688
  },
2252
2689
  [icon("menu")]
2253
2690
  ),
2254
- h15("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2255
- props.envLabel ? h15("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2691
+ h17("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2692
+ props.envLabel ? h17("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2256
2693
  ]),
2257
- drawer.value ? h15("div", { style: styles.drawer }, [
2694
+ drawer.value ? h17("div", { style: styles.drawer }, [
2258
2695
  nav(),
2259
- h15("div", {
2696
+ h17("div", {
2260
2697
  style: styles.scrim,
2261
2698
  "aria-hidden": "true",
2262
2699
  onClick: () => drawer.value = false
2263
2700
  })
2264
2701
  ]) : null
2265
2702
  ] : [nav()],
2266
- h15("main", { class: "fonderie-admin-main", style: styles.main }, body())
2703
+ h17("main", { class: "fonderie-admin-main", style: styles.main }, body())
2267
2704
  ]
2268
2705
  );
2269
2706
  };
@@ -2279,7 +2716,9 @@ export {
2279
2716
  EnvironmentScreen,
2280
2717
  MigrationsScreen,
2281
2718
  ModulesScreen,
2719
+ OperatorsScreen,
2282
2720
  RoutesScreen,
2721
+ SubscriberBilling,
2283
2722
  SubscriberScreen,
2284
2723
  TokensScreen,
2285
2724
  UsersScreen