From 9d8cb7b2b5fba0a76a4b40f43382297ef5150833 Mon Sep 17 00:00:00 2001 From: YuboZhang Date: Wed, 8 Jul 2026 20:23:09 +0800 Subject: [PATCH 1/2] fix: stop treating daily dates on the 1st as month-only labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The heuristic dateString.endsWith('-01') was used in both formatDate and getShortDate to decide whether a date is a monthly aggregate label (always YYYY-MM-01) or an actual daily date. This breaks when a daily data point falls on the 1st of the month — it gets displayed as just the month name instead of the specific date. Replace the heuristic with an explicit 'monthly' parameter so callers that render monthly aggregates pass true and daily-data callers pass false (the default). Co-Authored-By: Claude --- src/webview.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/webview.ts b/src/webview.ts index d0531a5b..4a3fee86 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -1416,7 +1416,7 @@ export class UsageWebviewProvider { ${this.renderCompositionChart( [...this.dailyDataForAllTime] .sort((a, b) => a.date.localeCompare(b.date)) - .map((d) => ({ label: this.getShortDate(d.date), data: d.data, key: d.date })) + .map((d) => ({ label: this.getShortDate(d.date, true), data: d.data, key: d.date })) )}
@@ -1439,7 +1439,7 @@ export class UsageWebviewProvider { .map( ({ date, data }) => ` - ${this.formatDate(date)} + ${this.formatDate(date, true)} ${I18n.formatCurrency(data.totalCost)} ${I18n.formatNumber(data.totalInputTokens)} ${I18n.formatNumber(data.totalOutputTokens)} @@ -3228,7 +3228,7 @@ export class UsageWebviewProvider { private renderAllTimeChart(): string { const sortedData = [...this.dailyDataForAllTime].sort((a, b) => a.date.localeCompare(b.date)); - return this.renderMainCostChart(sortedData); + return this.renderMainCostChart(sortedData, true); } /** @@ -3266,7 +3266,7 @@ export class UsageWebviewProvider { ); } - private renderMainCostChart(sortedData: { date: string; data: UsageData }[]): string { + private renderMainCostChart(sortedData: { date: string; data: UsageData }[], monthly = false): string { if (sortedData.length === 0) { return '
No data available
'; } @@ -3293,7 +3293,7 @@ export class UsageWebviewProvider { 'data-cost-output="' + cb.output + '" ' + 'data-cost-cachewrite="' + cb.cacheWrite + '" ' + 'data-cost-cacheread="' + cb.cacheRead + '" ' + - 'title="' + this.escapeHtml(this.formatDate(date)) + ': ' + I18n.formatCurrency(data.totalCost) + '">' + + 'title="' + this.escapeHtml(this.formatDate(date, monthly)) + ': ' + I18n.formatCurrency(data.totalCost) + '">' + costStack(data, barHeight) + '
' + '' @@ -3302,7 +3302,7 @@ export class UsageWebviewProvider { .join(''); const xlabels = sortedData - .map(({ date }) => '
' + this.getShortDate(date) + '
') + .map(({ date }) => '
' + this.getShortDate(date, monthly) + '
') .join(''); return ( @@ -3387,21 +3387,21 @@ export class UsageWebviewProvider { ); } - private getShortDate(dateString: string): string { + private getShortDate(dateString: string, monthly = false): string { // Parse 'YYYY-MM-DD' textually — new Date('YYYY-MM-DD') is UTC midnight, // which shifts the displayed day back by one in negative-UTC timezones. const [y, m, d] = dateString.split('-').map(Number); - // Month-only dates (first of month) label as YYYY/MM for monthly charts. - if (dateString.endsWith('-01')) { + // Month-only dates label as YYYY/MM for monthly charts. + if (monthly) { return `${y}/${String(m).padStart(2, '0')}`; } return `${m}/${d}`; } - private formatDate(dateString: string): string { + private formatDate(dateString: string, monthly = false): string { const date = new Date(dateString); - // Check if this is a month-only date (ends with -01) - if (dateString.endsWith('-01')) { + // Month-only dates: show month/year for monthly aggregate data. + if (monthly) { return date.toLocaleDateString(I18n.getLocale(), I18n.dateFormatOptions({ year: 'numeric', month: 'long' })); } // Standard date formatting for daily data, locale + timezone aware. From 735ab1097b6d6bcc7d36b0193d14f928d2cbbb34 Mon Sep 17 00:00:00 2001 From: YuboZhang Date: Wed, 8 Jul 2026 20:48:54 +0800 Subject: [PATCH 2/2] fix: monthly date labels show previous month in negative-UTC timezones formatDate uses new Date('YYYY-MM-DD') which interprets the string as UTC midnight. For users in timezones west of UTC (Americas, Europe), this rolls the date back by a day, causing monthly labels in the All tab to show the previous month (e.g. 'May 2026' for June 2026 data). Parse textually and build a Date via Date.UTC with an explicit timeZone: 'UTC' in toLocaleDateString instead. The date keys from dayKeyInZone / monthKeyInZone are already timezone-aware, so UTC formatting preserves the correct display. Co-Authored-By: Claude --- src/webview.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/webview.ts b/src/webview.ts index 4a3fee86..9ac7b465 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -3399,13 +3399,15 @@ export class UsageWebviewProvider { } private formatDate(dateString: string, monthly = false): string { - const date = new Date(dateString); - // Month-only dates: show month/year for monthly aggregate data. + // Parse textually to avoid UTC-midnight timezone shift (new Date('YYYY-MM-DD') + // is UTC midnight, which rolls back a day in negative-UTC timezones). The + // date keys are already timezone-correct from dayKeyInZone / monthKeyInZone. + const [y, m, d] = dateString.split('-').map(Number); + const date = new Date(Date.UTC(y, m - 1, d)); if (monthly) { - return date.toLocaleDateString(I18n.getLocale(), I18n.dateFormatOptions({ year: 'numeric', month: 'long' })); + return date.toLocaleDateString(I18n.getLocale(), { year: 'numeric', month: 'long', timeZone: 'UTC' }); } - // Standard date formatting for daily data, locale + timezone aware. - return date.toLocaleDateString(I18n.getLocale(), I18n.dateFormatOptions()); + return date.toLocaleDateString(I18n.getLocale(), { ...I18n.dateFormatOptions(), timeZone: 'UTC' }); } private getStyles(): string {