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