One shared switch on every chart, on its own row under the title. The bars stay readable in every range because the bar size follows the range:
| Range | One bar = | Bars | Swipe? |
|---|---|---|---|
| 7D | a day | 7 | no |
| 30D | a day | 30 | yes, opens on today (as now) |
| 90D | a week | 13 | no (today: 90 daily bars with swiping) |
| 1Y | a month | 12 | no |
| All | chosen by length: ≤31 days → day, ≤ ~6 months → week, longer → month | varies | no |
Today there are two different switches:
Proposed — one component, five equal buttons, full width under the title (dark and light theme):
Why its own row: at phone width the title already wraps next to three buttons; five would squeeze it further.
Invite Performance as the example. Every other chart behaves the same way.
| Bar size | Under each bar | Second line | Detail row |
|---|---|---|---|
| Day | day number (as now) | month on the first bar and on the 1st; "Today" on the last | Mon 28 Sep |
| Week | day the week starts | month on the first bar and when it changes; "This wk" on the last | Week of 21 Sep · or "(so far)" |
| Month | month (Sep) | year on the first bar and on January; "So far" on the last | September 2026 · or "(so far)" |
| Chart | Range today | Opens on | What follows the switch |
|---|---|---|---|
| Admin dashboard → Invite Performance | 7D · 30D · 90D (switch exists) | 7D | Everything in the section: the three numbers, the daily chart, "Where people drop out", "Which channel brings people in". The Expand page gets the same switch and opens on the range the card is showing. |
| Admin dashboard → Active users | none (fixed 7 days) | 7D | The chart only. DAU / WAU / MAU tiles above it stay as they are — they are already "today / this week / this month". |
| Admission History | 7 / 30 / 90 days (switch exists, different style) | 7D | The overview (Joined / Invited / Walked in and its bar), the chart, and the day-by-day list. |
| Daily Limit | none (fixed 7 days) | 7D | The chart and the "limit reached on N days" line. "Used today" and the limit control are about today and stay as they are. |
| Invites Per User | none (fixed 30 days) | 7D | Everything on the page: the top numbers, the chart and the "who is inviting" list (decided by Leong 09-28 — today the numbers and the list are all-time and only the chart is 30 days). |
From reading each chart's code and the backend behind it. Red = must be fixed first, amber = design decision, green = handled in the design.
/performance accepts 7 and 90; anything else, including 365, quietly becomes 30. /daily-history and the Invites Per User series fall back to their default above 90. The dashboard series has no range at all. So today, asking for 1Y would show 30 days with no error — the worst kind of wrong. Every endpoint needs a range parameter that rejects values it does not know.invite_codes.created_at / used_at, users.created_at, messages.created_at). 1Y means about 365 counts per chart, each over a whole table. The dashboard endpoint already takes 3.6–7 s in production for 7 days. Fix: add the date indexes (one migration) and count per week / month with a single grouped query, not per day. The Active users chart moves to its own endpoint, off the slow dashboard one.| # | Subtask |
|---|---|
| 1 | Backend: one range parameter (7d | 30d | 90d | 1y | all) on every chart endpoint; unknown values return 400, not a silent default. Each response says its bar size (day / week / month) and where "All" starts. |
| 2 | Backend: grouped weekly / monthly counts; distinct active users per week / month; Active users moves to its own endpoint and to Malaysia time. Date indexes dropped (2026-09-28): measured on production, a full year of Active users takes 123 ms and 1Y / All read every message anyway, so an index cannot speed them up; the migration numbers 464–468 are also held by unshipped branches. |
| 3 | App: one shared range switch; the shared chart learns week and month bars (labels, detail row, "so far"). |
| 4 | App: put the switch on the five charts, one page per subtask, with each page's numbers following the range as in the table above. |
| 5 | Tests + screenshots of every chart in every range, the guard extended, docs, web version. |
1 and 2 come first: without them the switch would show wrong data with no error (problem 1).
| # | Question | Decided |
|---|---|---|
| 1 | 90D as 13 weekly bars instead of 90 daily bars | Yes — as proposed |
| 2 | "All" starts where each chart's data starts, labelled "since …" | Yes — as proposed |
| 3 | Daily Limit on week / month bars: walked in + "limit reached on N days" | Yes — as proposed |
| 4 | Invites Per User: do the top numbers and the list follow the range? | Yes, they follow the range (Tora's suggestion, chosen by Leong) — the page never shows two time spans at once |
| 5 | Default range when a chart opens | 7D on every chart (changed from the proposal, which kept today's defaults) |
| 6 | Remember the last range on the device, or always open on 7D? | Always open on 7D — even if 1Y was picked last time |
| 7 | Week start, 1Y span, Active users time zone | As Tora suggested: weeks start Monday (90D = last 13 whole weeks, 91 days) · 1Y = last 12 calendar months, this month marked "so far" · Active users moves from UTC to Malaysia time (its 7D bars shift slightly once — that is the fix) |
11:44 MYT "go with a), the three things follow your suggestions." — building started, in the order of section 5.
Later the same day (v2.761): Leong removed the 1Y button (“remove the 1y filter from all the charts”). The switch is now 7D · 30D · 90D · All; month bars remain for a long All.
Shipped 2026-09-28 as v2.758: backend live (?range= on every chart endpoint, /active-series, codes_since); app on the web version (hoop-web.pages.dev) and in the next TestFlight build. One change from section 5: no date-index migration (measured on production, see subtask 2).
| Chart | Endpoint | Range limit today | Data since | Day in |
|---|---|---|---|---|
| Invite Performance | /v1/admin/invites/performance?days= | 7 / 30 / 90; others → 30 silently | 5 Aug 2026 (invite codes) | Malaysia |
| Active users | /v1/admin/invites/dashboard (series_7d) | fixed 7 | 9 Jun 2026 (messages) | UTC |
| Admission History | /v1/admin/invites/daily-history?days= | 1–90; above → 7 | 9 Jun 2026 (sign-ups); invites from 5 Aug | Malaysia |
| Daily Limit | same endpoint, 7 days | 1–90 | 15 Sep 2026 (limit counter + history) | Malaysia |
| Invites Per User | /v1/admin/invites/inviters | fixed 30 in the handler | 5 Aug 2026 | Malaysia |
Nothing deletes old rows from these tables, except disappearing messages that expire (they can lower past active-user counts slightly).