diff --git a/web/src/routes/console/admin/+layout.svelte b/web/src/routes/console/admin/+layout.svelte index ad7152c6..4b7214ed 100644 --- a/web/src/routes/console/admin/+layout.svelte +++ b/web/src/routes/console/admin/+layout.svelte @@ -14,12 +14,18 @@ } }); - const tabs = [ + // Billing is cloud-only — the underlying endpoint returns 404 in + // self-host. Hide the tab when adminStore.stats reports cloud_mode=false + // so a self-host operator doesn't see a tab that always 404s on click. + let cloudMode = $derived(adminStore.stats?.cloud_mode ?? false); + + let tabs = $derived([ { label: 'Users', href: '/console/admin' }, { label: 'Invitations', href: '/console/admin/invitations' }, { label: 'Audit Log', href: '/console/admin/audit-log' }, + ...(cloudMode ? [{ label: 'Billing', href: '/console/admin/billing' }] : []), { label: 'Settings', href: '/console/admin/settings' } - ]; + ]); function isActive(href: string): boolean { const path = page.url.pathname; diff --git a/web/src/routes/console/admin/billing/+page.svelte b/web/src/routes/console/admin/billing/+page.svelte new file mode 100644 index 00000000..ed0f9e4c --- /dev/null +++ b/web/src/routes/console/admin/billing/+page.svelte @@ -0,0 +1,359 @@ + + +
+ {#if loading} +
Loading billing stats...
+ {:else if error} +
+

{error}

+ +
+ {:else if stats} + + + {#if stats.cloud_unreachable} + + {:else if !stats.stripe_configured} + + {/if} + +
+ +
+ MRR + {#if stripeUnavailable} + N/A + {:else} + {mrrFormatted} + {/if} + Monthly recurring revenue +
+ + +
+ ARR + {#if stripeUnavailable} + N/A + {:else} + {arrFormatted} + {/if} + Annual run rate +
+ + +
+ Active Subscriptions + {#if stripeUnavailable} + N/A + {:else} + {stats.active_subscriptions} + {/if} + Currently paying customers +
+ + +
+ Customers by Plan + {#if Object.keys(stats.customers_by_plan).length === 0} + + {:else} + {plansBreakdown} + {/if} + All registered users +
+ + +
+ New Signups (30d) + {stats.new_signups_30d} + Pro signups in last 30 days +
+ + +
+ Churn (30d) + {#if stripeUnavailable} + N/A + {:else} + {churnFormatted} + {/if} + {stats.cancelled_30d} cancelled +
+
+ + + {/if} +
+ +