From c3ea940c3e286223fa43352831bbf2a107a827ea Mon Sep 17 00:00:00 2001 From: Carl <74130028+Carl723000@users.noreply.github.com> Date: Thu, 2 Jul 2026 01:44:05 +0800 Subject: [PATCH] fix: validate timezone so an invalid value can't break the dashboard (#51) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A hand-typed invalid IANA zone (e.g. "EST-5 Detroit") was stored as-is and fed to Intl.DateTimeFormat, which throws "Invalid time zone specified" — taking down the whole dashboard with an Error. I18n.setTimezone now validates the zone and falls back to the system zone on anything Intl rejects. Also adds an explicit claude-sonnet-5 pricing entry (Sonnet $3/$15 tier; already covered by family inference, now explicit). Unit-tested. Co-Authored-By: Claude --- CHANGELOG.md | 13 +++++++++---- src/i18n.ts | 19 +++++++++++++++++-- src/pricing.ts | 4 ++++ src/test/i18nTimezone.test.ts | 31 +++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 src/test/i18nTimezone.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3545a085..80917ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,20 @@ All notable changes to this fork compared to upstream [`ClaudeCodeUsage/ClaudeCodeUsage`](https://github.com/ClaudeCodeUsage/ClaudeCodeUsage) (last upstream release: 1.0.8). Format follows [Keep a Changelog](https://keepachangelog.com). -## [Unreleased] +## [2.1.1] — Unreleased ### Fixed +- **Invalid timezone no longer breaks the dashboard** — a hand-typed bad IANA + zone (e.g. `EST-5 Detroit`) made `Intl` throw "Invalid time zone specified" + and the whole dashboard showed *Error*. The timezone setting is now validated; + an invalid value falls back to the system zone. (#51) - **Sonnet 5 context window** — `contextWindowFor()` only recognised the 1M window via a "4.6+" pattern (e.g. `sonnet-4-6`), so `claude-sonnet-5` — which has no `-4-` segment — fell through to the 200K legacy default. The dashboard - and status-bar context bar now correctly show a 1M window for Sonnet 5. - -## [2.1.1] — Unreleased + and status-bar context bar now correctly show a 1M window for Sonnet 5. (#50, + @UfukTanriverdi8) +- **Sonnet 5 pricing** — added an explicit `claude-sonnet-5` entry at the Sonnet + $3 / $15 tier (it already resolved via family inference; now explicit). ### Added - **Monthly cost in the status bar** — the `statusBarMetric` setting gains a diff --git a/src/i18n.ts b/src/i18n.ts index e264ab2a..bddb840a 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1891,15 +1891,30 @@ export class I18n { return this.currentLanguage; } - /** IANA timezone (e.g. "Asia/Hong_Kong"), or '' to use the system zone. */ + /** IANA timezone (e.g. "Asia/Hong_Kong"), or '' to use the system zone. + * An invalid value (e.g. a hand-typed "EST-5 Detroit") is rejected and falls + * back to the system zone: `Intl.DateTimeFormat` throws on a bad `timeZone`, + * which otherwise crashed the whole dashboard with "Invalid time zone + * specified" (#51). */ static setTimezone(tz: string): void { - this.timezone = typeof tz === 'string' ? tz.trim() : ''; + const clean = typeof tz === 'string' ? tz.trim() : ''; + this.timezone = clean && I18n.isValidTimeZone(clean) ? clean : ''; } static getTimezone(): string { return this.timezone; } + /** True if `tz` is an IANA zone Intl accepts (so date formatting won't throw). */ + static isValidTimeZone(tz: string): boolean { + try { + new Intl.DateTimeFormat('en', { timeZone: tz }); + return true; + } catch { + return false; + } + } + /** Intl date-format options merged with the configured timezone (if any). */ static dateFormatOptions(extra: Intl.DateTimeFormatOptions = {}): Intl.DateTimeFormatOptions { return this.timezone ? { ...extra, timeZone: this.timezone } : extra; diff --git a/src/pricing.ts b/src/pricing.ts index 5a2cbd62..6fcaf4f0 100644 --- a/src/pricing.ts +++ b/src/pricing.ts @@ -198,6 +198,10 @@ const MODEL_PRICING: Record = { // Claude Opus 4 (2025-05-14) 'claude-opus-4-20250514': OPUS_LEGACY, + // Claude Sonnet 5 (same $3 / $15 Sonnet tier). Family inference already maps + // any "sonnet" model to SONNET, so this is an explicit anchor for clarity. + 'claude-sonnet-5': SONNET, + // Claude Sonnet 4.6 'claude-sonnet-4-6': SONNET, diff --git a/src/test/i18nTimezone.test.ts b/src/test/i18nTimezone.test.ts new file mode 100644 index 00000000..5d6172fb --- /dev/null +++ b/src/test/i18nTimezone.test.ts @@ -0,0 +1,31 @@ +import { test } from 'node:test'; +import * as assert from 'node:assert/strict'; +import { I18n } from '../i18n'; + +test('a valid IANA zone is accepted', () => { + I18n.setTimezone('Asia/Hong_Kong'); + assert.equal(I18n.getTimezone(), 'Asia/Hong_Kong'); +}); + +test('an invalid zone falls back to system (empty), never crashes (#51)', () => { + I18n.setTimezone('EST-5 Detroit'); // what the bug reporter typed + assert.equal(I18n.getTimezone(), ''); + // dateFormatOptions must not carry the bad value into Intl. + assert.doesNotThrow(() => new Intl.DateTimeFormat('en', I18n.dateFormatOptions())); +}); + +test('empty stays empty (system zone)', () => { + I18n.setTimezone(''); + assert.equal(I18n.getTimezone(), ''); +}); + +test('whitespace-padded invalid value is trimmed and rejected', () => { + I18n.setTimezone(' Not/AZone '); + assert.equal(I18n.getTimezone(), ''); +}); + +test('isValidTimeZone distinguishes real zones from junk', () => { + assert.equal(I18n.isValidTimeZone('America/New_York'), true); + assert.equal(I18n.isValidTimeZone('UTC'), true); + assert.equal(I18n.isValidTimeZone('EST-5 Detroit'), false); +});