Skip to content

fix(overview): wire activity heatmap to real activity_events aggregate#68

Merged
Copxer merged 1 commit into
mainfrom
fix/activity-heatmap-real-data
May 1, 2026
Merged

fix(overview): wire activity heatmap to real activity_events aggregate#68
Copxer merged 1 commit into
mainfrom
fix/activity-heatmap-real-data

Conversation

@Copxer
Copy link
Copy Markdown
Owner

@Copxer Copxer commented May 1, 2026

The Overview dashboard's activityHeatmap slice was a static MOCK_HEATMAP constant carried over from phase 0. Phase 3 shipped activity_events but never wired the heatmap to it. Last carryover from phases 0–4. Non-spec follow-up branch.

Summary

  • New GetOverviewDashboardQuery::activityHeatmap() aggregates activity_events.occurred_at over the last 90 days into a 7×6 grid ([day_of_week][four_hour_bucket]). Day index 0=Sun matches both Carbon's dayOfWeek and the JS Date#getDay() axis the heatmap component already uses.
  • Bucketing in PHP, not SQL. DAYOFWEEK() (MySQL) and strftime('%w', …) (SQLite) disagree on indexing AND on timezone handling — and the test suite runs on SQLite while prod uses MySQL. At phase-1 row counts (≤90d × low webhook traffic) loading the timestamps + iterating in PHP is sub-millisecond. Re-evaluate around ~100k events in the window.
  • 90-day window — long enough to surface a recurring rhythm, recent enough to reflect current habits.
  • MOCK_HEATMAP constant removed; class docblock graduates the slice to "real today" with the timezone caveat documented (hour buckets are sharper TZ exposure than day buckets).

Test plan

  • vendor/bin/pint --test passes.
  • php artisan test — 7 new tests across the heatmap; full suite 291 passed (was 282). The 41 failures are env-only POST CSRF (419) baseline; CI passes them.
  • npm run build clean.
  • Manual smoke (post-merge): observe the heatmap renders real intensities scaled against maxCount; on a fresh account it should render mostly muted (honest, not a bug).

Test coverage

Empty grid · day-and-hour placement · multi-event accumulation in same bucket · 90-day cutoff (just inside / just outside) · fixed 7×6 shape · bucket-boundary contract (00:00 / 03:59 / 04:00 / 11:59 / 12:00 / 23:59 → their canonical buckets).

Self-review notes

Self-review pass via superpowers:code-reviewer; addressed both recommendations:

  • Added the bucket-boundary contract test pinning 6 edge times to their canonical buckets — a regression there would silently misbucket a chunk of every account's events.
  • Doc-comment now explicitly calls out the hour-bucket timezone exposure (sharper than the existing day-bucket caveat on dailyCounts() because a 6h TZ shift moves events into different buckets, not just adjacent days).

Sparse-account UX (intentional phase-1 behavior): a fresh account with 7 days of events renders a mostly muted heatmap. That's an honest signal, not a bug. If it ever becomes a friction point, the fix is a component-level empty state ("collecting rhythm — check back in a week") rather than a query change.

Index status: at phase-1 row counts the cost is negligible. No new index. Watch item for EXPLAIN once a real account crosses ~100k events in the 90-day window.

What's left in the carryover bucket

None. After this lands, all phase-0 mock placeholders that have a real data source today are graduated. The remaining MOCK_KPIS (services / alerts / uptime) ride with their own future phases (5/6/7/8).

The Overview dashboard's activityHeatmap slice was a static
MOCK_HEATMAP constant carried over from phase 0 — phase 3 shipped
activity_events but never wired the heatmap to it. Replace the mock
with a real query.

- New GetOverviewDashboardQuery::activityHeatmap() aggregates
  activity_events.occurred_at over the last 90 days into a 7×6 grid
  ([day_of_week][four_hour_bucket]).
- Bucketing happens in PHP, not SQL — DAYOFWEEK() (MySQL) and
  strftime('%w', ...) (SQLite) disagree on indexing AND on timezone
  handling, and the test suite runs on SQLite while prod uses MySQL.
  At phase-1 row counts (≤90d × low webhook traffic) loading the
  timestamps and bucketing in PHP is sub-millisecond.
- 90-day window is long enough to surface a recurring rhythm but
  recent enough to reflect current habits.
- MOCK_HEATMAP constant removed; class docblock graduates the slice
  from "still mock" to "real today" with the timezone caveat called
  out explicitly (hour buckets are sharper exposure to app.timezone
  than the existing day-bucket case).

Tests: 7 new tests covering empty grid, day-and-hour placement,
multi-event accumulation, 90-day cutoff (just inside / just outside),
fixed 7×6 shape, and a bucket-boundary contract pinning 00:00 / 03:59
/ 04:00 / 11:59 / 12:00 / 23:59 to their canonical buckets.

Self-review pass via superpowers:code-reviewer; addressed both
recommendations (boundary test added, timezone caveat documented).
Sparse-account UX (a fresh account renders mostly muted) is intentional
phase-1 behavior — fix is a component-level empty state, not a query
change, if it ever becomes a friction point.
@Copxer Copxer merged commit 67842b6 into main May 1, 2026
1 check passed
@Copxer Copxer deleted the fix/activity-heatmap-real-data branch May 1, 2026 01:51
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