diff --git a/src/webview.ts b/src/webview.ts index d0531a5b..9ac7b465 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,25 +3387,27 @@ 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 { - const date = new Date(dateString); - // Check if this is a month-only date (ends with -01) - if (dateString.endsWith('-01')) { - return date.toLocaleDateString(I18n.getLocale(), I18n.dateFormatOptions({ year: 'numeric', month: 'long' })); + private formatDate(dateString: string, monthly = false): string { + // 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(), { 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 {