Skip to content

feat(web): time range filter dropdown with URL state#7

Draft
nengqi wants to merge 2 commits into
xingkaixin:mainfrom
nengqi:feat/web-time-range-filter
Draft

feat(web): time range filter dropdown with URL state#7
nengqi wants to merge 2 commits into
xingkaixin:mainfrom
nengqi:feat/web-time-range-filter

Conversation

@nengqi

@nengqi nengqi commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a dropdown in the toolbar to switch the active time window (7d / 30d / 90d / all, plus a custom from→to picker) without restarting the CLI. The selection is encoded in the URL so it survives reload and is shareable; existing --days/--from/--to CLI flags still seed the initial value.

Changes

Frontend (apps/web)

  • useTimeRange hook: parses ?range=7d|30d|90d|all or ?from=YYYY-MM-DD[&to=...] from URL search params, falls back to CLI flags from /api/config, exposes a setRange that writes back to the URL.
  • TimeRangeMenu component: native dropdown with preset radio items + custom <input type=\"date\"> from/to fields and Apply / Reset actions. Closes on outside click / Esc.
  • App.tsx: replaces the read-only "Last 7d · …" chip in the header with the new menu. Splits the mount-time fetch (config + bookmarks) from a range-driven effect that refetches /api/agents, /api/sessions, /api/dashboard whenever the active range changes. Live-update sync and search refetches now also carry the range.
  • lib/api.ts: fetchAgents/fetchSessions/fetchDashboard/fetchSearchResults accept an optional TimeRange and serialize it consistently.

Backend (packages/cli)

  • New resolveListWindow helper centralizes the "explicit query > CLI default" precedence and adds ?days=N support (and ?days=0 = all-time) to the agents/sessions/search handlers, matching the dashboard handler's existing behavior. Existing ?from=&to= semantics are unchanged.
  • Two new handler tests cover days=N filtering and days=0 overriding a configured CLI default.

Test plan

  • pnpm --filter codesesh test — 24/24 vitest pass (incl. 2 new cases).
  • pnpm lint — 0 warnings across all packages.
  • pnpm --filter codesesh build — CLI bundle + web dist OK.
  • CDP-driven smoke (against the running daemon at localhost:4321):
    • Default page → toolbar shows Last 7d ·CLI (CLI fallback).
    • Click menu → pick "Last 30 days" → URL becomes ?range=30d, label switches to Last 30d.
    • Direct nav ?range=all → label = All time.
    • Direct nav ?from=2026-01-01&to=2026-02-01 → label shows the range.
    • Sidebar agent counts respond to the active range (cursor: 130 → 0 between all-time and 7d).
  • curl /api/sessions?days={3,30,0} → 362 / 1008 / 1322 sessions; curl /api/agents?days=0 shows full counts; ?days=3 filters cursor to 0.

🤖 Generated with Claude Code

@nengqi

nengqi commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

@xingkaixin 这个想先问下方向再决定要不要 rebase——fc59d37 feat: add performance benchmarking script and scan window filteringLiveScanStore + 各 agent adapter 上加了后端 from/to 的 scan window,跟我这 PR 的前端时间过滤 dropdown 方向上重叠。

几种都能干:

  1. 改一下让前端消费你新加的后端 from/to API,丢掉客户端内存过滤
  2. 你已经有别的 UX 想法直接 close
  3. 其它

主要不想盲目 rebase 642 行结果方向不对。

@xingkaixin

Copy link
Copy Markdown
Owner

@xingkaixin 这个想先问下方向再决定要不要 rebase——fc59d37 feat: add performance benchmarking script and scan window filteringLiveScanStore + 各 agent adapter 上加了后端 from/to 的 scan window,跟我这 PR 的前端时间过滤 dropdown 方向上重叠。

几种都能干:

  1. 改一下让前端消费你新加的后端 from/to API,丢掉客户端内存过滤
  2. 你已经有别的 UX 想法直接 close
  3. 其它

主要不想盲目 rebase 642 行结果方向不对。

可以的,这个我一直想,但没有去做,感谢 PR。

Per maintainer feedback on PR xingkaixin#7 (option 1): consume the backend `from`/
`to` scan-window plumbing introduced in fc59d37 instead of doing
client-side in-memory filtering. The previous PR also re-implemented
`filterSessionsByActivityWindow` in handlers.ts; that has been dropped
since upstream main carries an identical helper now.

