mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
feat(usage): add Today period option to Usage & Analytics (#1063)
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.
This commit is contained in:
@@ -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" },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 && (
|
||||
<div className="flex w-full items-center gap-2 sm:w-auto sm:self-end">
|
||||
<div className="grid flex-1 grid-cols-4 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-5 items-center gap-1 rounded-lg border border-border bg-bg-subtle p-1 sm:flex sm:flex-none">
|
||||
{PERIODS.map((p) => (
|
||||
<button
|
||||
key={p.value}
|
||||
|
||||
Reference in New Issue
Block a user