From d1a8dd498759b9b14410364ffd55e54122030692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C5=A9ng=20A=20T=C3=B4=20Ri=20A?= Date: Thu, 14 May 2026 10:03:05 +0700 Subject: [PATCH] feat(usage): add Today period option to Usage & Analytics (#1063) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bổ sung lựa chọn Today vào bộ lọc thời gian của trang Usage & Analytics (trước đây chỉ có 24h, 7D, 30D, 60D). Khác biệt với 24h: - 24h: cuộn 24 giờ trước → hiện tại - Today: cố định từ 00:00 hôm nay (giờ local) → hiện tại Thay đổi: - page.js, UsageStats.js: thêm option Today vào danh sách PERIODS, đổi grid mobile từ 4 cột sang 5 cột để fit option mới. - api/usage/stats, api/usage/chart: cho phép giá trị period today. - usageRepo.js: + getUsageStats: dùng nhánh live history khi period = today, cutoff lấy từ 00:00 hôm nay theo local time. + getChartData: thêm 24 bucket theo giờ từ 00:00 → 23:59 hôm nay. --- src/app/(dashboard)/dashboard/usage/page.js | 1 + src/app/api/usage/chart/route.js | 2 +- src/app/api/usage/stats/route.js | 2 +- src/lib/db/repos/usageRepo.js | 39 +++++++++++++++++++-- src/shared/components/UsageStats.js | 3 +- 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/app/(dashboard)/dashboard/usage/page.js b/src/app/(dashboard)/dashboard/usage/page.js index 52a344d6..05daa2c4 100644 --- a/src/app/(dashboard)/dashboard/usage/page.js +++ b/src/app/(dashboard)/dashboard/usage/page.js @@ -6,6 +6,7 @@ import { UsageStats, RequestLogger, CardSkeleton, SegmentedControl } from "@/sha import RequestDetailsTab from "./components/RequestDetailsTab"; const PERIODS = [ + { value: "today", label: "Today" }, { value: "24h", label: "24h" }, { value: "7d", label: "7D" }, { value: "30d", label: "30D" }, diff --git a/src/app/api/usage/chart/route.js b/src/app/api/usage/chart/route.js index 3289ca84..063cedd6 100644 --- a/src/app/api/usage/chart/route.js +++ b/src/app/api/usage/chart/route.js @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { getChartData } from "@/lib/usageDb"; -const VALID_PERIODS = new Set(["24h", "7d", "30d", "60d"]); +const VALID_PERIODS = new Set(["today", "24h", "7d", "30d", "60d"]); export async function GET(request) { try { diff --git a/src/app/api/usage/stats/route.js b/src/app/api/usage/stats/route.js index 2e8c88ad..27e51090 100644 --- a/src/app/api/usage/stats/route.js +++ b/src/app/api/usage/stats/route.js @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { getUsageStats } from "@/lib/usageDb"; -const VALID_PERIODS = new Set(["24h", "7d", "30d", "60d", "all"]); +const VALID_PERIODS = new Set(["today", "24h", "7d", "30d", "60d", "all"]); export const dynamic = "force-dynamic"; diff --git a/src/lib/db/repos/usageRepo.js b/src/lib/db/repos/usageRepo.js index 2d84449e..63d0494e 100644 --- a/src/lib/db/repos/usageRepo.js +++ b/src/lib/db/repos/usageRepo.js @@ -415,7 +415,7 @@ export async function getUsageStats(period = "all") { } } - const useDailySummary = period !== "24h"; + const useDailySummary = period !== "24h" && period !== "today"; if (useDailySummary) { const periodDays = { "7d": 7, "30d": 30, "60d": 60 }; @@ -529,8 +529,15 @@ export async function getUsageStats(period = "all") { if (stats.byEndpoint[endpointKey] && new Date(ts) > new Date(stats.byEndpoint[endpointKey].lastUsed)) stats.byEndpoint[endpointKey].lastUsed = ts; } } else { - // 24h: live history - const cutoff = new Date(Date.now() - PERIOD_MS["24h"]).toISOString(); + // 24h / today: live history + let cutoff; + if (period === "today") { + const startOfDay = new Date(); + startOfDay.setHours(0, 0, 0, 0); + cutoff = startOfDay.toISOString(); + } else { + cutoff = new Date(Date.now() - PERIOD_MS["24h"]).toISOString(); + } const filtered = db.all( `SELECT timestamp, provider, model, connectionId, apiKey, endpoint, promptTokens, completionTokens, cost, tokens FROM usageHistory WHERE timestamp >= ?`, [cutoff] @@ -614,6 +621,32 @@ export async function getChartData(period = "7d") { const db = await getAdapter(); const now = Date.now(); + if (period === "today") { + const bucketCount = 24; + const bucketMs = 3600000; + const startOfDay = new Date(); + startOfDay.setHours(0, 0, 0, 0); + const startTime = startOfDay.getTime(); + const endTime = startTime + bucketCount * bucketMs; + const labelFn = (ts) => new Date(ts).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }); + const buckets = Array.from({ length: bucketCount }, (_, i) => ({ label: labelFn(startTime + i * bucketMs), tokens: 0, cost: 0 })); + + const rows = db.all( + `SELECT timestamp, promptTokens, completionTokens, cost FROM usageHistory WHERE timestamp >= ?`, + [new Date(startTime).toISOString()] + ); + for (const r of rows) { + const t = new Date(r.timestamp).getTime(); + if (t < startTime || t >= endTime) continue; + const idx = Math.floor((t - startTime) / bucketMs); + if (idx >= 0 && idx < bucketCount) { + buckets[idx].tokens += (r.promptTokens || 0) + (r.completionTokens || 0); + buckets[idx].cost += r.cost || 0; + } + } + return buckets; + } + if (period === "24h") { const bucketCount = 24; const bucketMs = 3600000; diff --git a/src/shared/components/UsageStats.js b/src/shared/components/UsageStats.js index 98e7b7fc..29dd7295 100644 --- a/src/shared/components/UsageStats.js +++ b/src/shared/components/UsageStats.js @@ -182,6 +182,7 @@ const TABLE_OPTIONS = [ ]; const PERIODS = [ + { value: "today", label: "Today" }, { value: "24h", label: "24h" }, { value: "7d", label: "7D" }, { value: "30d", label: "30D" }, @@ -412,7 +413,7 @@ export default function UsageStats({ period: periodProp, setPeriod: setPeriodPro {/* Period selector (hidden when controlled by parent) */} {!hidePeriodSelector && (
-
+
{PERIODS.map((p) => (