feat(ui): add resource and usage dashboard (/usage route)

adds a new /usage page that lets board operators see how much each ai
provider is consuming across any date window, with per-model breakdowns,
rolling 5h/24h/7d burn windows, weekly budget bars, and a deficit notch
when projected spend is on track to exceed the monthly budget.

- new GET /companies/:id/costs/by-provider endpoint aggregates cost events
  by provider + model with pro-rated billing type splits from heartbeat runs
- new GET /companies/:id/costs/window-spend endpoint returns rolling window
  spend (5h, 24h, 7d) per provider with no schema changes
- QuotaBar: reusable boxed-border progress bar with green/yellow/red
  threshold fill colors and optional deficit notch
- ProviderQuotaCard: per-provider card showing budget allocation bars,
  rolling windows, subscription usage, and model breakdown with token/cost
  share overlays
- Usage page: date preset toggles (mtd, 7d, 30d, ytd, all, custom),
  provider tabs, 30s polling plus ws invalidation on cost_event
- custom date range blocks queries until both dates are selected and
  treats boundaries as local-time (not utc midnight) so full days are
  included regardless of timezone
- query key to timestamp is floored to the nearest minute to prevent
  cache churn on every 30s refetch tick
This commit is contained in:
Sai Shankar
2026-03-08 03:18:37 +05:30
committed by Dotta
parent 3dc3347a58
commit 94018e0239
15 changed files with 803 additions and 4 deletions

View File

