@fonderie/vue-admin-screens 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/screens/AdminShell.ts
2
2
  import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/vue-config-admin-screens";
3
3
  import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/vue-courier-admin-screens";
4
- import { computed as computed2, defineComponent as defineComponent14, h as h16, onBeforeUnmount, onMounted, ref as ref7 } from "vue";
4
+ import { computed as computed3, defineComponent as defineComponent15, h as h17, onBeforeUnmount, onMounted, ref as ref9 } from "vue";
5
5
 
6
6
  // src/styles.ts
7
7
  var t = {
@@ -557,6 +557,12 @@ function empty(title, body, iconName = "check") {
557
557
  body ? h("div", body) : null
558
558
  ]);
559
559
  }
560
+ function refreshButton(onClick, busy, label = "Refresh") {
561
+ return h("button", { type: "button", style: styles.button, onClick, disabled: busy }, [
562
+ icon("refresh", 14),
563
+ busy ? "Working\u2026" : label
564
+ ]);
565
+ }
560
566
  function methodChip(method) {
561
567
  const tone = method === "GET" ? "ok" : method === "DELETE" ? "bad" : method === "POST" ? "info" : "warn";
562
568
  const color = toneColor[tone];
@@ -608,7 +614,7 @@ function table(headers, rows) {
608
614
  ]);
609
615
  }
610
616
  var td = (children, extra = {}) => h2("td", { style: { ...styles.td, ...extra } }, children);
611
- var refreshButton = (label, disabled, onClick) => h2("button", { type: "button", style: styles.button, disabled, onClick }, [
617
+ var refreshButton2 = (label, disabled, onClick) => h2("button", { type: "button", style: styles.button, disabled, onClick }, [
612
618
  icon("refresh", 14),
613
619
  disabled ? "Working\u2026" : label
614
620
  ]);
@@ -634,7 +640,7 @@ var AdminLogScreen = defineComponent({
634
640
  pageHeader(
635
641
  "Admin log",
636
642
  "Every request made through this surface, newest first \u2014 refused ones included.",
637
- [refreshButton("Refresh", isLoading.value, () => void refresh())]
643
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())]
638
644
  ),
639
645
  error.value ? h3(
640
646
  "p",
@@ -775,7 +781,7 @@ var AttentionScreen = defineComponent2({
775
781
  )
776
782
  ];
777
783
  },
778
- [refreshButton("Refresh", isLoading.value, () => void refresh())],
784
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())],
779
785
  {
780
786
  lead: a ? `What needs you on this deployment \xB7 checked ${new Date(a.generatedAt).toLocaleTimeString()}` : "What needs you on this deployment"
781
787
  }
@@ -955,7 +961,7 @@ var DoctorScreen = defineComponent4({
955
961
  report.value.ok ? "ok" : "bad",
956
962
  report.value.ok ? "all checks pass" : "a check failed"
957
963
  ) : null,
958
- refreshButton("Run again", isLoading.value, () => void refresh())
964
+ refreshButton2("Run again", isLoading.value, () => void refresh())
959
965
  ],
