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:
@@ -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>
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user