@fonderie/vue-admin-screens 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/screens/AdminShell.ts
2
2
  import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/vue-config-admin-screens";
3
3
  import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/vue-courier-admin-screens";
4
- import { computed as computed2, defineComponent as 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,24 @@ 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 ? 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,
1772
1984
  u ? [
1773
- h12("h2", { style: styles.subtitle }, [
1985
+ h13("h2", { style: styles.subtitle }, [
1774
1986
  u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
1775
1987
  " ",
1776
1988
  u.suspended ? pill("warn", "suspended") : null,
1777
1989
  u.deletedAt ? pill("neutral", "deleted") : null
1778
1990
  ]),
1779
- h12("table", { style: styles.table }, [
1780
- h12("tbody", [
1781
- row("id", h12("span", { style: styles.mono }, u.id)),
1991
+ h13("table", { style: styles.table }, [
1992
+ h13("tbody", [
1993
+ row("id", h13("span", { style: styles.mono }, u.id)),
1782
1994
  row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
1783
1995
  row("MFA", yesNo(u.mfaEnabled)),
1784
1996
  row(
@@ -1787,12 +1999,12 @@ var UsersScreen = defineComponent10({
1787
1999
  ),
1788
2000
  row(
1789
2001
  "last login",
1790
- u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h12("span", { style: styles.muted }, "never")
2002
+ u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h13("span", { style: styles.muted }, "never")
1791
2003
  ),
1792
2004
  row("created", new Date(u.createdAt).toLocaleString())
1793
2005
  ])
1794
2006
  ]),
1795
- h12("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
2007
+ h13("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
1796
2008
  u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
1797
2009
  "Suspend",
1798
2010
  isLoading.value,
@@ -1805,19 +2017,26 @@ var UsersScreen = defineComponent10({
1805
2017
  () => void revokeSessions().then(() => sessions.refresh())
1806
2018
  )
1807
2019
  ]),
1808
- h12("h2", { style: styles.subtitle }, "Live sessions"),
1809
- sessions.sessions.value.length === 0 ? h12("p", { style: styles.muted }, "None.") : h12(
2020
+ props.billingClient ? h13("h2", { style: styles.subtitle }, "Plan & credits") : null,
2021
+ props.billingClient ? h13(SubscriberBilling, {
2022
+ key: u.id,
2023
+ client: props.billingClient,
2024
+ subscriberType: "user",
2025
+ subscriberId: u.id
2026
+ }) : null,
2027
+ h13("h2", { style: styles.subtitle }, "Live sessions"),
2028
+ sessions.sessions.value.length === 0 ? h13("p", { style: styles.muted }, "None.") : h13(
1810
2029
  "ul",
1811
2030
  { style: styles.list },
1812
2031
  sessions.sessions.value.map(
1813
- (s) => h12("li", { key: s.id, style: styles.row }, [
1814
- h12("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
2032
+ (s) => h13("li", { key: s.id, style: styles.row }, [
2033
+ h13("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
1815
2034
  " ",
1816
- h12("span", { style: styles.muted }, describeLocation(s.location) ?? ""),
2035
+ h13("span", { style: styles.muted }, describeLocation(s.location) ?? ""),
1817
2036
  " ",
1818
- h12("span", { style: styles.muted }, s.userAgent ?? ""),
2037
+ h13("span", { style: styles.muted }, s.userAgent ?? ""),
1819
2038
  " ",
1820
- h12(
2039
+ h13(
1821
2040
  "span",
1822
2041
  { style: styles.muted },
1823
2042
  `since ${new Date(s.createdAt).toLocaleString()}`
@@ -1825,22 +2044,22 @@ var UsersScreen = defineComponent10({
1825
2044
  ])
1826
2045
  )
1827
2046
  ),
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(
2047
+ h13("h2", { style: styles.subtitle }, "Recent sign-ins"),
2048
+ history.events.value.length === 0 && !history.isLoading.value ? h13("p", { style: styles.muted }, "None recorded.") : h13(
1830
2049
  "ul",
1831
2050
  { style: styles.list },
1832
2051
  history.events.value.map(
1833
- (e) => h12("li", { key: e.id, style: styles.row }, [
2052
+ (e) => h13("li", { key: e.id, style: styles.row }, [
1834
2053
  pill(e.outcome === "success" ? "ok" : "bad", e.outcome),
1835
2054
  " ",
1836
- h12("span", { style: styles.muted }, e.method),
2055
+ h13("span", { style: styles.muted }, e.method),
1837
2056
  " ",
1838
- h12("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
2057
+ h13("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
1839
2058
  " ",
1840
- h12("span", { style: styles.muted }, describeLocation(e.location) ?? ""),
2059
+ h13("span", { style: styles.muted }, describeLocation(e.location) ?? ""),
1841
2060
  e.location?.proxy || e.location?.hosting ? [" ", pill("warn", e.location.proxy ? "proxy/VPN" : "hosting")] : "",
1842
2061
  " ",
1843
- h12("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
2062
+ h13("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
1844
2063
  ])
1845
2064
  )
1846
2065
  ),
@@ -1853,8 +2072,8 @@ var UsersScreen = defineComponent10({
1853
2072
 
1854
2073
  // src/screens/CatalogScreen.ts
1855
2074
  import { useAdminCatalog } from "@fonderie/vue-admin";
1856
- import { defineComponent as defineComponent11, h as h13 } from "vue";
1857
- var CatalogScreen = defineComponent11({
2075
+ import { defineComponent as defineComponent12, h as h14 } from "vue";
2076
+ var CatalogScreen = defineComponent12({
1858
2077
  name: "FonderieCatalogScreen",
1859
2078
  props: { client: { type: Object, required: true } },
1860
2079
  setup(props) {
@@ -1862,17 +2081,17 @@ var CatalogScreen = defineComponent11({
1862
2081
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
1863
2082
  return () => {
1864
2083
  const c = catalog.value;
1865
- return h13("div", { style: styles.container }, [
2084
+ return h14("div", { style: styles.container }, [
1866
2085
  pageHeader(
1867
2086
  "Catalog",
1868
2087
  "What you sell: plans as configured in code, and as stored in the database, side by side.",
1869
- [refreshButton("Refresh", isLoading.value, () => void refresh())]
2088
+ [refreshButton2("Refresh", isLoading.value, () => void refresh())]
1870
2089
  ),
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,
2090
+ error.value ? h14("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2091
+ isLoading.value && !c ? h14("p", { style: styles.status }, "Loading\u2026") : null,
1873
2092
  c ? [
1874
- h13("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
1875
- h13(
2093
+ h14("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
2094
+ h14(
1876
2095
  "pre",
1877
2096
  {
1878
2097
  style: {
@@ -1886,7 +2105,7 @@ var CatalogScreen = defineComponent11({
1886
2105
  },
1887
2106
  JSON.stringify(c.configured, null, 2)
1888
2107
  ),
1889
- h13("h2", { style: styles.subtitle }, "Stored (database)"),
2108
+ h14("h2", { style: styles.subtitle }, "Stored (database)"),
1890
2109
  c.stored.length === 0 ? empty(
1891
2110
  "No stored plans",
1892
2111
  "Plans are configured in code; nothing has been written to the database.",
@@ -1894,10 +2113,10 @@ var CatalogScreen = defineComponent11({
1894
2113
  ) : table(
1895
2114
  ["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
1896
2115
  c.stored.map(
1897
- (p) => h13("tr", { key: p.id }, [
2116
+ (p) => h14("tr", { key: p.id }, [
1898
2117
  td([
1899
- h13("strong", p.name),
1900
- p.description ? h13("div", { style: styles.muted }, p.description) : ""
2118
+ h14("strong", p.name),
2119
+ p.description ? h14("div", { style: styles.muted }, p.description) : ""
1901
2120
  ]),
1902
2121
  td(String(p.tier)),
1903
2122
  td(p.seats == null ? "\u221E" : String(p.seats)),
@@ -1905,7 +2124,7 @@ var CatalogScreen = defineComponent11({
1905
2124
  td(money(p.pricing?.yearly)),
1906
2125
  td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
1907
2126
  td(
1908
- h13(
2127
+ h14(
1909
2128
  "button",
1910
2129
  {
1911
2130
  type: "button",
@@ -1929,259 +2148,171 @@ var CatalogScreen = defineComponent11({
1929
2148
  });
1930
2149
 
1931
2150
  // 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({
2151
+ import { useAdminSubscribers } from "@fonderie/vue-admin";
2152
+ import { defineComponent as defineComponent13, h as h15, ref as ref7 } from "vue";
2153
+ var FILTERS = [
2154
+ { label: "All", match: () => true },
2155
+ { label: "Active", match: (s) => s === "active" },
2156
+ { label: "Trialing", match: (s) => s === "trialing" },
2157
+ { label: "Past due", match: (s) => s === "past_due" || s === "unpaid" },
2158
+ { label: "Canceled", match: (s) => s === "canceled" }
2159
+ ];
2160
+ var SubscriberScreen = defineComponent13({
1935
2161
  name: "FonderieSubscriberScreen",
1936
2162
  props: {
1937
2163
  client: { type: Object, required: true },
1938
- pageSize: { type: Number, default: 50 }
2164
+ pageSize: { type: Number, default: 50 },
2165
+ /**
2166
+ * A user subscriber opens on the Users page, where their plan and credits
2167
+ * live beside their account. Omit to open everything here.
2168
+ */
2169
+ onOpenUser: { type: Function, default: void 0 }
1939
2170
  },
1940
2171
  setup(props) {
1941
- const type = ref5("user");
1942
- const idInput = ref5("");
1943
- const subscriber = ref5(null);
1944
2172
  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 };
2173
+ const filter = ref7("All");
2174
+ const open = ref7(null);
2175
+ const type = ref7("workspace");
2176
+ const idInput = ref7("");
2177
+ const openRow = (t2, id) => {
2178
+ if (t2 === "user" && props.onOpenUser) props.onOpenUser(id);
2179
+ else open.value = { type: t2, id };
1951
2180
  };
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
2181
  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(
2182
+ const o = open.value;
2183
+ if (o) {
2184
+ return h15("div", { style: styles.container }, [
2185
+ pageHeader(`${o.type}/${o.id}`, "Plan, credits and what moved.", [
2186
+ h15(
1996
2187
  "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(
2013
- "button",
2014
- {
2015
- type: "button",
2016
- style: styles.button,
2017
- disabled: list.isLoading.value,
2018
- onClick: () => void list.refresh()
2019
- },
2020
- "Refresh"
2188
+ { type: "button", style: styles.buttonGhost, onClick: () => open.value = null },
2189
+ [icon("back", 14), "All subscriptions"]
2021
2190
  )
2022
- ]
2191
+ ]),
2192
+ h15(SubscriberBilling, {
2193
+ key: `${o.type}/${o.id}`,
2194
+ client: props.client,
2195
+ subscriberType: o.type,
2196
+ subscriberId: o.id
2197
+ })
2198
+ ]);
2199
+ }
2200
+ const match = FILTERS.find((f) => f.label === filter.value)?.match ?? (() => true);
2201
+ const rows = list.subscriptions.value.filter((s) => match(s.status));
2202
+ return h15("div", { style: styles.container }, [
2203
+ pageHeader(
2204
+ "Subscriptions",
2205
+ "Who pays, and who is behind. A user's plan and credits are also on their page under Users \u2014 free users appear only there.",
2206
+ [refreshButton(() => void list.refresh(), list.isLoading.value)]
2023
2207
  ),
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
- )
2208
+ h15("div", { style: { ...styles.toolbar, justifyContent: "space-between" } }, [
2209
+ h15(
2210
+ "fieldset",
2211
+ {
2212
+ style: {
2213
+ display: "flex",
2214
+ gap: "6px",
2215
+ flexWrap: "wrap",
2216
+ border: "none",
2217
+ margin: 0,
2218
+ padding: 0,
2219
+ minWidth: 0
2220
+ },
2221
+ "aria-label": "Filter by status"
2222
+ },
2223
+ FILTERS.map((f) => {
2224
+ const n = list.subscriptions.value.filter((s) => f.match(s.status)).length;
2225
+ const on = filter.value === f.label;
2226
+ return h15(
2227
+ "button",
2228
+ {
2229
+ key: f.label,
2230
+ type: "button",
2231
+ "aria-pressed": on,
2232
+ onClick: () => filter.value = f.label,
2233
+ style: { ...on ? styles.buttonPrimary : styles.button, height: "28px" }
2234
+ },
2235
+ [f.label, h15("span", { style: { opacity: 0.7 } }, String(n))]
2236
+ );
2237
+ })
2058
2238
  ),
2059
- list.isLoading.value ? h14("p", { style: styles.status }, "Loading\u2026") : null,
2060
- list.hasMore.value && !list.isLoading.value ? h14(
2061
- "button",
2239
+ h15(
2240
+ "form",
2062
2241
  {
2063
- type: "button",
2064
- style: { ...styles.button, marginTop: "8px" },
2065
- onClick: () => void list.loadMore()
2242
+ style: { display: "flex", gap: "6px" },
2243
+ onSubmit: (e) => {
2244
+ e.preventDefault();
2245
+ if (idInput.value.trim()) openRow(type.value, idInput.value.trim());
2246
+ }
2066
2247
  },
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(
2248
+ [
2249
+ h15(
2250
+ "select",
2251
+ {
2252
+ value: type.value,
2253
+ onChange: (e) => type.value = e.target.value,
2254
+ style: styles.input,
2255
+ "aria-label": "Type"
2256
+ },
2257
+ [
2258
+ h15("option", { value: "workspace" }, "workspace"),
2259
+ h15("option", { value: "user" }, "user")
2260
+ ]
2261
+ ),
2262
+ h15("input", {
2263
+ value: idInput.value,
2264
+ onInput: (e) => idInput.value = e.target.value,
2265
+ placeholder: "subscriber id",
2266
+ style: { ...styles.input, width: "220px" },
2267
+ "aria-label": "Subscriber id"
2268
+ }),
2269
+ h15(
2270
+ "button",
2271
+ { type: "submit", style: styles.button, disabled: !idInput.value.trim() },
2272
+ [icon("search", 14), "Open"]
2273
+ )
2274
+ ]
2275
+ )
2276
+ ]),
2277
+ list.error.value ? h15("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
2278
+ rows.length === 0 && !list.isLoading.value ? empty(
2279
+ filter.value === "All" ? "No subscriptions yet" : `No ${filter.value.toLowerCase()} subscriptions`,
2280
+ "Subscriptions appear here once someone checks out.",
2281
+ "subscriber"
2282
+ ) : table(
2283
+ ["Subscriber", "Plan", "Status", "Renews / ends"],
2284
+ rows.map((s) => {
2285
+ const end = periodEnd(s);
2286
+ return h15("tr", { key: s.id }, [
2287
+ td(
2288
+ h15(
2147
2289
  "button",
2148
2290
  {
2149
- type: "submit",
2150
- style: styles.buttonPrimary,
2151
- disabled: !/^\d+$/.test(amount.value.trim())
2291
+ type: "button",
2292
+ style: { ...styles.link, ...styles.mono },
2293
+ onClick: () => openRow(s.subscriberType, s.subscriberId)
2152
2294
  },
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
- ])
2295
+ `${s.subscriberType}/${s.subscriberId}`
2296
+ )
2297
+ ),
2298
+ td([
2299
+ h15("strong", s.plan),
2300
+ " ",
2301
+ h15("span", { style: styles.muted }, `\xB7 ${s.interval}`)
2302
+ ]),
2303
+ td([
2304
+ pill(statusTone(s.status), s.status),
2305
+ s.cancelAtPeriodEnd && s.status !== "canceled" ? h15("span", [" ", pill("warn", "cancels", false)]) : null
2306
+ ]),
2307
+ td(
2308
+ end ? [h15("span", { style: styles.muted }, end.label), ` ${end.date}`] : h15("span", { style: styles.muted }, "\u2014"),
2309
+ { whiteSpace: "nowrap" }
2168
2310
  )
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
2311
+ ]);
2312
+ })
2313
+ ),
2314
+ list.isLoading.value ? h15("p", { style: styles.status }, "Loading\u2026") : null,
2315
+ list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
2185
2316
  ]);
2186
2317
  };
2187
2318
  }
@@ -2189,33 +2320,33 @@ var SubscriberScreen = defineComponent12({
2189
2320
 
2190
2321
  // src/screens/AuditScreen.ts
2191
2322
  import { useAdminAudit } from "@fonderie/vue-admin";
2192
- import { defineComponent as defineComponent13, h as h15, ref as ref6 } from "vue";
2193
- var AuditScreen = defineComponent13({
2323
+ import { defineComponent as defineComponent14, h as h16, ref as ref8 } from "vue";
2324
+ var AuditScreen = defineComponent14({
2194
2325
  name: "FonderieAuditScreen",
2195
2326
  props: {
2196
2327
  client: { type: Object, required: true },
2197
2328
  pageSize: { type: Number, default: 50 }
2198
2329
  },
2199
2330
  setup(props) {
2200
- const draft = ref6({ workspaceId: "", type: "", actorId: "" });
2201
- const query = ref6({ limit: props.pageSize });
2331
+ const draft = ref8({ workspaceId: "", type: "", actorId: "" });
2332
+ const query = ref8({ limit: props.pageSize });
2202
2333
  const { events, hasMore, isLoading, error, refresh, loadMore } = useAdminAudit(
2203
2334
  props.client,
2204
2335
  query
2205
2336
  );
2206
- const field = (key, placeholder) => h15("input", {
2337
+ const field = (key, placeholder) => h16("input", {
2207
2338
  value: draft.value[key],
2208
2339
  onInput: (e) => draft.value = { ...draft.value, [key]: e.target.value },
2209
2340
  placeholder,
2210
2341
  style: styles.input,
2211
2342
  "aria-label": placeholder
2212
2343
  });
2213
- return () => h15("div", { style: styles.container }, [
2344
+ return () => h16("div", { style: styles.container }, [
2214
2345
  pageHeader(
2215
2346
  "Audit",
2216
2347
  "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
2217
2348
  ),
2218
- h15(
2349
+ h16(
2219
2350
  "form",
2220
2351
  {
2221
2352
  style: styles.toolbar,
@@ -2234,19 +2365,19 @@ var AuditScreen = defineComponent13({
2234
2365
  field("workspaceId", "workspace id (all if empty)"),
2235
2366
  field("type", "event type"),
2236
2367
  field("actorId", "actor id"),
2237
- h15(
2368
+ h16(
2238
2369
  "button",
2239
2370
  { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
2240
2371
  [icon("search", 14), "Filter"]
2241
2372
  ),
2242
- refreshButton("Refresh", isLoading.value, () => void refresh())
2373
+ refreshButton2("Refresh", isLoading.value, () => void refresh())
2243
2374
  ]
2244
2375
  ),
2245
- error.value ? h15("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2376
+ error.value ? h16("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2246
2377
  events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
2247
2378
  ["When", "Type", "Workspace", "Actor", "Request"],
2248
2379
  events.value.map(
2249
- (e) => h15("tr", { key: e.id }, [
2380
+ (e) => h16("tr", { key: e.id }, [
2250
2381
  td(new Date(e.createdAt).toLocaleString(), styles.muted),
2251
2382
  td(e.type, styles.mono),
2252
2383
  td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
@@ -2255,7 +2386,7 @@ var AuditScreen = defineComponent13({
2255
2386
  ])
2256
2387
  )
2257
2388
  ),
2258
- isLoading.value ? h15("p", { style: styles.status }, "Loading\u2026") : null,
2389
+ isLoading.value ? h16("p", { style: styles.status }, "Loading\u2026") : null,
2259
2390
  hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
2260
2391
  ]);
2261
2392
  }
@@ -2278,7 +2409,7 @@ var NAV = [
2278
2409
  group: "Money",
2279
2410
  items: [
2280
2411
  { page: "catalog", label: "Catalog", needs: "billing" },
2281
- { page: "subscriber", label: "Subscriber", needs: "billing" }
2412
+ { page: "subscriber", label: "Subscriptions", needs: "billing" }
2282
2413
  ]
2283
2414
  },
2284
2415
  { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
@@ -2312,7 +2443,7 @@ var ICON = {
2312
2443
  migrations: "migrations"
2313
2444
  };
2314
2445
  var NARROW = "(max-width: 820px)";
2315
- var AdminShell = defineComponent14({
2446
+ var AdminShell = defineComponent15({
2316
2447
  name: "FonderieAdminShell",
2317
2448
  props: {
2318
2449
  client: { type: Object, required: true },
@@ -2346,60 +2477,74 @@ var AdminShell = defineComponent14({
2346
2477
  emits: { navigate: (_page) => true },
2347
2478
  setup(props, { emit, slots }) {
2348
2479
  const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
2349
- const narrow = ref7(query?.matches ?? false);
2350
- const drawer = ref7(false);
2480
+ const narrow = ref9(query?.matches ?? false);
2481
+ const drawer = ref9(false);
2351
2482
  const onMedia = (e) => narrow.value = e.matches;
2352
2483
  onMounted(() => query?.addEventListener("change", onMedia));
2353
2484
  onBeforeUnmount(() => query?.removeEventListener("change", onMedia));
2354
- const own = ref7("attention");
2355
- const current = computed2(() => props.page ?? own.value);
2356
- const editing = ref7(null);
2485
+ const own = ref9("attention");
2486
+ const current = computed3(() => props.page ?? own.value);
2487
+ const editing = ref9(null);
2488
+ const openUser = ref9(void 0);
2357
2489
  const go = (p) => {
2358
2490
  editing.value = null;
2359
2491
  emit("navigate", p);
2360
2492
  if (props.page === void 0) own.value = p;
2361
2493
  drawer.value = false;
2362
2494
  };
2363
- const back = (label) => h16("div", { style: { padding: "24px 40px 0" } }, [
2364
- h16(
2495
+ const back = (label) => h17("div", { style: { padding: "24px 40px 0" } }, [
2496
+ h17(
2365
2497
  "button",
2366
2498
  { type: "button", style: styles.buttonGhost, onClick: () => editing.value = null },
2367
2499
  [icon("back", 14), label]
2368
2500
  )
2369
2501
  ]);
2370
- const missing = (text) => h16("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
2502
+ const missing = (text) => h17("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
2371
2503
  const body = () => {
2372
2504
  const c = props.client;
2373
2505
  switch (current.value) {
2374
2506
  case "attention":
2375
- return h16(AttentionScreen, { client: c });
2507
+ return h17(AttentionScreen, { client: c });
2376
2508
  case "modules":
2377
- return h16(ModulesScreen, { client: c });
2509
+ return h17(ModulesScreen, { client: c });
2378
2510
  case "doctor":
2379
- return h16(DoctorScreen, { client: c });
2511
+ return h17(DoctorScreen, { client: c });
2380
2512
  case "environment":
2381
- return h16(EnvironmentScreen, { client: c });
2513
+ return h17(EnvironmentScreen, { client: c });
2382
2514
  case "routes":
2383
- return h16(RoutesScreen, { client: c });
2515
+ return h17(RoutesScreen, { client: c });
2384
2516
  case "tokens":
2385
- return h16(TokensScreen, { client: c });
2517
+ return h17(TokensScreen, { client: c });
2386
2518
  case "operators":
2387
- return h16(OperatorsScreen, {
2519
+ return h17(OperatorsScreen, {
2388
2520
  client: c,
2389
2521
  ...props.currentOperator ? { me: props.currentOperator } : {}
2390
2522
  });
2391
2523
  case "migrations":
2392
- return h16(MigrationsScreen, { client: c });
2524
+ return h17(MigrationsScreen, { client: c });
2393
2525
  case "log":
2394
- return h16(AdminLogScreen, { client: c });
2526
+ return h17(AdminLogScreen, { client: c });
2395
2527
  case "catalog":
2396
- return props.billingClient ? h16(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
2528
+ return props.billingClient ? h17(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
2397
2529
  case "subscriber":
2398
- return props.billingClient ? h16(SubscriberScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to look up subscribers here.");
2530
+ return props.billingClient ? h17(SubscriberScreen, {
2531
+ client: props.billingClient,
2532
+ ...props.authClient ? {
2533
+ onOpenUser: (id) => {
2534
+ openUser.value = id;
2535
+ go("users");
2536
+ }
2537
+ } : {}
2538
+ }) : missing("Pass a BillingAdminClient to look up subscribers here.");
2399
2539
  case "audit":
2400
- return props.auditClient ? h16(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
2540
+ return props.auditClient ? h17(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
2401
2541
  case "users":
2402
- return props.authClient ? h16(UsersScreen, { client: props.authClient }) : missing("Pass an AuthAdminClient to look up users here.");
2542
+ return props.authClient ? h17(UsersScreen, {
2543
+ key: openUser.value ?? "list",
2544
+ client: props.authClient,
2545
+ ...props.billingClient ? { billingClient: props.billingClient } : {},
2546
+ ...openUser.value ? { openUserId: openUser.value } : {}
2547
+ }) : missing("Pass an AuthAdminClient to look up users here.");
2403
2548
  case "settings": {
2404
2549
  const cc = props.configClient;
2405
2550
  if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
@@ -2407,7 +2552,7 @@ var AdminShell = defineComponent14({
2407
2552
  if (e && e.kind !== "template")
2408
2553
  return [
2409
2554
  back("Config & secrets"),
2410
- h16(ConfigEditorScreen, {
2555
+ h17(ConfigEditorScreen, {
2411
2556
  client: cc,
2412
2557
  kind: e.kind,
2413
2558
  configKey: e.key,
@@ -2415,7 +2560,7 @@ var AdminShell = defineComponent14({
2415
2560
  onSaved: () => editing.value = null
2416
2561
  })
2417
2562
  ];
2418
- return h16(ConfigListScreen, {
2563
+ return h17(ConfigListScreen, {
2419
2564
  client: cc,
2420
2565
  ...props.environment !== void 0 ? { environment: props.environment } : {},
2421
2566
  "onSelect-config": (key) => editing.value = { kind: "config", key },
@@ -2432,13 +2577,13 @@ var AdminShell = defineComponent14({
2432
2577
  if (e?.kind === "template")
2433
2578
  return [
2434
2579
  back("Templates"),
2435
- h16(TemplateEditorScreen, {
2580
+ h17(TemplateEditorScreen, {
2436
2581
  client: kc,
2437
2582
  type: e.type,
2438
2583
  onSaved: () => editing.value = null
2439
2584
  })
2440
2585
  ];
2441
- return h16(TemplateListScreen, {
2586
+ return h17(TemplateListScreen, {
2442
2587
  client: kc,
2443
2588
  "onSelect-template": (t2) => editing.value = { kind: "template", type: t2.type }
2444
2589
  });
@@ -2448,11 +2593,11 @@ var AdminShell = defineComponent14({
2448
2593
  const visible = (i) => !i.needs || (i.needs === "operators" ? props.operators : i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : i.needs === "billing" ? props.billingClient : props.auditClient);
2449
2594
  const brand = () => {
2450
2595
  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(
2596
+ return h17("div", { style: styles.navBrand }, [
2597
+ h17("div", { style: styles.navMark }, [icon("mark", 15)]),
2598
+ h17("div", { style: { minWidth: 0, flex: 1 } }, [
2599
+ h17("div", { style: styles.navBrandTitle }, props.appName),
2600
+ props.envLabel ? h17(
2456
2601
  "span",
2457
2602
  {
2458
2603
  style: {
@@ -2469,7 +2614,7 @@ var AdminShell = defineComponent14({
2469
2614
  props.envLabel
2470
2615
  ) : null
2471
2616
  ]),
2472
- narrow.value ? h16(
2617
+ narrow.value ? h17(
2473
2618
  "button",
2474
2619
  {
2475
2620
  type: "button",
@@ -2481,7 +2626,7 @@ var AdminShell = defineComponent14({
2481
2626
  ) : null
2482
2627
  ]);
2483
2628
  };
2484
- const nav = () => h16(
2629
+ const nav = () => h17(
2485
2630
  "nav",
2486
2631
  {
2487
2632
  style: {
@@ -2492,17 +2637,17 @@ var AdminShell = defineComponent14({
2492
2637
  },
2493
2638
  [
2494
2639
  brand(),
2495
- h16(
2640
+ h17(
2496
2641
  "div",
2497
2642
  { style: styles.navScroll },
2498
2643
  NAV.map((g) => {
2499
2644
  const items = g.items.filter(visible);
2500
2645
  if (items.length === 0) return null;
2501
- return h16("div", { key: g.group }, [
2502
- h16("div", { style: styles.navGroup }, g.group),
2646
+ return h17("div", { key: g.group }, [
2647
+ h17("div", { style: styles.navGroup }, g.group),
2503
2648
  ...items.map((i) => {
2504
2649
  const active = current.value === i.page;
2505
- return h16(
2650
+ return h17(
2506
2651
  "button",
2507
2652
  {
2508
2653
  key: i.page,
@@ -2510,7 +2655,10 @@ var AdminShell = defineComponent14({
2510
2655
  class: "fonderie-admin-nav-item",
2511
2656
  style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
2512
2657
  "aria-current": active ? "page" : void 0,
2513
- onClick: () => go(i.page)
2658
+ onClick: () => {
2659
+ openUser.value = void 0;
2660
+ go(i.page);
2661
+ }
2514
2662
  },
2515
2663
  [icon(ICON[i.page], 16, active ? styles.navIconActive : void 0), i.label]
2516
2664
  );
@@ -2518,18 +2666,18 @@ var AdminShell = defineComponent14({
2518
2666
  ]);
2519
2667
  })
2520
2668
  ),
2521
- slots["footer"] ? h16("div", { style: styles.navFooter }, slots["footer"]()) : null
2669
+ slots["footer"] ? h17("div", { style: styles.navFooter }, slots["footer"]()) : null
2522
2670
  ]
2523
2671
  );
2524
2672
  return () => {
2525
2673
  const title = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label ?? "";
2526
- return h16(
2674
+ return h17(
2527
2675
  "div",
2528
2676
  { style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
2529
2677
  [
2530
2678
  ...narrow.value ? [
2531
- h16("div", { style: styles.topbar }, [
2532
- h16(
2679
+ h17("div", { style: styles.topbar }, [
2680
+ h17(
2533
2681
  "button",
2534
2682
  {
2535
2683
  type: "button",
@@ -2540,19 +2688,19 @@ var AdminShell = defineComponent14({
2540
2688
  },
2541
2689
  [icon("menu")]
2542
2690
  ),
2543
- h16("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2544
- props.envLabel ? h16("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2691
+ h17("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2692
+ props.envLabel ? h17("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2545
2693
  ]),
2546
- drawer.value ? h16("div", { style: styles.drawer }, [
2694
+ drawer.value ? h17("div", { style: styles.drawer }, [
2547
2695
  nav(),
2548
- h16("div", {
2696
+ h17("div", {
2549
2697
  style: styles.scrim,
2550
2698
  "aria-hidden": "true",
2551
2699
  onClick: () => drawer.value = false
2552
2700
  })
2553
2701
  ]) : null
2554
2702
  ] : [nav()],
2555
- h16("main", { class: "fonderie-admin-main", style: styles.main }, body())
2703
+ h17("main", { class: "fonderie-admin-main", style: styles.main }, body())
2556
2704
  ]
2557
2705
  );
2558
2706
  };
@@ -2570,6 +2718,7 @@ export {
2570
2718
  ModulesScreen,
2571
2719
  OperatorsScreen,
2572
2720
  RoutesScreen,
2721
+ SubscriberBilling,
2573
2722
  SubscriberScreen,
2574
2723
  TokensScreen,
2575
2724
  UsersScreen