What this adds:
- `useTimeRange` hook (apps/web/src/lib/useTimeRange.ts): URL-search-
  param-backed time range with CLI flag fallback. Encoding:
    ?range=7d|30d|90d|all|yesterday   preset / all-time / yesterday
    ?from=YYYY-MM-DD[&to=YYYY-MM-DD]  custom window
  Falls back to `rangeFromAppConfig(appConfig)` (whatever the CLI was
  launched with) when neither URL param is present.
- `TimeRangeMenu` component: header dropdown with presets (Yesterday,
  Last 1d/3d/7d/14d/30d/90d, All time) plus a custom from/to date pair.
- `windowFromTimeRange` helper in api.ts: maps a UI-side TimeRange union
  back into the existing `{from?, to?, days?}` window shape that the
  backend / fetch helpers already accept. Closed-inclusive semantics:
  yesterday and custom-with-`to` round `to` to end-of-day (-1ms).
- Extended `fetchAgents` and `fetchSearchResults` to also accept the
  window so the dropdown drives every list/dashboard/search refetch
  consistently. fetchSessions / fetchDashboard already accepted it.
- App.tsx: split the original mount-time effect into a config+bookmarks
  load and a window-driven reload effect that re-runs whenever the
  active range changes. syncLiveUpdate, search-results effect, and
  every fetch call site now use `activeWindow = windowFromTimeRange(
  timeRange)` as the single source of truth. Dropdown rendered next to
  the header subtitle, hidden in session-detail mode.

Tests:
- 7 unit tests for windowFromTimeRange covering null/undefined, all,
  preset days, yesterday with pinned system time, custom from-only,
  custom from+to closed-inclusive same-day, malformed dates degrading
  safely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nengqi nengqi force-pushed the feat/web-time-range-filter branch from f2fe344 to b244750 Compare May 7, 2026 13:10
…eview)

Codex flagged 6 issues, all conf >= 75. Fixes:

P1 (api.ts windowFromTimeRange) — preset { days } only returned `{ days }`,
but /api/sessions, /api/search and /api/agents do not parse `days`. Result:
selecting "Last 7d" narrowed only the dashboard while sessions, search,
and agent counts still showed everything. Now returns days AND a concrete
from/to derived from local-day arithmetic so every endpoint filters
consistently.

P1 (windowFromTimeRange) — `all` returned `{}`, which let CLI defaults
re-assert themselves on /sessions and the 30-day default fire on
/dashboard. The "All time" label thus contradicted the data. Now emits
explicit `from=0 (epoch), to=now` so the wide window overrides any
backend default.

P1 (handlers.ts handleGetAgents) — only read CLI defaults, not query.
Dropdown moved sessions list but agent counts stayed pinned to CLI
window. Now parses ?from/?to with defaults as fallback (one-line
parity with handleGetSessions / handleGetSearch).

P2 (windowFromTimeRange yesterday) — used `from + 86400000 - 1` for `to`,
which on DST transition days lands on the wrong calendar boundary
(spring-forward day overshoots into next day, fall-back day stops at 23:00).
Now uses setDate(+1) + setMilliseconds(-1) — DST-safe and correct on
month/year boundaries. Added a regression test pinning system time to
2026-03-09 (one day after US spring DST start).

P2 (useTimeRange.isValidIsoDate) — accepted "2026-02-31" because
new Date("2026-02-31") silently normalizes to 2026-03-03 and getTime()
is finite. URL ?from=2026-02-31 would query the wrong day. Added
round-trip check: parsed components must match the input.

P2 (TimeRangeMenu) — popover marked role="menu" but contains a custom
date-range form with inputs. ARIA menu semantics expect menuitem-style
descendants. Switched to role="dialog" with aria-label; preset list
becomes a role="radiogroup" with role="radio" buttons.

Tests: expanded windowFromTimeRange suite to 12 cases (preset days+from/to,
all=epoch+now, DST-safe yesterday, custom inclusive end-of-day, malformed
degrade) + 4 isValidIsoDate cases (round-trip, format, calendar-impossible).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nengqi

nengqi commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

@xingkaixin 按你说的方案 1 重写了 (f2fe344...50acc29 force push)。

