---
id: S03
parent: M015
milestone: M015
provides:
  - GET /api/v1/stats endpoint returning live technique_count and creator_count
  - Homepage stats scorecard component
requires:
  []
affects:
  []
key_files:
  - backend/routers/stats.py
  - backend/main.py
  - frontend/src/api/public-client.ts
  - frontend/src/pages/Home.tsx
  - frontend/src/App.css
key_decisions:
  - Used `or 0` fallback on scalar COUNT results for safety against empty tables
  - Used existing --color-* CSS custom properties to match real codebase conventions
patterns_established:
  - Simple read-only stats endpoint pattern: async SQLAlchemy COUNT queries with scalar result
observability_surfaces:
  - none
drill_down_paths:
  - .gsd/milestones/M015/slices/S03/tasks/T01-SUMMARY.md
  - .gsd/milestones/M015/slices/S03/tasks/T02-SUMMARY.md
duration: ""
verification_result: passed
completed_at: 2026-04-03T04:30:55.043Z
blocker_discovered: false
---

# S03: Homepage Stats Scorecard

**Added a live stats endpoint and homepage scorecard displaying article and creator counts in cyan-on-dark design.**

## What Happened

Two-task slice delivering social proof metrics on the Chrysopedia homepage.

**T01 — Backend stats endpoint:** Created `backend/routers/stats.py` with `GET /api/v1/stats` that runs COUNT(*) queries against TechniquePage and Creator tables via async SQLAlchemy. Returns `{"technique_count": N, "creator_count": N}`. Registered in `main.py` with the standard `/api/v1` prefix. Uses `or 0` fallback on scalar results for safety against empty tables.

**T02 — Frontend scorecard:** Added `fetchStats()` to `public-client.ts` following existing API client patterns. Home.tsx renders a `.home-stats` section between the nav-cards and random technique button, with stagger animation. CSS uses existing design tokens (`--color-accent`, `--color-bg-surface`, `--color-border`, `--color-text-muted`). The scorecard only renders when stats are non-null, avoiding flash of empty content.

Live result: homepage shows "21 ARTICLES" and "7 CREATORS" in a subtle, centered scorecard that communicates volume without being boastful (per R034).

## Verification

All slice-level verification checks pass:
1. `curl -s http://ub01:8096/api/v1/stats | python3 -c "assert d['technique_count']>0 and d['creator_count']>0"` — exits 0 (21, 7)
2. `curl -s -o /dev/null -w '%{http_code}' http://ub01:8096/api/v1/stats` — returns 200
3. `cd frontend && npm run build` — exits 0, builds in 976ms
4. Browser screenshot confirms `.home-stats` section visible with "21 ARTICLES" and "7 CREATORS" in cyan-on-dark styling

## Requirements Advanced

- R034 — Validated — homepage renders stats block with real counts from the API (technique_count=21, creator_count=7) in cyan-on-dark scorecard design.

## Requirements Validated

- R034 — GET /api/v1/stats returns {technique_count:21, creator_count:7}. Browser screenshot confirms scorecard visible with correct values and styling.

## New Requirements Surfaced

None.

## Requirements Invalidated or Re-scoped

None.

## Deviations

T02 used real CSS custom property names (--color-accent, --color-bg-surface) instead of the plan's shorthand names (--accent, --surface-1) because those are what the codebase actually uses.

## Known Limitations

Stats are fetched on every homepage load with no caching. Acceptable for current traffic (single-admin tool) but would benefit from Redis caching or a stale-while-revalidate header if traffic grows.

## Follow-ups

None.

## Files Created/Modified

- `backend/routers/stats.py` — New file — GET /api/v1/stats endpoint with COUNT queries for techniques and creators
- `backend/main.py` — Added stats router import and include_router registration
- `frontend/src/api/public-client.ts` — Added StatsResponse interface and fetchStats() function
- `frontend/src/pages/Home.tsx` — Added stats state, useEffect fetch, and scorecard section render
- `frontend/src/App.css` — Added .home-stats, .home-stats__metric, .home-stats__number, .home-stats__label styles
