feat(usage): add live end time option for custom date range (#4438)

* add glm-5.2 pricing

* feat(usage): add live end time option for custom date range

Add a "End time follows current time" checkbox in the custom date range
picker. When enabled, the end time becomes read-only and automatically
tracks the current moment, so usage data always reflects up-to-the-second
consumption from the chosen start time.

This is especially useful under the Coding Plan 5-hour quota window —
users can set the start time to when their 5h window began and keep
the end time live to monitor real-time token consumption.

* style(usage): fix prettier formatting for UsageDashboard

* fix(usage): include liveEndTime in React Query cache keys

Without liveEndTime in the query key, a live custom range and a fixed
custom range with the same stored endpoints share the same cache entry.
After the live range refreshes (endDate = now), switching to the fixed
range shows stale data fetched through "now" instead of the original
end time.
This commit is contained in:
Arc
2026-06-23 23:04:00 +08:00
committed by GitHub
parent 2db3163cf2
commit 9171ad752c
9 changed files with 115 additions and 14 deletions
+12 -3
View File
@@ -108,9 +108,18 @@ export function UsageDashboard() {
return getUsageRangePresetLabel(range.preset, t); return getUsageRangePresetLabel(range.preset, t);
} }
return `${new Date(resolvedRange.startDate * 1000).toLocaleString(locale)} - ${new Date( const startStr = new Date(resolvedRange.startDate * 1000).toLocaleString(
resolvedRange.endDate * 1000, locale,
).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]); }, [locale, range, resolvedRange.endDate, resolvedRange.startDate, t]);
// 顶栏下拉的选项池:Provider 列表只跟应用/时间范围走(不受自身选中值影响), // 顶栏下拉的选项池:Provider 列表只跟应用/时间范围走(不受自身选中值影响),
+70 -9
View File
@@ -7,6 +7,7 @@ import {
ChevronRight, ChevronRight,
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
Popover, Popover,
@@ -117,6 +118,9 @@ export function UsageDateRangePicker({
); );
const [draftStart, setDraftStart] = useState(resolvedRange.startDate); const [draftStart, setDraftStart] = useState(resolvedRange.startDate);
const [draftEnd, setDraftEnd] = useState(resolvedRange.endDate); const [draftEnd, setDraftEnd] = useState(resolvedRange.endDate);
const [draftLiveEnd, setDraftLiveEnd] = useState(
selection.preset === "custom" ? (selection.liveEndTime ?? false) : false,
);
const [displayMonth, setDisplayMonth] = useState( const [displayMonth, setDisplayMonth] = useState(
() => () =>
new Date( new Date(
@@ -136,6 +140,9 @@ export function UsageDateRangePicker({
const r = resolveUsageRange(selection); const r = resolveUsageRange(selection);
setDraftStart(r.startDate); setDraftStart(r.startDate);
setDraftEnd(r.endDate); setDraftEnd(r.endDate);
setDraftLiveEnd(
selection.preset === "custom" ? (selection.liveEndTime ?? false) : false,
);
setDisplayMonth( setDisplayMonth(
new Date( new Date(
fromTs(r.startDate).getFullYear(), fromTs(r.startDate).getFullYear(),
@@ -147,6 +154,15 @@ export function UsageDateRangePicker({
setError(null); setError(null);
}, [open, selection]); }, [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( const calendarDays = useMemo(
() => getCalendarDays(displayMonth), () => getCalendarDays(displayMonth),
[displayMonth], [displayMonth],
@@ -169,6 +185,14 @@ export function UsageDateRangePicker({
/* Pick a date from the calendar */ /* Pick a date from the calendar */
const handleDatePick = (day: Date) => { const handleDatePick = (day: Date) => {
setError(null); 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( const nextTs = setDateKeepTime(
activeField === "start" ? draftStart : draftEnd, activeField === "start" ? draftStart : draftEnd,
day, day,
@@ -211,6 +235,7 @@ export function UsageDateRangePicker({
preset: "custom", preset: "custom",
customStartDate: draftStart, customStartDate: draftStart,
customEndDate: draftEnd, customEndDate: draftEnd,
liveEndTime: draftLiveEnd,
}); });
setOpen(false); setOpen(false);
}; };
@@ -222,6 +247,7 @@ export function UsageDateRangePicker({
/* ── Field card (start / end) ── */ /* ── Field card (start / end) ── */
const renderField = (field: DraftField) => { const renderField = (field: DraftField) => {
const isActive = activeField === field; const isActive = activeField === field;
const isEndLive = field === "end" && draftLiveEnd;
const ts = field === "start" ? draftStart : draftEnd; const ts = field === "start" ? draftStart : draftEnd;
const setTs = field === "start" ? setDraftStart : setDraftEnd; const setTs = field === "start" ? setDraftStart : setDraftEnd;
const label = const label =
@@ -232,12 +258,16 @@ export function UsageDateRangePicker({
return ( return (
<div <div
className={cn( className={cn(
"rounded-lg border px-3 py-2 cursor-pointer transition-all", "rounded-lg border px-3 py-2 transition-all",
isActive isEndLive
? "border-primary ring-1 ring-primary/30 bg-primary/5" ? "border-border/30 bg-muted/30 cursor-not-allowed opacity-50"
: "border-border/50 hover:border-border", : isActive
? "border-primary ring-1 ring-primary/30 bg-primary/5 cursor-pointer"
: "border-border/50 hover:border-border cursor-pointer",
)} )}
onClick={() => setActiveField(field)} onClick={() => {
if (!isEndLive) setActiveField(field);
}}
> >
<div className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"> <div className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
{label} {label}
@@ -245,27 +275,41 @@ export function UsageDateRangePicker({
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Input <Input
type="date" type="date"
className="h-7 flex-1 border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0" className={cn(
"h-7 flex-1 border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0",
isEndLive && "pointer-events-none",
)}
value={fmtDate(ts)} value={fmtDate(ts)}
onChange={(e) => { onChange={(e) => {
if (isEndLive) return;
const next = parseDateInput(ts, e.target.value); const next = parseDateInput(ts, e.target.value);
setTs(next); setTs(next);
const d = fromTs(next); const d = fromTs(next);
setDisplayMonth(new Date(d.getFullYear(), d.getMonth(), 1)); setDisplayMonth(new Date(d.getFullYear(), d.getMonth(), 1));
setError(null); setError(null);
}} }}
onFocus={() => setActiveField(field)} onFocus={() => {
if (!isEndLive) setActiveField(field);
}}
readOnly={isEndLive}
/> />
<Input <Input
type="time" type="time"
step={60} step={60}
className="h-7 w-[90px] flex-none border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0" className={cn(
"h-7 w-[90px] flex-none border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0",
isEndLive && "pointer-events-none",
)}
value={fmtTime(ts)} value={fmtTime(ts)}
onChange={(e) => { onChange={(e) => {
if (isEndLive) return;
setTs(parseTimeInput(ts, e.target.value)); setTs(parseTimeInput(ts, e.target.value));
setError(null); setError(null);
}} }}
onFocus={() => setActiveField(field)} onFocus={() => {
if (!isEndLive) setActiveField(field);
}}
readOnly={isEndLive}
/> />
</div> </div>
</div> </div>
@@ -318,6 +362,23 @@ export function UsageDateRangePicker({
{renderField("start")} {renderField("start")}
{renderField("end")} {renderField("end")}
<label className="flex items-center gap-2 cursor-pointer select-none">
<Checkbox
checked={draftLiveEnd}
onCheckedChange={(checked) => {
const live = checked === true;
setDraftLiveEnd(live);
if (live) {
setDraftEnd(Math.floor(Date.now() / 1000));
setActiveField("start");
}
}}
/>
<span className="text-xs text-muted-foreground">
{t("usage.liveEndTime", "结束时间跟随当前时刻")}
</span>
</label>
{error && <p className="text-xs text-destructive">{error}</p>} {error && <p className="text-xs text-destructive">{error}</p>}
<div className="flex gap-2 pt-1"> <div className="flex gap-2 pt-1">
+2
View File
@@ -1473,6 +1473,8 @@
"customRangeHint": "Supports both date and time", "customRangeHint": "Supports both date and time",
"startTime": "Start Time", "startTime": "Start Time",
"endTime": "End Time", "endTime": "End Time",
"liveEndTime": "End time follows current time",
"liveEndTimeNow": "Now",
"input": "Input", "input": "Input",
"output": "Output", "output": "Output",
"cacheWrite": "Creation", "cacheWrite": "Creation",
+2
View File
@@ -1473,6 +1473,8 @@
"customRangeHint": "日付と時刻の両方に対応", "customRangeHint": "日付と時刻の両方に対応",
"startTime": "開始時刻", "startTime": "開始時刻",
"endTime": "終了時刻", "endTime": "終了時刻",
"liveEndTime": "終了時刻を現在時刻に追従",
"liveEndTimeNow": "現在",
"input": "Input", "input": "Input",
"output": "Output", "output": "Output",
"cacheWrite": "作成", "cacheWrite": "作成",
+2
View File
@@ -1445,6 +1445,8 @@
"customRangeHint": "支援日期與時間", "customRangeHint": "支援日期與時間",
"startTime": "開始時間", "startTime": "開始時間",
"endTime": "結束時間", "endTime": "結束時間",
"liveEndTime": "結束時間跟隨當前時刻",
"liveEndTimeNow": "現在",
"input": "Input", "input": "Input",
"output": "Output", "output": "Output",
"cacheWrite": "建立", "cacheWrite": "建立",
+2
View File
@@ -1473,6 +1473,8 @@
"customRangeHint": "支持日期与时间", "customRangeHint": "支持日期与时间",
"startTime": "开始时间", "startTime": "开始时间",
"endTime": "结束时间", "endTime": "结束时间",
"liveEndTime": "结束时间跟随当前时刻",
"liveEndTimeNow": "现在",
"input": "Input", "input": "Input",
"output": "Output", "output": "Output",
"cacheWrite": "创建", "cacheWrite": "创建",
+19 -1
View File
@@ -26,6 +26,7 @@ type RequestLogsKey = {
preset: UsageRangeSelection["preset"]; preset: UsageRangeSelection["preset"];
customStartDate?: number; customStartDate?: number;
customEndDate?: number; customEndDate?: number;
liveEndTime?: boolean;
appType?: string; appType?: string;
providerName?: string; providerName?: string;
model?: string; model?: string;
@@ -40,6 +41,7 @@ export const usageKeys = {
customStartDate: number | undefined, customStartDate: number | undefined,
customEndDate: number | undefined, customEndDate: number | undefined,
filters?: UsageScopeFilters, filters?: UsageScopeFilters,
liveEndTime?: boolean,
) => ) =>
[ [
...usageKeys.all, ...usageKeys.all,
@@ -47,6 +49,7 @@ export const usageKeys = {
preset, preset,
customStartDate ?? 0, customStartDate ?? 0,
customEndDate ?? 0, customEndDate ?? 0,
liveEndTime ?? false,
filters?.appType ?? null, filters?.appType ?? null,
filters?.providerName ?? null, filters?.providerName ?? null,
filters?.model ?? null, filters?.model ?? null,
@@ -55,7 +58,8 @@ export const usageKeys = {
preset: UsageRangeSelection["preset"], preset: UsageRangeSelection["preset"],
customStartDate: number | undefined, customStartDate: number | undefined,
customEndDate: number | undefined, customEndDate: number | undefined,
filters?: UsageScopeFilters, filters?: Pick<UsageScopeFilters, "providerName" | "model">,
liveEndTime?: boolean,
) => ) =>
[ [
...usageKeys.all, ...usageKeys.all,
@@ -63,6 +67,7 @@ export const usageKeys = {
preset, preset,
customStartDate ?? 0, customStartDate ?? 0,
customEndDate ?? 0, customEndDate ?? 0,
liveEndTime ?? false,
filters?.providerName ?? null, filters?.providerName ?? null,
filters?.model ?? null, filters?.model ?? null,
] as const, ] as const,
@@ -71,6 +76,7 @@ export const usageKeys = {
customStartDate: number | undefined, customStartDate: number | undefined,
customEndDate: number | undefined, customEndDate: number | undefined,
filters?: UsageScopeFilters, filters?: UsageScopeFilters,
liveEndTime?: boolean,
) => ) =>
[ [
...usageKeys.all, ...usageKeys.all,
@@ -78,6 +84,7 @@ export const usageKeys = {
preset, preset,
customStartDate ?? 0, customStartDate ?? 0,
customEndDate ?? 0, customEndDate ?? 0,
liveEndTime ?? false,
filters?.appType ?? null, filters?.appType ?? null,
filters?.providerName ?? null, filters?.providerName ?? null,
filters?.model ?? null, filters?.model ?? null,
@@ -87,6 +94,7 @@ export const usageKeys = {
customStartDate: number | undefined, customStartDate: number | undefined,
customEndDate: number | undefined, customEndDate: number | undefined,
filters?: UsageScopeFilters, filters?: UsageScopeFilters,
liveEndTime?: boolean,
) => ) =>
[ [
...usageKeys.all, ...usageKeys.all,
@@ -94,6 +102,7 @@ export const usageKeys = {
preset, preset,
customStartDate ?? 0, customStartDate ?? 0,
customEndDate ?? 0, customEndDate ?? 0,
liveEndTime ?? false,
filters?.appType ?? null, filters?.appType ?? null,
filters?.providerName ?? null, filters?.providerName ?? null,
filters?.model ?? null, filters?.model ?? null,
@@ -103,6 +112,7 @@ export const usageKeys = {
customStartDate: number | undefined, customStartDate: number | undefined,
customEndDate: number | undefined, customEndDate: number | undefined,
filters?: UsageScopeFilters, filters?: UsageScopeFilters,
liveEndTime?: boolean,
) => ) =>
[ [
...usageKeys.all, ...usageKeys.all,
@@ -110,6 +120,7 @@ export const usageKeys = {
preset, preset,
customStartDate ?? 0, customStartDate ?? 0,
customEndDate ?? 0, customEndDate ?? 0,
liveEndTime ?? false,
filters?.appType ?? null, filters?.appType ?? null,
filters?.providerName ?? null, filters?.providerName ?? null,
filters?.model ?? null, filters?.model ?? null,
@@ -121,6 +132,7 @@ export const usageKeys = {
key.preset, key.preset,
key.customStartDate ?? 0, key.customStartDate ?? 0,
key.customEndDate ?? 0, key.customEndDate ?? 0,
key.liveEndTime ?? false,
key.appType ?? "", key.appType ?? "",
key.providerName ?? "", key.providerName ?? "",
key.model ?? "", key.model ?? "",
@@ -159,6 +171,7 @@ export function useUsageSummary(
range.customStartDate, range.customStartDate,
range.customEndDate, range.customEndDate,
effective, effective,
range.liveEndTime,
), ),
queryFn: () => { queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range); const { startDate, endDate } = resolveUsageRange(range);
@@ -186,6 +199,7 @@ export function useUsageSummaryByApp(
range.customStartDate, range.customStartDate,
range.customEndDate, range.customEndDate,
filters, filters,
range.liveEndTime,
), ),
queryFn: () => { queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range); const { startDate, endDate } = resolveUsageRange(range);
@@ -213,6 +227,7 @@ export function useUsageTrends(
range.customStartDate, range.customStartDate,
range.customEndDate, range.customEndDate,
effective, effective,
range.liveEndTime,
), ),
queryFn: () => { queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range); const { startDate, endDate } = resolveUsageRange(range);
@@ -241,6 +256,7 @@ export function useProviderStats(
range.customStartDate, range.customStartDate,
range.customEndDate, range.customEndDate,
effective, effective,
range.liveEndTime,
), ),
queryFn: () => { queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range); const { startDate, endDate } = resolveUsageRange(range);
@@ -269,6 +285,7 @@ export function useModelStats(
range.customStartDate, range.customStartDate,
range.customEndDate, range.customEndDate,
effective, effective,
range.liveEndTime,
), ),
queryFn: () => { queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range); const { startDate, endDate } = resolveUsageRange(range);
@@ -296,6 +313,7 @@ export function useRequestLogs({
preset: range.preset, preset: range.preset,
customStartDate: range.customStartDate, customStartDate: range.customStartDate,
customEndDate: range.customEndDate, customEndDate: range.customEndDate,
liveEndTime: range.liveEndTime,
appType: filters.appType, appType: filters.appType,
providerName: filters.providerName, providerName: filters.providerName,
model: filters.model, model: filters.model,
+3 -1
View File
@@ -49,7 +49,9 @@ export function resolveUsageRange(
}; };
case "custom": { case "custom": {
const startDate = selection.customStartDate ?? endDate - DAY_SECONDS; const startDate = selection.customStartDate ?? endDate - DAY_SECONDS;
const customEndDate = selection.customEndDate ?? endDate; const customEndDate = selection.liveEndTime
? endDate
: (selection.customEndDate ?? endDate);
return { return {
startDate, startDate,
endDate: customEndDate, endDate: customEndDate,
+3
View File
@@ -152,6 +152,9 @@ export interface UsageRangeSelection {
preset: UsageRangePreset; preset: UsageRangePreset;
customStartDate?: number; customStartDate?: number;
customEndDate?: 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;
} }
/** /**