整个 PR 现在 scope 缩成纯前端 dropdown 消费后端 from/to API。Drop 了原 PR 里 handlers.ts 的 filterSessionsByActivityWindow 重复实现 (跟 fc59d37 完全重叠),只保留:

  • apps/web/src/lib/useTimeRange.ts:URL 状态化的 TimeRange (?range=7d|all|yesterday / ?from=YYYY-MM-DD&to=...),fallback 到 rangeFromAppConfig(config) (CLI 启动时的 window)
  • apps/web/src/components/TimeRangeMenu.tsx:header dropdown (Yesterday / 1d/3d/7d/14d/30d/90d / All time / Custom from-to)
  • apps/web/src/lib/api.ts:windowFromTimeRange(range) 把 TimeRange 转成 {from?,to?,days?} 喂现有 fetch helpers;扩 fetchAgentsfetchSearchResults 也接 window
  • apps/web/src/App.tsx:把 mount-time effect 拆成 config+bookmarks load 和 window-driven reload,搜索/syncLiveUpdate 都跟 activeWindow 走,header 渲染 TimeRangeMenu (session detail mode 隐藏)

跑 codex review 发现 6 个 conf >=75 issue,都 fix 了:

  • preset { days } 之前只返 days,但 /sessions /search /agents handler 不解析 days,只解析 from/to → "Last 7d" 只对 dashboard 生效,sessions 列表显示全部。改成 days + from + to 都返,让所有 endpoint 一致
  • all 之前返 {},但 dashboard 有 30 天 default,sessions 有 CLI default,文案是 "All time" 但数据不是。改成显式 from=0, to=now
  • handleGetAgents 之前只读 CLI defaults 不读 query → dropdown 选什么 agent counts 永远不变。在 handlers.ts 加 6 行 (parseDateParam 跟 sessions/search 一致)。这是 PR 里唯一的 backend touch,如果你不想合可以 drop,代价是 agent counts 跟 dropdown 脱钩
  • yesterday to = from + 86400000 - 1 在 DST 切换日错位 (春令时溢到次日 00:59,秋令时停在 22:59),改成 setDate(+1) + setMilliseconds(-1),加 DST regression test
  • isValidIsoDate("2026-02-31") 之前 false-pass (JS Date 静默 normalize 成 3/3),加 round-trip 校验
  • TimeRangeMenu 之前 role="menu" 但里面有 form 控件,改成 role="dialog" + 内部 preset 用 role="radiogroup"

测试:windowFromTimeRange 12 case (preset/all/DST-safe yesterday/custom inclusive/malformed degrade) + isValidIsoDate 4 case (format/round-trip/leap year/impossible date),pnpm --filter @codesesh/web test 全过 (19/19),pnpm lint 干净。

CDP 验证暂没跑 (我这边 server 还跑着旧 dist),你方便起 dev 验证下:

  • 选 "Last 7d" → dashboard / sessions list / agent counts 都应该收到 7 天数据
  • 选 "All time" → 三个都看全量
  • 选 yesterday → 都只显示昨天的
  • 自定义 from/to → 闭区间含起止两天
  • URL 改成 ?from=2026-02-31 → 不应该接受 (跳到 3/3 那种 bug 已修)

PR 缩到 +152/-53,handlers.ts 只动了 6 行 (handleGetAgents query parse 对齐 sessions/search),前端是核心。

@xingkaixin

Copy link
Copy Markdown
Owner

@xingkaixin 按你说的方案 1 重写了 (f2fe344...50acc29 force push)。

整个 PR 现在 scope 缩成纯前端 dropdown 消费后端 from/to API。Drop 了原 PR 里 handlers.ts 的 filterSessionsByActivityWindow 重复实现 (跟 fc59d37 完全重叠),只保留:

  • apps/web/src/lib/useTimeRange.ts:URL 状态化的 TimeRange (?range=7d|all|yesterday / ?from=YYYY-MM-DD&to=...),fallback 到 rangeFromAppConfig(config) (CLI 启动时的 window)
  • apps/web/src/components/TimeRangeMenu.tsx:header dropdown (Yesterday / 1d/3d/7d/14d/30d/90d / All time / Custom from-to)
  • apps/web/src/lib/api.ts:windowFromTimeRange(range) 把 TimeRange 转成 {from?,to?,days?} 喂现有 fetch helpers;扩 fetchAgentsfetchSearchResults 也接 window
  • apps/web/src/App.tsx:把 mount-time effect 拆成 config+bookmarks load 和 window-driven reload,搜索/syncLiveUpdate 都跟 activeWindow 走,header 渲染 TimeRangeMenu (session detail mode 隐藏)

