@fonderie/react-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.tsx
2
2
  import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/react-config-admin-screens";
3
3
  import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/react-courier-admin-screens";
4
- import { useEffect, useState as useState6 } from "react";
4
+ import { useEffect as useEffect2, useState as useState9 } from "react";
5
5
 
6
6
  // src/styles.ts
7
7
  var t = {
@@ -1073,33 +1073,265 @@ function MigrationsScreen({ client }) {
1073
1073
  ] });
1074
1074
  }
1075
1075
 
1076
- // src/screens/TokensScreen.tsx
1077
- import { useAdminTokens } from "@fonderie/react-admin";
1076
+ // src/screens/OperatorsScreen.tsx
1077
+ import { useAdminOperators } from "@fonderie/react-admin";
1078
1078
  import { useState as useState2 } from "react";
1079
1079
  import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1080
+ var LEVELS = [
1081
+ { label: "Read only", scopes: ["read"], hint: "look around, change nothing" },
1082
+ { label: "Editor", scopes: ["read", "write"], hint: "change config, templates, users" },
1083
+ {
1084
+ label: "Owner",
1085
+ scopes: ["read", "write", "secrets"],
1086
+ hint: "secrets, tokens and operators too"
1087
+ }
1088
+ ];
1089
+ var levelOf = (scopes) => scopes.includes("secrets") ? "Owner" : scopes.includes("write") ? "Editor" : "Read only";
1090
+ function Minted({ link, onClose }) {
1091
+ const [copied, setCopied] = useState2(false);
1092
+ const url = `${window.location.origin}${link.url}`;
1093
+ return /* @__PURE__ */ jsxs9(
1094
+ "div",
1095
+ {
1096
+ style: {
1097
+ ...styles.notice,
1098
+ background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
1099
+ borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1100
+ },
1101
+ children: [
1102
+ /* @__PURE__ */ jsxs9("strong", { children: [
1103
+ "Send this link to ",
1104
+ link.email,
1105
+ ". It is shown once."
1106
+ ] }),
1107
+ " It works once and expires",
1108
+ " ",
1109
+ new Date(link.expiresAt).toLocaleString(),
1110
+ ".",
1111
+ /* @__PURE__ */ jsxs9("div", { style: { display: "flex", gap: 8, marginTop: 8, alignItems: "center" }, children: [
1112
+ /* @__PURE__ */ jsx9("code", { style: { ...styles.code, flex: 1, padding: "7px 10px", wordBreak: "break-all" }, children: url }),
1113
+ /* @__PURE__ */ jsx9(
1114
+ "button",
1115
+ {
1116
+ type: "button",
1117
+ style: styles.button,
1118
+ onClick: () => {
1119
+ void navigator.clipboard?.writeText(url);
1120
+ setCopied(true);
1121
+ },
1122
+ children: copied ? "Copied" : "Copy"
1123
+ }
1124
+ ),
1125
+ /* @__PURE__ */ jsx9("button", { type: "button", style: styles.buttonGhost, onClick: onClose, "aria-label": "Dismiss", children: /* @__PURE__ */ jsx9(Icon, { name: "close", size: 14 }) })
1126
+ ] })
1127
+ ]
1128
+ }
1129
+ );
1130
+ }
1131
+ function OperatorsScreen({ client, me }) {
1132
+ const { report, isLoading, error, invite, recover, update, revokeLink } = useAdminOperators(client);
1133
+ const [email, setEmail] = useState2("");
1134
+ const [level, setLevel] = useState2("Editor");
1135
+ const [minted, setMinted] = useState2(null);
1136
+ const scopesFor = (label) => LEVELS.find((l) => l.label === label)?.scopes ?? ["read"];
1137
+ const status = (o) => o.disabledAt ? /* @__PURE__ */ jsx9(Pill, { tone: "neutral", children: "disabled" }) : o.locked ? /* @__PURE__ */ jsx9(Pill, { tone: "warn", children: "locked" }) : !o.enrolled ? /* @__PURE__ */ jsx9(Pill, { tone: "warn", children: "setting up" }) : /* @__PURE__ */ jsx9(Pill, { tone: "ok", children: "active" });
1138
+ return /* @__PURE__ */ jsxs9("div", { style: styles.container, children: [
1139
+ /* @__PURE__ */ jsx9(
1140
+ PageHeader,
1141
+ {
1142
+ title: "Operators",
1143
+ lead: "The people who can sign in to this console. Each one uses a password and an authenticator app; there is no sign-up."
1144
+ }
1145
+ ),
1146
+ error ? /* @__PURE__ */ jsx9("p", { style: styles.error, role: "alert", children: error.status === 403 && error.reason === "FORBIDDEN" ? "Managing operators needs the Owner level." : error.explanation }) : null,
1147
+ minted ? /* @__PURE__ */ jsx9(Minted, { link: minted, onClose: () => setMinted(null) }) : null,
1148
+ /* @__PURE__ */ jsxs9(
1149
+ "form",
1150
+ {
1151
+ style: {
1152
+ ...styles.card,
1153
+ display: "flex",
1154
+ gap: 8,
1155
+ alignItems: "center",
1156
+ flexWrap: "wrap",
1157
+ marginBottom: 20
1158
+ },
1159
+ onSubmit: (e) => {
1160
+ e.preventDefault();
1161
+ void invite({ email: email.trim(), scopes: scopesFor(level) }).then((l) => {
1162
+ setMinted(l);
1163
+ setEmail("");
1164
+ }).catch(() => {
1165
+ });
1166
+ },
1167
+ children: [
1168
+ /* @__PURE__ */ jsx9(Icon, { name: "users", size: 16 }),
1169
+ /* @__PURE__ */ jsx9("strong", { style: { fontSize: 13.5, marginRight: 4 }, children: "Invite" }),
1170
+ /* @__PURE__ */ jsx9(
1171
+ "input",
1172
+ {
1173
+ type: "email",
1174
+ required: true,
1175
+ value: email,
1176
+ onChange: (e) => setEmail(e.target.value),
1177
+ placeholder: "teammate@company.com",
1178
+ style: { ...styles.input, flex: 1, minWidth: 220 },
1179
+ "aria-label": "Email to invite"
1180
+ }
1181
+ ),
1182
+ /* @__PURE__ */ jsx9(
1183
+ "select",
1184
+ {
1185
+ value: level,
1186
+ onChange: (e) => setLevel(e.target.value),
1187
+ style: styles.input,
1188
+ "aria-label": "Access level",
1189
+ children: LEVELS.map((l) => /* @__PURE__ */ jsxs9("option", { value: l.label, children: [
1190
+ l.label,
1191
+ " \u2014 ",
1192
+ l.hint
1193
+ ] }, l.label))
1194
+ }
1195
+ ),
1196
+ /* @__PURE__ */ jsxs9("button", { type: "submit", style: styles.buttonPrimary, disabled: !email.trim(), children: [
1197
+ /* @__PURE__ */ jsx9(Icon, { name: "plus", size: 14 }),
1198
+ "Create invite link"
1199
+ ] })
1200
+ ]
1201
+ }
1202
+ ),
1203
+ isLoading && !report ? /* @__PURE__ */ jsx9("p", { style: styles.status, children: "Loading\u2026" }) : report && report.operators.length === 0 ? /* @__PURE__ */ jsx9(Empty, { icon: "users", title: "No operators yet" }) : report ? /* @__PURE__ */ jsxs9("table", { style: styles.table, children: [
1204
+ /* @__PURE__ */ jsx9("thead", { children: /* @__PURE__ */ jsxs9("tr", { children: [
1205
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Operator" }),
1206
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Access" }),
1207
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Status" }),
1208
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Last sign-in" }),
1209
+ /* @__PURE__ */ jsx9("th", { style: styles.th })
1210
+ ] }) }),
1211
+ /* @__PURE__ */ jsx9("tbody", { children: report.operators.map((o) => /* @__PURE__ */ jsxs9("tr", { children: [
1212
+ /* @__PURE__ */ jsxs9("td", { style: styles.td, children: [
1213
+ /* @__PURE__ */ jsx9("div", { style: { fontWeight: 600 }, children: o.name || o.email }),
1214
+ o.name ? /* @__PURE__ */ jsx9("div", { style: styles.muted, children: o.email }) : null
1215
+ ] }),
1216
+ /* @__PURE__ */ jsx9("td", { style: styles.td, children: /* @__PURE__ */ jsx9(
1217
+ "select",
1218
+ {
1219
+ value: levelOf(o.scopes),
1220
+ onChange: (e) => void update(o.id, { scopes: scopesFor(e.target.value) }).catch(() => {
1221
+ }),
1222
+ style: { ...styles.input, height: 28, fontSize: 12.5 },
1223
+ disabled: o.email === me,
1224
+ "aria-label": `Access level for ${o.email}`,
1225
+ children: LEVELS.map((l) => /* @__PURE__ */ jsx9("option", { value: l.label, children: l.label }, l.label))
1226
+ }
1227
+ ) }),
1228
+ /* @__PURE__ */ jsxs9("td", { style: styles.td, children: [
1229
+ status(o),
1230
+ o.enrolled && o.backupCodesLeft <= 2 && !o.disabledAt ? /* @__PURE__ */ jsxs9("div", { style: { ...styles.muted, marginTop: 4 }, children: [
1231
+ o.backupCodesLeft,
1232
+ " backup code(s) left"
1233
+ ] }) : null
1234
+ ] }),
1235
+ /* @__PURE__ */ jsx9("td", { style: { ...styles.td, ...styles.muted, whiteSpace: "nowrap" }, children: o.lastLoginAt ? new Date(o.lastLoginAt).toLocaleString() : "never" }),
1236
+ /* @__PURE__ */ jsx9("td", { style: { ...styles.td, textAlign: "right", whiteSpace: "nowrap" }, children: o.email === me ? /* @__PURE__ */ jsx9("span", { style: styles.muted, children: "you" }) : /* @__PURE__ */ jsxs9(Fragment4, { children: [
1237
+ /* @__PURE__ */ jsx9(
1238
+ "button",
1239
+ {
1240
+ type: "button",
1241
+ style: { ...styles.button, height: 28, marginRight: 6 },
1242
+ onClick: () => {
1243
+ if (window.confirm(
1244
+ `Create a recovery link for ${o.email}? It signs them out everywhere; the link sets a new password and a new authenticator.`
1245
+ ))
1246
+ void recover(o.id).then(setMinted).catch(() => {
1247
+ });
1248
+ },
1249
+ children: "Recovery link"
1250
+ }
1251
+ ),
1252
+ /* @__PURE__ */ jsx9(
1253
+ "button",
1254
+ {
1255
+ type: "button",
1256
+ style: {
1257
+ ...o.disabledAt ? styles.button : styles.buttonDanger,
1258
+ height: 28
1259
+ },
1260
+ onClick: () => {
1261
+ if (o.disabledAt || window.confirm(`Disable ${o.email}? They are signed out immediately.`))
1262
+ void update(o.id, { disabled: !o.disabledAt }).catch(() => {
1263
+ });
1264
+ },
1265
+ children: o.disabledAt ? "Enable" : "Disable"
1266
+ }
1267
+ )
1268
+ ] }) })
1269
+ ] }, o.id)) })
1270
+ ] }) : null,
1271
+ report && report.links.length > 0 ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
1272
+ /* @__PURE__ */ jsx9("h2", { style: styles.subtitle, children: "Pending links" }),
1273
+ /* @__PURE__ */ jsx9("ul", { style: styles.list, children: report.links.map((l) => /* @__PURE__ */ jsxs9(
1274
+ "li",
1275
+ {
1276
+ style: { ...styles.row, display: "flex", gap: 12, alignItems: "center" },
1277
+ children: [
1278
+ /* @__PURE__ */ jsx9(Pill, { tone: l.kind === "invite" ? "info" : "warn", dot: false, children: l.kind }),
1279
+ /* @__PURE__ */ jsxs9("span", { style: { flex: 1 }, children: [
1280
+ l.email,
1281
+ l.kind === "invite" ? /* @__PURE__ */ jsxs9("span", { style: styles.muted, children: [
1282
+ " \xB7 ",
1283
+ levelOf(l.scopes)
1284
+ ] }) : null
1285
+ ] }),
1286
+ /* @__PURE__ */ jsxs9("span", { style: styles.muted, children: [
1287
+ "expires ",
1288
+ new Date(l.expiresAt).toLocaleString()
1289
+ ] }),
1290
+ /* @__PURE__ */ jsx9(
1291
+ "button",
1292
+ {
1293
+ type: "button",
1294
+ style: { ...styles.buttonDanger, height: 28 },
1295
+ onClick: () => void revokeLink(l.id).catch(() => {
1296
+ }),
1297
+ children: "Revoke"
1298
+ }
1299
+ )
1300
+ ]
1301
+ },
1302
+ l.id
1303
+ )) })
1304
+ ] }) : null
1305
+ ] });
1306
+ }
1307
+
1308
+ // src/screens/TokensScreen.tsx
1309
+ import { useAdminTokens } from "@fonderie/react-admin";
1310
+ import { useState as useState3 } from "react";
1311
+ import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1080
1312
  var ALL = ["read", "write", "secrets"];
