trails/openspec/changes/profile-weekly-distance/design.md
Ullrich Schäfer d975cc7402
openspec: propose profile-weekly-distance (weekly bars)
A 12-week distance bar chart under the profile stats header, viewer-scoped,
built on profile-stats (#539). Design records the same no-cache rationale: one
indexed grouped aggregate over a 12-week slice (≤12 rows), gap-filled in JS.

Validates with `openspec validate --strict`. Specs only; no code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 07:29:23 +02:00

56 lines
2.2 KiB
Markdown

## Context
`profile-stats` added `getActivityStats` (a single indexed aggregate over stored
columns) and a `ProfileStats` header. This change adds a per-week breakdown
beneath it. `activities` has `distance`, `started_at`, `created_at`,
`visibility`, and `owner_id`-leading indexes.
## Goals / Non-Goals
**Goals**
- A compact weekly-distance trend on the profile, cheap and viewer-scoped.
**Non-Goals**
- No per-week elevation/time toggle, no selectable window in v1 (fixed 12
weeks).
- No hover tooltip framework — a native `title` per bar is enough.
- No "where I've been" map (separate idea).
## Decisions
### D1: One grouped aggregate — still no cache
`SELECT date_trunc('week', coalesce(started_at, created_at)) AS wk,
sum(distance) AS m
FROM journal.activities
WHERE owner_id = $1 [AND visibility = 'public']
AND coalesce(started_at, created_at) >= now() - interval '12 weeks'
GROUP BY wk`.
Bounded to 12 weeks and filtered on the `owner_id` index, this scans a tiny
recent slice and returns ≤ 12 rows — same cost class as the lifetime aggregate
(`profile-stats` §D1). No cache table; the documented escape hatch there covers
this too.
### D2: Gap-fill in JS
The query returns only weeks that have activities. The server fills the result
to a contiguous 12-week series (oldest → newest) with `0` for empty weeks, so
the chart always has a stable 12-bar axis. Week boundaries follow Postgres
`date_trunc('week', …)` (ISO weeks, Monday start).
### D3: Viewer scoping
Identical to `profile-stats`: `publicOnly: !isOwn`. Same WHERE clause shape;
remote-ingested rows (`owner_id NULL`) never appear.
### D4: Presentation
A small SVG bar chart (`WeeklyDistanceChart`) under the stats header: 12 bars,
heights normalized to the max week, each bar carrying a `title` with its week
distance (formatted via `stats.ts`). Renders nothing when every week is 0
(no distance in the window). Read-only, no interactivity beyond the native
title.
## Risks / Trade-offs
- **Fixed 12-week window** — a power user with a long history sees only the
recent quarter. Acceptable for a cadence glance; a selectable range is a
future enhancement.
- **Distance-only** — elevation/time trends are deferred to keep the chart
legible and the query single-column.