From d3f23994405a3e876c37567f9a5b8aa9a3b1e843 Mon Sep 17 00:00:00 2001 From: bowenliang123 Date: Fri, 9 Oct 2026 15:06:22 +0800 Subject: [PATCH] feat(overview): scope the first range option to today, not the last 24h MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Insights page's first range option read "Last 24 hours" while the ledger it scopes is day-grained, so the rolling window and the day floor underneath it disagreed: past midnight the 24h window still admitted yesterday's ledger day whole, and before 1am it admitted only part of it. "Today" is the local calendar day from midnight — the window the day- granular heatmap beside it already draws. `rangeStartOf` opens the window on the local midnight by date-field arithmetic (the harness's own `setHours(0, 0, 0, 0)` idiom, as in `startOfLocalDay`), which stays exact across DST-short and DST-long days where an epoch-ms subtract drifts into the neighbouring day. The harness helper itself is only reachable through a deep `@deepseek-ai/dsh-client-ui-chat/src/*` path — a package this plugin does not depend on — so the idiom is mirrored at its one call site instead of taking the dependency. Both locales carry the shorter 今天 / Today; the deprecated `ov.range.24h` key is gone rather than aliased. The specs that anchored time on raw epoch offsets silently assumed UTC (`11 * 86_400_000`), and the panel fixture at `NOW - 1h` would have failed whenever the suite runs in the first hour after midnight; every boundary is now anchored on a local `Date`, so the day floor is proved identically in every timezone. --- src/client/components/overviewPanel.tsx | 2 +- src/client/i18n.ts | 4 +-- src/client/overview.ts | 20 ++++++++--- tests/client/components/overviewPanel.spec.ts | 8 +++-- tests/client/overview.spec.ts | 36 +++++++++++-------- 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/client/components/overviewPanel.tsx b/src/client/components/overviewPanel.tsx index 64ec996..b3d37e7 100644 --- a/src/client/components/overviewPanel.tsx +++ b/src/client/components/overviewPanel.tsx @@ -75,7 +75,7 @@ export interface OverviewPanelProps { useWorkspaces?: unknown } -const RANGES: readonly OverviewRange[] = ['24h', '7d', '30d', 'all'] +const RANGES: readonly OverviewRange[] = ['today', '7d', '30d', 'all'] const SORTS: readonly OverviewSort[] = ['recent', 'tokens', 'context'] /** The heatmap's depth metrics, in toggle order (steps is the default). */ const METRICS: readonly HeatMetric[] = ['sessions', 'steps'] diff --git a/src/client/i18n.ts b/src/client/i18n.ts index fa32ee1..2f36ad4 100644 --- a/src/client/i18n.ts +++ b/src/client/i18n.ts @@ -350,7 +350,7 @@ export const DICT_ZH: Record = { 'ov.title': '上下文洞察', 'ov.unavailable': '会话列表暂不可用', 'ov.range.label': '统计范围', - 'ov.range.24h': '近 24 小时', + 'ov.range.today': '今天', 'ov.range.7d': '近 7 天', 'ov.range.30d': '近 30 天', 'ov.range.all': '全部', @@ -789,7 +789,7 @@ export const DICT_EN: Record = { 'ov.title': 'Context Insights', 'ov.unavailable': 'The session list is unavailable', 'ov.range.label': 'Range', - 'ov.range.24h': 'Last 24 hours', + 'ov.range.today': 'Today', 'ov.range.7d': 'Last 7 days', 'ov.range.30d': 'Last 30 days', 'ov.range.all': 'All', diff --git a/src/client/overview.ts b/src/client/overview.ts index 9f391e3..8577d58 100644 --- a/src/client/overview.ts +++ b/src/client/overview.ts @@ -328,11 +328,20 @@ function mergeSkillTallies(a: ActivityDay['skills'], b: NonNullable boolean { if (opts.day !== null) return key => key === opts.day diff --git a/tests/client/components/overviewPanel.spec.ts b/tests/client/components/overviewPanel.spec.ts index d677811..a41f6ff 100644 --- a/tests/client/components/overviewPanel.spec.ts +++ b/tests/client/components/overviewPanel.spec.ts @@ -20,6 +20,8 @@ const PROVIDERS = { const NOW = Date.now() const TODAY = dayKeyOf(NOW) ?? '' const YESTERDAY = dayKeyOf(NOW - 86_400_000) ?? '' +/** Local midnight of NOW — the "today" range's floor, so today's session stays inside it whatever hour the spec runs. */ +const MIDNIGHT = new Date(new Date(NOW).setHours(0, 0, 0, 0)).getTime() /** A wire-shaped timeline head. */ function timeline(over: Record = {}): Record { @@ -47,7 +49,7 @@ function sessionsSnapshot(): { ids: string[]; byId: Record { await click(rangeButtons[3]) // All assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 3, 'the stale session joins') assert.equal(queryAll(m.container, '.lc-stat-value')[0].textContent, '3') - await click(rangeButtons[0]) // Last 24 hours - assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 1, 'only the hour-fresh session stays') + await click(rangeButtons[0]) // Today + assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 1, "only today's session stays") await click(rangeButtons[1]) // Last 7 days assert.equal(queryAll(m.container, '.lc-ov-grid > .lc-ov-session').length, 1, 'only the freshest stays') await click(rangeButtons[2]) // back to 30d diff --git a/tests/client/overview.spec.ts b/tests/client/overview.spec.ts index 97cad14..24e8588 100644 --- a/tests/client/overview.spec.ts +++ b/tests/client/overview.spec.ts @@ -195,8 +195,9 @@ describe('billedOf / turnsOf', () => { describe('rangeStartOf', () => { test('each window’s start instant; all is unbounded', () => { - const now = 31 * 86_400_000 - assert.equal(rangeStartOf('24h', now), now - 86_400_000) + // Local anchors, so the calendar-day window reads the same in every zone. + const now = new Date(2026, 8, 20, 12).getTime() + assert.equal(rangeStartOf('today', now), new Date(2026, 8, 20).getTime(), 'today opens on the local midnight') assert.equal(rangeStartOf('7d', now), now - 7 * 86_400_000) assert.equal(rangeStartOf('30d', now), now - 30 * 86_400_000) assert.equal(rangeStartOf('all', now), null) @@ -204,28 +205,30 @@ describe('rangeStartOf', () => { }) describe('filterRows', () => { + const midnight = new Date(2026, 8, 11).getTime() + const now = midnight + 12 * 3_600_000 const rows = [ - rowOf({ id: 'old', title: 'ancient logs', updatedAt: 100 }), - rowOf({ id: 'new', title: 'fresh fix', cwd: '/repo/app', updatedAt: 10 * 86_400_000 }), + rowOf({ id: 'old', title: 'ancient logs', updatedAt: midnight - 20 * 86_400_000 }), + rowOf({ id: 'new', title: 'fresh fix', cwd: '/repo/app', updatedAt: now - 2 * 3_600_000 }), rowOf({ id: 'active', title: 'busy bee', - updatedAt: 10 * 86_400_000, + updatedAt: now - 2 * 3_600_000, activity: { days: { '2026-09-16': { tokens: 5, requests: 1 }, '2026-09-10': { tokens: 0, requests: 0 } } }, }), ] - const now = 11 * 86_400_000 test('the range window filters by last activity', () => { - assert.deepEqual(filterRows(rows, { range: '24h', day: null, query: '' }, now).map(r => r.id), ['new', 'active']) + assert.deepEqual(filterRows(rows, { range: 'today', day: null, query: '' }, now).map(r => r.id), ['new', 'active']) assert.deepEqual(filterRows(rows, { range: '7d', day: null, query: '' }, now).map(r => r.id), ['new', 'active']) assert.deepEqual(filterRows(rows, { range: 'all', day: null, query: '' }, now).map(r => r.id), ['old', 'new', 'active']) - // The 24h window's own boundary: inside by minutes, outside by an hour. - const hRows = [ - rowOf({ id: 'fresh', title: 'just now', updatedAt: now - 2 * 3_600_000 }), - rowOf({ id: 'yesterday', title: 'a day ago', updatedAt: now - 25 * 3_600_000 }), + // Today's own boundary: hours in, one minute before the local midnight out. + const dRows = [ + rowOf({ id: 'in', title: 'this morning', updatedAt: now - 2 * 3_600_000 }), + rowOf({ id: 'out', title: 'last night', updatedAt: midnight - 60_000 }), ] - assert.deepEqual(filterRows(hRows, { range: '24h', day: null, query: '' }, now).map(r => r.id), ['fresh']) + assert.deepEqual(filterRows(dRows, { range: 'today', day: null, query: '' }, now).map(r => r.id), ['in']) + assert.deepEqual(filterRows(dRows, { range: '7d', day: null, query: '' }, now).map(r => r.id), ['in', 'out'], 'last night is still inside the week') }) test('the day pin keeps only sessions contributing to that day', () => { @@ -744,10 +747,13 @@ describe('skillLoadsOf', () => { [day(1)]: { tokens: 1, requests: 1, skills: { tdd: { n: 3, last: NOW - 2000 }, grill: { n: 1, last: NOW - 500 } } }, }), ] - assert.deepEqual(skillLoadsOf(rows, { range: '24h', day: null }, NOW), [ + assert.deepEqual(skillLoadsOf(rows, { range: '7d', day: null }, NOW), [ { name: 'tdd', loads: 5, sessions: 2, last: NOW - 1000 }, { name: 'grill', loads: 1, sessions: 1, last: NOW - 500 }, ], 'the 10-days-ago entries fall below the floor') + assert.deepEqual(skillLoadsOf(rows, { range: 'today', day: null }, NOW), [ + { name: 'tdd', loads: 2, sessions: 1, last: NOW - 1000 }, + ], "today's floor admits this very day only") assert.deepEqual(skillLoadsOf(rows, { range: 'all', day: null }, NOW), [ { name: 'tdd', loads: 10, sessions: 2, last: NOW - 1000 }, { name: 'grill', loads: 1, sessions: 1, last: NOW - 500 }, @@ -845,8 +851,8 @@ describe('rowLoadedSkill', () => { }, }, }) - assert.equal(rowLoadedSkill(row, 'tdd', { range: '24h', day: null }, NOW), true, 'loaded today — inside the 24h floor') - assert.equal(rowLoadedSkill(row, 'grill', { range: '24h', day: null }, NOW), false, 'the 10-days-ago load falls below the floor') + assert.equal(rowLoadedSkill(row, 'tdd', { range: 'today', day: null }, NOW), true, "loaded today — inside today's floor") + assert.equal(rowLoadedSkill(row, 'grill', { range: 'today', day: null }, NOW), false, 'the 10-days-ago load falls below the floor') assert.equal(rowLoadedSkill(row, 'grill', { range: 'all', day: null }, NOW), true) assert.equal(rowLoadedSkill(row, 'tdd', { range: 'all', day: day(1) }, NOW), false, 'the pinned day admits only itself') assert.equal(rowLoadedSkill(row, 'grill', { range: 'all', day: day(10) }, NOW), true)