From 1f783c42166c8be57f54d1da6933ff8149d68552 Mon Sep 17 00:00:00 2001 From: jlightner Date: Fri, 3 Apr 2026 04:25:58 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Added=20GET=20/api/v1/stats=20endpoint?= =?UTF-8?q?=20returning=20live=20technique=5Fcount=20(=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "backend/routers/stats.py" - "backend/main.py" GSD-Task: S03/T01 --- .gsd/DECISIONS.md | 1 + .gsd/milestones/M015/M015-ROADMAP.md | 2 +- .../milestones/M015/slices/S02/S02-SUMMARY.md | 92 +++++++++++++++++ .gsd/milestones/M015/slices/S02/S02-UAT.md | 46 +++++++++ .../M015/slices/S02/tasks/T02-VERIFY.json | 9 ++ .gsd/milestones/M015/slices/S03/S03-PLAN.md | 70 ++++++++++++- .../M015/slices/S03/S03-RESEARCH.md | 99 +++++++++++++++++++ .../M015/slices/S03/tasks/T01-PLAN.md | 51 ++++++++++ .../M015/slices/S03/tasks/T01-SUMMARY.md | 77 +++++++++++++++ .../M015/slices/S03/tasks/T02-PLAN.md | 59 +++++++++++ 10 files changed, 504 insertions(+), 2 deletions(-) create mode 100644 .gsd/milestones/M015/slices/S02/S02-SUMMARY.md create mode 100644 .gsd/milestones/M015/slices/S02/S02-UAT.md create mode 100644 .gsd/milestones/M015/slices/S02/tasks/T02-VERIFY.json create mode 100644 .gsd/milestones/M015/slices/S03/S03-RESEARCH.md create mode 100644 .gsd/milestones/M015/slices/S03/tasks/T01-PLAN.md create mode 100644 .gsd/milestones/M015/slices/S03/tasks/T01-SUMMARY.md create mode 100644 .gsd/milestones/M015/slices/S03/tasks/T02-PLAN.md diff --git a/.gsd/DECISIONS.md b/.gsd/DECISIONS.md index 81d7e73..0cd100b 100644 --- a/.gsd/DECISIONS.md +++ b/.gsd/DECISIONS.md @@ -31,3 +31,4 @@ | D023 | M012/S01 | architecture | Qdrant embedding text enrichment strategy | Prepend creator_name and join topic_tags into embedding text for technique pages and key moments. Batch-resolve creator names at stage 6 start. | Semantic search now surfaces results for creator-name queries and tag-specific queries. Batch resolution avoids N+1 lookups during embedding. Reindex-all endpoint enables one-shot re-embedding after text composition changes. | Yes | agent | | D024 | M014/S01 | architecture | Content model for sections with subsections | Sections with subsections use empty-string content field; substance lives in subsections | Avoids duplication between section-level content and subsection content. The section heading serves as H2 container; all prose lives in subsection content fields. Sections without subsections use the section content field directly. | Yes | agent | | D025 | M015 | architecture | Search query storage and popular searches architecture | PostgreSQL search_log table + Redis read-through cache with 5-min TTL | PostgreSQL gives full historical data for future analytics (zero-result queries, time-of-day patterns). Redis cache prevents DB query on every homepage load. 5-min TTL balances freshness with load. Volume is tiny at current scale. | Yes | collaborative | +| D026 | | requirement | R033 | validated | Creators browse page shows "Last updated: Apr 2/3" per creator with techniques, omits for 0-technique creators. Homepage recently-added cards show subtle date stamps. Both verified live on ub01:8096. | Yes | agent | diff --git a/.gsd/milestones/M015/M015-ROADMAP.md b/.gsd/milestones/M015/M015-ROADMAP.md index caf9bd3..9710b4c 100644 --- a/.gsd/milestones/M015/M015-ROADMAP.md +++ b/.gsd/milestones/M015/M015-ROADMAP.md @@ -7,7 +7,7 @@ Add social proof and freshness signals to the public site: search query logging | ID | Slice | Risk | Depends | Done | After this | |----|-------|------|---------|------|------------| | S01 | Search Query Logging + Popular Searches API | medium | — | ✅ | GET /api/v1/search/popular returns top 10 queries from last 7 days, cached in Redis. search_log table has rows from real searches. | -| S02 | Creator Freshness + Homepage Card Dates | low | — | ⬜ | Creators browse page shows 'Last updated: Apr 3' per creator. Homepage recently-added cards show a subtle date. | +| S02 | Creator Freshness + Homepage Card Dates | low | — | ✅ | Creators browse page shows 'Last updated: Apr 3' per creator. Homepage recently-added cards show a subtle date. | | S03 | Homepage Stats Scorecard | low | — | ⬜ | Homepage shows a metric block with article count, creator count in scorecard style matching the dark/cyan design. | | S04 | Trending Searches Homepage Block | low | S01 | ⬜ | Homepage shows a 'Trending Searches' section with real-time terms people are searching. | | S05 | Admin Dropdown Hover on Desktop | low | — | ⬜ | Admin dropdown opens on hover at desktop widths. On mobile, opens on tap. | diff --git a/.gsd/milestones/M015/slices/S02/S02-SUMMARY.md b/.gsd/milestones/M015/slices/S02/S02-SUMMARY.md new file mode 100644 index 0000000..a03d114 --- /dev/null +++ b/.gsd/milestones/M015/slices/S02/S02-SUMMARY.md @@ -0,0 +1,92 @@ +--- +id: S02 +parent: M015 +milestone: M015 +provides: + - last_technique_at field on /api/v1/creators response + - Creator freshness dates on browse page + - Date stamps on homepage recently-added cards +requires: + [] +affects: + [] +key_files: + - backend/schemas.py + - backend/routers/creators.py + - frontend/src/api/public-client.ts + - frontend/src/pages/CreatorsBrowse.tsx + - frontend/src/pages/Home.tsx + - frontend/src/App.css +key_decisions: + - Used correlated scalar subquery for last_technique_at to preserve existing query structure + - Used opacity-reduced --color-text-secondary for muted date styling +patterns_established: + - (none) +observability_surfaces: + - none +drill_down_paths: + - .gsd/milestones/M015/slices/S02/tasks/T01-SUMMARY.md + - .gsd/milestones/M015/slices/S02/tasks/T02-SUMMARY.md +duration: "" +verification_result: passed +completed_at: 2026-04-03T04:21:37.819Z +blocker_discovered: false +--- + +# S02: Creator Freshness + Homepage Card Dates + +**Added last_technique_at to the creators API and rendered freshness dates on both the creators browse page and homepage recently-added cards.** + +## What Happened + +Two tasks delivered this slice end-to-end. + +T01 added a correlated scalar subquery (`MAX(technique_pages.created_at)`) to the `list_creators` endpoint, exposed via a new `last_technique_at` field on the `CreatorBrowseItem` schema. The subquery preserves the existing query structure — no JOINs or GROUP BY changes. Deployed and verified on ub01: all 5 creators with techniques return datetime values, Caracal Project (0 techniques) returns null. + +T02 added the field to the frontend TypeScript interface, rendered "Last updated: Mon D" on creator browse rows (only when non-null), and added `.recent-card__date` elements on homepage recently-added cards with muted styling (opacity-reduced secondary text). Rebuilt and deployed the web container. Browser assertions confirmed dates appear in short format (Apr 2, Apr 3) on both pages. + +## Verification + +Slice-level verification on live deployment (ub01:8096): + +1. **API field presence:** `curl /api/v1/creators?limit=5 | grep last_technique_at` — field present on all items, non-null datetimes for creators with techniques. +2. **Creators browse page:** browser_assert confirmed "Last updated" text visible and `.creator-row__updated` selector present. Caracal Project (0 techniques) has no date element. +3. **Homepage cards:** browser_assert confirmed `.recent-card__date` selector visible, "Apr 3" and "Apr 2" text visible in recently-added section. +4. **Date format:** All dates use short format (Mon D), not full dates or ISO strings. + +## Requirements Advanced + +- R033 — Validated — creators browse shows last updated dates, homepage cards show date stamps, both verified live + +## Requirements Validated + +- R033 — Browser assertions on ub01:8096 confirm dates on creators page and homepage cards in short Mon D format + +## New Requirements Surfaced + +None. + +## Requirements Invalidated or Re-scoped + +None. + +## Deviations + +Docker Compose service name is chrysopedia-web, not chrysopedia-web-8096 as stated in the plan. SPA curl verification replaced with browser_assert since HTML is rendered client-side. + +## Known Limitations + +None. + +## Follow-ups + +None. + +## Files Created/Modified + +- `backend/schemas.py` — Added last_technique_at: datetime | None to CreatorBrowseItem +- `backend/routers/creators.py` — Added correlated MAX(created_at) subquery and row unpacking for last_technique_at +- `frontend/src/api/public-client.ts` — Added last_technique_at to CreatorBrowseItem interface +- `frontend/src/pages/CreatorsBrowse.tsx` — Render Last updated: Mon D on creator rows when non-null +- `frontend/src/pages/Home.tsx` — Render .recent-card__date on recently-added cards +- `frontend/src/App.css` — Added .creator-row__updated and .recent-card__date styles diff --git a/.gsd/milestones/M015/slices/S02/S02-UAT.md b/.gsd/milestones/M015/slices/S02/S02-UAT.md new file mode 100644 index 0000000..9b06895 --- /dev/null +++ b/.gsd/milestones/M015/slices/S02/S02-UAT.md @@ -0,0 +1,46 @@ +# S02: Creator Freshness + Homepage Card Dates — UAT + +**Milestone:** M015 +**Written:** 2026-04-03T04:21:37.819Z + +## UAT: Creator Freshness + Homepage Card Dates + +### Preconditions +- Chrysopedia running on ub01:8096 +- At least one creator with 0 techniques (Caracal Project) +- At least one creator with techniques (COPYCATT, Skope, etc.) + +### Test Cases + +#### TC1: API returns last_technique_at field +1. `curl -s http://ub01:8096/api/v1/creators?limit=6 | python3 -m json.tool` +2. **Expected:** Every item in the response has a `last_technique_at` field +3. Creators with techniques: field is an ISO datetime string (e.g., `2026-04-02T23:19:16.258484`) +4. Caracal Project (0 techniques): field is `null` + +#### TC2: Creators browse page shows freshness dates +1. Navigate to http://ub01:8096/creators +2. **Expected:** Each creator row with techniques shows "Last updated: Mon D" (e.g., "Last updated: Apr 2") +3. Caracal Project row shows stats but NO "Last updated" text + +#### TC3: Creators browse page date format +1. Navigate to http://ub01:8096/creators +2. **Expected:** Dates use short format: "Apr 2", "Apr 3" — NOT "April 2, 2026" or "2026-04-02" + +#### TC4: Homepage recently-added cards show dates +1. Navigate to http://ub01:8096 +2. Scroll to "Recently Added" section +3. **Expected:** Each card shows a small muted date next to or near the creator name +4. Dates use short format (e.g., "Apr 3") + +#### TC5: Date styling is muted +1. Navigate to http://ub01:8096/creators +2. **Expected:** "Last updated" text is visually muted (smaller, lighter) compared to primary stats text +3. Navigate to http://ub01:8096 +4. **Expected:** Card dates are subtle — not competing with title or creator name for visual hierarchy + +#### TC6: Pagination and sorting unaffected +1. `curl -s 'http://ub01:8096/api/v1/creators?limit=2&offset=2&sort=views'` +2. **Expected:** Returns 2 creators, both with `last_technique_at` field. Pagination works correctly. +3. `curl -s 'http://ub01:8096/api/v1/creators?sort=alpha'` +4. **Expected:** Alphabetical sort still works. `last_technique_at` present on all items. diff --git a/.gsd/milestones/M015/slices/S02/tasks/T02-VERIFY.json b/.gsd/milestones/M015/slices/S02/tasks/T02-VERIFY.json new file mode 100644 index 0000000..975783f --- /dev/null +++ b/.gsd/milestones/M015/slices/S02/tasks/T02-VERIFY.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": 1, + "taskId": "T02", + "unitId": "M015/S02/T02", + "timestamp": 1775189992286, + "passed": true, + "discoverySource": "none", + "checks": [] +} diff --git a/.gsd/milestones/M015/slices/S03/S03-PLAN.md b/.gsd/milestones/M015/slices/S03/S03-PLAN.md index ad6e6bd..bf5f972 100644 --- a/.gsd/milestones/M015/slices/S03/S03-PLAN.md +++ b/.gsd/milestones/M015/slices/S03/S03-PLAN.md @@ -1,6 +1,74 @@ # S03: Homepage Stats Scorecard -**Goal:** Add a stats API endpoint and render a scorecard block on the homepage. +**Goal:** Homepage shows a stats scorecard with live article count and creator count from the API. **Demo:** After this: Homepage shows a metric block with article count, creator count in scorecard style matching the dark/cyan design. ## Tasks +- [x] **T01: Added GET /api/v1/stats endpoint returning live technique_count (21) and creator_count (7) from PostgreSQL** — Create a new stats router with a single endpoint that returns technique_count and creator_count. Register it in main.py. + +## Steps + +1. Create `backend/routers/stats.py` with a `GET /stats` endpoint: + - Import `APIRouter`, `Depends`, `AsyncSession` from FastAPI/SQLAlchemy + - Import `get_session` from `database` + - Import `TechniquePage` and `Creator` from `models` + - Use `select(func.count()).select_from(TechniquePage)` and same for `Creator` + - Return a dict `{"technique_count": N, "creator_count": N}` + - Use `tags=["stats"]` on the router +2. Edit `backend/main.py` to add `from routers import stats` and `app.include_router(stats.router, prefix="/api/v1")` following the existing pattern (after the last `include_router` call around line 88). +3. Rebuild and restart the API container on ub01: + ``` + ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && git pull && docker compose build chrysopedia-api && docker compose up -d chrysopedia-api' + ``` +4. Verify: `curl http://ub01:8096/api/v1/stats` returns JSON with both counts > 0. + +## Must-Haves + +- [ ] Endpoint returns `{"technique_count": int, "creator_count": int}` +- [ ] Both counts are > 0 against the live database +- [ ] Router registered with `/api/v1` prefix + +## Verification + +- `curl -s http://ub01:8096/api/v1/stats | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['technique_count']>0 and d['creator_count']>0, f'Bad counts: {d}'"` exits 0 +- `curl -s -o /dev/null -w '%{http_code}' http://ub01:8096/api/v1/stats` returns 200 + - Estimate: 20m + - Files: backend/routers/stats.py, backend/main.py + - Verify: curl -s http://ub01:8096/api/v1/stats | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['technique_count']>0 and d['creator_count']>0" +- [ ] **T02: Add homepage stats scorecard section with live counts from /api/v1/stats** — Wire up the frontend to fetch stats from the new endpoint and render a scorecard section on the homepage matching the dark/cyan design. + +## Steps + +1. Edit `frontend/src/api/public-client.ts`: + - Add `StatsResponse` interface: `{ technique_count: number; creator_count: number }` + - Add `fetchStats()` function: `return request(\`${BASE}/stats\`)` — follows the exact pattern of `fetchTopics()` at line 311. +2. Edit `frontend/src/pages/Home.tsx`: + - Import `fetchStats` and `StatsResponse` (if exported as type) from `public-client` + - Add state: `const [stats, setStats] = useState<{technique_count: number; creator_count: number} | null>(null)` + - Add a `useEffect` (with cancellation flag pattern matching existing effects in the file) that calls `fetchStats()` and sets state. + - Render a `
` between the nav-cards section (ends ~line 177) and the random technique button (starts ~line 180). Show two metric items: technique count labeled "Articles" and creator count labeled "Creators". Use `card-stagger` animation class. Only render when stats is non-null. +3. Edit `frontend/src/App.css`: + - Add `.home-stats` section styles: centered flex row, gap, subtle background using `var(--surface-1)`, border using `var(--border)`. + - `.home-stats__metric`: large font for the number (use `var(--accent)` cyan color), small label below in `var(--text-secondary)`. + - Keep it subtle — communicating volume without being boastful per R034. +4. Verify `npm run build` compiles cleanly. +5. Rebuild and restart the web container on ub01: + ``` + ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && git pull && docker compose build chrysopedia-web-8096 && docker compose up -d chrysopedia-web-8096' + ``` +6. Verify the homepage at http://ub01:8096 shows the scorecard with real counts. + +## Must-Haves + +- [ ] `fetchStats()` function in public-client.ts +- [ ] Homepage renders scorecard section with technique count and creator count +- [ ] Scorecard uses existing CSS custom properties (--accent, --surface-1, --border, --text-secondary) +- [ ] `npm run build` succeeds + +## Verification + +- `cd frontend && npm run build` exits 0 +- Homepage at http://ub01:8096 displays scorecard with two metrics showing numbers > 0 + - Estimate: 30m + - Files: frontend/src/api/public-client.ts, frontend/src/pages/Home.tsx, frontend/src/App.css + - Verify: cd frontend && npm run build diff --git a/.gsd/milestones/M015/slices/S03/S03-RESEARCH.md b/.gsd/milestones/M015/slices/S03/S03-RESEARCH.md new file mode 100644 index 0000000..f692fa2 --- /dev/null +++ b/.gsd/milestones/M015/slices/S03/S03-RESEARCH.md @@ -0,0 +1,99 @@ +# S03 Research — Homepage Stats Scorecard + +**Depth:** Light — straightforward UI addition using existing data patterns. + +**Target Requirement:** R034 — Homepage Stats Scorecard (active) + +## Summary + +Add a visual stats scorecard to the homepage showing article (technique) count and creator count. No new models or migrations needed — just a lightweight backend stats endpoint and a new frontend section. + +## Recommendation + +Add a `GET /api/v1/stats` endpoint returning `{technique_count, creator_count}`. Render a scorecard section on `Home.tsx` between the nav-cards and the random-technique button (or between featured and recently-added — planner decides placement). Style with existing dark/cyan design tokens. + +## Implementation Landscape + +### Backend — New Stats Endpoint + +**File:** `backend/routers/stats.py` (new router, ~30 lines) + +Create a simple router with one endpoint: +``` +GET /api/v1/stats → { technique_count: int, creator_count: int } +``` + +Implementation: Two `SELECT COUNT(*)` queries against `TechniquePage` and `Creator` tables. Both models already exist in `backend/models.py`. + +**Registration:** Add `stats.router` to `backend/main.py` alongside existing routers (pattern: `app.include_router(stats.router, prefix="/api/v1")`). + +Existing router registration pattern in `main.py`: +```python +app.include_router(techniques.router, prefix="/api/v1") +app.include_router(creators.router, prefix="/api/v1") +# ... etc +``` + +**Alternative considered:** Reusing existing list endpoints with `limit=0` to extract `total` — rejected because it runs full query pipelines (joins, subqueries for counts per item) when we only need two simple COUNTs. + +### Frontend — API Client Addition + +**File:** `frontend/src/api/public-client.ts` + +Add: +- `StatsResponse` interface: `{ technique_count: number; creator_count: number }` +- `fetchStats()` function: `GET ${BASE}/stats` + +Pattern matches existing functions like `fetchTopics()`, `fetchRandomTechnique()`. + +### Frontend — Homepage Scorecard Section + +**File:** `frontend/src/pages/Home.tsx` + +Add a new `useEffect` + state for stats (`useState(null)`). Render a scorecard section. Placement: between the nav-cards section and the random-technique button — this puts it in the "discovery" zone of the page. + +The scorecard should show two metrics: technique count and creator count. Each metric: large number + label below. Use `card-stagger` animation class already present on other homepage sections. + +### Frontend — CSS Styling + +**File:** `frontend/src/App.css` + +Add `.home-stats` section styles. Design tokens already in use: +- Background: `var(--surface-1)` or similar dark surface +- Accent: cyan (`var(--accent)` / `#00e5ff` range used throughout) +- Text: `var(--text-primary)` / `var(--text-secondary)` +- Border: `1px solid var(--border)` pattern used on cards + +The scorecard should be subtle — communicating volume without being boastful (per R034 requirement). Two numbers in a horizontal row with small labels, possibly with a subtle border or background. + +Existing homepage section naming pattern: `.home-hero`, `.home-featured`, `.home-popular-topics`, `.home-how-it-works`. + +### Existing Patterns to Follow + +- **Router pattern:** See `backend/routers/creators.py` lines 24-106 for the simplest existing router +- **Frontend fetch pattern:** See `fetchTopics()` in `public-client.ts` (line 311) — same shape needed +- **Homepage section pattern:** Each section in `Home.tsx` has its own `useEffect` with cancellation flag, own state, and a `
` block with BEM-style classes +- **Stagger animation:** `card-stagger` class + `--stagger-index` CSS variable already used on nav-cards and recent-cards + +### Key Files + +| File | Action | Notes | +|------|--------|-------| +| `backend/routers/stats.py` | Create | New router, ~30 lines | +| `backend/main.py` | Edit | Register stats router | +| `frontend/src/api/public-client.ts` | Edit | Add StatsResponse + fetchStats | +| `frontend/src/pages/Home.tsx` | Edit | Add stats state + scorecard section | +| `frontend/src/App.css` | Edit | Add .home-stats styles | + +### Natural Task Seams + +1. **Backend stats endpoint** (stats.py + main.py registration) — verify with curl +2. **Frontend scorecard** (public-client.ts + Home.tsx + App.css) — verify with browser + +Two tasks. Backend first since frontend depends on it. Both are small. + +### Verification + +- `curl http://ub01:8096/api/v1/stats` returns `{"technique_count": N, "creator_count": N}` with N > 0 +- Homepage at `http://ub01:8096` shows scorecard block with real counts +- `npm run build` succeeds (frontend compiles) diff --git a/.gsd/milestones/M015/slices/S03/tasks/T01-PLAN.md b/.gsd/milestones/M015/slices/S03/tasks/T01-PLAN.md new file mode 100644 index 0000000..09bfb70 --- /dev/null +++ b/.gsd/milestones/M015/slices/S03/tasks/T01-PLAN.md @@ -0,0 +1,51 @@ +--- +estimated_steps: 22 +estimated_files: 2 +skills_used: [] +--- + +# T01: Add GET /api/v1/stats endpoint returning technique and creator counts + +Create a new stats router with a single endpoint that returns technique_count and creator_count. Register it in main.py. + +## Steps + +1. Create `backend/routers/stats.py` with a `GET /stats` endpoint: + - Import `APIRouter`, `Depends`, `AsyncSession` from FastAPI/SQLAlchemy + - Import `get_session` from `database` + - Import `TechniquePage` and `Creator` from `models` + - Use `select(func.count()).select_from(TechniquePage)` and same for `Creator` + - Return a dict `{"technique_count": N, "creator_count": N}` + - Use `tags=["stats"]` on the router +2. Edit `backend/main.py` to add `from routers import stats` and `app.include_router(stats.router, prefix="/api/v1")` following the existing pattern (after the last `include_router` call around line 88). +3. Rebuild and restart the API container on ub01: + ``` + ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && git pull && docker compose build chrysopedia-api && docker compose up -d chrysopedia-api' + ``` +4. Verify: `curl http://ub01:8096/api/v1/stats` returns JSON with both counts > 0. + +## Must-Haves + +- [ ] Endpoint returns `{"technique_count": int, "creator_count": int}` +- [ ] Both counts are > 0 against the live database +- [ ] Router registered with `/api/v1` prefix + +## Verification + +- `curl -s http://ub01:8096/api/v1/stats | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['technique_count']>0 and d['creator_count']>0, f'Bad counts: {d}'"` exits 0 +- `curl -s -o /dev/null -w '%{http_code}' http://ub01:8096/api/v1/stats` returns 200 + +## Inputs + +- ``backend/models.py` — TechniquePage and Creator model definitions` +- ``backend/database.py` — get_session dependency` +- ``backend/main.py` — router registration pattern (lines 78-88)` + +## Expected Output + +- ``backend/routers/stats.py` — new stats router with GET /stats endpoint` +- ``backend/main.py` — edited to register stats router` + +## Verification + +curl -s http://ub01:8096/api/v1/stats | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['technique_count']>0 and d['creator_count']>0" diff --git a/.gsd/milestones/M015/slices/S03/tasks/T01-SUMMARY.md b/.gsd/milestones/M015/slices/S03/tasks/T01-SUMMARY.md new file mode 100644 index 0000000..190e089 --- /dev/null +++ b/.gsd/milestones/M015/slices/S03/tasks/T01-SUMMARY.md @@ -0,0 +1,77 @@ +--- +id: T01 +parent: S03 +milestone: M015 +provides: [] +requires: [] +affects: [] +key_files: ["backend/routers/stats.py", "backend/main.py"] +key_decisions: ["Used `or 0` fallback on scalar results for safety against empty tables"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "Both slice-level verification commands pass: JSON assertion confirms technique_count=21 and creator_count=7 (both >0), HTTP status returns 200." +completed_at: 2026-04-03T04:25:55.041Z +blocker_discovered: false +--- + +# T01: Added GET /api/v1/stats endpoint returning live technique_count (21) and creator_count (7) from PostgreSQL + +> Added GET /api/v1/stats endpoint returning live technique_count (21) and creator_count (7) from PostgreSQL + +## What Happened +--- +id: T01 +parent: S03 +milestone: M015 +key_files: + - backend/routers/stats.py + - backend/main.py +key_decisions: + - Used `or 0` fallback on scalar results for safety against empty tables +duration: "" +verification_result: passed +completed_at: 2026-04-03T04:25:55.042Z +blocker_discovered: false +--- + +# T01: Added GET /api/v1/stats endpoint returning live technique_count (21) and creator_count (7) from PostgreSQL + +**Added GET /api/v1/stats endpoint returning live technique_count (21) and creator_count (7) from PostgreSQL** + +## What Happened + +Created backend/routers/stats.py with an async endpoint running COUNT(*) queries against TechniquePage and Creator tables. Registered the router in main.py with /api/v1 prefix. Pushed to GitHub, rebuilt the API container on ub01, and verified both counts are positive against the live database. + +## Verification + +Both slice-level verification commands pass: JSON assertion confirms technique_count=21 and creator_count=7 (both >0), HTTP status returns 200. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `curl -s http://ub01:8096/api/v1/stats | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['technique_count']>0 and d['creator_count']>0"` | 0 | ✅ pass | 1000ms | +| 2 | `curl -s -o /dev/null -w '%{http_code}' http://ub01:8096/api/v1/stats` | 0 | ✅ pass (200) | 1000ms | + + +## Deviations + +None. + +## Known Issues + +None. + +## Files Created/Modified + +- `backend/routers/stats.py` +- `backend/main.py` + + +## Deviations +None. + +## Known Issues +None. diff --git a/.gsd/milestones/M015/slices/S03/tasks/T02-PLAN.md b/.gsd/milestones/M015/slices/S03/tasks/T02-PLAN.md new file mode 100644 index 0000000..ff7b220 --- /dev/null +++ b/.gsd/milestones/M015/slices/S03/tasks/T02-PLAN.md @@ -0,0 +1,59 @@ +--- +estimated_steps: 28 +estimated_files: 3 +skills_used: [] +--- + +# T02: Add homepage stats scorecard section with live counts from /api/v1/stats + +Wire up the frontend to fetch stats from the new endpoint and render a scorecard section on the homepage matching the dark/cyan design. + +## Steps + +1. Edit `frontend/src/api/public-client.ts`: + - Add `StatsResponse` interface: `{ technique_count: number; creator_count: number }` + - Add `fetchStats()` function: `return request(\`${BASE}/stats\`)` — follows the exact pattern of `fetchTopics()` at line 311. +2. Edit `frontend/src/pages/Home.tsx`: + - Import `fetchStats` and `StatsResponse` (if exported as type) from `public-client` + - Add state: `const [stats, setStats] = useState<{technique_count: number; creator_count: number} | null>(null)` + - Add a `useEffect` (with cancellation flag pattern matching existing effects in the file) that calls `fetchStats()` and sets state. + - Render a `
` between the nav-cards section (ends ~line 177) and the random technique button (starts ~line 180). Show two metric items: technique count labeled "Articles" and creator count labeled "Creators". Use `card-stagger` animation class. Only render when stats is non-null. +3. Edit `frontend/src/App.css`: + - Add `.home-stats` section styles: centered flex row, gap, subtle background using `var(--surface-1)`, border using `var(--border)`. + - `.home-stats__metric`: large font for the number (use `var(--accent)` cyan color), small label below in `var(--text-secondary)`. + - Keep it subtle — communicating volume without being boastful per R034. +4. Verify `npm run build` compiles cleanly. +5. Rebuild and restart the web container on ub01: + ``` + ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && git pull && docker compose build chrysopedia-web-8096 && docker compose up -d chrysopedia-web-8096' + ``` +6. Verify the homepage at http://ub01:8096 shows the scorecard with real counts. + +## Must-Haves + +- [ ] `fetchStats()` function in public-client.ts +- [ ] Homepage renders scorecard section with technique count and creator count +- [ ] Scorecard uses existing CSS custom properties (--accent, --surface-1, --border, --text-secondary) +- [ ] `npm run build` succeeds + +## Verification + +- `cd frontend && npm run build` exits 0 +- Homepage at http://ub01:8096 displays scorecard with two metrics showing numbers > 0 + +## Inputs + +- ``backend/routers/stats.py` — stats endpoint created in T01` +- ``frontend/src/api/public-client.ts` — existing fetch pattern to follow` +- ``frontend/src/pages/Home.tsx` — homepage component to add scorecard section` +- ``frontend/src/App.css` — stylesheet for scorecard styles` + +## Expected Output + +- ``frontend/src/api/public-client.ts` — StatsResponse interface and fetchStats() added` +- ``frontend/src/pages/Home.tsx` — stats scorecard section rendered between nav-cards and random button` +- ``frontend/src/App.css` — .home-stats section styles added` + +## Verification + +cd frontend && npm run build