Lifetime totals (count · distance · ascent · elapsed time) + a last-4-weeks count on the profile, viewer-scoped (public-only for visitors, full for the owner). Design records the cost decision: compute on the fly with a single aggregate over stored columns on the owner_id-leading indexes (cheap even for power users) — no cache table, no schema change — with a documented escape hatch to a user_activity_stats cache if profiling ever shows it's hot. Validates with `openspec validate --strict`. Specs only; no code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1.9 KiB
1.9 KiB
Why
A profile (/users/:username) is a list of routes and activities with no
sense of the person behind it — no "how far have they gone", no recent
cadence. Komoot leads its profile with lifetime totals (tours · distance ·
time); Strava with a "last 4 weeks" activity count. A small stats header makes
a profile feel like someone's outdoor life rather than a table, and every
number is already in the activities table.
What Changes
- Add a stats header to the profile page showing lifetime totals: activity count, total distance, total ascent, total time (elapsed).
- Add a recent-activity summary: the count of activities in the last 4 weeks (rolling).
- Scope the roll-up to what the viewer may see: a visitor sees public-only totals; the owner viewing their own profile sees totals over all their activities.
- Compute on the fly with a single indexed aggregate query — no cache table, no schema change (see design for the cost analysis and the future-cache escape hatch).
Capabilities
New Capabilities
profile-stats: the profile stats header — which totals are shown, how they are scoped by viewer/visibility, and how they are computed.
Modified Capabilities
Impact
- Journal app: a new aggregate query in
activities.server.ts(getActivityStats(ownerId, { publicOnly })); the profile loader (users.$username.server.ts) calls it and the page renders a stats header (reusing the sharedStatRowwhere it fits). - i18n:
journal.profileStats.*labels (en + de). - No schema change, no migration, no GPX parsing — aggregates use the
stored
distance/elevation_gain/durationcolumns over the existingowner_id-leading indexes.