Per-graph timeframe tabs on project stats - #222
Merged
Conversation
Every graph on /dashboard/projects/:id/stats was locked to a fixed 30-day window, so there was no way to ask whether a number had moved recently — the question that prompted this was whether /login requests fell after we started throttling, which a 30-day total averages away. Each card now carries its own 1H / 4H / 1D / 1W / 1M / 1Y / All tab strip and re-fetches only itself from the new GET /api/projects/:id/tracker-stats?range=&panel=, so narrowing one chart does not re-run the other eleven. The page still server-renders every panel at 1M, so the default view costs no extra request, and each card caches the ranges it has already fetched. Sub-day windows needed a data source the existing RPCs do not have: every tracker_* RPC reads a *_daily_stats rollup whose finest resolution is one UTC day. The new tracker_recent_* functions are raw-table twins reading public.tracker_events (row-per-event, pruned at 24h) with the same result shapes and top-N truncation, windowed by minutes. Devices / browsers / OS and exit pages have no sub-day source — the raw row keeps a user_agent string but no parsed columns, and exit sessions key on a date — so those four panels offer the rollup ranges only rather than quietly answering a different question. lib/tracker/panels.ts holds the (project, range, panel) -> payload mapping once, shared by the server page and the route handler, so a tab switch cannot reshape a chart whose data did not change. Verified against prod: the client bucket axis and tracker_recent_series agree exactly at 13 / 17 / 25 buckets for 1H / 4H / 1D. Sizing that axis by division alone was one short and silently dropped the oldest bucket on every sub-day tab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1L1xBbJJdiXJc1HNZpqzP
ThreatCrush Security Scan40 finding(s) HIGH/CRITICAL: 3 | MEDIUM: 28 | LOW: 9
Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
added a commit
that referenced
this pull request
Aug 30, 2026
) * Fix the 1D tab showing all-time totals on rollup-only stats panels Exit pages, devices, browsers and operating systems have no sub-day data source, so #222 gave them the rollup ranges only — with "1D" documented to mean today's UTC rollup rather than a rolling 24 hours. But the 1D range object is defined with `minutes: 1440` and no `days`, since it is a raw-event window for every other panel. resolveDays looks for `days`, finds nothing, and falls through to tracker_first_day, which returns the project's entire history. So those four panels answered the 1D tab with all-time totals, and on any site younger than 30 days 1D, 1M and All were byte-identical. rssamplifier.com is 15 days old, which is how this surfaced: its exit-pages card read 169,207 for /login at 1D, 1M and All alike, while the top-pages card — raw-backed, and therefore correct — read ~4k for the same path over the same 24 hours. Today's actual figure is 536. A sub-day window against a day-resolution rollup is one day, so rollupDays() collapses any raw range to 1 for these panels and passes real rollup ranges through untouched. The 1D tooltip is relabelled from "Last 24 hours, hourly buckets" to "Today so far, UTC day" for the same four panels, so the tab no longer promises a rolling window it cannot serve. Panels with a raw source are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaCMqhvNYfKtjLHmHQD1oq * Describe an empty rollup-only panel with its own range label EmptyRange looked the range description up in the global table, so an exit pages / devices / browsers / OS card with no data for 1D still said "last 24 hours, hourly buckets" while its own tab tooltip said "today so far, UTC day". Both call sites already hold the panel's range list; pass it through and read the label from there, falling back to the global lookup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaCMqhvNYfKtjLHmHQD1oq --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Every graph on
/dashboard/projects/:id/statswas locked to a fixed 30-day window, so there was no way to ask whether a number had moved recently. Each card now carries its own timeframe tabs:1H · 4H · 1D · 1W · 1M · 1Y · AllClicking a tab re-fetches only that panel from the new
GET /api/projects/:id/tracker-stats?range=&panel=, so narrowing one chart doesn't re-run the other eleven. The page still server-renders every panel at 1M, so the default view costs no extra request, and each card caches the ranges it has already fetched.Why the new RPCs
Every existing
tracker_*RPC reads a*_daily_statsrollup, whose finest resolution is one UTC calendar day — so 1H/4H/1D had no data source at all. The newtracker_recent_*functions are raw-table twins readingpublic.tracker_events(row-per-event, pruned at 24h), with the same result shapes and top-N truncation, windowed by minutes instead of days.Migration
20260830120000_tracker_recent_rpcs.sqlis already applied to prod (9 new functions, purely additive — no existing function was replaced), per the one-file-at-a-time MCP workflow this repo uses.Panels that can't do sub-day
Devices / Browsers / Operating systems and Exit pages offer the rollup ranges only. The raw event row keeps a
user_agentstring but no parseddevice_type/browser/oscolumns, and exit sessions key on alast_daydate rather than a timestamp. They show 5 tabs instead of 7 rather than quietly answering a different question; the API rejects an unsupported range for those panels instead of silently substituting one.Shape
lib/tracker/panels.tsholds the(project, range, panel) -> payloadmapping once, shared by the server page and the route handler, so a tab switch cannot reshape a chart whose data did not change. The portfolio page (/dashboard/analytics) aggregates across projects and drives its own page-wide range control, so it passes noprojectIdand renders the same cards with tabs hidden.Verification
Local
tsc/vitestare unavailable in this checkout (danglingnode_modules/.binsymlinks — see repo notes), so CI is the typecheck gate here.tracker_recent_seriesagree exactly at 13 / 17 / 25 buckets for 1H / 4H / 1D.That last check caught a real bug: sizing the axis by division alone was one bucket short, silently dropping the oldest bucket on every sub-day tab. A 60-minute window spans 13 five-minute bucket starts, not 12.
🤖 Generated with Claude Code
https://claude.ai/code/session_01A1L1xBbJJdiXJc1HNZpqzP