@@ -0,0 +1,238 @@
import type { CostByProviderModel, CostWindowSpendRow } from "@paperclipai/shared";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { QuotaBar } from "./QuotaBar";
import { formatCents, formatTokens } from "@/lib/utils";
interface ProviderQuotaCardProps {
provider: string;
rows: CostByProviderModel[];
/** company monthly budget in cents (0 means unlimited) */
budgetMonthlyCents: number;
/** total company spend in this period in cents, all providers */
totalCompanySpendCents: number;
/** spend in the current calendar week in cents, this provider only */
weekSpendCents: number;
/** rolling window rows for this provider: 5h, 24h, 7d */
windowRows: CostWindowSpendRow[];
showDeficitNotch: boolean;
}
function providerLabel(provider: string): string {
const map: Record<string, string> = {
anthropic: "Anthropic",
openai: "OpenAI",
google: "Google",
cursor: "Cursor",
jetbrains: "JetBrains AI",
};
return map[provider.toLowerCase()] ?? provider;
}
export function ProviderQuotaCard({
provider,
rows,
budgetMonthlyCents,
totalCompanySpendCents,
weekSpendCents,
windowRows,
showDeficitNotch,
}: ProviderQuotaCardProps) {
const totalInputTokens = rows.reduce((s, r) => s + r.inputTokens, 0);
const totalOutputTokens = rows.reduce((s, r) => s + r.outputTokens, 0);
const totalTokens = totalInputTokens + totalOutputTokens;
const totalCostCents = rows.reduce((s, r) => s + r.costCents, 0);
const totalApiRuns = rows.reduce((s, r) => s + r.apiRunCount, 0);
const totalSubRuns = rows.reduce((s, r) => s + r.subscriptionRunCount, 0);
const totalSubInputTokens = rows.reduce((s, r) => s + r.subscriptionInputTokens, 0);
const totalSubOutputTokens = rows.reduce((s, r) => s + r.subscriptionOutputTokens, 0);
const totalSubTokens = totalSubInputTokens + totalSubOutputTokens;
// sub share = sub tokens / (api tokens + sub tokens)
const allTokens = totalTokens + totalSubTokens;
const subSharePct = allTokens > 0 ? (totalSubTokens / allTokens) * 100 : 0;
// budget bars: use this provider's own spend vs its pro-rata share of budget
// pro-rata: if a provider is 40% of total spend, it gets 40% of the budget allocated.
// falls back to raw provider spend vs total budget when totalCompanySpend is 0.
const providerBudgetShare =
budgetMonthlyCents > 0 && totalCompanySpendCents > 0
? (totalCostCents / totalCompanySpendCents) * budgetMonthlyCents
: budgetMonthlyCents;
const budgetPct =
providerBudgetShare > 0
? Math.min(100, (totalCostCents / providerBudgetShare) * 100)
: 0;
const weeklyBudgetShare = providerBudgetShare > 0 ? providerBudgetShare / 4.33 : 0;
const weekPct =
weeklyBudgetShare > 0 ? Math.min(100, (weekSpendCents / weeklyBudgetShare) * 100) : 0;
const hasBudget = budgetMonthlyCents > 0;
return (
<Card>
<CardHeader className="px-4 pt-4 pb-0 gap-1">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<CardTitle className="text-sm font-semibold">
{providerLabel(provider)}
</CardTitle>
<CardDescription className="text-xs mt-0.5">
<span className="font-mono">{formatTokens(totalInputTokens)}</span> in
{" · "}
<span className="font-mono">{formatTokens(totalOutputTokens)}</span> out
{(totalApiRuns > 0 || totalSubRuns > 0) && (
<span className="ml-1.5">
·{" "}
{totalApiRuns > 0 && `~${totalApiRuns} api`}
{totalApiRuns > 0 && totalSubRuns > 0 && " / "}
{totalSubRuns > 0 && `~${totalSubRuns} sub`}
{" runs"}
</span>
)}
</CardDescription>
</div>
<span className="text-xl font-bold tabular-nums shrink-0">
{formatCents(totalCostCents)}
</span>
</div>
</CardHeader>
<CardContent className="px-4 pb-4 pt-3 space-y-4">
{hasBudget && (
<div className="space-y-3">
<QuotaBar
label="Period spend"
percentUsed={budgetPct}
leftLabel={formatCents(totalCostCents)}
rightLabel={`${Math.round(budgetPct)}% of allocation`}
showDeficitNotch={showDeficitNotch}
/>
<QuotaBar
label="This week"
percentUsed={weekPct}
leftLabel={formatCents(weekSpendCents)}
rightLabel={`~${formatCents(Math.round(weeklyBudgetShare))} / wk`}
showDeficitNotch={weekPct >= 100}
/>
</div>
)}
{/* rolling window consumption — always shown when data is available */}
{windowRows.length > 0 && (() => {
const WINDOWS = ["5h", "24h", "7d"] as const;
const windowMap = new Map(windowRows.map((r) => [r.window, r]));
const maxCents = Math.max(...windowRows.map((r) => r.costCents), 1);
return (
<>
<div className="border-t border-border" />
<div className="space-y-2">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
Rolling windows
</p>
<div className="space-y-2.5">
{WINDOWS.map((w) => {
const row = windowMap.get(w);
const cents = row?.costCents ?? 0;
const tokens = (row?.inputTokens ?? 0) + (row?.outputTokens ?? 0);
const barPct = maxCents > 0 ? (cents / maxCents) * 100 : 0;
return (
<div key={w} className="space-y-1">
<div className="flex items-center justify-between gap-2 text-xs">
<span className="font-mono text-muted-foreground w-6 shrink-0">{w}</span>
<span className="text-muted-foreground font-mono flex-1">
{formatTokens(tokens)} tok
</span>
<span className="font-medium tabular-nums">{formatCents(cents)}</span>
</div>
<div className="h-1.5 w-full border border-border overflow-hidden">
<div
className="h-full bg-primary/60 transition-[width] duration-150"
style={{ width: `${barPct}%` }}
/>
</div>
</div>
);
})}
</div>
</div>
</>
);
})()}
{/* subscription usage — shown when any subscription-billed runs exist */}
{totalSubRuns > 0 && (
<>
<div className="border-t border-border" />
<div className="space-y-2">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
Subscription
</p>
<p className="text-xs text-muted-foreground">
<span className="font-mono text-foreground">{totalSubRuns}</span> runs
{" · "}
<span className="font-mono text-foreground">{formatTokens(totalSubInputTokens)}</span> in
{" · "}
<span className="font-mono text-foreground">{formatTokens(totalSubOutputTokens)}</span> out
</p>
<div className="h-1.5 w-full border border-border overflow-hidden">
<div
className="h-full bg-primary/60 transition-[width] duration-150"
style={{ width: `${subSharePct}%` }}
/>
</div>
<p className="text-xs text-muted-foreground">
{Math.round(subSharePct)}% of token usage via subscription
</p>
</div>
</>
)}
{/* model breakdown — always shown, with token-share bars */}
{rows.length > 0 && (
<>
<div className="border-t border-border" />
<div className="space-y-3">
{rows.map((row) => {
const rowTokens = row.inputTokens + row.outputTokens;
const tokenPct = totalTokens > 0 ? (rowTokens / totalTokens) * 100 : 0;
const costPct = totalCostCents > 0 ? (row.costCents / totalCostCents) * 100 : 0;
return (
<div key={`${row.provider}:${row.model}`} className="space-y-1.5">
{/* model name and cost */}
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-muted-foreground truncate font-mono">
{row.model}
</span>
<div className="flex items-center gap-3 shrink-0 tabular-nums text-xs">
<span className="text-muted-foreground">
{formatTokens(rowTokens)} tok
</span>
<span className="font-medium">{formatCents(row.costCents)}</span>
</div>
</div>
{/* token share bar */}
<div className="relative h-1.5 w-full border border-border overflow-hidden">
<div
className="absolute inset-y-0 left-0 bg-primary/60 transition-[width] duration-150"
style={{ width: `${tokenPct}%` }}
title={`${Math.round(tokenPct)}% of provider tokens`}
/>
{/* cost share overlay — narrower, opaque, shows relative cost weight */}
<div
className="absolute inset-y-0 left-0 bg-primary transition-[width] duration-150"
style={{ width: `${costPct}%`, opacity: 0.85 }}
title={`${Math.round(costPct)}% of provider cost`}
/>
</div>
</div>
);
})}
</div>
</>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,65 @@
import { cn } from "@/lib/utils";
interface QuotaBarProps {
label: string;
// value between 0 and 100
percentUsed: number;
leftLabel: string;
rightLabel?: string;
// shows a 2px destructive notch at the fill tip when true
showDeficitNotch?: boolean;
className?: string;
}
function fillColor(pct: number): string {
if (pct > 90) return "bg-red-400";
if (pct > 70) return "bg-yellow-400";
return "bg-green-400";
}
export function QuotaBar({
label,
percentUsed,
leftLabel,
rightLabel,
showDeficitNotch = false,
className,
}: QuotaBarProps) {
const clampedPct = Math.min(100, Math.max(0, percentUsed));
// keep the notch visible even near the edges
const notchLeft = Math.min(clampedPct, 97);
return (
<div className={cn("space-y-1.5", className)}>
{/* row header */}
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-muted-foreground">{label}</span>
<div className="flex items-center gap-2 shrink-0">
<span className="text-xs font-medium tabular-nums">{leftLabel}</span>
{rightLabel && (
<span className="text-xs text-muted-foreground tabular-nums">{rightLabel}</span>
)}
</div>
</div>
{/* track — boxed border, square corners to match the theme */}
<div className="relative h-2 w-full border border-border overflow-hidden">
{/* fill */}
<div
className={cn(
"absolute inset-y-0 left-0 transition-[width,background-color] duration-150",
fillColor(clampedPct),
)}
style={{ width: `${clampedPct}%` }}
/>
{/* deficit notch — 2px wide, sits at the fill tip */}
{showDeficitNotch && clampedPct > 0 && (
<div
className="absolute inset-y-0 w-[2px] bg-destructive z-10"
style={{ left: `${notchLeft}%` }}
/>
)}
</div>
</div>
);
}

View File

@@ -4,6 +4,7 @@ import {
Target,
LayoutDashboard,
DollarSign,
Gauge,
History,
Search,
SquarePen,
@@ -107,6 +108,7 @@ export function Sidebar() {
<SidebarSection label="Company">
<SidebarNavItem to="/org" label="Org" icon={Network} />
<SidebarNavItem to="/costs" label="Costs" icon={DollarSign} />
<SidebarNavItem to="/usage" label="Usage" icon={Gauge} />
<SidebarNavItem to="/activity" label="Activity" icon={History} />
<SidebarNavItem to="/company/settings" label="Settings" icon={Settings} />
</SidebarSection>