Skip to content

Commit 7cf200e

Browse files
committed
fix: address review feedback on timezone inconsistencies
- config.ts: fix catch branch returning 'UTC' while warning says 'Asia/Shanghai'; both now consistently fall back to Asia/Shanghai - getOverview() now returns the timezone it used for SQL bucketing, and the overview API includes it in the response payload - page.tsx: buildHourlySeries accepts an optional timezone parameter and uses it to create the gap-fill label formatter, so gap labels match the server-side bucket labels instead of using the browser's local timezone
1 parent c47a2e5 commit 7cf200e

4 files changed

Lines changed: 22 additions & 12 deletions

File tree

app/api/overview/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type CachedOverview = {
1010
overview: Awaited<ReturnType<typeof getOverview>>["overview"] | null;
1111
empty: boolean;
1212
days: number;
13+
timezone: string;
1314
meta?: Awaited<ReturnType<typeof getOverview>>["meta"];
1415
filters?: Awaited<ReturnType<typeof getOverview>>["filters"];
1516
};
@@ -79,7 +80,7 @@ export async function GET(request: Request) {
7980
}
8081
}
8182

82-
const { overview, empty, days: appliedDays, meta, filters } = await getOverview(days, {
83+
const { overview, empty, days: appliedDays, meta, filters, timezone } = await getOverview(days, {
8384
model: model || undefined,
8485
route: route || undefined,
8586
page,
@@ -89,7 +90,7 @@ export async function GET(request: Request) {
8990
timezone: config.timezone
9091
});
9192

92-
const payload = { overview, empty, days: appliedDays, meta, filters };
93+
const payload = { overview, empty, days: appliedDays, meta, filters, timezone };
9394
setCached(cacheKey, payload);
9495
return NextResponse.json(payload, { status: 200 });
9596
} catch (error) {

app/page.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const PIE_COLORS = [
2323
];
2424

2525
type OverviewMeta = { page: number; pageSize: number; totalModels: number; totalPages: number };
26-
type OverviewAPIResponse = { overview: UsageOverview | null; empty: boolean; days: number; meta?: OverviewMeta; filters?: { models: string[]; routes: string[] } };
26+
type OverviewAPIResponse = { overview: UsageOverview | null; empty: boolean; days: number; timezone?: string; meta?: OverviewMeta; filters?: { models: string[]; routes: string[] } };
2727

2828
type PriceForm = {
2929
model: string;
@@ -65,15 +65,21 @@ const numericTooltipFormatter: TooltipProps<number, string>["formatter"] = (valu
6565
return [formatNumberWithCommas(numericValue), name];
6666
};
6767

68-
function formatHourKeyFromTs(ts: number) {
69-
const parts = hourFormatter.formatToParts(new Date(ts));
68+
function formatHourKeyFromTs(ts: number, formatter: Intl.DateTimeFormat) {
69+
const parts = formatter.formatToParts(new Date(ts));
7070
const month = parts.find((p) => p.type === "month")?.value ?? "00";
7171
const day = parts.find((p) => p.type === "day")?.value ?? "00";
7272
const hour = parts.find((p) => p.type === "hour")?.value ?? "00";
7373
return `${month}-${day} ${hour}`;
7474
}
7575

76-
function buildHourlySeries(series: UsageSeriesPoint[], rangeHours?: number) {
76+
function buildHourlySeries(series: UsageSeriesPoint[], rangeHours?: number, timezone?: string) {
77+
// Use the server's bucketing timezone for gap-fill labels so they match the
78+
// labels returned for real data points. Falls back to the module-level formatter
79+
// (browser timezone) when no timezone is provided.
80+
const gapFormatter = timezone
81+
? new Intl.DateTimeFormat("en-CA", { timeZone: timezone, month: "2-digit", day: "2-digit", hour: "2-digit", hour12: false })
82+
: hourFormatter;
7783
if (!series.length) return [] as UsageSeriesPoint[];
7884

7985
const withTs = series
@@ -97,7 +103,7 @@ function buildHourlySeries(series: UsageSeriesPoint[], rangeHours?: number) {
97103
filled.push(rest);
98104
} else {
99105
filled.push({
100-
label: formatHourKeyFromTs(ts),
106+
label: formatHourKeyFromTs(ts, gapFormatter),
101107
timestamp: new Date(ts).toISOString(),
102108
requests: 0,
103109
tokens: 0,
@@ -166,6 +172,7 @@ export default function DashboardPage() {
166172
}
167173
}, []);
168174
const [overview, setOverview] = useState<UsageOverview | null>(null);
175+
const [bucketTimezone, setBucketTimezone] = useState<string | undefined>(undefined);
169176
const [overviewError, setOverviewError] = useState<string | null>(null);
170177
const [overviewEmpty, setOverviewEmpty] = useState(false);
171178
const [loadingOverview, setLoadingOverview] = useState(true);
@@ -729,6 +736,7 @@ export default function DashboardPage() {
729736
const data: OverviewAPIResponse = await res.json();
730737
if (!active) return;
731738
setOverview(data.overview ?? null);
739+
setBucketTimezone(data.timezone);
732740
setOverviewEmpty(Boolean(data.empty));
733741
setOverviewError(null);
734742
setPage(data.meta?.page ?? 1);
@@ -759,8 +767,8 @@ export default function DashboardPage() {
759767
if (!overviewData?.byHour) return [] as UsageSeriesPoint[];
760768
if (hourRange === "all") return overviewData.byHour;
761769
const hours = hourRange === "24h" ? 24 : 72;
762-
return buildHourlySeries(overviewData.byHour, hours);
763-
}, [hourRange, overviewData?.byHour]);
770+
return buildHourlySeries(overviewData.byHour, hours, bucketTimezone);
771+
}, [hourRange, overviewData?.byHour, bucketTimezone]);
764772

765773
const hourlyLineStyle = useMemo(
766774
() => buildHourlyLineStyle(hourlySeries.length, 3),

lib/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function normalizeTimezone(raw: string | undefined): string {
1818
return value;
1919
} catch {
2020
console.warn(`TIMEZONE env var "${value}" is not a valid IANA timezone. Falling back to Asia/Shanghai.`);
21-
return "UTC";
21+
return "Asia/Shanghai";
2222
}
2323
}
2424

lib/queries/overview.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function normalizePageSize(value?: number | null) {
8585
export async function getOverview(
8686
daysInput?: number,
8787
opts?: { model?: string | null; route?: string | null; page?: number | null; pageSize?: number | null; start?: string | Date | null; end?: string | Date | null; timezone?: string | null }
88-
): Promise<{ overview: UsageOverview; empty: boolean; days: number; meta: OverviewMeta; filters: { models: string[]; routes: string[] } }> {
88+
): Promise<{ overview: UsageOverview; empty: boolean; days: number; meta: OverviewMeta; filters: { models: string[]; routes: string[] }; timezone: string }> {
8989
const startDate = parseDateInput(opts?.start);
9090
const endDate = parseDateInput(opts?.end);
9191
const hasCustomRange = startDate && endDate && endDate >= startDate;
@@ -338,6 +338,7 @@ export async function getOverview(
338338
empty: totalRequests === 0,
339339
days,
340340
meta: { page, pageSize, totalModels, totalPages },
341-
filters
341+
filters,
342+
timezone: tz
342343
};
343344
}

0 commit comments

Comments
 (0)