diff --git a/src/components/usage/UsageDashboard.tsx b/src/components/usage/UsageDashboard.tsx
index 9d49fcc27c..4eb2485d4a 100644
--- a/src/components/usage/UsageDashboard.tsx
+++ b/src/components/usage/UsageDashboard.tsx
@@ -108,9 +108,18 @@ export function UsageDashboard() {
return getUsageRangePresetLabel(range.preset, t);
}
- return `${new Date(resolvedRange.startDate * 1000).toLocaleString(locale)} - ${new Date(
- resolvedRange.endDate * 1000,
- ).toLocaleString(locale)}`;
+ const startStr = new Date(resolvedRange.startDate * 1000).toLocaleString(
+ locale,
+ );
+
+ if (range.liveEndTime) {
+ return `${startStr} → ${t("usage.liveEndTimeNow", "现在")}`;
+ }
+
+ const endStr = new Date(resolvedRange.endDate * 1000).toLocaleString(
+ locale,
+ );
+ return `${startStr} - ${endStr}`;
}, [locale, range, resolvedRange.endDate, resolvedRange.startDate, t]);
// 顶栏下拉的选项池:Provider 列表只跟应用/时间范围走(不受自身选中值影响),
diff --git a/src/components/usage/UsageDateRangePicker.tsx b/src/components/usage/UsageDateRangePicker.tsx
index 067e7eeb15..845795a654 100644
--- a/src/components/usage/UsageDateRangePicker.tsx
+++ b/src/components/usage/UsageDateRangePicker.tsx
@@ -7,6 +7,7 @@ import {
ChevronRight,
} from "lucide-react";
import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import {
Popover,
@@ -117,6 +118,9 @@ export function UsageDateRangePicker({
);
const [draftStart, setDraftStart] = useState(resolvedRange.startDate);
const [draftEnd, setDraftEnd] = useState(resolvedRange.endDate);
+ const [draftLiveEnd, setDraftLiveEnd] = useState(
+ selection.preset === "custom" ? (selection.liveEndTime ?? false) : false,
+ );
const [displayMonth, setDisplayMonth] = useState(
() =>
new Date(
@@ -136,6 +140,9 @@ export function UsageDateRangePicker({
const r = resolveUsageRange(selection);
setDraftStart(r.startDate);
setDraftEnd(r.endDate);
+ setDraftLiveEnd(
+ selection.preset === "custom" ? (selection.liveEndTime ?? false) : false,
+ );
setDisplayMonth(
new Date(
fromTs(r.startDate).getFullYear(),
@@ -147,6 +154,15 @@ export function UsageDateRangePicker({
setError(null);
}, [open, selection]);
+ // Keep draftEnd ticking when live mode is active and popover is open
+ useEffect(() => {
+ if (!open || !draftLiveEnd) return;
+ const tick = () => setDraftEnd(Math.floor(Date.now() / 1000));
+ tick();
+ const id = setInterval(tick, 1000);
+ return () => clearInterval(id);
+ }, [open, draftLiveEnd]);
+
const calendarDays = useMemo(
() => getCalendarDays(displayMonth),
[displayMonth],
@@ -169,6 +185,14 @@ export function UsageDateRangePicker({
/* Pick a date from the calendar */
const handleDatePick = (day: Date) => {
setError(null);
+
+ // When live end time is active, calendar only controls start date
+ if (draftLiveEnd) {
+ const nextTs = setDateKeepTime(draftStart, day);
+ setDraftStart(nextTs);
+ return;
+ }
+
const nextTs = setDateKeepTime(
activeField === "start" ? draftStart : draftEnd,
day,
@@ -211,6 +235,7 @@ export function UsageDateRangePicker({
preset: "custom",
customStartDate: draftStart,
customEndDate: draftEnd,
+ liveEndTime: draftLiveEnd,
});
setOpen(false);
};
@@ -222,6 +247,7 @@ export function UsageDateRangePicker({
/* ── Field card (start / end) ── */
const renderField = (field: DraftField) => {
const isActive = activeField === field;
+ const isEndLive = field === "end" && draftLiveEnd;
const ts = field === "start" ? draftStart : draftEnd;
const setTs = field === "start" ? setDraftStart : setDraftEnd;
const label =
@@ -232,12 +258,16 @@ export function UsageDateRangePicker({
return (
setActiveField(field)}
+ onClick={() => {
+ if (!isEndLive) setActiveField(field);
+ }}
>
@@ -318,6 +362,23 @@ export function UsageDateRangePicker({
{renderField("start")}
{renderField("end")}
+
+
{error &&
{error}
}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index d5457095fe..3f760c5065 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -1472,6 +1472,8 @@
"customRangeHint": "Supports both date and time",
"startTime": "Start Time",
"endTime": "End Time",
+ "liveEndTime": "End time follows current time",
+ "liveEndTimeNow": "Now",
"input": "Input",
"output": "Output",
"cacheWrite": "Creation",
diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json
index b39eb55b28..86a5c3db3f 100644
--- a/src/i18n/locales/ja.json
+++ b/src/i18n/locales/ja.json
@@ -1472,6 +1472,8 @@
"customRangeHint": "日付と時刻の両方に対応",
"startTime": "開始時刻",
"endTime": "終了時刻",
+ "liveEndTime": "終了時刻を現在時刻に追従",
+ "liveEndTimeNow": "現在",
"input": "Input",
"output": "Output",
"cacheWrite": "作成",
diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json
index b6d7335f45..887e5f5185 100644
--- a/src/i18n/locales/zh-TW.json
+++ b/src/i18n/locales/zh-TW.json
@@ -1444,6 +1444,8 @@
"customRangeHint": "支援日期與時間",
"startTime": "開始時間",
"endTime": "結束時間",
+ "liveEndTime": "結束時間跟隨當前時刻",
+ "liveEndTimeNow": "現在",
"input": "Input",
"output": "Output",
"cacheWrite": "建立",
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index ae55b2396c..b00e12df64 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -1472,6 +1472,8 @@
"customRangeHint": "支持日期与时间",
"startTime": "开始时间",
"endTime": "结束时间",
+ "liveEndTime": "结束时间跟随当前时刻",
+ "liveEndTimeNow": "现在",
"input": "Input",
"output": "Output",
"cacheWrite": "创建",
diff --git a/src/lib/query/usage.ts b/src/lib/query/usage.ts
index f0aa307447..ea95dc0cc2 100644
--- a/src/lib/query/usage.ts
+++ b/src/lib/query/usage.ts
@@ -26,6 +26,7 @@ type RequestLogsKey = {
preset: UsageRangeSelection["preset"];
customStartDate?: number;
customEndDate?: number;
+ liveEndTime?: boolean;
appType?: string;
providerName?: string;
model?: string;
@@ -40,6 +41,7 @@ export const usageKeys = {
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
+ liveEndTime?: boolean,
) =>
[
...usageKeys.all,
@@ -47,6 +49,7 @@ export const usageKeys = {
preset,
customStartDate ?? 0,
customEndDate ?? 0,
+ liveEndTime ?? false,
filters?.appType ?? null,
filters?.providerName ?? null,
filters?.model ?? null,
@@ -55,7 +58,8 @@ export const usageKeys = {
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
- filters?: UsageScopeFilters,
+ filters?: Pick,
+ liveEndTime?: boolean,
) =>
[
...usageKeys.all,
@@ -63,6 +67,7 @@ export const usageKeys = {
preset,
customStartDate ?? 0,
customEndDate ?? 0,
+ liveEndTime ?? false,
filters?.providerName ?? null,
filters?.model ?? null,
] as const,
@@ -71,6 +76,7 @@ export const usageKeys = {
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
+ liveEndTime?: boolean,
) =>
[
...usageKeys.all,
@@ -78,6 +84,7 @@ export const usageKeys = {
preset,
customStartDate ?? 0,
customEndDate ?? 0,
+ liveEndTime ?? false,
filters?.appType ?? null,
filters?.providerName ?? null,
filters?.model ?? null,
@@ -87,6 +94,7 @@ export const usageKeys = {
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
+ liveEndTime?: boolean,
) =>
[
...usageKeys.all,
@@ -94,6 +102,7 @@ export const usageKeys = {
preset,
customStartDate ?? 0,
customEndDate ?? 0,
+ liveEndTime ?? false,
filters?.appType ?? null,
filters?.providerName ?? null,
filters?.model ?? null,
@@ -103,6 +112,7 @@ export const usageKeys = {
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
+ liveEndTime?: boolean,
) =>
[
...usageKeys.all,
@@ -110,6 +120,7 @@ export const usageKeys = {
preset,
customStartDate ?? 0,
customEndDate ?? 0,
+ liveEndTime ?? false,
filters?.appType ?? null,
filters?.providerName ?? null,
filters?.model ?? null,
@@ -121,6 +132,7 @@ export const usageKeys = {
key.preset,
key.customStartDate ?? 0,
key.customEndDate ?? 0,
+ key.liveEndTime ?? false,
key.appType ?? "",
key.providerName ?? "",
key.model ?? "",
@@ -159,6 +171,7 @@ export function useUsageSummary(
range.customStartDate,
range.customEndDate,
effective,
+ range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
@@ -186,6 +199,7 @@ export function useUsageSummaryByApp(
range.customStartDate,
range.customEndDate,
filters,
+ range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
@@ -213,6 +227,7 @@ export function useUsageTrends(
range.customStartDate,
range.customEndDate,
effective,
+ range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
@@ -241,6 +256,7 @@ export function useProviderStats(
range.customStartDate,
range.customEndDate,
effective,
+ range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
@@ -269,6 +285,7 @@ export function useModelStats(
range.customStartDate,
range.customEndDate,
effective,
+ range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
@@ -296,6 +313,7 @@ export function useRequestLogs({
preset: range.preset,
customStartDate: range.customStartDate,
customEndDate: range.customEndDate,
+ liveEndTime: range.liveEndTime,
appType: filters.appType,
providerName: filters.providerName,
model: filters.model,
diff --git a/src/lib/usageRange.ts b/src/lib/usageRange.ts
index 4b70ee1d24..6cedcf3200 100644
--- a/src/lib/usageRange.ts
+++ b/src/lib/usageRange.ts
@@ -49,7 +49,9 @@ export function resolveUsageRange(
};
case "custom": {
const startDate = selection.customStartDate ?? endDate - DAY_SECONDS;
- const customEndDate = selection.customEndDate ?? endDate;
+ const customEndDate = selection.liveEndTime
+ ? endDate
+ : (selection.customEndDate ?? endDate);
return {
startDate,
endDate: customEndDate,
diff --git a/src/types/usage.ts b/src/types/usage.ts
index 6e6f957dd8..256d58aec6 100644
--- a/src/types/usage.ts
+++ b/src/types/usage.ts
@@ -152,6 +152,9 @@ export interface UsageRangeSelection {
preset: UsageRangePreset;
customStartDate?: number;
customEndDate?: number;
+ /** When true (custom mode only), endDate resolves to "now" instead of the
+ * fixed customEndDate snapshot, and the end-time field becomes read-only. */
+ liveEndTime?: boolean;
}
/**