1081
1313
  function TokensScreen({ client }) {
1082
1314
  const { report, isLoading, error, issue, revoke } = useAdminTokens(client);
1083
- const [name, setName] = useState2("");
1084
- const [scopes, setScopes] = useState2(["read"]);
1085
- const [days, setDays] = useState2("");
1086
- const [minted, setMinted] = useState2(null);
1315
+ const [name, setName] = useState3("");
1316
+ const [scopes, setScopes] = useState3(["read"]);
1317
+ const [days, setDays] = useState3("");
1318
+ const [minted, setMinted] = useState3(null);
1087
1319
  const toggle = (s) => setScopes((prev) => prev.includes(s) ? prev.filter((x) => x !== s) : [...prev, s]);
1088
- return /* @__PURE__ */ jsxs9("div", { style: styles.container, children: [
1089
- /* @__PURE__ */ jsx9(
1320
+ return /* @__PURE__ */ jsxs10("div", { style: styles.container, children: [
1321
+ /* @__PURE__ */ jsx10(
1090
1322
  PageHeader,
1091
1323
  {
1092
- title: "Access",
1093
- lead: "Who can open this surface: the root token's strength, scoped tokens you have issued, and any legacy per-module tokens."
1324
+ title: "Tokens",
1325
+ lead: "Credentials for machines: the root token's strength, scoped tokens for the CLI and CI, and any legacy per-module tokens. People sign in as operators."
1094
1326
  }
1095
1327
  ),
1096
- error ? /* @__PURE__ */ jsx9("p", { style: styles.error, role: "alert", children: error.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.explanation }) : null,
1097
- /* @__PURE__ */ jsx9("h2", { style: { ...styles.subtitle, marginTop: 0 }, children: "Root token" }),
1098
- report ? /* @__PURE__ */ jsxs9("div", { style: { ...styles.card, display: "flex", gap: 12, alignItems: "flex-start" }, children: [
1099
- /* @__PURE__ */ jsx9(Icon, { name: "lock", size: 18, style: { marginTop: 2 } }),
1100
- /* @__PURE__ */ jsxs9("div", { children: [
1101
- /* @__PURE__ */ jsx9(Pill, { tone: report.admin.ok ? "ok" : "bad", children: report.admin.ok ? "strong" : "weak" }),
1102
- report.admin.problems.map((p) => /* @__PURE__ */ jsx9(
1328
+ error ? /* @__PURE__ */ jsx10("p", { style: styles.error, role: "alert", children: error.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.explanation }) : null,
1329
+ /* @__PURE__ */ jsx10("h2", { style: { ...styles.subtitle, marginTop: 0 }, children: "Root token" }),
1330
+ report ? /* @__PURE__ */ jsxs10("div", { style: { ...styles.card, display: "flex", gap: 12, alignItems: "flex-start" }, children: [
1331
+ /* @__PURE__ */ jsx10(Icon, { name: "lock", size: 18, style: { marginTop: 2 } }),
1332
+ /* @__PURE__ */ jsxs10("div", { children: [
1333
+ /* @__PURE__ */ jsx10(Pill, { tone: report.admin.ok ? "ok" : "bad", children: report.admin.ok ? "strong" : "weak" }),
1334
+ report.admin.problems.map((p) => /* @__PURE__ */ jsx10(
1103
1335
  "div",
1104
1336
  {
1105
1337
  style: { marginTop: 6, color: "var(--fonderie-danger,#e00)", fontSize: 13 },
@@ -1109,9 +1341,9 @@ function TokensScreen({ client }) {
1109
1341
  ))
1110
1342
  ] })
1111
1343
  ] }) : null,
1112
- /* @__PURE__ */ jsx9("h2", { style: styles.subtitle, children: "Issued tokens" }),
1113
- report?.issued === null ? /* @__PURE__ */ jsx9("p", { style: styles.muted, children: "Issuing is off \u2014 give AdminModule a store and run its migrations." }) : /* @__PURE__ */ jsxs9(Fragment4, { children: [
1114
- minted ? /* @__PURE__ */ jsxs9(
1344
+ /* @__PURE__ */ jsx10("h2", { style: styles.subtitle, children: "Issued tokens" }),
1345
+ report?.issued === null ? /* @__PURE__ */ jsx10("p", { style: styles.muted, children: "Issuing is off \u2014 give AdminModule a store and run its migrations." }) : /* @__PURE__ */ jsxs10(Fragment5, { children: [
1346
+ minted ? /* @__PURE__ */ jsxs10(
1115
1347
  "div",
1116
1348
  {
1117
1349
  style: {
@@ -1120,11 +1352,11 @@ function TokensScreen({ client }) {
1120
1352
  borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1121
1353
  },
1122
1354
  children: [
1123
- /* @__PURE__ */ jsx9("strong", { children: "Copy this now \u2014 it is never shown again." }),
1355
+ /* @__PURE__ */ jsx10("strong", { children: "Copy this now \u2014 it is never shown again." }),
1124
1356
  " (",
1125
1357
  minted.name,
1126
1358
  ")",
1127
- /* @__PURE__ */ jsx9(
1359
+ /* @__PURE__ */ jsx10(
1128
1360
  "div",
1129
1361
  {
1130
1362
  style: {
@@ -1140,7 +1372,7 @@ function TokensScreen({ client }) {
1140
1372
  ]
1141
1373
  }
1142
1374
  ) : null,
1143
- /* @__PURE__ */ jsxs9(
1375
+ /* @__PURE__ */ jsxs10(
1144
1376
  "form",
1145
1377
  {
1146
1378
  style: styles.toolbar,
@@ -1159,7 +1391,7 @@ function TokensScreen({ client }) {
1159
1391
  });
1160
1392
  },
1161
1393
  children: [
1162
- /* @__PURE__ */ jsx9(
1394
+ /* @__PURE__ */ jsx10(
1163
1395
  "input",
1164
1396
  {
1165
1397
  value: name,
@@ -1169,7 +1401,7 @@ function TokensScreen({ client }) {
1169
1401
  "aria-label": "Name"
1170
1402
  }
1171
1403
  ),
1172
- ALL.map((s) => /* @__PURE__ */ jsxs9(
1404
+ ALL.map((s) => /* @__PURE__ */ jsxs10(
1173
1405
  "label",
1174
1406
  {
1175
1407
  style: {
@@ -1181,14 +1413,14 @@ function TokensScreen({ client }) {
1181
1413
  fontSize: 13
1182
1414
  },
1183
1415
  children: [
1184
- /* @__PURE__ */ jsx9("input", { type: "checkbox", checked: scopes.includes(s), onChange: () => toggle(s) }),
1416
+ /* @__PURE__ */ jsx10("input", { type: "checkbox", checked: scopes.includes(s), onChange: () => toggle(s) }),
1185
1417
  " ",
1186
1418
  s
1187
1419
  ]
1188
1420
  },
1189
1421
  s
1190
1422
  )),
1191
- /* @__PURE__ */ jsx9(
1423
+ /* @__PURE__ */ jsx10(
1192
1424
  "input",
1193
1425
  {
1194
1426
  value: days,
@@ -1198,14 +1430,14 @@ function TokensScreen({ client }) {
1198
1430
  "aria-label": "Days"
1199
1431
  }
1200
1432
  ),
1201
- /* @__PURE__ */ jsxs9(
1433
+ /* @__PURE__ */ jsxs10(
1202
1434
  "button",
1203
1435
  {
1204
1436
  type: "submit",
1205
1437
  style: styles.buttonPrimary,
1206
1438
  disabled: !name.trim() || scopes.length === 0 || isLoading,
1207
1439
  children: [
1208
- /* @__PURE__ */ jsx9(Icon, { name: "plus", size: 14 }),
1440
+ /* @__PURE__ */ jsx10(Icon, { name: "plus", size: 14 }),
1209
1441
  "Issue token"
1210
1442
  ]
1211
1443
  }
@@ -1213,32 +1445,32 @@ function TokensScreen({ client }) {
1213
1445
  ]
1214
1446
  }
1215
1447
  ),
1216
- report?.issued && report.issued.length > 0 ? /* @__PURE__ */ jsxs9("table", { style: styles.table, children: [
1217
- /* @__PURE__ */ jsx9("thead", { children: /* @__PURE__ */ jsxs9("tr", { children: [
1218
- /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Name" }),
1219
- /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Scopes" }),
1220
- /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Created" }),
1221
- /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Expires" }),
1222
- /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Last used" }),
1223
- /* @__PURE__ */ jsx9("th", { style: styles.th })
1448
+ report?.issued && report.issued.length > 0 ? /* @__PURE__ */ jsxs10("table", { style: styles.table, children: [
1449
+ /* @__PURE__ */ jsx10("thead", { children: /* @__PURE__ */ jsxs10("tr", { children: [
1450
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Name" }),
1451
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Scopes" }),
1452
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Created" }),
1453
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Expires" }),
1454
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Last used" }),
1455
+ /* @__PURE__ */ jsx10("th", { style: styles.th })
1224
1456
  ] }) }),
1225
- /* @__PURE__ */ jsx9("tbody", { children: report.issued.map((t2) => /* @__PURE__ */ jsxs9("tr", { children: [
1226
- /* @__PURE__ */ jsxs9("td", { style: styles.td, children: [
1457
+ /* @__PURE__ */ jsx10("tbody", { children: report.issued.map((t2) => /* @__PURE__ */ jsxs10("tr", { children: [
1458
+ /* @__PURE__ */ jsxs10("td", { style: styles.td, children: [
1227
1459
  t2.name,
1228
- t2.revokedAt ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
1460
+ t2.revokedAt ? /* @__PURE__ */ jsxs10(Fragment5, { children: [
1229
1461
  " ",
1230
- /* @__PURE__ */ jsx9(Pill, { tone: "neutral", children: "revoked" })
1462
+ /* @__PURE__ */ jsx10(Pill, { tone: "neutral", children: "revoked" })
1231
1463
  ] }) : null
1232
1464
  ] }),
1233
- /* @__PURE__ */ jsx9("td", { style: { ...styles.td, ...styles.mono }, children: t2.scopes.join(", ") }),
1234
- /* @__PURE__ */ jsxs9("td", { style: { ...styles.td, ...styles.muted }, children: [
1465
+ /* @__PURE__ */ jsx10("td", { style: { ...styles.td, ...styles.mono }, children: t2.scopes.join(", ") }),
1466
+ /* @__PURE__ */ jsxs10("td", { style: { ...styles.td, ...styles.muted }, children: [
1235
1467
  new Date(t2.createdAt).toLocaleDateString(),
1236
1468
  " \xB7 ",
1237
1469
  t2.createdBy
1238
1470
  ] }),
1239
- /* @__PURE__ */ jsx9("td", { style: { ...styles.td, ...styles.muted }, children: t2.expiresAt ? new Date(t2.expiresAt).toLocaleDateString() : "never" }),
1240
- /* @__PURE__ */ jsx9("td", { style: { ...styles.td, ...styles.muted }, children: t2.lastUsedAt ? new Date(t2.lastUsedAt).toLocaleString() : "never" }),
1241
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: t2.revokedAt ? null : /* @__PURE__ */ jsx9(
1471
+ /* @__PURE__ */ jsx10("td", { style: { ...styles.td, ...styles.muted }, children: t2.expiresAt ? new Date(t2.expiresAt).toLocaleDateString() : "never" }),
1472
+ /* @__PURE__ */ jsx10("td", { style: { ...styles.td, ...styles.muted }, children: t2.lastUsedAt ? new Date(t2.lastUsedAt).toLocaleString() : "never" }),
1473
+ /* @__PURE__ */ jsx10("td", { style: styles.td, children: t2.revokedAt ? null : /* @__PURE__ */ jsx10(
1242
1474
  "button",
1243
1475
  {
1244
1476
  type: "button",
@@ -1254,13 +1486,13 @@ function TokensScreen({ client }) {
1254
1486
  }
1255
1487
  ) })
1256
1488
  ] }, t2.id)) })
1257
- ] }) : /* @__PURE__ */ jsx9(Empty, { icon: "tokens", title: "No scoped tokens yet", children: "Issue a read-only token for a dashboard or a teammate instead of sharing the root." })
1489
+ ] }) : /* @__PURE__ */ jsx10(Empty, { icon: "tokens", title: "No scoped tokens yet", children: "Issue a read-only token for a dashboard or a teammate instead of sharing the root." })
1258
1490
  ] }),
1259
- /* @__PURE__ */ jsx9("h2", { style: styles.subtitle, children: "Legacy per-brick tokens" }),
1260
- report && report.legacy.length === 0 ? /* @__PURE__ */ jsx9("p", { style: styles.muted, children: "None \u2014 every brick's admin surface goes through this token." }) : /* @__PURE__ */ jsx9("ul", { style: styles.list, children: report?.legacy.map((l) => /* @__PURE__ */ jsxs9("li", { style: styles.row, children: [
1261
- /* @__PURE__ */ jsx9("span", { style: styles.mono, children: l.module }),
1491
+ /* @__PURE__ */ jsx10("h2", { style: styles.subtitle, children: "Legacy per-brick tokens" }),
1492
+ report && report.legacy.length === 0 ? /* @__PURE__ */ jsx10("p", { style: styles.muted, children: "None \u2014 every brick's admin surface goes through this token." }) : /* @__PURE__ */ jsx10("ul", { style: styles.list, children: report?.legacy.map((l) => /* @__PURE__ */ jsxs10("li", { style: styles.row, children: [
1493
+ /* @__PURE__ */ jsx10("span", { style: styles.mono, children: l.module }),
1262
1494
  " ",
1263
- /* @__PURE__ */ jsx9("span", { style: styles.muted, children: "still registers its standalone routes with its own token (deprecated)" })
1495
+ /* @__PURE__ */ jsx10("span", { style: styles.muted, children: "still registers its standalone routes with its own token (deprecated)" })
1264
1496
  ] }, l.module)) })
1265
1497
  ] });
1266
1498
  }
@@ -1273,11 +1505,218 @@ import {
1273
1505
  useAdminUserSessions,
1274
1506
  useAdminUsers
1275
1507
  } from "@fonderie/react-admin";
1276
- import { useState as useState3 } from "react";
1277
- import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1278
- function UsersScreen({ client, pageSize = 50 }) {
1279
- const [input, setInput] = useState3("");
1280
- const [selected, setSelected] = useState3(null);
1508
+ import { useState as useState6 } from "react";
1509
+
1510
+ // src/billing.ts
1511
+ import { useEffect, useState as useState4 } from "react";
1512
+ function periodEnd(s) {
1513
+ if (!s.currentPeriodEnd) return null;
1514
+ const date = new Date(s.currentPeriodEnd).toLocaleDateString();
1515
+ if (s.status === "canceled")
1516
+ return { label: new Date(s.currentPeriodEnd) < /* @__PURE__ */ new Date() ? "Ended" : "Ends", date };
1517
+ if (s.cancelAtPeriodEnd) return { label: "Ends", date };
1518
+ return { label: "Renews", date };
1519
+ }
1520
+ var statusTone = (status) => status === "active" ? "ok" : status === "trialing" ? "info" : status === "past_due" || status === "unpaid" ? "warn" : "neutral";
1521
+ function useSubscriptionIndex(client) {
1522
+ const [index, setIndex] = useState4(null);
1523
+ useEffect(() => {
1524
+ if (!client) return;
1525
+ let live = true;
1526
+ (async () => {
1527
+ const map = /* @__PURE__ */ new Map();
1528
+ let cursor;
1529
+ for (let page = 0; page < 20; page++) {
1530
+ const { result } = await client.listSubscriptions({
1531
+ limit: 100,
1532
+ ...cursor ? { cursor } : {}
1533
+ });
1534
+ for (const s of result.subscriptions) {
1535
+ const key = `${s.subscriberType}/${s.subscriberId}`;
1536
+ if (!map.has(key)) map.set(key, s);
1537
+ }
1538
+ if (!result.nextCursor) break;
1539
+ cursor = result.nextCursor;
1540
+ }
1541
+ if (live) setIndex(map);
1542
+ })().catch(() => {
1543
+ if (live) setIndex(/* @__PURE__ */ new Map());
1544
+ });
1545
+ return () => {
1546
+ live = false;
1547
+ };
1548
+ }, [client]);
1549
+ return index;
1550
+ }
1551
+
1552
+ // src/screens/SubscriberBilling.tsx
1553
+ import { useAdminSubscriber } from "@fonderie/react-admin";
1554
+ import { useState as useState5 } from "react";
1555
+ import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1556
+ function SubscriberBilling({ client, subscriber }) {
1557
+ const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(client, subscriber, { limit: 20 });
1558
+ const [amount, setAmount] = useState5("");
1559
+ const [note, setNote] = useState5("");
1560
+ const [granted, setGranted] = useState5(null);
1561
+ if (isLoading && !wallet && !subscription) return /* @__PURE__ */ jsx11("p", { style: styles.status, children: "Loading billing\u2026" });
1562
+ const end = subscription ? periodEnd(subscription) : null;
1563
+ return /* @__PURE__ */ jsxs11(Fragment6, { children: [
1564
+ error ? /* @__PURE__ */ jsx11("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
1565
+ /* @__PURE__ */ jsxs11("div", { style: { ...styles.grid, marginBottom: 16 }, children: [
1566
+ /* @__PURE__ */ jsxs11("div", { style: styles.card, children: [
1567
+ /* @__PURE__ */ jsx11("div", { style: styles.statLabel, children: "Plan" }),
1568
+ /* @__PURE__ */ jsxs11(
1569
+ "div",
1570
+ {
1571
+ style: {
1572
+ display: "flex",
1573
+ alignItems: "center",
1574
+ gap: 8,
1575
+ marginTop: 6,
1576
+ flexWrap: "wrap"
1577
+ },
1578
+ children: [
1579
+ /* @__PURE__ */ jsx11("strong", { style: { fontSize: 18 }, children: subscription ? subscription.plan : "Free" }),
1580
+ subscription ? /* @__PURE__ */ jsxs11(Fragment6, { children: [
1581
+ /* @__PURE__ */ jsxs11("span", { style: styles.muted, children: [
1582
+ "\xB7 ",
1583
+ subscription.interval
1584
+ ] }),
1585
+ /* @__PURE__ */ jsx11(Pill, { tone: statusTone(subscription.status), children: subscription.status })
1586
+ ] }) : /* @__PURE__ */ jsx11("span", { style: styles.muted, children: "no subscription" })
1587
+ ]
1588
+ }
1589
+ ),
1590
+ end ? /* @__PURE__ */ jsxs11("div", { style: styles.statHint, children: [
1591
+ end.label,
1592
+ " ",
1593
+ end.date
1594
+ ] }) : null,
1595
+ subscription?.providerSubscriptionId ? /* @__PURE__ */ jsx11("div", { style: { ...styles.statHint, fontFamily: "var(--fonderie-mono,monospace)" }, children: subscription.providerSubscriptionId }) : null
1596
+ ] }),
1597
+ /* @__PURE__ */ jsxs11("div", { style: styles.card, children: [
1598
+ /* @__PURE__ */ jsx11("div", { style: styles.statLabel, children: "Credits" }),
1599
+ wallet ? /* @__PURE__ */ jsxs11(Fragment6, { children: [
1600
+ /* @__PURE__ */ jsxs11("div", { style: styles.statValue, children: [
1601
+ wallet.balance,
1602
+ " ",
1603
+ /* @__PURE__ */ jsx11("span", { style: { fontSize: 14, fontWeight: 500 }, children: wallet.currency })
1604
+ ] }),
1605
+ /* @__PURE__ */ jsxs11("div", { style: styles.statHint, children: [
1606
+ wallet.purchased ?? "0",
1607
+ " permanent \xB7 ",
1608
+ wallet.granted ?? "0",
1609
+ " plan allowance",
1610
+ wallet.grantedExpiresAt ? ` (expires ${new Date(wallet.grantedExpiresAt).toLocaleDateString()})` : ""
1611
+ ] })
1612
+ ] }) : /* @__PURE__ */ jsx11("div", { style: styles.statHint, children: "No wallet \u2014 billing has no wallet configuration." })
1613
+ ] })
1614
+ ] }),
1615
+ wallet ? /* @__PURE__ */ jsxs11(
1616
+ "form",
1617
+ {
1618
+ style: {
1619
+ ...styles.card,
1620
+ display: "flex",
1621
+ gap: 8,
1622
+ alignItems: "center",
1623
+ flexWrap: "wrap",
1624
+ marginBottom: 16
1625
+ },
1626
+ onSubmit: (e) => {
1627
+ e.preventDefault();
1628
+ const key = `admin-grant-${subscriber.type}-${subscriber.id}-${Date.now()}`;
1629
+ void grant({
1630
+ amount: amount.trim(),
1631
+ ...note ? { description: note } : {},
1632
+ idempotencyKey: key
1633
+ }).then(() => {
1634
+ setGranted(`Granted ${amount.trim()} ${wallet.currency}.`);
1635
+ setAmount("");
1636
+ setNote("");
1637
+ }).catch(() => {
1638
+ });
1639
+ },
1640
+ children: [
1641
+ /* @__PURE__ */ jsx11("strong", { style: { fontSize: 13.5, marginRight: 4 }, children: "Grant credits" }),
1642
+ /* @__PURE__ */ jsx11(
1643
+ "input",
1644
+ {
1645
+ value: amount,
1646
+ onChange: (e) => setAmount(e.target.value),
1647
+ placeholder: `amount (${wallet.currency})`,
1648
+ inputMode: "numeric",
1649
+ style: { ...styles.input, width: 150 },
1650
+ "aria-label": "Amount"
1651
+ }
1652
+ ),
1653
+ /* @__PURE__ */ jsx11(
1654
+ "input",
1655
+ {
1656
+ value: note,
1657
+ onChange: (e) => setNote(e.target.value),
1658
+ placeholder: "reason (optional)",
1659
+ style: { ...styles.input, flex: 1, minWidth: 180 },
1660
+ "aria-label": "Reason"
1661
+ }
1662
+ ),
1663
+ /* @__PURE__ */ jsxs11(
1664
+ "button",
1665
+ {
1666
+ type: "submit",
1667
+ style: styles.buttonPrimary,
1668
+ disabled: !/^\d+$/.test(amount.trim()),
1669
+ children: [
1670
+ /* @__PURE__ */ jsx11(Icon, { name: "plus", size: 14 }),
1671
+ "Grant"
1672
+ ]
1673
+ }
1674
+ ),
1675
+ granted ? /* @__PURE__ */ jsx11("span", { style: styles.ok, children: granted }) : null
1676
+ ]
1677
+ }
1678
+ ) : null,
1679
+ wallet ? ledger.length === 0 ? /* @__PURE__ */ jsx11(Empty, { icon: "subscriber", title: "No credit movements yet" }) : /* @__PURE__ */ jsxs11(Fragment6, { children: [
1680
+ /* @__PURE__ */ jsxs11("table", { style: styles.table, children: [
1681
+ /* @__PURE__ */ jsx11("thead", { children: /* @__PURE__ */ jsxs11("tr", { children: [
1682
+ /* @__PURE__ */ jsx11("th", { style: styles.th, children: "When" }),
1683
+ /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Kind" }),
1684
+ /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Amount" }),
1685
+ /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Note" })
1686
+ ] }) }),
1687
+ /* @__PURE__ */ jsx11("tbody", { children: ledger.map((t2) => /* @__PURE__ */ jsxs11("tr", { children: [
1688
+ /* @__PURE__ */ jsx11("td", { style: { ...styles.td, ...styles.muted, whiteSpace: "nowrap" }, children: new Date(t2.createdAt).toLocaleString() }),
1689
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: t2.type }),
1690
+ /* @__PURE__ */ jsx11("td", { style: { ...styles.td, ...styles.mono }, children: t2.amount }),
1691
+ /* @__PURE__ */ jsx11("td", { style: { ...styles.td, ...styles.muted }, children: t2.description ?? "" })
1692
+ ] }, t2.id)) })
1693
+ ] }),
1694
+ hasMoreLedger ? /* @__PURE__ */ jsx11(
1695
+ "button",
1696
+ {
1697
+ type: "button",
1698
+ style: { ...styles.button, marginTop: 8 },
1699
+ onClick: () => void loadMoreLedger(),
1700
+ children: "Load more"
1701
+ }
1702
+ ) : null
1703
+ ] }) : null
1704
+ ] });
1705
+ }
1706
+
1707
+ // src/screens/UsersScreen.tsx
1708
+ import { Fragment as Fragment7, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1709
+ function UsersScreen({
1710
+ client,
1711
+ pageSize = 50,
1712
+ billingClient,
1713
+ openUserId
1714
+ }) {
1715
+ const [input, setInput] = useState6("");
1716
+ const [selected, setSelected] = useState6(
1717
+ openUserId ? { id: openUserId } : null
1718
+ );
1719
+ const plans = useSubscriptionIndex(billingClient);
1281
1720
  const list = useAdminUsers(client, { limit: pageSize });
1282
1721
  const { user, isLoading, error, suspend, unsuspend, revokeSessions } = useAdminUser(
1283
1722
  client,
@@ -1286,20 +1725,20 @@ function UsersScreen({ client, pageSize = 50 }) {
1286
1725
  const userId = user?.id ?? null;
1287
1726
  const sessions = useAdminUserSessions(client, userId);
1288
1727
  const history = useAdminLoginHistory(client, userId, { limit: 20 });
1289
- const yesNo = (v) => v ? /* @__PURE__ */ jsx10(Pill, { tone: "ok", children: "yes" }) : /* @__PURE__ */ jsx10(Pill, { tone: "neutral", children: "no" });
1728
+ const yesNo = (v) => v ? /* @__PURE__ */ jsx12(Pill, { tone: "ok", children: "yes" }) : /* @__PURE__ */ jsx12(Pill, { tone: "neutral", children: "no" });
1290
1729
  const clear = () => {
1291
1730
  setSelected(null);
1292
1731
  setInput("");
1293
1732
  };
1294
- return /* @__PURE__ */ jsxs10("div", { style: styles.container, children: [
1295
- /* @__PURE__ */ jsx10(
1733
+ return /* @__PURE__ */ jsxs12("div", { style: styles.container, children: [
1734
+ /* @__PURE__ */ jsx12(
1296
1735
  PageHeader,
1297
1736
  {
1298
1737
  title: "Users",
1299
1738
  lead: "Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
1300
1739
  }
1301
1740
  ),
1302
- /* @__PURE__ */ jsxs10(
1741
+ /* @__PURE__ */ jsxs12(
1303
1742
  "form",
1304
1743
  {
1305
1744
  style: styles.toolbar,
@@ -1309,7 +1748,7 @@ function UsersScreen({ client, pageSize = 50 }) {
1309
1748
  setSelected(email ? { email } : null);
1310
1749
  },
1311
1750
  children: [
1312
- /* @__PURE__ */ jsx10(
1751
+ /* @__PURE__ */ jsx12(
1313
1752
  "input",
1314
1753
  {
1315
1754
  type: "email",
@@ -1320,14 +1759,14 @@ function UsersScreen({ client, pageSize = 50 }) {
1320
1759
  "aria-label": "Email"
1321
1760
  }
1322
1761
  ),
1323
- /* @__PURE__ */ jsxs10("button", { type: "submit", style: styles.buttonPrimary, disabled: isLoading, children: [
1324
- /* @__PURE__ */ jsx10(Icon, { name: "search", size: 14 }),
1762
+ /* @__PURE__ */ jsxs12("button", { type: "submit", style: styles.buttonPrimary, disabled: isLoading, children: [
1763
+ /* @__PURE__ */ jsx12(Icon, { name: "search", size: 14 }),
1325
1764
  "Look up"
1326
1765
  ] }),
1327
- selected ? /* @__PURE__ */ jsxs10("button", { type: "button", style: styles.buttonGhost, onClick: clear, children: [
1328
- /* @__PURE__ */ jsx10(Icon, { name: "back", size: 14 }),
1766
+ selected ? /* @__PURE__ */ jsxs12("button", { type: "button", style: styles.buttonGhost, onClick: clear, children: [
1767
+ /* @__PURE__ */ jsx12(Icon, { name: "back", size: 14 }),
1329
1768
  "All users"
1330
- ] }) : /* @__PURE__ */ jsx10(
1769
+ ] }) : /* @__PURE__ */ jsx12(
1331
1770
  "button",
1332
1771
  {
1333
1772
  type: "button",
@@ -1340,18 +1779,19 @@ function UsersScreen({ client, pageSize = 50 }) {
1340
1779
  ]
1341
1780
  }
1342
1781
  ),
1343
- error ? /* @__PURE__ */ jsx10("p", { style: styles.error, role: "alert", children: error.status === 404 ? "No user with that email." : error.explanation }) : null,
1344
- !selected ? /* @__PURE__ */ jsxs10(Fragment5, { children: [
1345
- list.error ? /* @__PURE__ */ jsx10("p", { style: styles.error, role: "alert", children: list.error.explanation }) : null,
1346
- list.users.length === 0 && !list.isLoading ? /* @__PURE__ */ jsx10(Empty, { icon: "users", title: "No users yet", children: "Sign-ups appear here as they happen." }) : /* @__PURE__ */ jsxs10("table", { style: styles.table, children: [
1347
- /* @__PURE__ */ jsx10("thead", { children: /* @__PURE__ */ jsxs10("tr", { children: [
1348
- /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Email" }),
1349
- /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Name" }),
1350
- /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Created" }),
1351
- /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Status" })
1782
+ error ? /* @__PURE__ */ jsx12("p", { style: styles.error, role: "alert", children: error.status === 404 ? "No user with that email." : error.explanation }) : null,
1783
+ !selected ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
1784
+ list.error ? /* @__PURE__ */ jsx12("p", { style: styles.error, role: "alert", children: list.error.explanation }) : null,
1785
+ list.users.length === 0 && !list.isLoading ? /* @__PURE__ */ jsx12(Empty, { icon: "users", title: "No users yet", children: "Sign-ups appear here as they happen." }) : /* @__PURE__ */ jsxs12("table", { style: styles.table, children: [
1786
+ /* @__PURE__ */ jsx12("thead", { children: /* @__PURE__ */ jsxs12("tr", { children: [
1787
+ /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Email" }),
1788
+ /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Name" }),
1789
+ billingClient ? /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Plan" }) : null,
1790
+ /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Created" }),
1791
+ /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Status" })
1352
1792
  ] }) }),
1353
- /* @__PURE__ */ jsx10("tbody", { children: list.users.map((u) => /* @__PURE__ */ jsxs10("tr", { children: [
1354
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: /* @__PURE__ */ jsx10(
1793
+ /* @__PURE__ */ jsx12("tbody", { children: list.users.map((u) => /* @__PURE__ */ jsxs12("tr", { children: [
1794
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: /* @__PURE__ */ jsx12(
1355
1795
  "button",
1356
1796
  {
1357
1797
  type: "button",
@@ -1360,17 +1800,28 @@ function UsersScreen({ client, pageSize = 50 }) {
1360
1800
  children: u.email
1361
1801
  }
1362
1802
  ) }),
1363
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: `${u.firstName ?? ""} ${u.lastName ?? ""}`.trim() || /* @__PURE__ */ jsx10("span", { style: styles.muted, children: "\u2014" }) }),
1364
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: new Date(u.createdAt).toLocaleDateString() }),
1365
- /* @__PURE__ */ jsxs10("td", { style: styles.td, children: [
1366
- u.suspended ? /* @__PURE__ */ jsx10(Pill, { tone: "warn", children: "suspended" }) : null,
1367
- u.deletedAt ? /* @__PURE__ */ jsx10(Pill, { tone: "neutral", children: "deleted" }) : null,
1368
- !u.suspended && !u.deletedAt ? /* @__PURE__ */ jsx10(Pill, { tone: "ok", children: "active" }) : null
1803
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: `${u.firstName ?? ""} ${u.lastName ?? ""}`.trim() || /* @__PURE__ */ jsx12("span", { style: styles.muted, children: "\u2014" }) }),
1804
+ billingClient ? /* @__PURE__ */ jsx12("td", { style: styles.td, children: (() => {
1805
+ const sub = plans?.get(`user/${u.id}`);
1806
+ if (!plans) return /* @__PURE__ */ jsx12("span", { style: styles.muted, children: "\u2026" });
1807
+ if (!sub || sub.status === "canceled")
1808
+ return /* @__PURE__ */ jsx12("span", { style: styles.muted, children: "free" });
1809
+ return /* @__PURE__ */ jsxs12(Fragment7, { children: [
1810
+ /* @__PURE__ */ jsx12("strong", { children: sub.plan }),
1811
+ " ",
1812
+ sub.status !== "active" ? /* @__PURE__ */ jsx12(Pill, { tone: statusTone(sub.status), children: sub.status }) : null
1813
+ ] });
1814
+ })() }) : null,
1815
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: new Date(u.createdAt).toLocaleDateString() }),
1816
+ /* @__PURE__ */ jsxs12("td", { style: styles.td, children: [
1817
+ u.suspended ? /* @__PURE__ */ jsx12(Pill, { tone: "warn", children: "suspended" }) : null,
1818
+ u.deletedAt ? /* @__PURE__ */ jsx12(Pill, { tone: "neutral", children: "deleted" }) : null,
1819
+ !u.suspended && !u.deletedAt ? /* @__PURE__ */ jsx12(Pill, { tone: "ok", children: "active" }) : null
1369
1820
  ] })
1370
1821
  ] }, u.id)) })
1371
1822
  ] }),
1372
- list.isLoading ? /* @__PURE__ */ jsx10("p", { style: styles.status, children: "Loading\u2026" }) : null,
1373
- list.hasMore && !list.isLoading ? /* @__PURE__ */ jsx10(
1823
+ list.isLoading ? /* @__PURE__ */ jsx12("p", { style: styles.status, children: "Loading\u2026" }) : null,
1824
+ list.hasMore && !list.isLoading ? /* @__PURE__ */ jsx12(
1374
1825
  "button",
1375
1826
  {
1376
1827
  type: "button",
@@ -1380,48 +1831,48 @@ function UsersScreen({ client, pageSize = 50 }) {
1380
1831
  }
1381
1832
  ) : null
1382
1833
  ] }) : null,
1383
- user ? /* @__PURE__ */ jsxs10(Fragment5, { children: [
1384
- /* @__PURE__ */ jsxs10("h2", { style: styles.subtitle, children: [
1834
+ user ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
1835
+ /* @__PURE__ */ jsxs12("h2", { style: styles.subtitle, children: [
1385
1836
  user.firstName || user.lastName ? `${user.firstName} ${user.lastName}`.trim() : user.email,
1386
1837
  " ",
1387
- user.suspended ? /* @__PURE__ */ jsx10(Pill, { tone: "warn", children: "suspended" }) : null,
1388
- user.deletedAt ? /* @__PURE__ */ jsx10(Pill, { tone: "neutral", children: "deleted" }) : null
1838
+ user.suspended ? /* @__PURE__ */ jsx12(Pill, { tone: "warn", children: "suspended" }) : null,
1839
+ user.deletedAt ? /* @__PURE__ */ jsx12(Pill, { tone: "neutral", children: "deleted" }) : null
1389
1840
  ] }),
1390
- /* @__PURE__ */ jsx10("table", { style: styles.table, children: /* @__PURE__ */ jsxs10("tbody", { children: [
1391
- /* @__PURE__ */ jsxs10("tr", { children: [
1392
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: "id" }),
1393
- /* @__PURE__ */ jsx10("td", { style: { ...styles.td, ...styles.mono }, children: user.id })
1841
+ /* @__PURE__ */ jsx12("table", { style: styles.table, children: /* @__PURE__ */ jsxs12("tbody", { children: [
1842
+ /* @__PURE__ */ jsxs12("tr", { children: [
1843
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: "id" }),
1844
+ /* @__PURE__ */ jsx12("td", { style: { ...styles.td, ...styles.mono }, children: user.id })
1394
1845
  ] }),
1395
- /* @__PURE__ */ jsxs10("tr", { children: [
1396
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: "email" }),
1397
- /* @__PURE__ */ jsxs10("td", { style: styles.td, children: [
1846
+ /* @__PURE__ */ jsxs12("tr", { children: [
1847
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: "email" }),
1848
+ /* @__PURE__ */ jsxs12("td", { style: styles.td, children: [
1398
1849
  user.email,
1399
1850
  " \xB7 verified ",
1400
1851
  yesNo(user.isEmailVerified)
1401
1852
  ] })
1402
1853
  ] }),
1403
- /* @__PURE__ */ jsxs10("tr", { children: [
1404
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: "MFA" }),
1405
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: yesNo(user.mfaEnabled) })
1854
+ /* @__PURE__ */ jsxs12("tr", { children: [
1855
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: "MFA" }),
1856
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: yesNo(user.mfaEnabled) })
1406
1857
  ] }),
1407
- /* @__PURE__ */ jsxs10("tr", { children: [
1408
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: "provider" }),
1409
- /* @__PURE__ */ jsxs10("td", { style: styles.td, children: [
1858
+ /* @__PURE__ */ jsxs12("tr", { children: [
1859
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: "provider" }),
1860
+ /* @__PURE__ */ jsxs12("td", { style: styles.td, children: [
1410
1861
  user.provider || "password",
1411
1862
  user.hasPassword ? "" : " \xB7 no password set"
1412
1863
  ] })
1413
1864
  ] }),
1414
- /* @__PURE__ */ jsxs10("tr", { children: [
1415
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: "last login" }),
1416
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: user.lastLogin ? new Date(user.lastLogin).toLocaleString() : /* @__PURE__ */ jsx10("span", { style: styles.muted, children: "never" }) })
1865
+ /* @__PURE__ */ jsxs12("tr", { children: [
1866
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: "last login" }),
1867
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: user.lastLogin ? new Date(user.lastLogin).toLocaleString() : /* @__PURE__ */ jsx12("span", { style: styles.muted, children: "never" }) })
1417
1868
  ] }),
1418
- /* @__PURE__ */ jsxs10("tr", { children: [
1419
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: "created" }),
1420
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: new Date(user.createdAt).toLocaleString() })
1869
+ /* @__PURE__ */ jsxs12("tr", { children: [
1870
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: "created" }),
1871
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: new Date(user.createdAt).toLocaleString() })
1421
1872
  ] })
1422
1873
  ] }) }),
1423
- /* @__PURE__ */ jsxs10("div", { style: { ...styles.toolbar, marginTop: 12 }, children: [
1424
- user.suspended ? /* @__PURE__ */ jsx10(
1874
+ /* @__PURE__ */ jsxs12("div", { style: { ...styles.toolbar, marginTop: 12 }, children: [
1875
+ user.suspended ? /* @__PURE__ */ jsx12(
1425
1876
  "button",
1426
1877
  {
1427
1878
  type: "button",
@@ -1430,7 +1881,7 @@ function UsersScreen({ client, pageSize = 50 }) {
1430
1881
  disabled: isLoading,
1431
1882
  children: "Unsuspend"
1432
1883
  }
1433
- ) : /* @__PURE__ */ jsx10(
1884
+ ) : /* @__PURE__ */ jsx12(
1434
1885
  "button",
1435
1886
  {
1436
1887
  type: "button",
@@ -1440,7 +1891,7 @@ function UsersScreen({ client, pageSize = 50 }) {
1440
1891
  children: "Suspend"
1441
1892
  }
1442
1893
  ),
1443
- /* @__PURE__ */ jsx10(
1894
+ /* @__PURE__ */ jsx12(
1444
1895
  "button",
1445
1896
  {
1446
1897
  type: "button",
@@ -1451,38 +1902,48 @@ function UsersScreen({ client, pageSize = 50 }) {
1451
1902
  }
1452
1903
  )
1453
1904
  ] }),
1454
- /* @__PURE__ */ jsx10("h2", { style: styles.subtitle, children: "Live sessions" }),
1455
- sessions.sessions.length === 0 ? /* @__PURE__ */ jsx10("p", { style: styles.muted, children: "None." }) : /* @__PURE__ */ jsx10("ul", { style: styles.list, children: sessions.sessions.map((s) => /* @__PURE__ */ jsxs10("li", { style: styles.row, children: [
1456
- /* @__PURE__ */ jsx10("span", { style: styles.mono, children: s.ipAddress ?? "\u2014" }),
1905
+ billingClient ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
1906
+ /* @__PURE__ */ jsx12("h2", { style: styles.subtitle, children: "Plan & credits" }),
1907
+ /* @__PURE__ */ jsx12(
1908
+ SubscriberBilling,
1909
+ {
1910
+ client: billingClient,
1911
+ subscriber: { type: "user", id: user.id }
1912
+ }
1913
+ )
1914
+ ] }) : null,
1915
+ /* @__PURE__ */ jsx12("h2", { style: styles.subtitle, children: "Live sessions" }),
1916
+ sessions.sessions.length === 0 ? /* @__PURE__ */ jsx12("p", { style: styles.muted, children: "None." }) : /* @__PURE__ */ jsx12("ul", { style: styles.list, children: sessions.sessions.map((s) => /* @__PURE__ */ jsxs12("li", { style: styles.row, children: [
1917
+ /* @__PURE__ */ jsx12("span", { style: styles.mono, children: s.ipAddress ?? "\u2014" }),
1457
1918
  " ",
1458
- describeLocation(s.location) ? /* @__PURE__ */ jsxs10("span", { style: styles.muted, children: [
1919
+ describeLocation(s.location) ? /* @__PURE__ */ jsxs12("span", { style: styles.muted, children: [
1459
1920
  describeLocation(s.location),
1460
1921
  " "
1461
1922
  ] }) : null,
1462
- /* @__PURE__ */ jsx10("span", { style: styles.muted, children: s.userAgent ?? "" }),
1923
+ /* @__PURE__ */ jsx12("span", { style: styles.muted, children: s.userAgent ?? "" }),
1463
1924
  " ",
1464
- /* @__PURE__ */ jsxs10("span", { style: styles.muted, children: [
1925
+ /* @__PURE__ */ jsxs12("span", { style: styles.muted, children: [
1465
1926
  "since ",
1466
1927
  new Date(s.createdAt).toLocaleString()
1467
1928
  ] })
1468
1929
  ] }, s.id)) }),
1469
- /* @__PURE__ */ jsx10("h2", { style: styles.subtitle, children: "Recent sign-ins" }),
1470
- history.events.length === 0 && !history.isLoading ? /* @__PURE__ */ jsx10("p", { style: styles.muted, children: "None recorded." }) : /* @__PURE__ */ jsx10("ul", { style: styles.list, children: history.events.map((e) => /* @__PURE__ */ jsxs10("li", { style: styles.row, children: [
1471
- /* @__PURE__ */ jsx10(Pill, { tone: e.outcome === "success" ? "ok" : "bad", children: e.outcome }),
1930
+ /* @__PURE__ */ jsx12("h2", { style: styles.subtitle, children: "Recent sign-ins" }),
1931
+ history.events.length === 0 && !history.isLoading ? /* @__PURE__ */ jsx12("p", { style: styles.muted, children: "None recorded." }) : /* @__PURE__ */ jsx12("ul", { style: styles.list, children: history.events.map((e) => /* @__PURE__ */ jsxs12("li", { style: styles.row, children: [
1932
+ /* @__PURE__ */ jsx12(Pill, { tone: e.outcome === "success" ? "ok" : "bad", children: e.outcome }),
1472
1933
  " ",
1473
- /* @__PURE__ */ jsx10("span", { style: styles.muted, children: e.method }),
1934
+ /* @__PURE__ */ jsx12("span", { style: styles.muted, children: e.method }),
1474
1935
  " ",
1475
- /* @__PURE__ */ jsx10("span", { style: styles.mono, children: e.ipAddress ?? "\u2014" }),
1936
+ /* @__PURE__ */ jsx12("span", { style: styles.mono, children: e.ipAddress ?? "\u2014" }),
1476
1937
  " ",
1477
- describeLocation(e.location) ? /* @__PURE__ */ jsx10("span", { style: styles.muted, children: describeLocation(e.location) }) : null,
1478
- e.location?.proxy || e.location?.hosting ? /* @__PURE__ */ jsxs10(Fragment5, { children: [
1938
+ describeLocation(e.location) ? /* @__PURE__ */ jsx12("span", { style: styles.muted, children: describeLocation(e.location) }) : null,
1939
+ e.location?.proxy || e.location?.hosting ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
1479
1940
  " ",
1480
- /* @__PURE__ */ jsx10(Pill, { tone: "warn", children: e.location.proxy ? "proxy/VPN" : "hosting" })
1941
+ /* @__PURE__ */ jsx12(Pill, { tone: "warn", children: e.location.proxy ? "proxy/VPN" : "hosting" })
1481
1942
  ] }) : null,
1482
1943
  " ",
1483
- /* @__PURE__ */ jsx10("span", { style: styles.muted, children: new Date(e.createdAt).toLocaleString() })
1944
+ /* @__PURE__ */ jsx12("span", { style: styles.muted, children: new Date(e.createdAt).toLocaleString() })
1484
1945
  ] }, e.id)) }),
1485
- history.hasMore && !history.isLoading ? /* @__PURE__ */ jsx10(
1946
+ history.hasMore && !history.isLoading ? /* @__PURE__ */ jsx12(
1486
1947
  "button",
1487
1948
  {
1488
1949
  type: "button",
@@ -1497,24 +1958,24 @@ function UsersScreen({ client, pageSize = 50 }) {
1497
1958
 
1498
1959
  // src/screens/CatalogScreen.tsx
1499
1960
  import { useAdminCatalog } from "@fonderie/react-admin";
1500
- import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1961
+ import { Fragment as Fragment8, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1501
1962
  function CatalogScreen({ client }) {
1502
1963
  const { catalog, isLoading, error, refresh, deletePlan } = useAdminCatalog(client);
1503
1964
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
1504
- return /* @__PURE__ */ jsxs11("div", { style: styles.container, children: [
1505
- /* @__PURE__ */ jsx11(
1965
+ return /* @__PURE__ */ jsxs13("div", { style: styles.container, children: [
1966
+ /* @__PURE__ */ jsx13(
1506
1967
  PageHeader,
1507
1968
  {
1508
1969
  title: "Catalog",
1509
1970
  lead: "What you sell: plans as configured in code, and as stored in the database, side by side.",
1510
- actions: /* @__PURE__ */ jsx11(RefreshButton, { onClick: () => void refresh(), busy: isLoading })
1971
+ actions: /* @__PURE__ */ jsx13(RefreshButton, { onClick: () => void refresh(), busy: isLoading })
1511
1972
  }
1512
1973
  ),
1513
- error ? /* @__PURE__ */ jsx11("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
1514
- isLoading && !catalog ? /* @__PURE__ */ jsx11("p", { style: styles.status, children: "Loading\u2026" }) : null,
1515
- catalog ? /* @__PURE__ */ jsxs11(Fragment6, { children: [
1516
- /* @__PURE__ */ jsx11("h2", { style: { ...styles.subtitle, marginTop: 0 }, children: "Configured (code)" }),
1517
- /* @__PURE__ */ jsx11(
1974
+ error ? /* @__PURE__ */ jsx13("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
1975
+ isLoading && !catalog ? /* @__PURE__ */ jsx13("p", { style: styles.status, children: "Loading\u2026" }) : null,
1976
+ catalog ? /* @__PURE__ */ jsxs13(Fragment8, { children: [
1977
+ /* @__PURE__ */ jsx13("h2", { style: { ...styles.subtitle, marginTop: 0 }, children: "Configured (code)" }),
1978
+ /* @__PURE__ */ jsx13(
1518
1979
  "pre",
1519
1980
  {
1520
1981
  style: {
@@ -1528,32 +1989,32 @@ function CatalogScreen({ client }) {
1528
1989
  children: JSON.stringify(catalog.configured, null, 2)
1529
1990
  }
1530
1991
  ),
1531
- /* @__PURE__ */ jsx11("h2", { style: styles.subtitle, children: "Stored (database)" }),
1532
- catalog.stored.length === 0 ? /* @__PURE__ */ jsx11(Empty, { icon: "catalog", title: "No stored plans", children: "Plans are configured in code; nothing has been written to the database." }) : /* @__PURE__ */ jsxs11("table", { style: styles.table, children: [
1533
- /* @__PURE__ */ jsx11("thead", { children: /* @__PURE__ */ jsxs11("tr", { children: [
1534
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Plan" }),
1535
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Tier" }),
1536
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Seats" }),
1537
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Monthly" }),
1538
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Yearly" }),
1539
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Trial" }),
1540
- /* @__PURE__ */ jsx11("th", { style: styles.th })
1992
+ /* @__PURE__ */ jsx13("h2", { style: styles.subtitle, children: "Stored (database)" }),
1993
+ catalog.stored.length === 0 ? /* @__PURE__ */ jsx13(Empty, { icon: "catalog", title: "No stored plans", children: "Plans are configured in code; nothing has been written to the database." }) : /* @__PURE__ */ jsxs13("table", { style: styles.table, children: [
1994
+ /* @__PURE__ */ jsx13("thead", { children: /* @__PURE__ */ jsxs13("tr", { children: [
1995
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Plan" }),
1996
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Tier" }),
1997
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Seats" }),
1998
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Monthly" }),
1999
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Yearly" }),
2000
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Trial" }),
2001
+ /* @__PURE__ */ jsx13("th", { style: styles.th })
1541
2002
  ] }) }),
1542
- /* @__PURE__ */ jsx11("tbody", { children: catalog.stored.map((p) => /* @__PURE__ */ jsxs11("tr", { children: [
1543
- /* @__PURE__ */ jsxs11("td", { style: styles.td, children: [
1544
- /* @__PURE__ */ jsx11("strong", { children: p.name }),
1545
- p.description ? /* @__PURE__ */ jsx11("div", { style: styles.muted, children: p.description }) : null
2003
+ /* @__PURE__ */ jsx13("tbody", { children: catalog.stored.map((p) => /* @__PURE__ */ jsxs13("tr", { children: [
2004
+ /* @__PURE__ */ jsxs13("td", { style: styles.td, children: [
2005
+ /* @__PURE__ */ jsx13("strong", { children: p.name }),
2006
+ p.description ? /* @__PURE__ */ jsx13("div", { style: styles.muted, children: p.description }) : null
1546
2007
  ] }),
1547
- /* @__PURE__ */ jsx11("td", { style: styles.td, children: p.tier }),
1548
- /* @__PURE__ */ jsx11("td", { style: styles.td, children: p.seats ?? "\u221E" }),
1549
- /* @__PURE__ */ jsxs11("td", { style: styles.td, children: [
2008
+ /* @__PURE__ */ jsx13("td", { style: styles.td, children: p.tier }),
2009
+ /* @__PURE__ */ jsx13("td", { style: styles.td, children: p.seats ?? "\u221E" }),
2010
+ /* @__PURE__ */ jsxs13("td", { style: styles.td, children: [
1550
2011
  money(p.pricing?.monthly),
1551
2012
  " ",
1552
2013
  p.pricing?.currency ?? ""
1553
2014
  ] }),
1554
- /* @__PURE__ */ jsx11("td", { style: styles.td, children: money(p.pricing?.yearly) }),
1555
- /* @__PURE__ */ jsx11("td", { style: styles.td, children: p.trialDays ? `${p.trialDays} d` : "\u2014" }),
1556
- /* @__PURE__ */ jsx11("td", { style: styles.td, children: /* @__PURE__ */ jsx11(
2015
+ /* @__PURE__ */ jsx13("td", { style: styles.td, children: money(p.pricing?.yearly) }),
2016
+ /* @__PURE__ */ jsx13("td", { style: styles.td, children: p.trialDays ? `${p.trialDays} d` : "\u2014" }),
2017
+ /* @__PURE__ */ jsx13("td", { style: styles.td, children: /* @__PURE__ */ jsx13(
1557
2018
  "button",
1558
2019
  {
1559
2020
  type: "button",
@@ -1573,114 +2034,148 @@ function CatalogScreen({ client }) {
1573
2034
  }
1574
2035
 
1575
2036
  // src/screens/SubscriberScreen.tsx
1576
- import { useAdminSubscriber, useAdminSubscribers } from "@fonderie/react-admin";
1577
- import { useState as useState4 } from "react";
1578
- import { Fragment as Fragment7, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1579
- function SubscriberScreen({ client, pageSize = 50 }) {
1580
- const [type, setType] = useState4("user");
1581
- const [idInput, setIdInput] = useState4("");
1582
- const [subscriber, setSubscriber] = useState4(null);
2037
+ import { useAdminSubscribers } from "@fonderie/react-admin";
2038
+ import { useState as useState7 } from "react";
2039
+ import { Fragment as Fragment9, jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2040
+ var FILTERS = [
2041
+ { label: "All", match: () => true },
2042
+ { label: "Active", match: (s) => s === "active" },
2043
+ { label: "Trialing", match: (s) => s === "trialing" },
2044
+ { label: "Past due", match: (s) => s === "past_due" || s === "unpaid" },
2045
+ { label: "Canceled", match: (s) => s === "canceled" }
2046
+ ];
2047
+ function SubscriberScreen({ client, pageSize = 50, onOpenUser }) {
1583
2048
  const list = useAdminSubscribers(client, { limit: pageSize });
1584
- const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(client, subscriber, { limit: 20 });
1585
- const [amount, setAmount] = useState4("");
1586
- const [note, setNote] = useState4("");
1587
- const [granted, setGranted] = useState4(null);
1588
- const open = (t2, id) => {
1589
- setGranted(null);
1590
- setType(t2);
1591
- setIdInput(id);
1592
- setSubscriber({ type: t2, id });
1593
- };
1594
- return /* @__PURE__ */ jsxs12("div", { style: styles.container, children: [
1595
- /* @__PURE__ */ jsx12(
2049
+ const [filter, setFilter] = useState7("All");
2050
+ const [open, setOpen] = useState7(null);
2051
+ const [type, setType] = useState7("workspace");
2052
+ const [idInput, setIdInput] = useState7("");
2053
+ if (open) {
2054
+ return /* @__PURE__ */ jsxs14("div", { style: styles.container, children: [
2055
+ /* @__PURE__ */ jsx14(
2056
+ PageHeader,
2057
+ {
2058
+ title: `${open.type}/${open.id}`,
2059
+ lead: "Plan, credits and what moved.",
2060
+ actions: /* @__PURE__ */ jsxs14("button", { type: "button", style: styles.buttonGhost, onClick: () => setOpen(null), children: [
2061
+ /* @__PURE__ */ jsx14(Icon, { name: "back", size: 14 }),
2062
+ "All subscriptions"
2063
+ ] })
2064
+ }
2065
+ ),
2066
+ /* @__PURE__ */ jsx14(SubscriberBilling, { client, subscriber: open })
2067
+ ] });
2068
+ }
2069
+ const match = FILTERS.find((f) => f.label === filter)?.match ?? (() => true);
2070
+ const rows = list.subscriptions.filter((s) => match(s.status));
2071
+ const openRow = (t2, id) => t2 === "user" && onOpenUser ? onOpenUser(id) : setOpen({ type: t2, id });
2072
+ return /* @__PURE__ */ jsxs14("div", { style: styles.container, children: [
2073
+ /* @__PURE__ */ jsx14(
1596
2074
  PageHeader,
1597
2075
  {
1598
- title: "Subscribers",
1599
- lead: "Who is subscribed. Open one for their plan, wallet and ledger, and to grant credit."
2076
+ title: "Subscriptions",
2077
+ lead: "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.",
2078
+ actions: /* @__PURE__ */ jsx14(RefreshButton, { onClick: () => void list.refresh(), busy: list.isLoading })
1600
2079
  }
1601
2080
  ),
1602
- /* @__PURE__ */ jsxs12(
1603
- "form",
1604
- {
1605
- style: styles.toolbar,
1606
- onSubmit: (e) => {
1607
- e.preventDefault();
1608
- setGranted(null);
1609
- setSubscriber(idInput.trim() ? { type, id: idInput.trim() } : null);
1610
- },
1611
- children: [
1612
- /* @__PURE__ */ jsxs12(
1613
- "select",
1614
- {
1615
- value: type,
1616
- onChange: (e) => setType(e.target.value),
1617
- style: styles.input,
1618
- "aria-label": "Type",
1619
- children: [
1620
- /* @__PURE__ */ jsx12("option", { value: "user", children: "user" }),
1621
- /* @__PURE__ */ jsx12("option", { value: "workspace", children: "workspace" })
1622
- ]
1623
- }
1624
- ),
1625
- /* @__PURE__ */ jsx12(
1626
- "input",
1627
- {
1628
- value: idInput,
1629
- onChange: (e) => setIdInput(e.target.value),
1630
- placeholder: "subscriber id",
1631
- style: { ...styles.input, minWidth: 280 },
1632
- "aria-label": "Subscriber id"
1633
- }
1634
- ),
1635
- /* @__PURE__ */ jsxs12("button", { type: "submit", style: styles.buttonPrimary, disabled: isLoading, children: [
1636
- /* @__PURE__ */ jsx12(Icon, { name: "search", size: 14 }),
1637
- "Look up"
1638
- ] }),
1639
- subscriber ? /* @__PURE__ */ jsxs12(
1640
- "button",
1641
- {
1642
- type: "button",
1643
- style: styles.button,
1644
- onClick: () => {
1645
- setSubscriber(null);
1646
- setIdInput("");
1647
- setGranted(null);
2081
+ /* @__PURE__ */ jsxs14("div", { style: { ...styles.toolbar, justifyContent: "space-between" }, children: [
2082
+ /* @__PURE__ */ jsx14(
2083
+ "fieldset",
2084
+ {
2085
+ style: {
2086
+ display: "flex",
2087
+ gap: 6,
2088
+ flexWrap: "wrap",
2089
+ border: "none",
2090
+ margin: 0,
2091
+ padding: 0,
2092
+ minWidth: 0
2093
+ },
2094
+ "aria-label": "Filter by status",
2095
+ children: FILTERS.map((f) => {
2096
+ const n = list.subscriptions.filter((s) => f.match(s.status)).length;
2097
+ const on = filter === f.label;
2098
+ return /* @__PURE__ */ jsxs14(
2099
+ "button",
2100
+ {
2101
+ type: "button",
2102
+ "aria-pressed": on,
2103
+ onClick: () => setFilter(f.label),
2104
+ style: { ...on ? styles.buttonPrimary : styles.button, height: 28 },
2105
+ children: [
2106
+ f.label,
2107
+ /* @__PURE__ */ jsx14("span", { style: { opacity: 0.7 }, children: n })
2108
+ ]
1648
2109
  },
1649
- children: [
1650
- /* @__PURE__ */ jsx12(Icon, { name: "back", size: 14 }),
1651
- "All subscribers"
1652
- ]
1653
- }
1654
- ) : /* @__PURE__ */ jsx12(
1655
- "button",
1656
- {
1657
- type: "button",
1658
- style: styles.button,
1659
- onClick: () => void list.refresh(),
1660
- disabled: list.isLoading,
1661
- children: "Refresh"
1662
- }
1663
- )
1664
- ]
2110
+ f.label
2111
+ );
2112
+ })
2113
+ }
2114
+ ),
2115
+ /* @__PURE__ */ jsxs14(
2116
+ "form",
2117
+ {
2118
+ style: { display: "flex", gap: 6 },
2119
+ onSubmit: (e) => {
2120
+ e.preventDefault();
2121
+ if (idInput.trim()) openRow(type, idInput.trim());
2122
+ },
2123
+ children: [
2124
+ /* @__PURE__ */ jsxs14(
2125
+ "select",
2126
+ {
2127
+ value: type,
2128
+ onChange: (e) => setType(e.target.value),
2129
+ style: styles.input,
2130
+ "aria-label": "Type",
2131
+ children: [
2132
+ /* @__PURE__ */ jsx14("option", { value: "workspace", children: "workspace" }),
2133
+ /* @__PURE__ */ jsx14("option", { value: "user", children: "user" })
2134
+ ]
2135
+ }
2136
+ ),
2137
+ /* @__PURE__ */ jsx14(
2138
+ "input",
2139
+ {
2140
+ value: idInput,
2141
+ onChange: (e) => setIdInput(e.target.value),
2142
+ placeholder: "subscriber id",
2143
+ style: { ...styles.input, width: 220 },
2144
+ "aria-label": "Subscriber id"
2145
+ }
2146
+ ),
2147
+ /* @__PURE__ */ jsxs14("button", { type: "submit", style: styles.button, disabled: !idInput.trim(), children: [
2148
+ /* @__PURE__ */ jsx14(Icon, { name: "search", size: 14 }),
2149
+ "Open"
2150
+ ] })
2151
+ ]
2152
+ }
2153
+ )
2154
+ ] }),
2155
+ list.error ? /* @__PURE__ */ jsx14("p", { style: styles.error, role: "alert", children: list.error.explanation }) : null,
2156
+ rows.length === 0 && !list.isLoading ? /* @__PURE__ */ jsx14(
2157
+ Empty,
2158
+ {
2159
+ icon: "subscriber",
2160
+ title: filter === "All" ? "No subscriptions yet" : `No ${filter.toLowerCase()} subscriptions`,
2161
+ children: "Subscriptions appear here once someone checks out."
1665
2162
  }
1666
- ),
1667
- error ? /* @__PURE__ */ jsx12("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
1668
- !subscriber ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
1669
- list.error ? /* @__PURE__ */ jsx12("p", { style: styles.error, role: "alert", children: list.error.explanation }) : null,
1670
- list.subscriptions.length === 0 && !list.isLoading ? /* @__PURE__ */ jsx12(Empty, { icon: "subscriber", title: "No subscribers yet", children: "Subscriptions appear here once someone checks out." }) : /* @__PURE__ */ jsxs12("table", { style: styles.table, children: [
1671
- /* @__PURE__ */ jsx12("thead", { children: /* @__PURE__ */ jsxs12("tr", { children: [
1672
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Subscriber" }),
1673
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Plan" }),
1674
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Status" }),
1675
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Renews" })
1676
- ] }) }),
1677
- /* @__PURE__ */ jsx12("tbody", { children: list.subscriptions.map((s) => /* @__PURE__ */ jsxs12("tr", { children: [
1678
- /* @__PURE__ */ jsx12("td", { style: styles.td, children: /* @__PURE__ */ jsxs12(
2163
+ ) : /* @__PURE__ */ jsxs14("table", { style: styles.table, children: [
2164
+ /* @__PURE__ */ jsx14("thead", { children: /* @__PURE__ */ jsxs14("tr", { children: [
2165
+ /* @__PURE__ */ jsx14("th", { style: styles.th, children: "Subscriber" }),
2166
+ /* @__PURE__ */ jsx14("th", { style: styles.th, children: "Plan" }),
2167
+ /* @__PURE__ */ jsx14("th", { style: styles.th, children: "Status" }),
2168
+ /* @__PURE__ */ jsx14("th", { style: styles.th, children: "Renews / ends" })
2169
+ ] }) }),
2170
+ /* @__PURE__ */ jsx14("tbody", { children: rows.map((s) => {
2171
+ const end = periodEnd(s);
2172
+ return /* @__PURE__ */ jsxs14("tr", { children: [
2173
+ /* @__PURE__ */ jsx14("td", { style: styles.td, children: /* @__PURE__ */ jsxs14(
1679
2174
  "button",
1680
2175
  {
1681
2176
  type: "button",
1682
2177
  style: { ...styles.link, ...styles.mono },
1683
- onClick: () => open(s.subscriberType, s.subscriberId),
2178
+ onClick: () => openRow(s.subscriberType, s.subscriberId),
1684
2179
  children: [
1685
2180
  s.subscriberType,
1686
2181
  "/",
@@ -1688,206 +2183,56 @@ function SubscriberScreen({ client, pageSize = 50 }) {
1688
2183
  ]
1689
2184
  }
1690
2185
  ) }),
1691
- /* @__PURE__ */ jsxs12("td", { style: styles.td, children: [
1692
- /* @__PURE__ */ jsx12("strong", { children: s.plan }),
1693
- " \xB7 ",
1694
- s.interval
2186
+ /* @__PURE__ */ jsxs14("td", { style: styles.td, children: [
2187
+ /* @__PURE__ */ jsx14("strong", { children: s.plan }),
2188
+ " ",
2189
+ /* @__PURE__ */ jsxs14("span", { style: styles.muted, children: [
2190
+ "\xB7 ",
2191
+ s.interval
2192
+ ] })
1695
2193
  ] }),
1696
- /* @__PURE__ */ jsxs12("td", { style: styles.td, children: [
1697
- /* @__PURE__ */ jsx12(
1698
- Pill,
1699
- {
1700
- tone: s.status === "active" ? "ok" : s.status === "trialing" ? "info" : s.status === "past_due" ? "warn" : "neutral",
1701
- children: s.status
1702
- }
1703
- ),
1704
- s.cancelAtPeriodEnd ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
2194
+ /* @__PURE__ */ jsxs14("td", { style: styles.td, children: [
2195
+ /* @__PURE__ */ jsx14(Pill, { tone: statusTone(s.status), children: s.status }),
2196
+ s.cancelAtPeriodEnd && s.status !== "canceled" ? /* @__PURE__ */ jsxs14(Fragment9, { children: [
1705
2197
  " ",
1706
- /* @__PURE__ */ jsx12(Pill, { tone: "warn", dot: false, children: "cancels" })
2198
+ /* @__PURE__ */ jsx14(Pill, { tone: "warn", dot: false, children: "cancels" })
1707
2199
  ] }) : null
1708
2200
  ] }),
1709
- /* @__PURE__ */ jsx12("td", { style: styles.td, children: s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : /* @__PURE__ */ jsx12("span", { style: styles.muted, children: "\u2014" }) })
1710
- ] }, s.id)) })
1711
- ] }),
1712
- list.isLoading ? /* @__PURE__ */ jsx12("p", { style: styles.status, children: "Loading\u2026" }) : null,
1713
- list.hasMore && !list.isLoading ? /* @__PURE__ */ jsx12(
1714
- "button",
1715
- {
1716
- type: "button",
1717
- style: { ...styles.button, marginTop: 8 },
1718
- onClick: () => void list.loadMore(),
1719
- children: "Load more"
1720
- }
1721
- ) : null
1722
- ] }) : null,
1723
- subscriber && !isLoading ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
1724
- /* @__PURE__ */ jsx12("h2", { style: styles.subtitle, children: "Subscription" }),
1725
- subscription ? /* @__PURE__ */ jsx12("table", { style: styles.table, children: /* @__PURE__ */ jsxs12("tbody", { children: [
1726
- /* @__PURE__ */ jsxs12("tr", { children: [
1727
- /* @__PURE__ */ jsx12("td", { style: styles.td, children: "plan" }),
1728
- /* @__PURE__ */ jsxs12("td", { style: styles.td, children: [
1729
- /* @__PURE__ */ jsx12("strong", { children: subscription.plan }),
1730
- " \xB7 ",
1731
- subscription.interval
1732
- ] })
1733
- ] }),
1734
- /* @__PURE__ */ jsxs12("tr", { children: [
1735
- /* @__PURE__ */ jsx12("td", { style: styles.td, children: "status" }),
1736
- /* @__PURE__ */ jsxs12("td", { style: styles.td, children: [
1737
- subscription.status,
1738
- subscription.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""
1739
- ] })
1740
- ] }),
1741
- /* @__PURE__ */ jsxs12("tr", { children: [
1742
- /* @__PURE__ */ jsx12("td", { style: styles.td, children: "period" }),
1743
- /* @__PURE__ */ jsxs12("td", { style: styles.td, children: [
1744
- subscription.currentPeriodStart ? new Date(subscription.currentPeriodStart).toLocaleDateString() : "\u2014",
1745
- " ",
1746
- "\u2192",
2201
+ /* @__PURE__ */ jsx14("td", { style: { ...styles.td, whiteSpace: "nowrap" }, children: end ? /* @__PURE__ */ jsxs14(Fragment9, { children: [
2202
+ /* @__PURE__ */ jsx14("span", { style: styles.muted, children: end.label }),
1747
2203
  " ",
1748
- subscription.currentPeriodEnd ? new Date(subscription.currentPeriodEnd).toLocaleDateString() : "\u2014"
1749
- ] })
1750
- ] }),
1751
- /* @__PURE__ */ jsxs12("tr", { children: [
1752
- /* @__PURE__ */ jsx12("td", { style: styles.td, children: "provider" }),
1753
- /* @__PURE__ */ jsx12("td", { style: { ...styles.td, ...styles.mono }, children: subscription.providerSubscriptionId ?? "\u2014" })
1754
- ] })
1755
- ] }) }) : /* @__PURE__ */ jsx12("p", { style: styles.muted, children: "No subscription." }),
1756
- /* @__PURE__ */ jsx12("h2", { style: styles.subtitle, children: "Wallet" }),
1757
- wallet ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
1758
- /* @__PURE__ */ jsxs12(
1759
- "p",
1760
- {
1761
- style: {
1762
- ...styles.card,
1763
- display: "flex",
1764
- alignItems: "baseline",
1765
- gap: 8,
1766
- flexWrap: "wrap"
1767
- },
1768
- children: [
1769
- /* @__PURE__ */ jsx12(
1770
- "strong",
1771
- {
1772
- style: {
1773
- fontSize: 22,
1774
- letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
1775
- },
1776
- children: wallet.balance
1777
- }
1778
- ),
1779
- " ",
1780
- wallet.currency,
1781
- " ",
1782
- /* @__PURE__ */ jsxs12("span", { style: styles.muted, children: [
1783
- "(granted ",
1784
- wallet.granted ?? "0",
1785
- " \xB7 purchased ",
1786
- wallet.purchased ?? "0",
1787
- ") \xB7 minor units, precision ",
1788
- wallet.precision
1789
- ] })
1790
- ]
1791
- }
1792
- ),
1793
- /* @__PURE__ */ jsxs12(
1794
- "form",
1795
- {
1796
- style: styles.toolbar,
1797
- onSubmit: (e) => {
1798
- e.preventDefault();
1799
- const key = `admin-grant-${subscriber.type}-${subscriber.id}-${Date.now()}`;
1800
- void grant({
1801
- amount: amount.trim(),
1802
- ...note ? { description: note } : {},
1803
- idempotencyKey: key
1804
- }).then(() => {
1805
- setGranted(`Granted ${amount.trim()} ${wallet.currency}.`);
1806
- setAmount("");
1807
- setNote("");
1808
- }).catch(() => {
1809
- });
1810
- },
1811
- children: [
1812
- /* @__PURE__ */ jsx12(
1813
- "input",
1814
- {
1815
- value: amount,
1816
- onChange: (e) => setAmount(e.target.value),
1817
- placeholder: "amount (minor units)",
1818
- style: styles.input,
1819
- "aria-label": "Amount"
1820
- }
1821
- ),
1822
- /* @__PURE__ */ jsx12(
1823
- "input",
1824
- {
1825
- value: note,
1826
- onChange: (e) => setNote(e.target.value),
1827
- placeholder: "reason (optional)",
1828
- style: { ...styles.input, minWidth: 220 },
1829
- "aria-label": "Reason"
1830
- }
1831
- ),
1832
- /* @__PURE__ */ jsxs12(
1833
- "button",
1834
- {
1835
- type: "submit",
1836
- style: styles.buttonPrimary,
1837
- disabled: !/^\d+$/.test(amount.trim()),
1838
- children: [
1839
- /* @__PURE__ */ jsx12(Icon, { name: "plus", size: 14 }),
1840
- "Grant"
1841
- ]
1842
- }
1843
- ),
1844
- granted ? /* @__PURE__ */ jsx12("span", { style: styles.ok, children: granted }) : null
1845
- ]
1846
- }
1847
- ),
1848
- /* @__PURE__ */ jsx12("h2", { style: styles.subtitle, children: "Ledger" }),
1849
- ledger.length === 0 ? /* @__PURE__ */ jsx12(Empty, { icon: "subscriber", title: "Nothing moved yet" }) : /* @__PURE__ */ jsxs12("table", { style: styles.table, children: [
1850
- /* @__PURE__ */ jsx12("thead", { children: /* @__PURE__ */ jsxs12("tr", { children: [
1851
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "When" }),
1852
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Kind" }),
1853
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Amount" }),
1854
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Note" })
1855
- ] }) }),
1856
- /* @__PURE__ */ jsx12("tbody", { children: ledger.map((t2) => /* @__PURE__ */ jsxs12("tr", { children: [
1857
- /* @__PURE__ */ jsx12("td", { style: { ...styles.td, ...styles.muted }, children: new Date(t2.createdAt).toLocaleString() }),
1858
- /* @__PURE__ */ jsx12("td", { style: styles.td, children: t2.type }),
1859
- /* @__PURE__ */ jsx12("td", { style: { ...styles.td, ...styles.mono }, children: t2.amount }),
1860
- /* @__PURE__ */ jsx12("td", { style: { ...styles.td, ...styles.muted }, children: t2.description ?? "" })
1861
- ] }, t2.id)) })
1862
- ] }),
1863
- hasMoreLedger ? /* @__PURE__ */ jsx12(
1864
- "button",
1865
- {
1866
- type: "button",
1867
- style: { ...styles.button, marginTop: 8 },
1868
- onClick: () => void loadMoreLedger(),
1869
- children: "Load more"
1870
- }
1871
- ) : null
1872
- ] }) : /* @__PURE__ */ jsx12("p", { style: styles.muted, children: "No wallet \u2014 billing has no wallet configuration." })
1873
- ] }) : null
2204
+ end.date
2205
+ ] }) : /* @__PURE__ */ jsx14("span", { style: styles.muted, children: "\u2014" }) })
2206
+ ] }, s.id);
2207
+ }) })
2208
+ ] }),
2209
+ list.isLoading ? /* @__PURE__ */ jsx14("p", { style: styles.status, children: "Loading\u2026" }) : null,
2210
+ list.hasMore && !list.isLoading ? /* @__PURE__ */ jsx14(
2211
+ "button",
2212
+ {
2213
+ type: "button",
2214
+ style: { ...styles.button, marginTop: 8 },
2215
+ onClick: () => void list.loadMore(),
2216
+ children: "Load more"
2217
+ }
2218
+ ) : null
1874
2219
  ] });
1875
2220
  }
1876
2221
 
1877
2222
  // src/screens/AuditScreen.tsx
1878
2223
  import { useAdminAudit } from "@fonderie/react-admin";
1879
- import { useState as useState5 } from "react";
1880
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2224
+ import { useState as useState8 } from "react";
2225
+ import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
1881
2226
  function AuditScreen({ client, pageSize = 50 }) {
1882
- const [draft, setDraft] = useState5({ workspaceId: "", type: "", actorId: "" });
1883
- const [filter, setFilter] = useState5(
2227
+ const [draft, setDraft] = useState8({ workspaceId: "", type: "", actorId: "" });
2228
+ const [filter, setFilter] = useState8(
1884
2229
  {}
1885
2230
  );
1886
2231
  const { events, hasMore, isLoading, error, refresh, loadMore } = useAdminAudit(client, {
1887
2232
  ...filter,
1888
2233
  limit: pageSize
1889
2234
  });
1890
- const field = (key, placeholder) => /* @__PURE__ */ jsx13(
2235
+ const field = (key, placeholder) => /* @__PURE__ */ jsx15(
1891
2236
  "input",
1892
2237
  {
1893
2238
  value: draft[key],
@@ -1897,15 +2242,15 @@ function AuditScreen({ client, pageSize = 50 }) {
1897
2242
  "aria-label": placeholder
1898
2243
  }
1899
2244
  );
1900
- return /* @__PURE__ */ jsxs13("div", { style: styles.container, children: [
1901
- /* @__PURE__ */ jsx13(
2245
+ return /* @__PURE__ */ jsxs15("div", { style: styles.container, children: [
2246
+ /* @__PURE__ */ jsx15(
1902
2247
  PageHeader,
1903
2248
  {
1904
2249
  title: "Audit",
1905
2250
  lead: "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
1906
2251
  }
1907
2252
  ),
1908
- /* @__PURE__ */ jsxs13(
2253
+ /* @__PURE__ */ jsxs15(
1909
2254
  "form",
1910
2255
  {
1911
2256
  style: styles.toolbar,
@@ -1921,11 +2266,11 @@ function AuditScreen({ client, pageSize = 50 }) {
1921
2266
  field("workspaceId", "workspace id (all if empty)"),
1922
2267
  field("type", "event type"),
1923
2268
  field("actorId", "actor id"),
1924
- /* @__PURE__ */ jsxs13("button", { type: "submit", style: styles.buttonPrimary, disabled: isLoading, children: [
1925
- /* @__PURE__ */ jsx13(Icon, { name: "search", size: 14 }),
2269
+ /* @__PURE__ */ jsxs15("button", { type: "submit", style: styles.buttonPrimary, disabled: isLoading, children: [
2270
+ /* @__PURE__ */ jsx15(Icon, { name: "search", size: 14 }),
1926
2271
  "Filter"
1927
2272
  ] }),
1928
- /* @__PURE__ */ jsx13(
2273
+ /* @__PURE__ */ jsx15(
1929
2274
  "button",
1930
2275
  {
1931
2276
  type: "button",
@@ -1938,25 +2283,25 @@ function AuditScreen({ client, pageSize = 50 }) {
1938
2283
  ]
1939
2284
  }
1940
2285
  ),
1941
- error ? /* @__PURE__ */ jsx13("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
1942
- events.length === 0 && !isLoading ? /* @__PURE__ */ jsx13(Empty, { icon: "audit", title: "Nothing recorded for this filter" }) : /* @__PURE__ */ jsxs13("table", { style: styles.table, children: [
1943
- /* @__PURE__ */ jsx13("thead", { children: /* @__PURE__ */ jsxs13("tr", { children: [
1944
- /* @__PURE__ */ jsx13("th", { style: styles.th, children: "When" }),
1945
- /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Type" }),
1946
- /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Workspace" }),
1947
- /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Actor" }),
1948
- /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Request" })
2286
+ error ? /* @__PURE__ */ jsx15("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
2287
+ events.length === 0 && !isLoading ? /* @__PURE__ */ jsx15(Empty, { icon: "audit", title: "Nothing recorded for this filter" }) : /* @__PURE__ */ jsxs15("table", { style: styles.table, children: [
2288
+ /* @__PURE__ */ jsx15("thead", { children: /* @__PURE__ */ jsxs15("tr", { children: [
2289
+ /* @__PURE__ */ jsx15("th", { style: styles.th, children: "When" }),
2290
+ /* @__PURE__ */ jsx15("th", { style: styles.th, children: "Type" }),
2291
+ /* @__PURE__ */ jsx15("th", { style: styles.th, children: "Workspace" }),
2292
+ /* @__PURE__ */ jsx15("th", { style: styles.th, children: "Actor" }),
2293
+ /* @__PURE__ */ jsx15("th", { style: styles.th, children: "Request" })
1949
2294
  ] }) }),
1950
- /* @__PURE__ */ jsx13("tbody", { children: events.map((e) => /* @__PURE__ */ jsxs13("tr", { children: [
1951
- /* @__PURE__ */ jsx13("td", { style: { ...styles.td, ...styles.muted }, children: new Date(e.createdAt).toLocaleString() }),
1952
- /* @__PURE__ */ jsx13("td", { style: { ...styles.td, ...styles.mono }, children: e.type }),
1953
- /* @__PURE__ */ jsx13("td", { style: { ...styles.td, ...styles.mono }, children: String(e.payload["workspaceId"] ?? "\u2014") }),
1954
- /* @__PURE__ */ jsx13("td", { style: { ...styles.td, ...styles.mono }, children: e.actorId ?? "\u2014" }),
1955
- /* @__PURE__ */ jsx13("td", { style: { ...styles.td, ...styles.muted }, children: e.requestId ?? "" })
2295
+ /* @__PURE__ */ jsx15("tbody", { children: events.map((e) => /* @__PURE__ */ jsxs15("tr", { children: [
2296
+ /* @__PURE__ */ jsx15("td", { style: { ...styles.td, ...styles.muted }, children: new Date(e.createdAt).toLocaleString() }),
2297
+ /* @__PURE__ */ jsx15("td", { style: { ...styles.td, ...styles.mono }, children: e.type }),
2298
+ /* @__PURE__ */ jsx15("td", { style: { ...styles.td, ...styles.mono }, children: String(e.payload["workspaceId"] ?? "\u2014") }),
2299
+ /* @__PURE__ */ jsx15("td", { style: { ...styles.td, ...styles.mono }, children: e.actorId ?? "\u2014" }),
2300
+ /* @__PURE__ */ jsx15("td", { style: { ...styles.td, ...styles.muted }, children: e.requestId ?? "" })
1956
2301
  ] }, e.id)) })
1957
2302
  ] }),
1958
- isLoading ? /* @__PURE__ */ jsx13("p", { style: styles.status, children: "Loading\u2026" }) : null,
1959
- hasMore && !isLoading ? /* @__PURE__ */ jsx13(
2303
+ isLoading ? /* @__PURE__ */ jsx15("p", { style: styles.status, children: "Loading\u2026" }) : null,
2304
+ hasMore && !isLoading ? /* @__PURE__ */ jsx15(
1960
2305
  "button",
1961
2306
  {
1962
2307
  type: "button",
@@ -1969,7 +2314,7 @@ function AuditScreen({ client, pageSize = 50 }) {
1969
2314
  }
1970
2315
 
1971
2316
  // src/screens/AdminShell.tsx
1972
- import { Fragment as Fragment8, jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2317
+ import { Fragment as Fragment10, jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
1973
2318
  var ICON = {
1974
2319
  attention: "attention",
1975
2320
  modules: "modules",
@@ -1984,13 +2329,14 @@ var ICON = {
1984
2329
  audit: "audit",
1985
2330
  log: "log",
1986
2331
  tokens: "tokens",
2332
+ operators: "users",
1987
2333
  migrations: "migrations"
1988
2334
  };
1989
2335
  var NARROW = "(max-width: 820px)";
1990
2336
  function useNarrow() {
1991
2337
  const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
1992
- const [narrow, setNarrow] = useState6(() => query?.matches ?? false);
1993
- useEffect(() => {
2338
+ const [narrow, setNarrow] = useState9(() => query?.matches ?? false);
2339
+ useEffect2(() => {
1994
2340
  if (!query) return;
1995
2341
  const on = (e) => setNarrow(e.matches);
1996
2342
  query.addEventListener("change", on);
@@ -2014,7 +2360,7 @@ var NAV = [
2014
2360
  group: "Money",
2015
2361
  items: [
2016
2362
  { page: "catalog", label: "Catalog", needs: "billing" },
2017
- { page: "subscriber", label: "Subscriber", needs: "billing" }
2363
+ { page: "subscriber", label: "Subscriptions", needs: "billing" }
2018
2364
  ]
2019
2365
  },
2020
2366
  { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
@@ -2024,7 +2370,8 @@ var NAV = [
2024
2370
  items: [
2025
2371
  { page: "audit", label: "Audit", needs: "audit" },
2026
2372
  { page: "log", label: "Admin log" },
2027
- { page: "tokens", label: "Access" },
2373
+ { page: "operators", label: "Operators", needs: "operators" },
2374
+ { page: "tokens", label: "Tokens" },
2028
2375
  { page: "migrations", label: "Migrations" }
2029
2376
  ]
2030
2377
  }
@@ -2041,70 +2388,96 @@ function AdminShell({
2041
2388
  onNavigate,
2042
2389
  appName = "Admin",
2043
2390
  envLabel,
2044
- footer
2391
+ footer,
2392
+ operators = false,
2393
+ currentOperator
2045
2394
  }) {
2046
2395
  const narrow = useNarrow();
2047
- const [drawer, setDrawer] = useState6(false);
2048
- const [own, setOwn] = useState6("attention");
2396
+ const [drawer, setDrawer] = useState9(false);
2397
+ const [own, setOwn] = useState9("attention");
2398
+ const [openUser, setOpenUser] = useState9(void 0);
2049
2399
  const current = page ?? own;
2050
2400
  const go = (p) => {
2051
2401
  if (onNavigate) onNavigate(p);
2052
2402
  if (page === void 0) setOwn(p);
2053
2403
  setDrawer(false);
2054
2404
  };
2055
- const [editing, setEditing] = useState6(null);
2405
+ const [editing, setEditing] = useState9(null);
2056
2406
  const has = {
2057
2407
  config: Boolean(configClient),
2058
2408
  courier: Boolean(courierClient),
2059
2409
  auth: Boolean(authClient),
2060
2410
  billing: Boolean(billingClient),
2061
- audit: Boolean(auditClient)
2411
+ audit: Boolean(auditClient),
2412
+ operators
2062
2413
  };
2063
2414
  let body;
2064
2415
  switch (current) {
2065
2416
  case "attention":
2066
- body = /* @__PURE__ */ jsx14(AttentionScreen, { client });
2417
+ body = /* @__PURE__ */ jsx16(AttentionScreen, { client });
2067
2418
  break;
2068
2419
  case "modules":
2069
- body = /* @__PURE__ */ jsx14(ModulesScreen, { client });
2420
+ body = /* @__PURE__ */ jsx16(ModulesScreen, { client });
2070
2421
  break;
2071
2422
  case "doctor":
2072
- body = /* @__PURE__ */ jsx14(DoctorScreen, { client });
2423
+ body = /* @__PURE__ */ jsx16(DoctorScreen, { client });
2073
2424
  break;
2074
2425
  case "environment":
2075
- body = /* @__PURE__ */ jsx14(EnvironmentScreen, { client });
2426
+ body = /* @__PURE__ */ jsx16(EnvironmentScreen, { client });
2076
2427
  break;
2077
2428
  case "routes":
2078
- body = /* @__PURE__ */ jsx14(RoutesScreen, { client });
2429
+ body = /* @__PURE__ */ jsx16(RoutesScreen, { client });
2079
2430
  break;
2080
2431
  case "tokens":
2081
- body = /* @__PURE__ */ jsx14(TokensScreen, { client });
2432
+ body = /* @__PURE__ */ jsx16(TokensScreen, { client });
2433
+ break;
2434
+ case "operators":
2435
+ body = /* @__PURE__ */ jsx16(OperatorsScreen, { client, me: currentOperator });
2082
2436
  break;
2083
2437
  case "migrations":
2084
- body = /* @__PURE__ */ jsx14(MigrationsScreen, { client });
2438
+ body = /* @__PURE__ */ jsx16(MigrationsScreen, { client });
2085
2439
  break;
2086
2440
  case "log":
2087
- body = /* @__PURE__ */ jsx14(AdminLogScreen, { client });
2441
+ body = /* @__PURE__ */ jsx16(AdminLogScreen, { client });
2088
2442
  break;
2089
2443
  case "catalog":
2090
- body = billingClient ? /* @__PURE__ */ jsx14(CatalogScreen, { client: billingClient }) : /* @__PURE__ */ jsx14("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a BillingAdminClient to see the catalog here." });
2444
+ body = billingClient ? /* @__PURE__ */ jsx16(CatalogScreen, { client: billingClient }) : /* @__PURE__ */ jsx16("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a BillingAdminClient to see the catalog here." });
2091
2445
  break;
2092
2446
  case "subscriber":
2093
- body = billingClient ? /* @__PURE__ */ jsx14(SubscriberScreen, { client: billingClient }) : /* @__PURE__ */ jsx14("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a BillingAdminClient to look up subscribers here." });
2447
+ body = billingClient ? /* @__PURE__ */ jsx16(
2448
+ SubscriberScreen,
2449
+ {
2450
+ client: billingClient,
2451
+ ...authClient ? {
2452
+ onOpenUser: (id) => {
2453
+ setOpenUser(id);
2454
+ go("users");
2455
+ }
2456
+ } : {}
2457
+ }
2458
+ ) : /* @__PURE__ */ jsx16("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a BillingAdminClient to look up subscribers here." });
2094
2459
  break;
2095
2460
  case "audit":
2096
- body = auditClient ? /* @__PURE__ */ jsx14(AuditScreen, { client: auditClient }) : /* @__PURE__ */ jsx14("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass an AuditAdminClient to see the audit trail here." });
2461
+ body = auditClient ? /* @__PURE__ */ jsx16(AuditScreen, { client: auditClient }) : /* @__PURE__ */ jsx16("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass an AuditAdminClient to see the audit trail here." });
2097
2462
  break;
2098
2463
  case "users":
2099
- body = authClient ? /* @__PURE__ */ jsx14(UsersScreen, { client: authClient }) : /* @__PURE__ */ jsx14("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass an AuthAdminClient to look up users here." });
2464
+ body = authClient ? /* @__PURE__ */ jsx16(
2465
+ UsersScreen,
2466
+ {
2467
+ client: authClient,
2468
+ billingClient,
2469
+ openUserId: openUser
2470
+ },
2471
+ openUser ?? "list"
2472
+ ) : /* @__PURE__ */ jsx16("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass an AuthAdminClient to look up users here." });
2100
2473
  break;
2101
2474
  case "settings":
2102
- body = !configClient ? /* @__PURE__ */ jsx14("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a ConfigAdminClient to manage config and secrets here." }) : editing && editing.kind !== "template" ? /* @__PURE__ */ jsxs14(Fragment8, { children: [
2103
- /* @__PURE__ */ jsx14("div", { style: { padding: "24px 40px 0" }, children: /* @__PURE__ */ jsxs14("button", { type: "button", style: styles.buttonGhost, onClick: () => setEditing(null), children: [
2104
- /* @__PURE__ */ jsx14(Icon, { name: "back", size: 14 }),
2475
+ body = !configClient ? /* @__PURE__ */ jsx16("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a ConfigAdminClient to manage config and secrets here." }) : editing && editing.kind !== "template" ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
2476
+ /* @__PURE__ */ jsx16("div", { style: { padding: "24px 40px 0" }, children: /* @__PURE__ */ jsxs16("button", { type: "button", style: styles.buttonGhost, onClick: () => setEditing(null), children: [
2477
+ /* @__PURE__ */ jsx16(Icon, { name: "back", size: 14 }),
2105
2478
  "Config & secrets"
2106
2479
  ] }) }),
2107
- /* @__PURE__ */ jsx14(
2480
+ /* @__PURE__ */ jsx16(
2108
2481
  ConfigEditorScreen,
2109
2482
  {
2110
2483
  client: configClient,
@@ -2114,7 +2487,7 @@ function AdminShell({
2114
2487
  onSaved: () => setEditing(null)
2115
2488
  }
2116
2489
  )
2117
- ] }) : /* @__PURE__ */ jsx14(
2490
+ ] }) : /* @__PURE__ */ jsx16(
2118
2491
  ConfigListScreen,
2119
2492
  {
2120
2493
  client: configClient,
@@ -2127,12 +2500,12 @@ function AdminShell({
2127
2500
  );
2128
2501
  break;
2129
2502
  case "templates":
2130
- body = !courierClient ? /* @__PURE__ */ jsx14("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a CourierAdminClient to manage templates here." }) : editing?.kind === "template" ? /* @__PURE__ */ jsxs14(Fragment8, { children: [
2131
- /* @__PURE__ */ jsx14("div", { style: { padding: "24px 40px 0" }, children: /* @__PURE__ */ jsxs14("button", { type: "button", style: styles.buttonGhost, onClick: () => setEditing(null), children: [
2132
- /* @__PURE__ */ jsx14(Icon, { name: "back", size: 14 }),
2503
+ body = !courierClient ? /* @__PURE__ */ jsx16("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a CourierAdminClient to manage templates here." }) : editing?.kind === "template" ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
2504
+ /* @__PURE__ */ jsx16("div", { style: { padding: "24px 40px 0" }, children: /* @__PURE__ */ jsxs16("button", { type: "button", style: styles.buttonGhost, onClick: () => setEditing(null), children: [
2505
+ /* @__PURE__ */ jsx16(Icon, { name: "back", size: 14 }),
2133
2506
  "Templates"
2134
2507
  ] }) }),
2135
- /* @__PURE__ */ jsx14(
2508
+ /* @__PURE__ */ jsx16(
2136
2509
  TemplateEditorScreen,
2137
2510
  {
2138
2511
  client: courierClient,
@@ -2140,7 +2513,7 @@ function AdminShell({
2140
2513
  onSaved: () => setEditing(null)
2141
2514
  }
2142
2515
  )
2143
- ] }) : /* @__PURE__ */ jsx14(
2516
+ ] }) : /* @__PURE__ */ jsx16(
2144
2517
  TemplateListScreen,
2145
2518
  {
2146
2519
  client: courierClient,
@@ -2150,11 +2523,11 @@ function AdminShell({
2150
2523
  break;
2151
2524
  }
2152
2525
  const production = envLabel ? /^prod/i.test(envLabel) : false;
2153
- const brand = /* @__PURE__ */ jsxs14("div", { style: styles.navBrand, children: [
2154
- /* @__PURE__ */ jsx14("div", { style: styles.navMark, children: /* @__PURE__ */ jsx14(Icon, { name: "mark", size: 15 }) }),
2155
- /* @__PURE__ */ jsxs14("div", { style: { minWidth: 0, flex: 1 }, children: [
2156
- /* @__PURE__ */ jsx14("div", { style: styles.navBrandTitle, children: appName }),
2157
- envLabel ? /* @__PURE__ */ jsx14(
2526
+ const brand = /* @__PURE__ */ jsxs16("div", { style: styles.navBrand, children: [
2527
+ /* @__PURE__ */ jsx16("div", { style: styles.navMark, children: /* @__PURE__ */ jsx16(Icon, { name: "mark", size: 15 }) }),
2528
+ /* @__PURE__ */ jsxs16("div", { style: { minWidth: 0, flex: 1 }, children: [
2529
+ /* @__PURE__ */ jsx16("div", { style: styles.navBrandTitle, children: appName }),
2530
+ envLabel ? /* @__PURE__ */ jsx16(
2158
2531
  "span",
2159
2532
  {
2160
2533
  style: {
@@ -2171,18 +2544,18 @@ function AdminShell({
2171
2544
  }
2172
2545
  ) : null
2173
2546
  ] }),
2174
- narrow ? /* @__PURE__ */ jsx14(
2547
+ narrow ? /* @__PURE__ */ jsx16(
2175
2548
  "button",
2176
2549
  {
2177
2550
  type: "button",
2178
2551
  style: styles.buttonGhost,
2179
2552
  "aria-label": "Close menu",
2180
2553
  onClick: () => setDrawer(false),
2181
- children: /* @__PURE__ */ jsx14(Icon, { name: "close" })
2554
+ children: /* @__PURE__ */ jsx16(Icon, { name: "close" })
2182
2555
  }
2183
2556
  ) : null
2184
2557
  ] });
2185
- const nav = /* @__PURE__ */ jsxs14(
2558
+ const nav = /* @__PURE__ */ jsxs16(
2186
2559
  "nav",
2187
2560
  {
2188
2561
  style: {
@@ -2192,14 +2565,14 @@ function AdminShell({
2192
2565
  "aria-label": "Admin",
2193
2566
  children: [
2194
2567
  brand,
2195
- /* @__PURE__ */ jsx14("div", { style: styles.navScroll, children: NAV.map((g) => {
2568
+ /* @__PURE__ */ jsx16("div", { style: styles.navScroll, children: NAV.map((g) => {
2196
2569
  const items = g.items.filter((i) => !i.needs || has[i.needs]);
2197
2570
  if (items.length === 0) return null;
2198
- return /* @__PURE__ */ jsxs14("div", { children: [
2199
- /* @__PURE__ */ jsx14("div", { style: styles.navGroup, children: g.group }),
2571
+ return /* @__PURE__ */ jsxs16("div", { children: [
2572
+ /* @__PURE__ */ jsx16("div", { style: styles.navGroup, children: g.group }),
2200
2573
  items.map((i) => {
2201
2574
  const active = current === i.page;
2202
- return /* @__PURE__ */ jsxs14(
2575
+ return /* @__PURE__ */ jsxs16(
2203
2576
  "button",
2204
2577
  {
2205
2578
  type: "button",
@@ -2208,10 +2581,11 @@ function AdminShell({
2208
2581
  "aria-current": active ? "page" : void 0,
2209
2582
  onClick: () => {
2210
2583
  setEditing(null);
2584
+ setOpenUser(void 0);
2211
2585
  go(i.page);
2212
2586
  },
2213
2587
  children: [
2214
- /* @__PURE__ */ jsx14(Icon, { name: ICON[i.page], style: active ? styles.navIconActive : void 0 }),
2588
+ /* @__PURE__ */ jsx16(Icon, { name: ICON[i.page], style: active ? styles.navIconActive : void 0 }),
2215
2589
  i.label
2216
2590
  ]
2217
2591
  },
@@ -2220,15 +2594,15 @@ function AdminShell({
2220
2594
  })
2221
2595
  ] }, g.group);
2222
2596
  }) }),
2223
- footer ? /* @__PURE__ */ jsx14("div", { style: styles.navFooter, children: footer }) : null
2597
+ footer ? /* @__PURE__ */ jsx16("div", { style: styles.navFooter, children: footer }) : null
2224
2598
  ]
2225
2599
  }
2226
2600
  );
2227
2601
  const title = NAV.flatMap((g) => g.items).find((i) => i.page === current)?.label ?? "";
2228
- return /* @__PURE__ */ jsxs14("div", { style: { ...styles.shell, ...narrow ? { flexDirection: "column" } : {} }, children: [
2229
- narrow ? /* @__PURE__ */ jsxs14(Fragment8, { children: [
2230
- /* @__PURE__ */ jsxs14("div", { style: styles.topbar, children: [
2231
- /* @__PURE__ */ jsx14(
2602
+ return /* @__PURE__ */ jsxs16("div", { style: { ...styles.shell, ...narrow ? { flexDirection: "column" } : {} }, children: [
2603
+ narrow ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
2604
+ /* @__PURE__ */ jsxs16("div", { style: styles.topbar, children: [
2605
+ /* @__PURE__ */ jsx16(
2232
2606
  "button",
2233
2607
  {
2234
2608
  type: "button",
@@ -2236,18 +2610,18 @@ function AdminShell({
2236
2610
  "aria-label": "Open menu",
2237
2611
  "aria-expanded": drawer,
2238
2612
  onClick: () => setDrawer(true),
2239
- children: /* @__PURE__ */ jsx14(Icon, { name: "menu" })
2613
+ children: /* @__PURE__ */ jsx16(Icon, { name: "menu" })
2240
2614
  }
2241
2615
  ),
2242
- /* @__PURE__ */ jsx14("span", { style: { fontWeight: 600, fontSize: 14 }, children: title }),
2243
- envLabel ? /* @__PURE__ */ jsx14("span", { style: { ...styles.badge, marginLeft: "auto" }, children: envLabel }) : null
2616
+ /* @__PURE__ */ jsx16("span", { style: { fontWeight: 600, fontSize: 14 }, children: title }),
2617
+ envLabel ? /* @__PURE__ */ jsx16("span", { style: { ...styles.badge, marginLeft: "auto" }, children: envLabel }) : null
2244
2618
  ] }),
2245
- drawer ? /* @__PURE__ */ jsxs14("div", { style: styles.drawer, children: [
2619
+ drawer ? /* @__PURE__ */ jsxs16("div", { style: styles.drawer, children: [
2246
2620
  nav,
2247
- /* @__PURE__ */ jsx14("div", { style: styles.scrim, onClick: () => setDrawer(false), "aria-hidden": "true" })
2621
+ /* @__PURE__ */ jsx16("div", { style: styles.scrim, onClick: () => setDrawer(false), "aria-hidden": "true" })
2248
2622
  ] }) : null
2249
2623
  ] }) : nav,
2250
- /* @__PURE__ */ jsx14("main", { className: "fonderie-admin-main", style: styles.main, children: body })
2624
+ /* @__PURE__ */ jsx16("main", { className: "fonderie-admin-main", style: styles.main, children: body })
2251
2625
  ] });
2252
2626
  }
2253
2627
  export {
@@ -2260,7 +2634,9 @@ export {
2260
2634
  EnvironmentScreen,
2261
2635
  MigrationsScreen,
2262
2636
  ModulesScreen,
2637
+ OperatorsScreen,
2263
2638
  RoutesScreen,
2639
+ SubscriberBilling,
2264
2640
  SubscriberScreen,
2265
2641
  TokensScreen,
2266
2642
  UsersScreen