跑 codex review 发现 6 个 conf >=75 issue,都 fix 了:

  • preset { days } 之前只返 days,但 /sessions /search /agents handler 不解析 days,只解析 from/to → "Last 7d" 只对 dashboard 生效,sessions 列表显示全部。改成 days + from + to 都返,让所有 endpoint 一致
  • all 之前返 {},但 dashboard 有 30 天 default,sessions 有 CLI default,文案是 "All time" 但数据不是。改成显式 from=0, to=now
  • handleGetAgents 之前只读 CLI defaults 不读 query → dropdown 选什么 agent counts 永远不变。在 handlers.ts 加 6 行 (parseDateParam 跟 sessions/search 一致)。这是 PR 里唯一的 backend touch,如果你不想合可以 drop,代价是 agent counts 跟 dropdown 脱钩
  • yesterday to = from + 86400000 - 1 在 DST 切换日错位 (春令时溢到次日 00:59,秋令时停在 22:59),改成 setDate(+1) + setMilliseconds(-1),加 DST regression test
  • isValidIsoDate("2026-02-31") 之前 false-pass (JS Date 静默 normalize 成 3/3),加 round-trip 校验
  • TimeRangeMenu 之前 role="menu" 但里面有 form 控件,改成 role="dialog" + 内部 preset 用 role="radiogroup"

测试:windowFromTimeRange 12 case (preset/all/DST-safe yesterday/custom inclusive/malformed degrade) + isValidIsoDate 4 case (format/round-trip/leap year/impossible date),pnpm --filter @codesesh/web test 全过 (19/19),pnpm lint 干净。

CDP 验证暂没跑 (我这边 server 还跑着旧 dist),你方便起 dev 验证下:

  • 选 "Last 7d" → dashboard / sessions list / agent counts 都应该收到 7 天数据
  • 选 "All time" → 三个都看全量
  • 选 yesterday → 都只显示昨天的
  • 自定义 from/to → 闭区间含起止两天
  • URL 改成 ?from=2026-02-31 → 不应该接受 (跳到 3/3 那种 bug 已修)

PR 缩到 +152/-53,handlers.ts 只动了 6 行 (handleGetAgents query parse 对齐 sessions/search),前端是核心。

我这边测下来,当前实现还差两个关键点。

第一,TimeRangeMenu 更适合放在最顶层 app header,和 search / version / CLI window chip 同一层。这个筛选语义是全局展示窗口,会影响 dashboard、agent counts、session list、search results。放在 Dashboard/内容 header 里,切到 agent 或 session 时语义会变局部,使用上会觉得筛选状态消失。

第二,当前后端 query from/to 的实际能力是过滤 LiveScanStore.getSnapshot()。这个 snapshot 在 CLI 启动时已经按 startupScanOptions 裁过。于是前端从 14d 切到 30d 时,只能在 14d 的内存集合里继续过滤,拿不到 15-30d 的 session。

我建议按这个方向改:

  1. UI 层
  • 把 TimeRangeMenu 移到顶层 header。
  • range 作为 app-level state,同步到 URL。
  • 所有内部 Link/nav/search result/bookmark 跳转都保留当前 location.search,避免从 //claudecode 时丢掉 ?range=14d
  • session detail 可以继续按 id 打开,但周边 sidebar/count/search/dashboard 都跟 activeWindow 走。
  1. 后端层
  • 把 CLI 的 --days/--from/--to 作为默认展示窗口,提供给 /api/config
  • LiveScanStore 维护一个可扩展的数据窗口。
  • API 收到比当前 snapshot 更宽的 from/to 时,触发一次 expanded scan 或从缓存加载更宽数据,然后再返回过滤结果。
  • 需要 de-dupe in-flight scan,避免连续点击 7d/14d/30d 触发多次重复扫描。
  • 当前 snapshot 已覆盖请求窗口时,直接内存过滤返回。

这样启动时仍然可以默认 7d,页面里切 30d / all time 时服务端会补齐数据。这个路线能实现“无需重启 CLI 改时间范围”。

@nengqi nengqi marked this pull request as draft May 7, 2026 16:07
@nengqi

nengqi commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

收到 review,先转 draft。

两个改动 (TimeRangeMenu 上提到顶层 header + LiveScanStore expandable scan 的后端架构改造) 都比较深,尤其后端那个涉及 in-flight scan de-dupe / window 缓存边界,我这边需要时间想清楚再动手。想好了 ready for review 再 @ 你。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants