Skip to content

feat(stats): add 1-month, all-time and custom time ranges to dashboard statistics - #10164

Open
SkySabre wants to merge 2 commits into
AstrBotDevs:masterfrom
SkySabre:feat/stats-extended-time-ranges
Open

SkySabre wants to merge 2 commits into
AstrBotDevs:masterfrom
SkySabre:feat/stats-extended-time-ranges

Conversation

@SkySabre

@SkySabre SkySabre commented Sep 20, 2026

Copy link
Copy Markdown

Motivation

The statistics page (Data & LogsStatistics) currently only offers 1 day / 3 days / 1 week ranges, and some metrics (Today's model calls, Total messages) do not follow the selected range, so it is hard to inspect longer-term usage or a specific past period.

This PR extends the range selector to 1 day / 3 days / 1 week / 1 month / All / Custom and makes the affected metrics follow the selected range. It enhances the existing statistics page rather than adding a new entry point.

Modifications / 改动点

Backend

  • astrbot/dashboard/services/stat_service.py
    • get_stat(offset_sec, end_ts) — supports a custom window end; offset_sec <= 0 opens the full window since deployment (start taken from the earliest platform_stats record).
    • Message time series — hourly buckets for windows ≤ 7 days; daily buckets for longer windows; records after the last bucket are merged into it instead of being dropped.
    • New response fields — range_start, range_end, bucket_seconds, range_message_count (messages inside the selected window, distinct from the global message_count).
    • get_provider_token_stats(days, start_ts, end_ts) — the 1/3/7-day restriction is removed; start_ts/end_ts select a custom range; days <= 0 selects the full window; trend buckets adapt between hourly and daily; new range_start / range_end / bucket_seconds.
  • astrbot/dashboard/api/stats.py — new optional query parameters (end_ts for /stats, start_ts + end_ts for /stats/provider-tokens); the legacy /api/stat/get and /api/stat/provider-tokens routes accept them as well.

Frontend

  • dashboard/src/views/stats/StatsPage.vue — six range presets (1d/3d/7d/30d/all/custom) with a custom date-range picker (no request is sent until "Apply"); resolveRange() builds the request parameters for each preset; the message-trend metric shows the windowed range_message_count; the model-calls overview card follows the selected range.
  • dashboard/src/api/v1.ts and dashboard/src/api/generated/openapi-v1/types.gen.ts — new query parameters; the generated client was refreshed with pnpm generate:api.
  • i18n — new strings for zh-CN, en-US, ja-JP and ru-RU.

Spec and docs

  • openspec/openapi-v1.yaml — documents end_ts / start_ts, allows days: 0.
  • docs/zh/use/webui.md, docs/en/use/webui.md — the Statistics section now lists the new range options (checked against the actual page).

Tests

  • tests/unit/test_stat_service.py — new cases for the all-time window with daily buckets, the 30-day window, and the provider custom/all-time windows.

Compatibility

  • Omitting the new parameters keeps the previous behavior unchanged (days=1/3/7 still use hourly buckets).
  • Response bodies only add fields; nothing is renamed or removed. All new query parameters are optional, so existing callers are unaffected.

Screenshots or Test Results / 运行截图或测试结果

1-month range selected — daily buckets, metrics follow the range:

Custom range applied:

Verification steps and results on this branch:

  1. uv run pytest tests/unit/test_stat_service.py26 passed
  2. uv run ruff format --check . / uv run ruff check .513 files already formatted / All checks passed
  3. pnpm --dir dashboard install --frozen-lockfile && pnpm --dir dashboard run buildsucceeds (includes the vue-tsc type check)
  4. cd docs && pnpm run docs:buildbuild complete
  5. Started the backend and the dashboard dev server, then exercised the API with a logged-in session:
    • /api/v1/stats?offset_sec=86400 — hourly buckets, new fields present (backward compatible)
    • /api/v1/stats?offset_sec=0 — full window since deployment
    • /api/v1/stats?offset_sec=86400&end_ts=... — the window end follows end_ts
    • /api/v1/stats/provider-tokens?days=30bucket_seconds=86400 (previously clamped to 1 day)
    • /api/v1/stats/provider-tokens?days=0 — full window
    • custom ranges: 5-day span → bucket_seconds=3600; 20-day span → bucket_seconds=86400
    • legacy /api/stat/get and /api/stat/provider-tokens accept the new parameters

Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”

  • 📚 I checked the affected WebUI instructions and screenshots in docs/zh and docs/en against the changed navigation, page structure, and labels, and updated them in this PR (or explained why no documentation update is needed). For renamed, moved, or merged entry points, I included an old entry → new entry mapping in the documentation and changelog.
    / 我已对照变化后的 WebUI 入口、页面结构和术语,核对并在本 PR 中更新 docs/zhdocs/en 的相关操作说明与截图(或说明无需更新文档的原因)。入口改名、移动或合并时,已在文档和 changelog 中补充 旧入口 → 新入口 对照。

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Expand dashboard statistics to support longer, all-time, and custom analysis windows while keeping metrics and trends aligned with the selected range.

New Features:

  • Extend dashboard statistics with 1-day, 3-day, 1-week, 1-month, all-time, and custom date-range selection.
  • Support custom and all-time windows for message and provider token statistics through optional API parameters.
  • Adapt statistics trends between hourly and daily buckets based on the selected range and expose range metadata.

Bug Fixes:

  • Make message counts and model-call overview metrics reflect the selected statistics window.
  • Preserve records at the end of a statistics window when aggregating trend data.

Enhancements:

  • Add an apply-based custom date-range picker to the statistics page without sending requests before confirmation.
  • Add localized labels and update the OpenAPI specification and WebUI documentation for the expanded range options.

Documentation:

  • Update English and Chinese WebUI documentation to describe the expanded Statistics range selector.

Tests:

  • Add coverage for all-time, monthly, custom, bounded, and daily-bucket statistics windows.

…d statistics

- get_stat() accepts end_ts; offset_sec <= 0 opens an all-time window starting
  from the earliest PlatformStat record; windows > 7 days bucket by day
  instead of by hour, and records after the last bucket are merged into it
- return range_start, range_end, bucket_seconds and range_message_count
  (messages inside the selected window, kept separate from the global
  message_count)
- provider tokens: drop the 1/3/7-day restriction, accept start_ts/end_ts,
  treat days <= 0 as all-time, and adapt hourly/daily trend buckets
- dashboard: six range presets (1d/3d/7d/30d/all/custom) with a custom
  date-range picker; overview card and trend metric follow the selection
- i18n: add the new range strings for zh-CN, en-US, ja-JP and ru-RU
- openapi: document end_ts/start_ts and allow days=0; regenerate the client
- docs: note the new range options in docs/zh and docs/en
- tests: cover all-time/daily bucketing and the provider custom/all-time windows

Omitting the new parameters keeps the previous behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/dashboard/services/stat_service.py" line_range="266" />
<code_context>
         try:
-            now = int(time.time())
-            start_time = now - offset_sec
+            now = int(end_ts) if end_ts else int(time.time())
+            if offset_sec and offset_sec > 0:
+                start_time = now - offset_sec
</code_context>
<issue_to_address>
**issue (bug_risk):** Historical windows do not constrain either statistics query to the requested end time. `get_stat` and `get_provider_token_stats` select every record at or after the start, so records occurring after `end_ts` are included in range totals, rankings, and trend-tail aggregation.

**Triggers:** When a caller requests a historical range with `end_ts` earlier than the current time and records exist after that timestamp.

**Suggested fix:** Add an upper-bound predicate such as `timestamp <= window_end` / `created_at <= end_time` to both queries, and keep the same bound in all range aggregations.
</issue_to_address>

### Comment 2
<location path="astrbot/dashboard/services/stat_service.py" line_range="447" />
<code_context>
+            if now_local - range_start_local <= timedelta(days=7):
+                bucket_step = timedelta(hours=1)
+            else:
+                bucket_step = timedelta(days=1)
+            # Keep the existing convention of aligning the start to the minute
+            range_start_local = range_start_local.replace(
</code_context>
<issue_to_address>
**issue (bug_risk):** Provider records are always assigned to hourly bucket timestamps, even when `bucket_step` is one day. Daily `bucket_timestamps` are aligned to midnight, so records created at non-midnight times have no matching series bucket and disappear from the displayed daily trend.

**Triggers:** When the selected provider-token window exceeds seven days.

**Suggested fix:** When using daily buckets, truncate `created_at_local` to the start of the day before constructing `bucket_ts`; retain hourly truncation for hourly buckets.
</issue_to_address>

### Comment 3
<location path="dashboard/src/views/stats/StatsPage.vue" line_range="841-847" />
<code_context>
+watch(selectedRange, async (value) => {
+  // When switching to "Custom", wait until the dates are picked and
+  // applied before sending a request
+  if (value === 'custom' && !hasAppliedCustomRange.value) return
   try {
     await Promise.all([fetchBaseStats(), fetchProviderStats()])
</code_context>
<issue_to_address>
**issue (bug_risk):** After one custom range has been applied, switching away and then selecting `custom` immediately sends a request for the previously applied dates before the user clicks Apply. This violates the no-request-until-Apply behavior and can replace the displayed data with a stale custom range while the user is editing dates.

**Triggers:** When a user applies a custom range, selects another preset, and later selects Custom again.

**Suggested fix:** Track whether the current custom inputs have been applied, clear or invalidate the applied range when entering Custom, and skip the watcher request until Apply is clicked.

```suggestion
watch(selectedRange, async (value) => {
  // When switching to "Custom", clear the previously applied range and
  // wait until the dates are picked and applied before sending a request
  if (value === 'custom') {
    appliedCustomStart.value = null
    appliedCustomEnd.value = null
    return
  }
  try {
    await Promise.all([fetchBaseStats(), fetchProviderStats()])
  } catch (error) {
    console.error('Failed to refresh stats range:', error)
    errorMessage.value = t('errors.rangeFailed')
  }
})
```
</issue_to_address>

Sourcery assessment

Approval pending. 3 findings to address first.

Blocking findings: astrbot/dashboard/services/stat_service.py:266, astrbot/dashboard/services/stat_service.py:447, dashboard/src/views/stats/StatsPage.vue:847


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread astrbot/dashboard/services/stat_service.py
Comment thread astrbot/dashboard/services/stat_service.py
Comment thread dashboard/src/views/stats/StatsPage.vue
@SkySabre SkySabre changed the title feat(stats): add 1-month, all-time and custom time ranges to dashboar… feat(stats): add 1-month, all-time and custom time ranges to dashboard statistics Sep 20, 2026
Address review findings on the extended statistics ranges:

- constrain both get_stat and get_provider_token_stats queries with an
  exclusive next-second upper bound so records after end_ts no longer leak
  into window totals, rankings or the trend tail
- map provider records to daily buckets by local midnight when the bucket
  step is one day, so long-window trends keep records created between
  midnights
- clear the previously applied custom range when re-entering "Custom" and
  wait for Apply before requesting, matching the documented behavior
- tests: cover records after end_ts for messages and provider tokens, and
  non-midnight records in daily provider buckets

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourcery assessment

Approved.

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.

1 participant