Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7a01ee2
feat(trpc): reference canonical PostHog insights by semantic key for …
saddlepaddle Aug 13, 2026
e8a3245
feat(admin,trpc): mirror company metrics dashboard from PostHog and p…
saddlepaddle Aug 13, 2026
0d2a0d1
docs(plans): close out admin company dashboard plan (PR #6410)
saddlepaddle Aug 13, 2026
bde93a6
fix(trpc): qualify auth-schema tables and complete cohort window edge…
saddlepaddle Aug 13, 2026
4a0b3df
feat(trpc): compute MRR on demand via Stripe's Query Run API
saddlepaddle Aug 13, 2026
c51412f
fix(admin,trpc): never block requests on the Sigma MRR run; independe…
saddlepaddle Aug 13, 2026
59373cf
feat(admin,trpc): daily MRR with 7d/35d/180d ranges; ARPA replaces mi…
saddlepaddle Aug 13, 2026
83f6217
feat(admin): Stripe-style MRR stat with period delta, range Select, P…
saddlepaddle Aug 13, 2026
2f19ab3
feat(admin,trpc): previous-period tooltip, clean dates, interval buck…
saddlepaddle Aug 13, 2026
303d5c0
feat(admin,trpc): drop the ARPA/ARPU metric
saddlepaddle Aug 13, 2026
b249b24
feat(admin,trpc): DAU + rolling-7d WAU tile
saddlepaddle Aug 13, 2026
a07f425
fix(admin,trpc): serve the insight registry from a pure subpath export
saddlepaddle Aug 13, 2026
eff51cb
feat(admin,trpc): enterprise ARR from Stripe subscriptions, surfacing…
saddlepaddle Aug 13, 2026
795ca04
fix(admin): stretch Enterprise ARR card to fill its grid cell
saddlepaddle Aug 13, 2026
8266277
feat(admin,trpc): split DAU and WAU into separate tiles; month-aware …
saddlepaddle Aug 13, 2026
1c7f2a3
feat(admin): PostHog-style funnel rendering
saddlepaddle Aug 13, 2026
51bb4a3
feat(admin): funnel hover breakdowns and PostHog-style retention grid
saddlepaddle Aug 13, 2026
b81757d
feat(admin,ui,trpc): calendar-week WAU with dashed partial week; tool…
saddlepaddle Aug 13, 2026
a7e36a9
fix(admin): white values in funnel tooltips
saddlepaddle Aug 13, 2026
45f8ee7
fix(admin): funnel tooltips adopt the chart tooltip color recipe
saddlepaddle Aug 13, 2026
f027574
feat(admin): audience tabs — Company (default), Product, Growth
saddlepaddle Aug 13, 2026
4ae82f6
feat(admin,trpc): cash, gross burn, and runway from Mercury on the Co…
saddlepaddle Aug 13, 2026
8e294a3
feat(admin,trpc): Runway-style Company tab — stat row, cash trajector…
saddlepaddle Aug 13, 2026
fe4c13d
fix(trpc): merge case-variant Mercury counterparties in burn-by-vendor
saddlepaddle Aug 13, 2026
4df12e2
feat(admin,trpc): net burn, enterprise ARR by account, retention mean…
saddlepaddle Aug 13, 2026
e14f912
fix(admin): label enterprise ARR as inside MRR and cash as raise-incl…
saddlepaddle Aug 13, 2026
dfc2367
fix(admin): clamp net burn axis at zero
saddlepaddle Aug 13, 2026
60a59d4
fix(admin): UTC date labels, shared CohortGrid, drop pre-operating bu…
saddlepaddle Aug 13, 2026
e468e1e
feat(admin): full-width cash chart with net burn and runway beneath
saddlepaddle Aug 13, 2026
6a536d0
fix(admin): center metric card values in the available space
saddlepaddle Aug 13, 2026
75a38bc
style(admin): thicker cohort grid cells
saddlepaddle Aug 13, 2026
41d5b7f
style(admin): gutter between the size column and cohort cells
saddlepaddle Aug 13, 2026
6a30a8b
ci(deploy): pass MERCURY_API_TOKEN to api and admin production deploys
saddlepaddle Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ jobs:
QSTASH_CURRENT_SIGNING_KEY: ${{ secrets.QSTASH_CURRENT_SIGNING_KEY }}
QSTASH_NEXT_SIGNING_KEY: ${{ secrets.QSTASH_NEXT_SIGNING_KEY }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
MERCURY_API_TOKEN: ${{ secrets.MERCURY_API_TOKEN }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
STRIPE_PRO_MONTHLY_PRICE_ID: ${{ secrets.STRIPE_PRO_MONTHLY_PRICE_ID }}
STRIPE_PRO_YEARLY_PRICE_ID: ${{ secrets.STRIPE_PRO_YEARLY_PRICE_ID }}
Expand Down Expand Up @@ -161,6 +162,7 @@ jobs:
--env QSTASH_CURRENT_SIGNING_KEY=$QSTASH_CURRENT_SIGNING_KEY \
--env QSTASH_NEXT_SIGNING_KEY=$QSTASH_NEXT_SIGNING_KEY \
--env STRIPE_SECRET_KEY=$STRIPE_SECRET_KEY \
--env MERCURY_API_TOKEN=$MERCURY_API_TOKEN \
--env STRIPE_WEBHOOK_SECRET=$STRIPE_WEBHOOK_SECRET \
--env STRIPE_PRO_MONTHLY_PRICE_ID=$STRIPE_PRO_MONTHLY_PRICE_ID \
--env STRIPE_PRO_YEARLY_PRICE_ID=$STRIPE_PRO_YEARLY_PRICE_ID \
Expand Down Expand Up @@ -401,6 +403,7 @@ jobs:
KV_URL: ${{ secrets.KV_URL }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
MERCURY_API_TOKEN: ${{ secrets.MERCURY_API_TOKEN }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
STRIPE_PRO_MONTHLY_PRICE_ID: ${{ secrets.STRIPE_PRO_MONTHLY_PRICE_ID }}
STRIPE_PRO_YEARLY_PRICE_ID: ${{ secrets.STRIPE_PRO_YEARLY_PRICE_ID }}
Expand Down Expand Up @@ -431,6 +434,7 @@ jobs:
--env KV_URL=$KV_URL \
--env RESEND_API_KEY=$RESEND_API_KEY \
--env STRIPE_SECRET_KEY=$STRIPE_SECRET_KEY \
--env MERCURY_API_TOKEN=$MERCURY_API_TOKEN \
--env STRIPE_WEBHOOK_SECRET=$STRIPE_WEBHOOK_SECRET \
--env STRIPE_PRO_MONTHLY_PRICE_ID=$STRIPE_PRO_MONTHLY_PRICE_ID \
--env STRIPE_PRO_YEARLY_PRICE_ID=$STRIPE_PRO_YEARLY_PRICE_ID \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
SidebarRail,
} from "@superset/ui/sidebar";
import { usePathname } from "next/navigation";
import { LuChevronRight, LuHouse, LuUsers } from "react-icons/lu";
import { LuChevronRight, LuHouse } from "react-icons/lu";

import { AppSidebarHeader } from "./components/AppSidebarHeader";
import { NavUser } from "./components/NavUser";
Expand All @@ -34,18 +34,10 @@ const topLevelNav = [
},
];

const sections = [
{
title: "User Management",
items: [
{
title: "All Users",
url: "/users",
icon: LuUsers,
},
],
},
];
const sections: {
title: string;
items: { title: string; url: string; icon: typeof LuHouse }[];
}[] = [];

export interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
user: NonNullable<RouterOutputs["user"]["me"]>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { useQuery } from "@tanstack/react-query";

import { useTRPC } from "@/trpc/react";

import { InsightTileFrame } from "../InsightTileFrame";

// Where the money goes: average monthly outflow per counterparty over the
// last 3 complete months — the actionable half of a burn chart.
export function BurnByVendorTile() {
const trpc = useTRPC();
const query = useQuery(trpc.business.getCashFlow.queryOptions());

const unavailableReason =
query.data && !query.data.available ? query.data.reason : null;
const vendors = query.data?.available ? query.data.topVendors : [];
const maxSpend = vendors[0]?.avgMonthlyUsd ?? 0;

return (
<InsightTileFrame
title="Burn by vendor (Mercury)"
description="Avg monthly outflow per counterparty, last 3 complete months"
lastRefresh={query.data?.available ? query.data.asOf : null}
isLoading={query.isLoading}
error={query.error}
empty={vendors.length === 0}
emptyLabel={
unavailableReason ? `Unavailable: ${unavailableReason}` : "No data"
}
>
<div className="space-y-2">
{vendors.map((vendor) => (
<div key={vendor.name} className="flex items-center gap-2 text-xs">
<span className="w-40 shrink-0 truncate" title={vendor.name}>
{vendor.name}
</span>
<div className="bg-muted/40 h-4 flex-1 overflow-hidden rounded-sm">
<div
className="h-full rounded-sm"
style={{
width: `${maxSpend > 0 ? (vendor.avgMonthlyUsd / maxSpend) * 100 : 0}%`,
background: "var(--chart-1)",
}}
/>
</div>
<span className="w-20 shrink-0 text-right tabular-nums">
${vendor.avgMonthlyUsd.toLocaleString()}/mo
</span>
</div>
))}
</div>
</InsightTileFrame>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BurnByVendorTile } from "./BurnByVendorTile";
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use client";

import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@superset/ui/chart";
import { useQuery } from "@tanstack/react-query";
import { Area, AreaChart, XAxis, YAxis } from "recharts";

import { useTRPC } from "@/trpc/react";

import { makeDateAxis } from "../../utils/chartAxis";
import { InsightTileFrame } from "../InsightTileFrame";

const chartConfig = {
cashUsd: { label: "total cash", color: "var(--chart-2)" },
} satisfies ChartConfig;

// Total cash across all Mercury accounts + Treasury, reconstructed from the
// transaction history. Tranche wires show as steps up; burn as gentle slope.
export function CashBalanceTile() {
const trpc = useTRPC();
const query = useQuery(trpc.business.getCashFlow.queryOptions());

const unavailableReason =
query.data && !query.data.available ? query.data.reason : null;
const points = query.data?.available ? query.data.cashSeries : [];
const xAxis = makeDateAxis(points.map((point) => point.date));

return (
<InsightTileFrame
title="Cash — total balance (Mercury)"
description="Checking + savings + Treasury over time; fundraise tranches are the steps"
lastRefresh={query.data?.available ? query.data.asOf : null}
isLoading={query.isLoading}
error={query.error}
empty={points.length === 0}
emptyLabel={
unavailableReason ? `Unavailable: ${unavailableReason}` : "No data"
}
>
<ChartContainer config={chartConfig} className="h-[240px] w-full">
<AreaChart data={points}>
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
fontSize={11}
ticks={xAxis.ticks}
tickFormatter={xAxis.tickFormatter}
/>
<YAxis
tickLine={false}
axisLine={false}
width={64}
tickFormatter={(v: number) => `$${(v / 1_000_000).toFixed(1)}M`}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<Area
dataKey="cashUsd"
stroke="var(--color-cashUsd)"
fill="var(--color-cashUsd)"
fillOpacity={0.15}
strokeWidth={2}
type="stepAfter"
/>
</AreaChart>
</ChartContainer>
</InsightTileFrame>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CashBalanceTile } from "./CashBalanceTile";
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";

import { useQuery } from "@tanstack/react-query";

import { useTRPC } from "@/trpc/react";

import { formatMonth } from "../../utils/chartAxis";
import { CohortGrid, type CohortRow } from "../CohortGrid";
import { InsightTileFrame } from "../InsightTileFrame";

// Cohort survival from Neon subscriptions: rows are signup months, columns
// are months since subscribing, cells are % still subscribed. Shares the
// cohort-triangle rendering with weekly retention.
export function ChurnHeatmapTile() {
const trpc = useTRPC();
const query = useQuery(
trpc.business.getChurnCohorts.queryOptions({ months: 7 }),
);

const data = query.data ?? [];
const cohortMonths = [...new Set(data.map((row) => row.cohort_month))];
const offsets = [...new Set(data.map((row) => row.month_offset))].sort(
(a, b) => a - b,
);
const byCell = new Map(
data.map((row) => [`${row.cohort_month}:${row.month_offset}`, row]),
);

const rows: CohortRow[] = cohortMonths.map((cohort) => ({
key: cohort,
label: formatMonth(cohort),
size: byCell.get(`${cohort}:0`)?.cohort_size ?? 0,
cells: offsets.map((offset) => {
const cell = byCell.get(`${cohort}:${offset}`);
return cell
? { pct: cell.surviving_pct, state: "complete" as const }
: { pct: null, state: "future" as const };
}),
}));

return (
<InsightTileFrame
title="Paid churn — cohort survival"
description="% of subscriptions started each month still active k months later (Neon, enterprise excluded)"
isLoading={query.isLoading}
error={query.error}
empty={data.length === 0}
>
<CohortGrid
columnLabels={offsets.map((offset) => `Month ${offset}`)}
rows={rows}
labelHeader="Cohort"
sizeHeader="Subs"
/>
</InsightTileFrame>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ChurnHeatmapTile } from "./ChurnHeatmapTile";
112 changes: 112 additions & 0 deletions apps/admin/src/app/(dashboard)/components/CohortGrid/CohortGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"use client";

import { cn } from "@superset/ui/utils";

// Shared cohort-triangle rendering for retention and survival grids: solid
// chips scaled by percentage, dashed outline while a period is still in
// progress, and nothing at all for periods a cohort hasn't reached.
export type CohortCellState = "complete" | "inProgress" | "future";

export interface CohortCell {
pct: number | null;
state: CohortCellState;
}

export interface CohortRow {
key: string;
label: string;
size: number;
cells: CohortCell[];
emphasis?: boolean;
}

interface CohortGridProps {
columnLabels: string[];
rows: CohortRow[];
labelHeader?: string;
sizeHeader?: string;
}

function Cell({ cell, isBaseline }: { cell: CohortCell; isBaseline: boolean }) {
if (cell.state === "future" || cell.pct === null) {
return <div />;
}
if (cell.state === "inProgress") {
return (
<div className="border-border text-muted-foreground rounded-md border border-dashed px-1 py-3 text-center tabular-nums">
{cell.pct.toFixed(1)}%
</div>
);
}
return (
<div
className={cn(
"rounded-md px-1 py-3 text-center tabular-nums",
(isBaseline || cell.pct >= 45) && "text-white",
)}
style={{
background: isBaseline
? "var(--chart-1)"
: `color-mix(in oklch, var(--chart-1) ${Math.max(8, Math.round(cell.pct))}%, transparent)`,
}}
>
{cell.pct.toFixed(1)}%
</div>
);
}

export function CohortGrid({
columnLabels,
rows,
labelHeader = "Cohort",
sizeHeader = "Size",
}: CohortGridProps) {
return (
<div className="overflow-x-auto">
<div
className="grid min-w-[860px] items-center gap-1 text-xs"
style={{
gridTemplateColumns: `5.5rem 3.5rem repeat(${columnLabels.length}, minmax(3.5rem, 1fr))`,
}}
>
<span className="text-muted-foreground">{labelHeader}</span>
<span className="text-muted-foreground pr-3 text-right">
{sizeHeader}
</span>
{columnLabels.map((label) => (
<span key={label} className="text-muted-foreground text-center">
{label}
</span>
))}

{rows.map((row) => [
<span
key={`${row.key}-label`}
className={cn(
"whitespace-nowrap",
row.emphasis ? "font-medium" : "text-muted-foreground",
)}
>
{row.label}
</span>,
<span
key={`${row.key}-size`}
className={cn(
"pr-3 text-right tabular-nums",
row.emphasis && "font-medium",
)}
>
{row.size}
</span>,
...columnLabels.map((label, index) => (
<Cell
key={`${row.key}-${label}`}
cell={row.cells[index] ?? { pct: null, state: "future" }}
isBaseline={index === 0}
/>
)),
])}
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions apps/admin/src/app/(dashboard)/components/CohortGrid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { CohortCell, CohortCellState, CohortRow } from "./CohortGrid";
export { CohortGrid } from "./CohortGrid";
Loading
Loading