mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-31 01:35:16 +08:00
fix(ui): preserve local usage bucket dates
Daily usage buckets represent local calendar dates, but standard date parsing interprets their ISO-like form as UTC and shifts them in non-UTC time zones. Parse date-only buckets directly in local time so heatmap totals stay on the reported day.
This commit is contained in:
@@ -42,7 +42,7 @@ export function buildTokenActivity(series: UsageSeriesPoint[], options: TokenAct
|
||||
let observedEnd: Date | undefined;
|
||||
|
||||
for (const point of series) {
|
||||
const date = startOfLocalDay(new Date(point.bucket));
|
||||
const date = startOfLocalDay(parseActivityDate(point.bucket));
|
||||
if (!isFiniteDate(date)) {
|
||||
continue;
|
||||
}
|
||||
@@ -111,6 +111,22 @@ export function buildTokenActivity(series: UsageSeriesPoint[], options: TokenAct
|
||||
};
|
||||
}
|
||||
|
||||
function parseActivityDate(bucket: string): Date {
|
||||
const dateOnly = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(bucket.trim());
|
||||
if (!dateOnly) {
|
||||
return new Date(bucket);
|
||||
}
|
||||
|
||||
const year = Number(dateOnly[1]);
|
||||
const month = Number(dateOnly[2]);
|
||||
const day = Number(dateOnly[3]);
|
||||
const date = new Date(year, month - 1, day);
|
||||
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
||||
return new Date(Number.NaN);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
export function activityDateKey(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
|
||||
@@ -3,32 +3,48 @@ import test from "node:test";
|
||||
import { buildTokenActivity, activityDateKey } from "@ccr/ui/lib/usage-activity.ts";
|
||||
|
||||
test("buildTokenActivity summarizes observed token days and streaks", () => {
|
||||
const summary = buildTokenActivity(
|
||||
[
|
||||
{ bucket: "2026-06-02", totalTokens: 10 },
|
||||
{ bucket: "2026-06-03", totalTokens: 30 },
|
||||
{ bucket: "2026-06-04", totalTokens: 60 },
|
||||
{ bucket: "2026-06-05", totalTokens: -20 },
|
||||
{ bucket: "not-a-date", totalTokens: 999 }
|
||||
],
|
||||
{ minWeeks: 2 }
|
||||
);
|
||||
withTimezone("America/New_York", () => {
|
||||
const summary = buildTokenActivity(
|
||||
[
|
||||
{ bucket: "2026-06-02", totalTokens: 10 },
|
||||
{ bucket: "2026-06-03", totalTokens: 30 },
|
||||
{ bucket: "2026-06-04", totalTokens: 60 },
|
||||
{ bucket: "2026-06-05", totalTokens: -20 },
|
||||
{ bucket: "not-a-date", totalTokens: 999 }
|
||||
],
|
||||
{ minWeeks: 2 }
|
||||
);
|
||||
|
||||
assert.equal(summary.totalTokens, 100);
|
||||
assert.equal(summary.activeDays, 3);
|
||||
assert.equal(summary.dayCount, 4);
|
||||
assert.equal(summary.longestStreak, 3);
|
||||
assert.equal(summary.maxTokens, 60);
|
||||
assert.equal(summary.weekCount, 2);
|
||||
assert.equal(summary.cells.length, 14);
|
||||
assert.equal(summary.totalTokens, 100);
|
||||
assert.equal(summary.activeDays, 3);
|
||||
assert.equal(summary.dayCount, 4);
|
||||
assert.equal(summary.longestStreak, 3);
|
||||
assert.equal(summary.maxTokens, 60);
|
||||
assert.equal(summary.weekCount, 2);
|
||||
assert.equal(summary.cells.length, 14);
|
||||
|
||||
const cellsByDate = new Map(summary.cells.map((cell) => [cell.dateKey, cell]));
|
||||
assert.equal(cellsByDate.get("2026-06-02")?.intensity, 1);
|
||||
assert.equal(cellsByDate.get("2026-06-03")?.intensity, 3);
|
||||
assert.equal(cellsByDate.get("2026-06-04")?.intensity, 4);
|
||||
assert.equal(cellsByDate.get("2026-06-05")?.intensity, 0);
|
||||
const cellsByDate = new Map(summary.cells.map((cell) => [cell.dateKey, cell]));
|
||||
assert.equal(cellsByDate.get("2026-06-02")?.intensity, 1);
|
||||
assert.equal(cellsByDate.get("2026-06-03")?.intensity, 3);
|
||||
assert.equal(cellsByDate.get("2026-06-04")?.intensity, 4);
|
||||
assert.equal(cellsByDate.get("2026-06-05")?.intensity, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test("activityDateKey formats local calendar dates", () => {
|
||||
assert.equal(activityDateKey(new Date(2026, 0, 5, 14, 30)), "2026-01-05");
|
||||
});
|
||||
|
||||
function withTimezone(timezone, run) {
|
||||
const previousTimezone = process.env.TZ;
|
||||
process.env.TZ = timezone;
|
||||
try {
|
||||
run();
|
||||
} finally {
|
||||
if (previousTimezone === undefined) {
|
||||
delete process.env.TZ;
|
||||
} else {
|
||||
process.env.TZ = previousTimezone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user