960
966
  {
961
967
  loadingText: "Running the checks\u2026",
@@ -1158,7 +1164,7 @@ var MigrationsScreen = defineComponent7({
1158
1164
  ...behind.map((m) => section(m, r.everApplied))
1159
1165
  ].filter(Boolean);
1160
1166
  },
1161
- [refreshButton("Refresh", isLoading.value, () => void refresh())],
1167
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())],
1162
1168
  {
1163
1169
  lead: "Schema changes each module ships, and whether this database has them.",
1164
1170
  errorText: (e) => e.status === 403 ? "Applying migrations needs a token with the write scope." : e.explanation
@@ -1673,28 +1679,233 @@ import {
1673
1679
  useAdminUserSessions,
1674
1680
  useAdminUsers
1675
1681
  } from "@fonderie/vue-admin";
1676
- import { computed, defineComponent as defineComponent10, h as h12, ref as ref4 } from "vue";
1677
- var UsersScreen = defineComponent10({
1682
+ import { computed as computed2, defineComponent as defineComponent11, h as h13, ref as ref6 } from "vue";
1683
+
1684
+ // src/billing.ts
1685
+ import { ref as ref4 } from "vue";
1686
+ function periodEnd(s) {
1687
+ if (!s.currentPeriodEnd) return null;
1688
+ const date = new Date(s.currentPeriodEnd).toLocaleDateString();
1689
+ if (s.status === "canceled")
1690
+ return { label: new Date(s.currentPeriodEnd) < /* @__PURE__ */ new Date() ? "Ended" : "Ends", date };
1691
+ if (s.cancelAtPeriodEnd) return { label: "Ends", date };
1692
+ return { label: "Renews", date };
1693
+ }
1694
+ var statusTone = (status) => status === "active" ? "ok" : status === "trialing" ? "info" : status === "past_due" || status === "unpaid" ? "warn" : "neutral";
1695
+ function useSubscriptionIndex(client) {
1696
+ const index = ref4(null);
1697
+ if (!client) return index;
1698
+ void (async () => {
1699
+ const map = /* @__PURE__ */ new Map();
1700
+ let cursor;
1701
+ for (let page2 = 0; page2 < 20; page2++) {
1702
+ const { result } = await client.listSubscriptions({
1703
+ limit: 100,
1704
+ ...cursor ? { cursor } : {}
1705
+ });
1706
+ for (const s of result.subscriptions) {
1707
+ const key = `${s.subscriberType}/${s.subscriberId}`;
1708
+ if (!map.has(key)) map.set(key, s);
1709
+ }
1710
+ if (!result.nextCursor) break;
1711
+ cursor = result.nextCursor;
1712
+ }
1713
+ index.value = map;
1714
+ })().catch(() => {
1715
+ index.value = /* @__PURE__ */ new Map();
1716
+ });
1717
+ return index;
1718
+ }
1719
+
1720
+ // src/screens/SubscriberBilling.ts
1721
+ import { useAdminSubscriber } from "@fonderie/vue-admin";
1722
+ import { computed, defineComponent as defineComponent10, h as h12, ref as ref5 } from "vue";
1723
+ var SubscriberBilling = defineComponent10({
1724
+ name: "FonderieSubscriberBilling",
1725
+ props: {
1726
+ client: { type: Object, required: true },
1727
+ subscriberType: { type: String, required: true },
1728
+ subscriberId: { type: String, required: true }
1729
+ },
1730
+ setup(props) {
1731
+ const subscriber = computed(() => ({ type: props.subscriberType, id: props.subscriberId }));
1732
+ const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(props.client, subscriber, { limit: 20 });
1733
+ const amount = ref5("");
1734
+ const note = ref5("");
1735
+ const granted = ref5(null);
1736
+ return () => {
1737
+ const s = subscription.value;
1738
+ const w = wallet.value;
1739
+ if (isLoading.value && !w && !s) return h12("p", { style: styles.status }, "Loading billing\u2026");
1740
+ const end = s ? periodEnd(s) : null;
1741
+ return h12("div", [
1742
+ error.value ? h12("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1743
+ h12("div", { style: { ...styles.grid, marginBottom: "16px" } }, [
1744
+ h12("div", { style: styles.card }, [
1745
+ h12("div", { style: styles.statLabel }, "Plan"),
1746
+ h12(
1747
+ "div",
1748
+ {
1749
+ style: {
1750
+ display: "flex",
1751
+ alignItems: "center",
1752
+ gap: "8px",
1753
+ marginTop: "6px",
1754
+ flexWrap: "wrap"
1755
+ }
1756
+ },
1757
+ [
1758
+ h12("strong", { style: { fontSize: "18px" } }, s ? s.plan : "Free"),
1759
+ ...s ? [
1760
+ h12("span", { style: styles.muted }, `\xB7 ${s.interval}`),
1761
+ pill(statusTone(s.status), s.status)
1762
+ ] : [h12("span", { style: styles.muted }, "no subscription")]
1763
+ ]
1764
+ ),
1765
+ end ? h12("div", { style: styles.statHint }, `${end.label} ${end.date}`) : null,
1766
+ s?.providerSubscriptionId ? h12(
1767
+ "div",
1768
+ { style: { ...styles.statHint, fontFamily: "var(--fonderie-mono,monospace)" } },
1769
+ s.providerSubscriptionId
1770
+ ) : null
1771
+ ]),
1772
+ h12("div", { style: styles.card }, [
1773
+ h12("div", { style: styles.statLabel }, "Credits"),
1774
+ ...w ? [
1775
+ h12("div", { style: styles.statValue }, [
1776
+ `${w.balance} `,
1777
+ h12("span", { style: { fontSize: "14px", fontWeight: 500 } }, w.currency)
1778
+ ]),
1779
+ h12(
1780
+ "div",
1781
+ { style: styles.statHint },
1782
+ `${w.purchased ?? "0"} permanent \xB7 ${w.granted ?? "0"} plan allowance${w.grantedExpiresAt ? ` (expires ${new Date(w.grantedExpiresAt).toLocaleDateString()})` : ""}`
1783
+ )
1784
+ ] : [
1785
+ h12(
1786
+ "div",
1787
+ { style: styles.statHint },
1788
+ "No wallet \u2014 billing has no wallet configuration."
1789
+ )
1790
+ ]
1791
+ ])
1792
+ ]),
1793
+ w ? h12(
1794
+ "form",
1795
+ {
1796
+ style: {
1797
+ ...styles.card,
1798
+ display: "flex",
1799
+ gap: "8px",
1800
+ alignItems: "center",
1801
+ flexWrap: "wrap",
1802
+ marginBottom: "16px"
1803
+ },
1804
+ onSubmit: (e) => {
1805
+ e.preventDefault();
1806
+ const key = `admin-grant-${props.subscriberType}-${props.subscriberId}-${Date.now()}`;
1807
+ const a = amount.value.trim();
1808
+ void grant({
1809
+ amount: a,
1810
+ ...note.value ? { description: note.value } : {},
1811
+ idempotencyKey: key
1812
+ }).then(() => {
1813
+ granted.value = `Granted ${a} ${w.currency}.`;
1814
+ amount.value = "";
1815
+ note.value = "";
1816
+ }).catch(() => {
1817
+ });
1818
+ }
1819
+ },
1820
+ [
1821
+ h12("strong", { style: { fontSize: "13.5px", marginRight: "4px" } }, "Grant credits"),
1822
+ h12("input", {
1823
+ value: amount.value,
1824
+ onInput: (e) => amount.value = e.target.value,
1825
+ placeholder: `amount (${w.currency})`,
1826
+ inputmode: "numeric",
1827
+ style: { ...styles.input, width: "150px" },
1828
+ "aria-label": "Amount"
1829
+ }),
1830
+ h12("input", {
1831
+ value: note.value,
1832
+ onInput: (e) => note.value = e.target.value,
1833
+ placeholder: "reason (optional)",
1834
+ style: { ...styles.input, flex: 1, minWidth: "180px" },
1835
+ "aria-label": "Reason"
1836
+ }),
1837
+ h12(
1838
+ "button",
1839
+ {
1840
+ type: "submit",
1841
+ style: styles.buttonPrimary,
1842
+ disabled: !/^\d+$/.test(amount.value.trim())
1843
+ },
1844
+ [icon("plus", 14), "Grant"]
1845
+ ),
1846
+ granted.value ? h12("span", { style: styles.ok }, granted.value) : null
1847
+ ]
1848
+ ) : null,
1849
+ w ? ledger.value.length === 0 ? empty("No credit movements yet", void 0, "subscriber") : h12("div", [
1850
+ table(
1851
+ ["When", "Kind", "Amount", "Note"],
1852
+ ledger.value.map(
1853
+ (t2) => h12("tr", { key: t2.id }, [
1854
+ td(new Date(t2.createdAt).toLocaleString(), {
1855
+ ...styles.muted,
1856
+ whiteSpace: "nowrap"
1857
+ }),
1858
+ td(t2.type),
1859
+ td(t2.amount, styles.mono),
1860
+ td(t2.description ?? "", styles.muted)
1861
+ ])
1862
+ )
1863
+ ),
1864
+ hasMoreLedger.value ? loadMoreButton(() => void loadMoreLedger()) : null
1865
+ ]) : null
1866
+ ]);
1867
+ };
1868
+ }
1869
+ });
1870
+
1871
+ // src/screens/UsersScreen.ts
1872
+ var UsersScreen = defineComponent11({
1678
1873
  name: "FonderieUsersScreen",
1679
1874
  props: {
1680
1875
  client: { type: Object, required: true },
1681
- pageSize: { type: Number, default: 50 }
1876
+ pageSize: { type: Number, default: 50 },
1877
+ // Given ⇒ a Plan column, and each user's plan, credits and grant form on
1878
+ // their page. Everyone has a wallet; no subscription means the free tier.
1879
+ billingClient: { type: Object, default: void 0 },
1880
+ // Open this user on arrival (e.g. from the Subscriptions list).
1881
+ openUserId: { type: String, default: void 0 }
1682
1882
  },
1683
1883
  setup(props) {
1684
- const input = ref4("");
1685
- const email = ref4("");
1686
- const selectedId = ref4("");
1884
+ const input = ref6("");
1885
+ const email = ref6("");
1886
+ const selectedId = ref6(props.openUserId ?? "");
1887
+ const plans = useSubscriptionIndex(props.billingClient);
1687
1888
  const list = useAdminUsers(props.client, { limit: props.pageSize });
1688
1889
  const { user, isLoading, error, suspend, unsuspend, revokeSessions } = useAdminUser(
1689
1890
  props.client,
1690
1891
  { email, id: selectedId }
1691
1892
  );
1692
- const userId = computed(() => user.value?.id ?? null);
1893
+ const userId = computed2(() => user.value?.id ?? null);
1693
1894
  const sessions = useAdminUserSessions(props.client, userId);
1694
1895
  const history = useAdminLoginHistory(props.client, userId, { limit: 20 });
1695
- const showList = computed(() => !email.value && !selectedId.value);
1896
+ const showList = computed2(() => !email.value && !selectedId.value);
1696
1897
  const yesNo = (v) => v ? pill("ok", "yes") : pill("neutral", "no");
1697
- const row = (k, v) => h12("tr", [td(k), td(v)]);
1898
+ const row = (k, v) => h13("tr", [td(k), td(v)]);
1899
+ const planCell = (id) => {
1900
+ if (!plans.value) return h13("span", { style: styles.muted }, "\u2026");
1901
+ const sub = plans.value.get(`user/${id}`);
1902
+ if (!sub || sub.status === "canceled") return h13("span", { style: styles.muted }, "free");
1903
+ return [
1904
+ h13("strong", sub.plan),
1905
+ " ",
1906
+ sub.status !== "active" ? pill(statusTone(sub.status), sub.status) : null
1907
+ ];
1908
+ };
1698
1909
  const clear = () => {
1699
1910
  email.value = "";
1700
1911
  selectedId.value = "";
@@ -1702,12 +1913,12 @@ var UsersScreen = defineComponent10({
1702
1913
  };
1703
1914
  return () => {
1704
1915
  const u = user.value;
1705
- return h12("div", { style: styles.container }, [
1916
+ return h13("div", { style: styles.container }, [
1706
1917
  pageHeader(
1707
1918
  "Users",
1708
1919
  "Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
1709
1920
  ),
1710
- h12(
1921
+ h13(
1711
1922
  "form",
1712
1923
  {
1713
1924
  style: styles.toolbar,
@@ -1718,7 +1929,7 @@ var UsersScreen = defineComponent10({
1718
1929
  }
1719
1930
  },
1720
1931
  [
1721
- h12("input", {
1932
+ h13("input", {
1722
1933
  type: "email",
1723
1934
  value: input.value,
1724
1935
  onInput: (e) => input.value = e.target.value,
@@ -1726,22 +1937,22 @@ var UsersScreen = defineComponent10({
1726
1937
  style: { ...styles.input, minWidth: "280px" },
1727
1938
  "aria-label": "Email"
1728
1939
  }),
1729
- h12(
1940
+ h13(
1730
1941
  "button",
1731
1942
  { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1732
1943
  [icon("search", 14), "Look up"]
1733
1944
  ),
1734
- showList.value ? refreshButton("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost)
1945
+ showList.value ? refreshButton2("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost)
1735
1946
  ]
1736
1947
  ),
1737
- showList.value ? h12("div", [
1738
- list.error.value ? h12("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1948
+ showList.value ? h13("div", [
1949
+ list.error.value ? h13("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1739
1950
  list.users.value.length === 0 && !list.isLoading.value ? empty("No users yet", "Sign-ups appear here as they happen.", "users") : table(
1740
- ["Email", "Name", "Created", "Status"],
1951
+ props.billingClient ? ["Email", "Name", "Plan", "Created", "Status"] : ["Email", "Name", "Created", "Status"],
1741
1952
  list.users.value.map(
1742
- (u2) => h12("tr", { key: u2.id }, [
1953
+ (u2) => h13("tr", { key: u2.id }, [
1743
1954
  td(
1744
- h12(
1955
+ h13(
1745
1956
  "button",
1746
1957
  {
1747
1958
  type: "button",
@@ -1752,8 +1963,9 @@ var UsersScreen = defineComponent10({
1752
1963
  )
1753
1964
  ),
1754
1965
  td(
1755
- `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || h12("span", { style: styles.muted }, "\u2014")
1966
+ `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || h13("span", { style: styles.muted }, "\u2014")
1756
1967
  ),
1968
+ props.billingClient ? td(planCell(u2.id)) : null,
1757
1969
  td(new Date(u2.createdAt).toLocaleDateString()),
1758
1970
  td(
1759
1971
  u2.suspended ? pill("warn", "suspended") : u2.deletedAt ? pill("neutral", "deleted") : pill("ok", "active")
@@ -1761,24 +1973,42 @@ var UsersScreen = defineComponent10({
1761
1973
  ])
1762
1974
  )
1763
1975
  ),
1764
- list.isLoading.value ? h12("p", { style: styles.status }, "Loading\u2026") : null,
1976
+ list.isLoading.value ? h13("p", { style: styles.status }, "Loading\u2026") : null,
1765
1977
  list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
1766
1978
  ]) : null,
1767
- error.value ? h12(
1979
+ error.value && !(error.value.status === 404 && selectedId.value) ? h13(
1768
1980
  "p",
1769
1981
  { style: styles.error, role: "alert" },
1770
1982
  error.value.status === 404 ? "No user with that email." : error.value.explanation
1771
1983
  ) : null,
1984
+ // Opened by id (e.g. from Subscriptions) and the account is gone —
1985
+ // deleted or purged — while its billing rows remain. Say so, and
1986
+ // still show the money.
1987
+ error.value?.status === 404 && selectedId.value ? [
1988
+ h13("div", { style: styles.notice, role: "status" }, [
1989
+ h13("strong", "No account with id "),
1990
+ h13("code", { style: styles.code }, selectedId.value),
1991
+ h13("strong", "."),
1992
+ " It was deleted, or never existed here. Its billing records remain."
1993
+ ]),
1994
+ props.billingClient ? h13("h2", { style: styles.subtitle }, "Plan & credits") : null,
1995
+ props.billingClient ? h13(SubscriberBilling, {
1996
+ key: `missing-${selectedId.value}`,
1997
+ client: props.billingClient,
1998
+ subscriberType: "user",
1999
+ subscriberId: selectedId.value
2000
+ }) : null
2001
+ ] : null,
1772
2002
  u ? [
1773
- h12("h2", { style: styles.subtitle }, [
2003
+ h13("h2", { style: styles.subtitle }, [
1774
2004
  u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
1775
2005
  " ",
1776
2006
  u.suspended ? pill("warn", "suspended") : null,
1777
2007
  u.deletedAt ? pill("neutral", "deleted") : null
1778
2008
  ]),
1779
- h12("table", { style: styles.table }, [
1780
- h12("tbody", [
1781
- row("id", h12("span", { style: styles.mono }, u.id)),
2009
+ h13("table", { style: styles.table }, [
2010
+ h13("tbody", [
2011
+ row("id", h13("span", { style: styles.mono }, u.id)),
1782
2012
  row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
1783
2013
  row("MFA", yesNo(u.mfaEnabled)),
1784
2014
  row(
@@ -1787,12 +2017,12 @@ var UsersScreen = defineComponent10({
1787
2017
  ),
1788
2018
  row(
1789
2019
  "last login",
1790
- u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h12("span", { style: styles.muted }, "never")
2020
+ u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h13("span", { style: styles.muted }, "never")
1791
2021
  ),
1792
2022
  row("created", new Date(u.createdAt).toLocaleString())
1793
2023
  ])
1794
2024
  ]),
1795
- h12("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
2025
+ h13("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
1796
2026
  u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
1797
2027
  "Suspend",
1798
2028
  isLoading.value,
@@ -1805,19 +2035,26 @@ var UsersScreen = defineComponent10({
1805
2035
  () => void revokeSessions().then(() => sessions.refresh())
1806
2036
  )
1807
2037
  ]),
1808
- h12("h2", { style: styles.subtitle }, "Live sessions"),
1809
- sessions.sessions.value.length === 0 ? h12("p", { style: styles.muted }, "None.") : h12(
2038
+ props.billingClient ? h13("h2", { style: styles.subtitle }, "Plan & credits") : null,
2039
+ props.billingClient ? h13(SubscriberBilling, {
2040
+ key: u.id,
2041
+ client: props.billingClient,
2042
+ subscriberType: "user",
2043
+ subscriberId: u.id
2044
+ }) : null,
2045
+ h13("h2", { style: styles.subtitle }, "Live sessions"),
2046
+ sessions.sessions.value.length === 0 ? h13("p", { style: styles.muted }, "None.") : h13(
1810
2047
  "ul",
1811
2048
  { style: styles.list },
1812
2049
  sessions.sessions.value.map(
1813
- (s) => h12("li", { key: s.id, style: styles.row }, [
1814
- h12("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
2050
+ (s) => h13("li", { key: s.id, style: styles.row }, [
2051
+ h13("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
1815
2052
  " ",
1816
- h12("span", { style: styles.muted }, describeLocation(s.location) ?? ""),
2053
+ h13("span", { style: styles.muted }, describeLocation(s.location) ?? ""),
1817
2054
  " ",
1818
- h12("span", { style: styles.muted }, s.userAgent ?? ""),
2055
+ h13("span", { style: styles.muted }, s.userAgent ?? ""),
1819
2056
  " ",
1820
- h12(
2057
+ h13(
1821
2058
  "span",
1822
2059
  { style: styles.muted },
1823
2060
  `since ${new Date(s.createdAt).toLocaleString()}`
@@ -1825,22 +2062,22 @@ var UsersScreen = defineComponent10({
1825
2062
  ])
1826
2063
  )
1827
2064
  ),
1828
- h12("h2", { style: styles.subtitle }, "Recent sign-ins"),
1829
- history.events.value.length === 0 && !history.isLoading.value ? h12("p", { style: styles.muted }, "None recorded.") : h12(
2065
+ h13("h2", { style: styles.subtitle }, "Recent sign-ins"),
2066
+ history.events.value.length === 0 && !history.isLoading.value ? h13("p", { style: styles.muted }, "None recorded.") : h13(
1830
2067
  "ul",
1831
2068
  { style: styles.list },
1832
2069
  history.events.value.map(
1833
- (e) => h12("li", { key: e.id, style: styles.row }, [
2070
+ (e) => h13("li", { key: e.id, style: styles.row }, [
1834
2071
  pill(e.outcome === "success" ? "ok" : "bad", e.outcome),
1835
2072
  " ",
1836
- h12("span", { style: styles.muted }, e.method),
2073
+ h13("span", { style: styles.muted }, e.method),
1837
2074
  " ",
1838
- h12("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
2075
+ h13("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
1839
2076
  " ",
1840
- h12("span", { style: styles.muted }, describeLocation(e.location) ?? ""),
2077
+ h13("span", { style: styles.muted }, describeLocation(e.location) ?? ""),
1841
2078
  e.location?.proxy || e.location?.hosting ? [" ", pill("warn", e.location.proxy ? "proxy/VPN" : "hosting")] : "",
1842
2079
  " ",
1843
- h12("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
2080
+ h13("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
1844
2081
  ])
1845
2082
  )
1846
2083
  ),
@@ -1853,8 +2090,8 @@ var UsersScreen = defineComponent10({
1853
2090
 
1854
2091
  // src/screens/CatalogScreen.ts
1855
2092
  import { useAdminCatalog } from "@fonderie/vue-admin";
1856
- import { defineComponent as defineComponent11, h as h13 } from "vue";
1857
- var CatalogScreen = defineComponent11({
2093
+ import { defineComponent as defineComponent12, h as h14 } from "vue";
2094
+ var CatalogScreen = defineComponent12({
1858
2095
  name: "FonderieCatalogScreen",
1859
2096
  props: { client: { type: Object, required: true } },
1860
2097
  setup(props) {
@@ -1862,17 +2099,17 @@ var CatalogScreen = defineComponent11({
1862
2099
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
1863
2100
  return () => {
1864
2101
  const c = catalog.value;
1865
- return h13("div", { style: styles.container }, [
2102
+ return h14("div", { style: styles.container }, [
1866
2103
  pageHeader(
1867
2104
  "Catalog",
1868
2105
  "What you sell: plans as configured in code, and as stored in the database, side by side.",
1869
- [refreshButton("Refresh", isLoading.value, () => void refresh())]
2106
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())]
1870
2107
  ),
1871
- error.value ? h13("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1872
- isLoading.value && !c ? h13("p", { style: styles.status }, "Loading\u2026") : null,
2108
+ error.value ? h14("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2109
+ isLoading.value && !c ? h14("p", { style: styles.status }, "Loading\u2026") : null,
1873
2110
  c ? [
1874
- h13("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
1875
- h13(
2111
+ h14("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
2112
+ h14(
1876
2113
  "pre",
1877
2114
  {
1878
2115
  style: {
@@ -1886,7 +2123,7 @@ var CatalogScreen = defineComponent11({
1886
2123
  },
1887
2124
  JSON.stringify(c.configured, null, 2)
1888
2125
  ),
1889
- h13("h2", { style: styles.subtitle }, "Stored (database)"),
2126
+ h14("h2", { style: styles.subtitle }, "Stored (database)"),
1890
2127
  c.stored.length === 0 ? empty(
1891
2128
  "No stored plans",
1892
2129
  "Plans are configured in code; nothing has been written to the database.",
@@ -1894,10 +2131,10 @@ var CatalogScreen = defineComponent11({
1894
2131
  ) : table(
1895
2132
  ["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
1896
2133
  c.stored.map(
1897
- (p) => h13("tr", { key: p.id }, [
2134
+ (p) => h14("tr", { key: p.id }, [
1898
2135
  td([
1899
- h13("strong", p.name),
1900
- p.description ? h13("div", { style: styles.muted }, p.description) : ""
2136
+ h14("strong", p.name),
2137
+ p.description ? h14("div", { style: styles.muted }, p.description) : ""
1901
2138
  ]),
1902
2139
  td(String(p.tier)),
1903
2140
  td(p.seats == null ? "\u221E" : String(p.seats)),
@@ -1905,7 +2142,7 @@ var CatalogScreen = defineComponent11({
1905
2142
  td(money(p.pricing?.yearly)),
1906
2143
  td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
1907
2144
  td(
1908
- h13(
2145
+ h14(
1909
2146
  "button",
1910
2147
  {
1911
2148
  type: "button",
@@ -1929,259 +2166,171 @@ var CatalogScreen = defineComponent11({
1929
2166
  });
1930
2167
 
1931
2168
  // src/screens/SubscriberScreen.ts
1932
- import { useAdminSubscriber, useAdminSubscribers } from "@fonderie/vue-admin";
1933
- import { defineComponent as defineComponent12, h as h14, ref as ref5 } from "vue";
1934
- var SubscriberScreen = defineComponent12({
2169
+ import { useAdminSubscribers } from "@fonderie/vue-admin";
2170
+ import { defineComponent as defineComponent13, h as h15, ref as ref7 } from "vue";
2171
+ var FILTERS = [
2172
+ { label: "All", match: () => true },
2173
+ { label: "Active", match: (s) => s === "active" },
2174
+ { label: "Trialing", match: (s) => s === "trialing" },
2175
+ { label: "Past due", match: (s) => s === "past_due" || s === "unpaid" },
2176
+ { label: "Canceled", match: (s) => s === "canceled" }
2177
+ ];
2178
+ var SubscriberScreen = defineComponent13({
1935
2179
  name: "FonderieSubscriberScreen",
1936
2180
  props: {
1937
2181
  client: { type: Object, required: true },
1938
- pageSize: { type: Number, default: 50 }
2182
+ pageSize: { type: Number, default: 50 },
2183
+ /**
2184
+ * A user subscriber opens on the Users page, where their plan and credits
2185
+ * live beside their account. Omit to open everything here.
2186
+ */
2187
+ onOpenUser: { type: Function, default: void 0 }
1939
2188
  },
1940
2189
  setup(props) {
1941
- const type = ref5("user");
1942
- const idInput = ref5("");
1943
- const subscriber = ref5(null);
1944
2190
  const list = useAdminSubscribers(props.client, { limit: props.pageSize });
1945
- const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(props.client, subscriber, { limit: 20 });
1946
- const open = (t2, id) => {
1947
- granted.value = null;
1948
- type.value = t2;
1949
- idInput.value = id;
1950
- subscriber.value = { type: t2, id };
2191
+ const filter = ref7("All");
2192
+ const open = ref7(null);
2193
+ const type = ref7("workspace");
2194
+ const idInput = ref7("");
2195
+ const openRow = (t2, id) => {
2196
+ if (t2 === "user" && props.onOpenUser) props.onOpenUser(id);
2197
+ else open.value = { type: t2, id };
1951
2198
  };
1952
- const amount = ref5("");
1953
- const note = ref5("");
1954
- const granted = ref5(null);
1955
- const input = (model, placeholder, extra = {}) => h14("input", {
1956
- value: model.value,
1957
- onInput: (e) => model.value = e.target.value,
1958
- placeholder,
1959
- style: { ...styles.input, ...extra },
1960
- "aria-label": placeholder
1961
- });
1962
2199
  return () => {
1963
- const s = subscription.value;
1964
- const w = wallet.value;
1965
- return h14("div", { style: styles.container }, [
1966
- pageHeader(
1967
- "Subscribers",
1968
- "Who is subscribed. Open one for their plan, wallet and ledger, and to grant credit."
1969
- ),
1970
- h14(
1971
- "form",
1972
- {
1973
- style: styles.toolbar,
1974
- onSubmit: (e) => {
1975
- e.preventDefault();
1976
- granted.value = null;
1977
- subscriber.value = idInput.value.trim() ? { type: type.value, id: idInput.value.trim() } : null;
1978
- }
1979
- },
1980
- [
1981
- h14(
1982
- "select",
1983
- {
1984
- value: type.value,
1985
- onChange: (e) => type.value = e.target.value,
1986
- style: styles.input,
1987
- "aria-label": "Type"
1988
- },
1989
- [
1990
- h14("option", { value: "user" }, "user"),
1991
- h14("option", { value: "workspace" }, "workspace")
1992
- ]
1993
- ),
1994
- input(idInput, "subscriber id", { minWidth: "280px" }),
1995
- h14(
1996
- "button",
1997
- { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1998
- [icon("search", 14), "Look up"]
1999
- ),
2000
- subscriber.value ? h14(
2001
- "button",
2002
- {
2003
- type: "button",
2004
- style: styles.button,
2005
- onClick: () => {
2006
- subscriber.value = null;
2007
- idInput.value = "";
2008
- granted.value = null;
2009
- }
2010
- },
2011
- [icon("back", 14), "All subscribers"]
2012
- ) : h14(
2200
+ const o = open.value;
2201
+ if (o) {
2202
+ return h15("div", { style: styles.container }, [
2203
+ pageHeader(`${o.type}/${o.id}`, "Plan, credits and what moved.", [
2204
+ h15(
2013
2205
  "button",
2014
- {
2015
- type: "button",
2016
- style: styles.button,
2017
- disabled: list.isLoading.value,
2018
- onClick: () => void list.refresh()
2019
- },
2020
- "Refresh"
2206
+ { type: "button", style: styles.buttonGhost, onClick: () => open.value = null },
2207
+ [icon("back", 14), "All subscriptions"]
2021
2208
  )
2022
- ]
2209
+ ]),
2210
+ h15(SubscriberBilling, {
2211
+ key: `${o.type}/${o.id}`,
2212
+ client: props.client,
2213
+ subscriberType: o.type,
2214
+ subscriberId: o.id
2215
+ })
2216
+ ]);
2217
+ }
2218
+ const match = FILTERS.find((f) => f.label === filter.value)?.match ?? (() => true);
2219
+ const rows = list.subscriptions.value.filter((s) => match(s.status));
2220
+ return h15("div", { style: styles.container }, [
2221
+ pageHeader(
2222
+ "Subscriptions",
2223
+ "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.",
2224
+ [refreshButton(() => void list.refresh(), list.isLoading.value)]
2023
2225
  ),
2024
- !subscriber.value ? h14("div", [
2025
- list.error.value ? h14("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
2026
- list.subscriptions.value.length === 0 && !list.isLoading.value ? empty(
2027
- "No subscribers yet",
2028
- "Subscriptions appear here once someone checks out.",
2029
- "subscriber"
2030
- ) : table(
2031
- ["Subscriber", "Plan", "Status", "Renews"],
2032
- list.subscriptions.value.map(
2033
- (sub) => h14("tr", { key: sub.id }, [
2034
- td(
2035
- h14(
2036
- "button",
2037
- {
2038
- type: "button",
2039
- style: { ...styles.link, ...styles.mono },
2040
- onClick: () => open(sub.subscriberType, sub.subscriberId)
2041
- },
2042
- `${sub.subscriberType}/${sub.subscriberId}`
2043
- )
2044
- ),
2045
- td(`${sub.plan} \xB7 ${sub.interval}`),
2046
- td([
2047
- pill(
2048
- sub.status === "active" ? "ok" : sub.status === "trialing" ? "info" : sub.status === "past_due" ? "warn" : "neutral",
2049
- sub.status
2050
- ),
2051
- sub.cancelAtPeriodEnd ? [" ", pill("warn", "cancels", false)] : null
2052
- ]),
2053
- td(
2054
- sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : h14("span", { style: styles.muted }, "\u2014")
2055
- )
2056
- ])
2057
- )
2226
+ h15("div", { style: { ...styles.toolbar, justifyContent: "space-between" } }, [
2227
+ h15(
2228
+ "fieldset",
2229
+ {
2230
+ style: {
2231
+ display: "flex",
2232
+ gap: "6px",
2233
+ flexWrap: "wrap",
2234
+ border: "none",
2235
+ margin: 0,
2236
+ padding: 0,
2237
+ minWidth: 0
2238
+ },
2239
+ "aria-label": "Filter by status"
2240
+ },
2241
+ FILTERS.map((f) => {
2242
+ const n = list.subscriptions.value.filter((s) => f.match(s.status)).length;
2243
+ const on = filter.value === f.label;
2244
+ return h15(
2245
+ "button",
2246
+ {
2247
+ key: f.label,
2248
+ type: "button",
2249
+ "aria-pressed": on,
2250
+ onClick: () => filter.value = f.label,
2251
+ style: { ...on ? styles.buttonPrimary : styles.button, height: "28px" }
2252
+ },
2253
+ [f.label, h15("span", { style: { opacity: 0.7 } }, String(n))]
2254
+ );
2255
+ })
2058
2256
  ),
2059
- list.isLoading.value ? h14("p", { style: styles.status }, "Loading\u2026") : null,
2060
- list.hasMore.value && !list.isLoading.value ? h14(
2061
- "button",
2257
+ h15(
2258
+ "form",
2062
2259
  {
2063
- type: "button",
2064
- style: { ...styles.button, marginTop: "8px" },
2065
- onClick: () => void list.loadMore()
2260
+ style: { display: "flex", gap: "6px" },
2261
+ onSubmit: (e) => {
2262
+ e.preventDefault();
2263
+ if (idInput.value.trim()) openRow(type.value, idInput.value.trim());
2264
+ }
2066
2265
  },
2067
- "Load more"
2068
- ) : null
2069
- ]) : null,
2070
- error.value ? h14("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2071
- subscriber.value && !isLoading.value ? [
2072
- h14("h2", { style: styles.subtitle }, "Subscription"),
2073
- s ? h14("table", { style: styles.table }, [
2074
- h14("tbody", [
2075
- h14("tr", [td("plan"), td([h14("strong", s.plan), ` \xB7 ${s.interval}`])]),
2076
- h14("tr", [
2077
- td("status"),
2078
- td(`${s.status}${s.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""}`)
2079
- ]),
2080
- h14("tr", [
2081
- td("period"),
2082
- td(
2083
- `${s.currentPeriodStart ? new Date(s.currentPeriodStart).toLocaleDateString() : "\u2014"} \u2192 ${s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : "\u2014"}`
2084
- )
2085
- ]),
2086
- h14("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
2087
- ])
2088
- ]) : h14("p", { style: styles.muted }, "No subscription."),
2089
- h14("h2", { style: styles.subtitle }, "Wallet"),
2090
- w ? [
2091
- h14(
2092
- "p",
2093
- {
2094
- style: {
2095
- ...styles.card,
2096
- display: "flex",
2097
- alignItems: "baseline",
2098
- gap: "8px",
2099
- flexWrap: "wrap"
2100
- }
2101
- },
2102
- [
2103
- h14(
2104
- "strong",
2105
- {
2106
- style: {
2107
- fontSize: "22px",
2108
- letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
2109
- }
2110
- },
2111
- w.balance
2112
- ),
2113
- ` ${w.currency} `,
2114
- h14(
2115
- "span",
2116
- { style: styles.muted },
2117
- `(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
2118
- )
2119
- ]
2120
- ),
2121
- h14(
2122
- "form",
2123
- {
2124
- style: styles.toolbar,
2125
- onSubmit: (e) => {
2126
- e.preventDefault();
2127
- const sub = subscriber.value;
2128
- if (!sub) return;
2129
- const key = `admin-grant-${sub.type}-${sub.id}-${Date.now()}`;
2130
- const amt = amount.value.trim();
2131
- void grant({
2132
- amount: amt,
2133
- ...note.value ? { description: note.value } : {},
2134
- idempotencyKey: key
2135
- }).then(() => {
2136
- granted.value = `Granted ${amt} ${w.currency}.`;
2137
- amount.value = "";
2138
- note.value = "";
2139
- }).catch(() => {
2140
- });
2141
- }
2142
- },
2143
- [
2144
- input(amount, "amount (minor units)"),
2145
- input(note, "reason (optional)", { minWidth: "220px" }),
2146
- h14(
2266
+ [
2267
+ h15(
2268
+ "select",
2269
+ {
2270
+ value: type.value,
2271
+ onChange: (e) => type.value = e.target.value,
2272
+ style: styles.input,
2273
+ "aria-label": "Type"
2274
+ },
2275
+ [
2276
+ h15("option", { value: "workspace" }, "workspace"),
2277
+ h15("option", { value: "user" }, "user")
2278
+ ]
2279
+ ),
2280
+ h15("input", {
2281
+ value: idInput.value,
2282
+ onInput: (e) => idInput.value = e.target.value,
2283
+ placeholder: "subscriber id",
2284
+ style: { ...styles.input, width: "220px" },
2285
+ "aria-label": "Subscriber id"
2286
+ }),
2287
+ h15(
2288
+ "button",
2289
+ { type: "submit", style: styles.button, disabled: !idInput.value.trim() },
2290
+ [icon("search", 14), "Open"]
2291
+ )
2292
+ ]
2293
+ )
2294
+ ]),
2295
+ list.error.value ? h15("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
2296
+ rows.length === 0 && !list.isLoading.value ? empty(
2297
+ filter.value === "All" ? "No subscriptions yet" : `No ${filter.value.toLowerCase()} subscriptions`,
2298
+ "Subscriptions appear here once someone checks out.",
2299
+ "subscriber"
2300
+ ) : table(
2301
+ ["Subscriber", "Plan", "Status", "Renews / ends"],
2302
+ rows.map((s) => {
2303
+ const end = periodEnd(s);
2304
+ return h15("tr", { key: s.id }, [
2305
+ td(
2306
+ h15(
2147
2307
  "button",
2148
2308
  {
2149
- type: "submit",
2150
- style: styles.buttonPrimary,
2151
- disabled: !/^\d+$/.test(amount.value.trim())
2309
+ type: "button",
2310
+ style: { ...styles.link, ...styles.mono },
2311
+ onClick: () => openRow(s.subscriberType, s.subscriberId)
2152
2312
  },
2153
- [icon("plus", 14), "Grant"]
2154
- ),
2155
- granted.value ? h14("span", { style: styles.ok }, granted.value) : null
2156
- ]
2157
- ),
2158
- h14("h2", { style: styles.subtitle }, "Ledger"),
2159
- ledger.value.length === 0 ? empty("Nothing moved yet", void 0, "subscriber") : table(
2160
- ["When", "Kind", "Amount", "Note"],
2161
- ledger.value.map(
2162
- (t2) => h14("tr", { key: t2.id }, [
2163
- td(new Date(t2.createdAt).toLocaleString(), styles.muted),
2164
- td(t2.type),
2165
- td(t2.amount, styles.mono),
2166
- td(t2.description ?? "", styles.muted)
2167
- ])
2313
+ `${s.subscriberType}/${s.subscriberId}`
2314
+ )
2315
+ ),
2316
+ td([
2317
+ h15("strong", s.plan),
2318
+ " ",
2319
+ h15("span", { style: styles.muted }, `\xB7 ${s.interval}`)
2320
+ ]),
2321
+ td([
2322
+ pill(statusTone(s.status), s.status),
2323
+ s.cancelAtPeriodEnd && s.status !== "canceled" ? h15("span", [" ", pill("warn", "cancels", false)]) : null
2324
+ ]),
2325
+ td(
2326
+ end ? [h15("span", { style: styles.muted }, end.label), ` ${end.date}`] : h15("span", { style: styles.muted }, "\u2014"),
2327
+ { whiteSpace: "nowrap" }
2168
2328
  )
2169
- ),
2170
- hasMoreLedger.value ? h14(
2171
- "button",
2172
- {
2173
- type: "button",
2174
- style: { ...styles.button, marginTop: "8px" },
2175
- onClick: () => void loadMoreLedger()
2176
- },
2177
- "Load more"
2178
- ) : null
2179
- ] : h14(
2180
- "p",
2181
- { style: styles.muted },
2182
- "No wallet \u2014 billing has no wallet configuration."
2183
- )
2184
- ] : null
2329
+ ]);
2330
+ })
2331
+ ),
2332
+ list.isLoading.value ? h15("p", { style: styles.status }, "Loading\u2026") : null,
2333
+ list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
2185
2334
  ]);
2186
2335
  };
2187
2336
  }
@@ -2189,33 +2338,33 @@ var SubscriberScreen = defineComponent12({
2189
2338
 
2190
2339
  // src/screens/AuditScreen.ts
2191
2340
  import { useAdminAudit } from "@fonderie/vue-admin";
2192
- import { defineComponent as defineComponent13, h as h15, ref as ref6 } from "vue";
2193
- var AuditScreen = defineComponent13({
2341
+ import { defineComponent as defineComponent14, h as h16, ref as ref8 } from "vue";
2342
+ var AuditScreen = defineComponent14({
2194
2343
  name: "FonderieAuditScreen",
2195
2344
  props: {
2196
2345
  client: { type: Object, required: true },
2197
2346
  pageSize: { type: Number, default: 50 }
2198
2347
  },
2199
2348
  setup(props) {
2200
- const draft = ref6({ workspaceId: "", type: "", actorId: "" });
2201
- const query = ref6({ limit: props.pageSize });
2349
+ const draft = ref8({ workspaceId: "", type: "", actorId: "" });
2350
+ const query = ref8({ limit: props.pageSize });
2202
2351
  const { events, hasMore, isLoading, error, refresh, loadMore } = useAdminAudit(
2203
2352
  props.client,
2204
2353
  query
2205
2354
  );
2206
- const field = (key, placeholder) => h15("input", {
2355
+ const field = (key, placeholder) => h16("input", {
2207
2356
  value: draft.value[key],
2208
2357
  onInput: (e) => draft.value = { ...draft.value, [key]: e.target.value },
2209
2358
  placeholder,
2210
2359
  style: styles.input,
2211
2360
  "aria-label": placeholder
2212
2361
  });
2213
- return () => h15("div", { style: styles.container }, [
2362
+ return () => h16("div", { style: styles.container }, [
2214
2363
  pageHeader(
2215
2364
  "Audit",
2216
2365
  "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
2217
2366
  ),
2218
- h15(
2367
+ h16(
2219
2368
  "form",
2220
2369
  {
2221
2370
  style: styles.toolbar,
@@ -2234,19 +2383,19 @@ var AuditScreen = defineComponent13({
2234
2383
  field("workspaceId", "workspace id (all if empty)"),
2235
2384
  field("type", "event type"),
2236
2385
  field("actorId", "actor id"),
2237
- h15(
2386
+ h16(
2238
2387
  "button",
2239
2388
  { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
2240
2389
  [icon("search", 14), "Filter"]
2241
2390
  ),
2242
- refreshButton("Refresh", isLoading.value, () => void refresh())
2391
+ refreshButton2("Refresh", isLoading.value, () => void refresh())
2243
2392
  ]
2244
2393
  ),
2245
- error.value ? h15("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2394
+ error.value ? h16("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2246
2395
  events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
2247
2396
  ["When", "Type", "Workspace", "Actor", "Request"],
2248
2397
  events.value.map(
2249
- (e) => h15("tr", { key: e.id }, [
2398
+ (e) => h16("tr", { key: e.id }, [
2250
2399
  td(new Date(e.createdAt).toLocaleString(), styles.muted),
2251
2400
  td(e.type, styles.mono),
2252
2401
  td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
@@ -2255,7 +2404,7 @@ var AuditScreen = defineComponent13({
2255
2404
  ])
2256
2405
  )
2257
2406
  ),
2258
- isLoading.value ? h15("p", { style: styles.status }, "Loading\u2026") : null,
2407
+ isLoading.value ? h16("p", { style: styles.status }, "Loading\u2026") : null,
2259
2408
  hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
2260
2409
  ]);
2261
2410
  }
@@ -2278,7 +2427,7 @@ var NAV = [
2278
2427
  group: "Money",
2279
2428
  items: [
2280
2429
  { page: "catalog", label: "Catalog", needs: "billing" },
2281
- { page: "subscriber", label: "Subscriber", needs: "billing" }
2430
+ { page: "subscriber", label: "Subscriptions", needs: "billing" }
2282
2431
  ]
2283
2432
  },
2284
2433
  { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
@@ -2312,7 +2461,7 @@ var ICON = {
2312
2461
  migrations: "migrations"
2313
2462
  };
2314
2463
  var NARROW = "(max-width: 820px)";
2315
- var AdminShell = defineComponent14({
2464
+ var AdminShell = defineComponent15({
2316
2465
  name: "FonderieAdminShell",
2317
2466
  props: {
2318
2467
  client: { type: Object, required: true },
@@ -2346,60 +2495,74 @@ var AdminShell = defineComponent14({
2346
2495
  emits: { navigate: (_page) => true },
2347
2496
  setup(props, { emit, slots }) {
2348
2497
  const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
2349
- const narrow = ref7(query?.matches ?? false);
2350
- const drawer = ref7(false);
2498
+ const narrow = ref9(query?.matches ?? false);
2499
+ const drawer = ref9(false);
2351
2500
  const onMedia = (e) => narrow.value = e.matches;
2352
2501
  onMounted(() => query?.addEventListener("change", onMedia));
2353
2502
  onBeforeUnmount(() => query?.removeEventListener("change", onMedia));
2354
- const own = ref7("attention");
2355
- const current = computed2(() => props.page ?? own.value);
2356
- const editing = ref7(null);
2503
+ const own = ref9("attention");
2504
+ const current = computed3(() => props.page ?? own.value);
2505
+ const editing = ref9(null);
2506
+ const openUser = ref9(void 0);
2357
2507
  const go = (p) => {
2358
2508
  editing.value = null;
2359
2509
  emit("navigate", p);
2360
2510
  if (props.page === void 0) own.value = p;
2361
2511
  drawer.value = false;
2362
2512
  };
2363
- const back = (label) => h16("div", { style: { padding: "24px 40px 0" } }, [
2364
- h16(
2513
+ const back = (label) => h17("div", { style: { padding: "24px 40px 0" } }, [
2514
+ h17(
2365
2515
  "button",
2366
2516
  { type: "button", style: styles.buttonGhost, onClick: () => editing.value = null },
2367
2517
  [icon("back", 14), label]
2368
2518
  )
2369
2519
  ]);
2370
- const missing = (text) => h16("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
2520
+ const missing = (text) => h17("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
2371
2521
  const body = () => {
2372
2522
  const c = props.client;
2373
2523
  switch (current.value) {
2374
2524
  case "attention":
2375
- return h16(AttentionScreen, { client: c });
2525
+ return h17(AttentionScreen, { client: c });
2376
2526
  case "modules":
2377
- return h16(ModulesScreen, { client: c });
2527
+ return h17(ModulesScreen, { client: c });
2378
2528
  case "doctor":
2379
- return h16(DoctorScreen, { client: c });
2529
+ return h17(DoctorScreen, { client: c });
2380
2530
  case "environment":
2381
- return h16(EnvironmentScreen, { client: c });
2531
+ return h17(EnvironmentScreen, { client: c });
2382
2532
  case "routes":
2383
- return h16(RoutesScreen, { client: c });
2533
+ return h17(RoutesScreen, { client: c });
2384
2534
  case "tokens":
2385
- return h16(TokensScreen, { client: c });
2535
+ return h17(TokensScreen, { client: c });
2386
2536
  case "operators":
2387
- return h16(OperatorsScreen, {
2537
+ return h17(OperatorsScreen, {
2388
2538
  client: c,
2389
2539
  ...props.currentOperator ? { me: props.currentOperator } : {}
2390
2540
  });
2391
2541
  case "migrations":
2392
- return h16(MigrationsScreen, { client: c });
2542
+ return h17(MigrationsScreen, { client: c });
2393
2543
  case "log":
2394
- return h16(AdminLogScreen, { client: c });
2544
+ return h17(AdminLogScreen, { client: c });
2395
2545
  case "catalog":
2396
- return props.billingClient ? h16(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
2546
+ return props.billingClient ? h17(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
2397
2547
  case "subscriber":
2398
- return props.billingClient ? h16(SubscriberScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to look up subscribers here.");
2548
+ return props.billingClient ? h17(SubscriberScreen, {
2549
+ client: props.billingClient,
2550
+ ...props.authClient ? {
2551
+ onOpenUser: (id) => {
2552
+ openUser.value = id;
2553
+ go("users");
2554
+ }
2555
+ } : {}
2556
+ }) : missing("Pass a BillingAdminClient to look up subscribers here.");
2399
2557
  case "audit":
2400
- return props.auditClient ? h16(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
2558
+ return props.auditClient ? h17(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
2401
2559
  case "users":
2402
- return props.authClient ? h16(UsersScreen, { client: props.authClient }) : missing("Pass an AuthAdminClient to look up users here.");
2560
+ return props.authClient ? h17(UsersScreen, {
2561
+ key: openUser.value ?? "list",
2562
+ client: props.authClient,
2563
+ ...props.billingClient ? { billingClient: props.billingClient } : {},
2564
+ ...openUser.value ? { openUserId: openUser.value } : {}
2565
+ }) : missing("Pass an AuthAdminClient to look up users here.");
2403
2566
  case "settings": {
2404
2567
  const cc = props.configClient;
2405
2568
  if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
@@ -2407,7 +2570,7 @@ var AdminShell = defineComponent14({
2407
2570
  if (e && e.kind !== "template")
2408
2571
  return [
2409
2572
  back("Config & secrets"),
2410
- h16(ConfigEditorScreen, {
2573
+ h17(ConfigEditorScreen, {
2411
2574
  client: cc,
2412
2575
  kind: e.kind,
2413
2576
  configKey: e.key,
@@ -2415,7 +2578,7 @@ var AdminShell = defineComponent14({
2415
2578
  onSaved: () => editing.value = null
2416
2579
  })
2417
2580
  ];
2418
- return h16(ConfigListScreen, {
2581
+ return h17(ConfigListScreen, {
2419
2582
  client: cc,
2420
2583
  ...props.environment !== void 0 ? { environment: props.environment } : {},
2421
2584
  "onSelect-config": (key) => editing.value = { kind: "config", key },
@@ -2432,13 +2595,13 @@ var AdminShell = defineComponent14({
2432
2595
  if (e?.kind === "template")
2433
2596
  return [
2434
2597
  back("Templates"),
2435
- h16(TemplateEditorScreen, {
2598
+ h17(TemplateEditorScreen, {
2436
2599
  client: kc,
2437
2600
  type: e.type,
2438
2601
  onSaved: () => editing.value = null
2439
2602
  })
2440
2603
  ];
2441
- return h16(TemplateListScreen, {
2604
+ return h17(TemplateListScreen, {
2442
2605
  client: kc,
2443
2606
  "onSelect-template": (t2) => editing.value = { kind: "template", type: t2.type }
2444
2607
  });
@@ -2448,11 +2611,11 @@ var AdminShell = defineComponent14({
2448
2611
  const visible = (i) => !i.needs || (i.needs === "operators" ? props.operators : i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : i.needs === "billing" ? props.billingClient : props.auditClient);
2449
2612
  const brand = () => {
2450
2613
  const production = props.envLabel ? /^prod/i.test(props.envLabel) : false;
2451
- return h16("div", { style: styles.navBrand }, [
2452
- h16("div", { style: styles.navMark }, [icon("mark", 15)]),
2453
- h16("div", { style: { minWidth: 0, flex: 1 } }, [
2454
- h16("div", { style: styles.navBrandTitle }, props.appName),
2455
- props.envLabel ? h16(
2614
+ return h17("div", { style: styles.navBrand }, [
2615
+ h17("div", { style: styles.navMark }, [icon("mark", 15)]),
2616
+ h17("div", { style: { minWidth: 0, flex: 1 } }, [
2617
+ h17("div", { style: styles.navBrandTitle }, props.appName),
2618
+ props.envLabel ? h17(
2456
2619
  "span",
2457
2620
  {
2458
2621
  style: {
@@ -2469,7 +2632,7 @@ var AdminShell = defineComponent14({
2469
2632
  props.envLabel
2470
2633
  ) : null
2471
2634
  ]),
2472
- narrow.value ? h16(
2635
+ narrow.value ? h17(
2473
2636
  "button",
2474
2637
  {
2475
2638
  type: "button",
@@ -2481,7 +2644,7 @@ var AdminShell = defineComponent14({
2481
2644
  ) : null
2482
2645
  ]);
2483
2646
  };
2484
- const nav = () => h16(
2647
+ const nav = () => h17(
2485
2648
  "nav",
2486
2649
  {
2487
2650
  style: {
@@ -2492,17 +2655,17 @@ var AdminShell = defineComponent14({
2492
2655
  },
2493
2656
  [
2494
2657
  brand(),
2495
- h16(
2658
+ h17(
2496
2659
  "div",
2497
2660
  { style: styles.navScroll },
2498
2661
  NAV.map((g) => {
2499
2662
  const items = g.items.filter(visible);
2500
2663
  if (items.length === 0) return null;
2501
- return h16("div", { key: g.group }, [
2502
- h16("div", { style: styles.navGroup }, g.group),
2664
+ return h17("div", { key: g.group }, [
2665
+ h17("div", { style: styles.navGroup }, g.group),
2503
2666
  ...items.map((i) => {
2504
2667
  const active = current.value === i.page;
2505
- return h16(
2668
+ return h17(
2506
2669
  "button",
2507
2670
  {
2508
2671
  key: i.page,
@@ -2510,7 +2673,10 @@ var AdminShell = defineComponent14({
2510
2673
  class: "fonderie-admin-nav-item",
2511
2674
  style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
2512
2675
  "aria-current": active ? "page" : void 0,
2513
- onClick: () => go(i.page)
2676
+ onClick: () => {
2677
+ openUser.value = void 0;
2678
+ go(i.page);
2679
+ }
2514
2680
  },
2515
2681
  [icon(ICON[i.page], 16, active ? styles.navIconActive : void 0), i.label]
2516
2682
  );
@@ -2518,18 +2684,18 @@ var AdminShell = defineComponent14({
2518
2684
  ]);
2519
2685
  })
2520
2686
  ),
2521
- slots["footer"] ? h16("div", { style: styles.navFooter }, slots["footer"]()) : null
2687
+ slots["footer"] ? h17("div", { style: styles.navFooter }, slots["footer"]()) : null
2522
2688
  ]
2523
2689
  );
2524
2690
  return () => {
2525
2691
  const title = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label ?? "";
2526
- return h16(
2692
+ return h17(
2527
2693
  "div",
2528
2694
  { style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
2529
2695
  [
2530
2696
  ...narrow.value ? [
2531
- h16("div", { style: styles.topbar }, [
2532
- h16(
2697
+ h17("div", { style: styles.topbar }, [
2698
+ h17(
2533
2699
  "button",
2534
2700
  {
2535
2701
  type: "button",
@@ -2540,19 +2706,19 @@ var AdminShell = defineComponent14({
2540
2706
  },
2541
2707
  [icon("menu")]
2542
2708
  ),
2543
- h16("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2544
- props.envLabel ? h16("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2709
+ h17("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2710
+ props.envLabel ? h17("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2545
2711
  ]),
2546
- drawer.value ? h16("div", { style: styles.drawer }, [
2712
+ drawer.value ? h17("div", { style: styles.drawer }, [
2547
2713
  nav(),
2548
- h16("div", {
2714
+ h17("div", {
2549
2715
  style: styles.scrim,
2550
2716
  "aria-hidden": "true",
2551
2717
  onClick: () => drawer.value = false
2552
2718
  })
2553
2719
  ]) : null
2554
2720
  ] : [nav()],
2555
- h16("main", { class: "fonderie-admin-main", style: styles.main }, body())
2721
+ h17("main", { class: "fonderie-admin-main", style: styles.main }, body())
2556
2722
  ]
2557
2723
  );
2558
2724
  };
@@ -2570,6 +2736,7 @@ export {
2570
2736
  ModulesScreen,
2571
2737
  OperatorsScreen,
2572
2738
  RoutesScreen,
2739
+ SubscriberBilling,
2573
2740
  SubscriberScreen,
2574
2741
  TokensScreen,
2575
2742
  UsersScreen