profile-weekly-distance: weekly distance bar chart on the profile

Implements the profile-weekly-distance change (specs/profile-weekly-distance):

- getWeeklyDistance(ownerId, { publicOnly, weeks=12 }) in activities.server.ts:
  one query that gap-fills in SQL — generate_series of week-starts LEFT JOINed
  to activities — so it returns exactly 12 contiguous { weekStart, distance }
  rows with matching Postgres week boundaries, viewer-scoped, no cache, no
  schema change.
- WeeklyDistanceChart: SVG bars normalized to the busiest week (empty weeks keep
  their slot as zero-height bars), per-bar title distance, localized label;
  renders nothing when there's no distance in the window. Mounted under the
  ProfileStats header.
- i18n journal.profileStats.weeklyDistance (en + de).

Tests: WeeklyDistanceChart component (jsdom: bar count incl. zero weeks, empty
→ hidden, normalization); e2e creates an activity with distance and asserts the
chart renders. typecheck + lint + unit (journal 321) green; verified in the
browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-06-13 07:46:06 +02:00
parent e0a92b56aa
commit ceda9f1877
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
10 changed files with 176 additions and 16 deletions

View file

@ -1,20 +1,20 @@
## 1. Aggregate query
- [ ] 1.1 Add `getWeeklyDistance(ownerId, { publicOnly, weeks = 12 })` to `activities.server.ts`: `sum(distance)` grouped by `date_trunc('week', coalesce(started_at, created_at))` over the last `weeks` weeks, viewer-scoped.
- [ ] 1.2 Gap-fill to a contiguous oldest→newest series with `0` for empty weeks; return `{ weekStart, distance }[]`.
- [x] 1.1 Added `getWeeklyDistance(ownerId, { publicOnly, weeks = 12 })` to `activities.server.ts`: `sum(distance)` per week, viewer-scoped.
- [x] 1.2 Gap-fill done **in SQL** (`generate_series` of week-starts LEFT JOINed to the activities) — returns exactly N contiguous `{ weekStart, distance }` rows, oldest→newest, and guarantees the week boundaries match Postgres `date_trunc('week', …)` (no JS/Postgres boundary drift). Filter conditions live in the JOIN's ON clause so empty weeks survive.
## 2. Loader & view
- [ ] 2.1 Profile loader passes the series (same `publicOnly: !isOwn` scoping; only when content is visible).
- [ ] 2.2 `WeeklyDistanceChart` component: SVG bars normalized to the max week, per-bar `title` distance (via `stats.ts`), localized label; renders nothing when every week is 0. Mount under `ProfileStats`.
- [x] 2.1 Profile loader fetches it in parallel with the stats, same `publicOnly: !isOwn` scoping, only when content is visible.
- [x] 2.2 `WeeklyDistanceChart`: SVG bars normalized to the busiest week, per-bar `title` distance (via `stats.ts`), localized label; renders nothing when every week is 0. Mounted under `ProfileStats`.
## 3. i18n
- [ ] 3.1 Add `journal.profileStats.weeklyDistance*` (label + per-bar readout) to en + de.
- [x] 3.1 Added `journal.profileStats.weeklyDistance` (label) to en + de. Per-bar readout is the language-neutral km distance.
## 4. Tests & checks
- [ ] 4.1 Unit: gap-fill produces 12 contiguous weeks with zeros in the right slots.
- [ ] 4.2 Component (jsdom): renders 12 bars for a series; nothing when all zero; tallest bar = busiest week.
- [ ] 4.3 E2E: create activities, assert the weekly chart renders on the profile.
- [ ] 4.4 `pnpm typecheck && pnpm lint && pnpm test && pnpm test:e2e` green.
- [x] 4.1 Gap-fill is in SQL (no JS helper to unit-test) → covered by the e2e (a real activity produces a non-zero bar over a full 12-week axis).
- [x] 4.2 Component (jsdom): one bar per week incl. zero weeks; nothing when all zero; heights normalized to the busiest week.
- [x] 4.3 E2E `profile-weekly-distance.test.ts`: create an activity with distance → the weekly chart renders on the profile. Passes locally.
- [x] 4.4 typecheck + lint + unit (journal 321) green; new e2e passes locally. Full `pnpm test:e2e` runs in CI.