feat(stats): personal member-stats recap page (LC-536) #511

Merged
longjacksonle merged 3 commits from feat/LC-536-member-stats into main 2026-07-06 03:52:37 +02:00

What

LC-536: a personal "wrapped" recap. A new Your stats page (/stats, linked from the sidebar under Kudos) shows the signed-in member their own activity: messages sent, active days, kudos received, reactions received/given, member-since date, and their top 5 channels.

Why

The app already tracks all of this (messages, reactions, kudos) but never showed a member their own totals. It is a cheap, self-contained recap on top of existing tables - no new writes, no schema change.

How

Three commits, each compiles + tests green on its own:

  1. Aggregation. New db::stats::member_stats(chat, user_id) rolls up the counts in a handful of scoped queries. All message-derived figures share one deleted_at IS NULL AND is_system = 0 AND quarantined = 0 predicate so they agree; reactions-received excludes self-reactions; top-channels excludes DMs (room_type != 'dm'). No migration.

  2. Page. GET /stats (auth-gated, self-only - the handler always reads the caller's own id, nothing to privacy-gate) renders a tile grid plus a top-channels list, reachable from a new sidebar link. en + es strings.

  3. Tests. Direct aggregation coverage (deleted/other-user messages excluded, self-reaction not counted, DM exclusion, top-channel tally) plus a route smoke test (renders for an authed member; 303 for anon).

Notes

  • Self-only surface: no cross-user stats are ever exposed, so unlike the hovercard there is no privacy gate to reason about.
  • No operator action: additive, read-only, no env var or schema change.

Testing

just check, just test, just test-saas all green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu

## What LC-536: a personal "wrapped" recap. A new **Your stats** page (`/stats`, linked from the sidebar under Kudos) shows the signed-in member their own activity: messages sent, active days, kudos received, reactions received/given, member-since date, and their top 5 channels. ## Why The app already tracks all of this (messages, reactions, kudos) but never showed a member their own totals. It is a cheap, self-contained recap on top of existing tables - no new writes, no schema change. ## How Three commits, each compiles + tests green on its own: 1. **Aggregation.** New `db::stats::member_stats(chat, user_id)` rolls up the counts in a handful of scoped queries. All message-derived figures share one `deleted_at IS NULL AND is_system = 0 AND quarantined = 0` predicate so they agree; reactions-received excludes self-reactions; top-channels excludes DMs (`room_type != 'dm'`). No migration. 2. **Page.** `GET /stats` (auth-gated, self-only - the handler always reads the caller's own id, nothing to privacy-gate) renders a tile grid plus a top-channels list, reachable from a new sidebar link. en + es strings. 3. **Tests.** Direct aggregation coverage (deleted/other-user messages excluded, self-reaction not counted, DM exclusion, top-channel tally) plus a route smoke test (renders for an authed member; 303 for anon). ## Notes - Self-only surface: no cross-user stats are ever exposed, so unlike the hovercard there is no privacy gate to reason about. - No operator action: additive, read-only, no env var or schema change. ## Testing `just check`, `just test`, `just test-saas` all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
New read-only `db::stats::member_stats` that rolls up a single user's own chat.db activity: messages sent, distinct active days, reactions given, reactions received (self-reactions excluded), kudos received, and top 5 channels (DMs excluded). Countable messages share one `deleted_at IS NULL AND is_system = 0 AND quarantined = 0` predicate so every message-derived figure agrees. No migration - pure aggregation over existing tables.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
Surface member_stats on a self-service "Your stats" page: a tile grid (messages sent, active days, kudos received, reactions received/given, member-since date) plus a top-channels list. Auth-gated and self-only - the handler always reads the caller's own id, so there is nothing to privacy-gate. Reachable from a new sidebar nav link next to Kudos. en + es strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
test(stats): member_stats aggregation + /stats page render (LC-536)
All checks were successful
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 3s
check-secrets / TruffleHog (pull_request) Successful in 11s
check-secrets / Kingfisher (pull_request) Successful in 12s
Check / clippy + fmt + tests (pull_request) Successful in 3m13s
Create release / Create release from merged PR (pull_request) Has been skipped
8a10f79871
Direct aggregation coverage (deleted/other-user messages excluded, self-reactions not counted as received, top-channel tally + DM exclusion via room 1) plus a route smoke test: GET /stats renders the self recap for an authed member and 303s an anonymous request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1pu1NZoT3Uxb7AcxVyTgu
longjacksonle deleted branch feat/LC-536-member-stats 2026-07-06 03:52:37 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!511
No description provided.