feat(usage): add All Time period option and refine overview cards

- Add "all" period option to usage dashboard and chart API
- Aggregate all days in getChartData when period is "all"
- Center overview card metrics and adjust font size to prevent truncation

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
decolua
2026-09-22 15:31:24 +07:00
parent d1de324586
commit b53260ca54
5 changed files with 48 additions and 20 deletions

View File

@@ -9,25 +9,25 @@ const fmtCost = (n) => `$${(n || 0).toFixed(2)}`;
export default function OverviewCards({ stats }) { export default function OverviewCards({ stats }) {
return ( return (
<div className="grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 sm:gap-4"> <div className="grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 sm:gap-4">
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3"> <Card className="flex min-w-0 flex-col items-center text-center gap-1 px-3 py-3 sm:px-4">
<span className="text-text-muted text-sm uppercase font-semibold">Total Requests</span> <span className="text-text-muted text-xs uppercase font-semibold sm:text-sm">Total Requests</span>
<span className="truncate text-2xl font-bold">{fmt(stats.totalRequests)}</span> <span className="w-full truncate text-lg font-bold xl:text-xl" title={fmt(stats.totalRequests)}>{fmt(stats.totalRequests)}</span>
</Card> </Card>
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3"> <Card className="flex min-w-0 flex-col items-center text-center gap-1 px-3 py-3 sm:px-4">
<span className="text-text-muted text-sm uppercase font-semibold">Total Input Tokens</span> <span className="text-text-muted text-xs uppercase font-semibold sm:text-sm">Total Input Tokens</span>
<span className="truncate text-2xl font-bold text-primary">{fmt(stats.totalPromptTokens)}</span> <span className="w-full truncate text-lg font-bold text-primary xl:text-xl" title={fmt(stats.totalPromptTokens)}>{fmt(stats.totalPromptTokens)}</span>
</Card> </Card>
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3"> <Card className="flex min-w-0 flex-col items-center text-center gap-1 px-3 py-3 sm:px-4">
<span className="text-text-muted text-sm uppercase font-semibold">Cached Tokens</span> <span className="text-text-muted text-xs uppercase font-semibold sm:text-sm">Cached Tokens</span>
<span className="truncate text-2xl font-bold text-info">{fmt(stats.totalCachedTokens)}</span> <span className="w-full truncate text-lg font-bold text-info xl:text-xl" title={fmt(stats.totalCachedTokens)}>{fmt(stats.totalCachedTokens)}</span>
</Card> </Card>
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3"> <Card className="flex min-w-0 flex-col items-center text-center gap-1 px-3 py-3 sm:px-4">
<span className="text-text-muted text-sm uppercase font-semibold">Output Tokens</span> <span className="text-text-muted text-xs uppercase font-semibold sm:text-sm">Output Tokens</span>
<span className="truncate text-2xl font-bold text-success">{fmt(stats.totalCompletionTokens)}</span> <span className="w-full truncate text-lg font-bold text-success xl:text-xl" title={fmt(stats.totalCompletionTokens)}>{fmt(stats.totalCompletionTokens)}</span>
</Card> </Card>
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3"> <Card className="flex min-w-0 flex-col items-center text-center gap-1 px-3 py-3 sm:px-4">
<span className="text-text-muted text-sm uppercase font-semibold">Est. Cost</span> <span className="text-text-muted text-xs uppercase font-semibold sm:text-sm">Est. Cost</span>
<span className="truncate text-2xl font-bold text-warning">~{fmtCost(stats.totalCost)}</span> <span className="w-full truncate text-lg font-bold text-warning xl:text-xl" title={`~${fmtCost(stats.totalCost)}`}>~{fmtCost(stats.totalCost)}</span>
<span className="text-[10px] text-text-muted">Estimated, not actual billing</span> <span className="text-[10px] text-text-muted">Estimated, not actual billing</span>
</Card> </Card>
</div> </div>

View File

@@ -11,6 +11,7 @@ const PERIODS = [
{ value: "7d", label: "7D" }, { value: "7d", label: "7D" },
{ value: "30d", label: "30D" }, { value: "30d", label: "30D" },
{ value: "60d", label: "60D" }, { value: "60d", label: "60D" },
{ value: "all", label: "All" },
]; ];
export default function UsagePage() { export default function UsagePage() {

View File

@@ -1,7 +1,7 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { getChartData } from "@/lib/usageDb"; import { getChartData } from "@/lib/usageDb";
const VALID_PERIODS = new Set(["today", "24h", "7d", "30d", "60d"]); const VALID_PERIODS = new Set(["today", "24h", "7d", "30d", "60d", "all"]);
export async function GET(request) { export async function GET(request) {
try { try {

View File

@@ -335,12 +335,12 @@ export async function getUsageHistory(filter = {}) {
function loadDaysInRange(adapter, maxDays) { function loadDaysInRange(adapter, maxDays) {
if (maxDays == null) { if (maxDays == null) {
return adapter.all(`SELECT dateKey, data FROM usageDaily`); return adapter.all(`SELECT dateKey, data FROM usageDaily ORDER BY dateKey ASC`);
} }
const today = new Date(); const today = new Date();
const cutoff = new Date(today.getFullYear(), today.getMonth(), today.getDate() - maxDays + 1); const cutoff = new Date(today.getFullYear(), today.getMonth(), today.getDate() - maxDays + 1);
const cutoffKey = `${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, "0")}-${String(cutoff.getDate()).padStart(2, "0")}`; const cutoffKey = `${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, "0")}-${String(cutoff.getDate()).padStart(2, "0")}`;
return adapter.all(`SELECT dateKey, data FROM usageDaily WHERE dateKey >= ?`, [cutoffKey]); return adapter.all(`SELECT dateKey, data FROM usageDaily WHERE dateKey >= ? ORDER BY dateKey ASC`, [cutoffKey]);
} }
export async function getUsageStats(period = "all") { export async function getUsageStats(period = "all") {
@@ -718,9 +718,35 @@ export async function getChartData(period = "7d") {
return buckets; return buckets;
} }
const labelFn = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
if (period === "all") {
const dayRows = loadDaysInRange(db, null);
if (!dayRows.length) return [];
const dayMap = {};
for (const r of dayRows) dayMap[r.dateKey] = parseJson(r.data, {});
const earliest = new Date(dayRows[0].dateKey + "T00:00:00");
const today = new Date();
today.setHours(0, 0, 0, 0);
const diffDays = Math.max(1, Math.round((today - earliest) / 86400000) + 1);
return Array.from({ length: diffDays }, (_, i) => {
const d = new Date(earliest);
d.setDate(d.getDate() + i);
const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const dayData = dayMap[dateKey];
return {
label: labelFn(d),
tokens: dayData ? (dayData.promptTokens || 0) + (dayData.completionTokens || 0) : 0,
cost: dayData ? (dayData.cost || 0) : 0,
requests: dayData ? (dayData.requests || 0) : 0,
};
});
}
const bucketCount = period === "7d" ? 7 : period === "30d" ? 30 : 60; const bucketCount = period === "7d" ? 7 : period === "30d" ? 30 : 60;
const today = new Date(); const today = new Date();
const labelFn = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
// Build map of dateKey → day data // Build map of dateKey → day data
const dayRows = loadDaysInRange(db, bucketCount); const dayRows = loadDaysInRange(db, bucketCount);

View File

@@ -200,6 +200,7 @@ const PERIODS = [
{ value: "7d", label: "7D" }, { value: "7d", label: "7D" },
{ value: "30d", label: "30D" }, { value: "30d", label: "30D" },
{ value: "60d", label: "60D" }, { value: "60d", label: "60D" },
{ value: "all", label: "All" },
]; ];
export default function UsageStats({ period: periodProp, setPeriod: setPeriodProp, hidePeriodSelector = false } = {}) { export default function UsageStats({ period: periodProp, setPeriod: setPeriodProp, hidePeriodSelector = false } = {}) {
@@ -448,7 +449,7 @@ export default function UsageStats({ period: periodProp, setPeriod: setPeriodPro
{/* Period selector (hidden when controlled by parent) */} {/* Period selector (hidden when controlled by parent) */}
{!hidePeriodSelector && ( {!hidePeriodSelector && (
<div className="flex w-full items-center gap-2 sm:w-auto sm:self-end"> <div className="flex w-full items-center gap-2 sm:w-auto sm:self-end">
<div className="grid flex-1 grid-cols-5 items-center gap-1 rounded-lg border border-border bg-bg-subtle p-1 sm:flex sm:flex-none"> <div className="grid flex-1 grid-cols-6 items-center gap-1 rounded-lg border border-border bg-bg-subtle p-1 sm:flex sm:flex-none">
{PERIODS.map((p) => ( {PERIODS.map((p) => (
<button <button
key={p.value} key={p.value}