diff --git a/.gsd/milestones/M008/M008-ROADMAP.md b/.gsd/milestones/M008/M008-ROADMAP.md
index f8a6366..f6a1b6d 100644
--- a/.gsd/milestones/M008/M008-ROADMAP.md
+++ b/.gsd/milestones/M008/M008-ROADMAP.md
@@ -7,5 +7,5 @@ Fix the broken and embarrassing things first. Key moment links that 404, test da
| ID | Slice | Risk | Depends | Done | After this |
|----|-------|------|---------|------|------------|
| S01 | Fix Key Moment Search Links | high | — | ✅ | Search 'compression', click any key moment result → lands on parent technique page with key moment visible |
-| S02 | Trust & Credibility Cleanup | low | — | ⬜ | Creators page has no TestCreator. Footer shows clean version info. Search results have no yellow jargon banner. |
+| S02 | Trust & Credibility Cleanup | low | — | ✅ | Creators page has no TestCreator. Footer shows clean version info. Search results have no yellow jargon banner. |
| S03 | Homepage Cards & Creator Metric Polish | low | — | ⬜ | Homepage technique cards show sub-topic tags and key moment count. Creator pages show technique count by topic instead of '0 views'. |
diff --git a/.gsd/milestones/M008/slices/S02/S02-SUMMARY.md b/.gsd/milestones/M008/slices/S02/S02-SUMMARY.md
new file mode 100644
index 0000000..e48fd1a
--- /dev/null
+++ b/.gsd/milestones/M008/slices/S02/S02-SUMMARY.md
@@ -0,0 +1,100 @@
+---
+id: S02
+parent: M008
+milestone: M008
+provides:
+ - Hidden creator filtering in list_creators()
+ - Clean search results without jargon banner
+ - v0.8.0 version identifier
+requires:
+ []
+affects:
+ - S03
+key_files:
+ - backend/models.py
+ - backend/routers/creators.py
+ - alembic/versions/009_add_creator_hidden_flag.py
+ - frontend/src/pages/SearchResults.tsx
+ - frontend/src/App.css
+ - frontend/src/components/AppFooter.tsx
+ - frontend/package.json
+key_decisions:
+ - Manual migration instead of autogenerate (no DB connection from dev machine)
+ - Removed fallbackUsed state entirely since TS strict mode flags unused variables
+patterns_established:
+ - Soft-delete via hidden boolean column for excluding records from public-facing queries while preserving data
+observability_surfaces:
+ - none
+drill_down_paths:
+ - .gsd/milestones/M008/slices/S02/tasks/T01-SUMMARY.md
+ - .gsd/milestones/M008/slices/S02/tasks/T02-SUMMARY.md
+duration: ""
+verification_result: passed
+completed_at: 2026-03-31T05:15:54.785Z
+blocker_discovered: false
+---
+
+# S02: Trust & Credibility Cleanup
+
+**Removed test data from Creators page, eliminated yellow jargon banner from search results, cleaned up footer version display, and bumped to v0.8.0.**
+
+## What Happened
+
+Two tasks addressed three credibility issues:
+
+**T01 — Hide TestCreator:** Added a `hidden` boolean column to the Creator model (server_default='false'). Migration 009 adds the column and marks slug='testcreator' as hidden=true. The `list_creators()` endpoint now filters `Creator.hidden != True` on both the main query and total count query, so hidden creators don't appear in the browse page or affect pagination.
+
+**T02 — Frontend cleanup (3 items):**
+1. Removed the yellow "semantic search unavailable" fallback banner from SearchResults.tsx — both the JSX block and the `.search-fallback-banner` CSS rule. Also removed the `fallbackUsed` state variable (TS strict mode required it since the variable became unused after banner removal).
+2. Simplified AppFooter to hide the commit info section entirely when `__GIT_COMMIT__` is 'dev', instead of displaying the unhelpful plain text "dev".
+3. Bumped package.json version from 0.1.0 to 0.8.0.
+
+Frontend build passes with zero errors.
+
+## Verification
+
+All slice-level checks pass:
+- Creator model includes 'hidden' column (Python import check)
+- No 'search-fallback-banner' references in SearchResults.tsx or App.css (grep returns 0 matches)
+- package.json version is "0.8.0"
+- `npm run build` succeeds with zero errors (built in 775ms)
+
+## Requirements Advanced
+
+- R007 — Creators browse page now filters out test/hidden creators, showing only real content
+- R005 — Search results no longer show misleading yellow fallback banner
+
+## Requirements Validated
+
+None.
+
+## New Requirements Surfaced
+
+None.
+
+## Requirements Invalidated or Re-scoped
+
+None.
+
+## Deviations
+
+1. Migration written manually instead of via `alembic revision --autogenerate` — no DB connection from dev machine. Must verify model-migration sync on ub01.
+2. Removed `fallbackUsed` state variable entirely — plan said to keep it but TS strict mode flagged it as unused after banner JSX removal, which would break the build.
+
+## Known Limitations
+
+alembic check cannot run locally — migration 009 must be verified and applied on ub01 during deployment.
+
+## Follow-ups
+
+Run `docker exec chrysopedia-api alembic upgrade head` on ub01 to apply migration 009 and verify TestCreator is hidden.
+
+## Files Created/Modified
+
+- `backend/models.py` — Added hidden: Mapped[bool] column to Creator class
+- `backend/routers/creators.py` — Added Creator.hidden != True filter to list_creators() query and count query
+- `alembic/versions/009_add_creator_hidden_flag.py` — New migration: adds hidden column and marks testcreator as hidden
+- `frontend/src/pages/SearchResults.tsx` — Removed fallback banner JSX and fallbackUsed state
+- `frontend/src/App.css` — Removed .search-fallback-banner CSS rule
+- `frontend/src/components/AppFooter.tsx` — Hide commit section when __GIT_COMMIT__ is 'dev'
+- `frontend/package.json` — Version bumped from 0.1.0 to 0.8.0
diff --git a/.gsd/milestones/M008/slices/S02/S02-UAT.md b/.gsd/milestones/M008/slices/S02/S02-UAT.md
new file mode 100644
index 0000000..3f58998
--- /dev/null
+++ b/.gsd/milestones/M008/slices/S02/S02-UAT.md
@@ -0,0 +1,41 @@
+# S02: Trust & Credibility Cleanup — UAT
+
+**Milestone:** M008
+**Written:** 2026-03-31T05:15:54.785Z
+
+## UAT: S02 — Trust & Credibility Cleanup
+
+### Preconditions
+- Migration 009 applied on ub01 (`docker exec chrysopedia-api alembic upgrade head`)
+- Frontend rebuilt and deployed (`docker compose build chrysopedia-web-8096 && docker compose up -d chrysopedia-web-8096`)
+
+### Test 1: TestCreator hidden from Creators page
+1. Navigate to http://ub01:8096/creators
+2. Scroll through the full creator list
+3. **Expected:** No creator named "TestCreator" or with slug "testcreator" appears
+4. Verify the total creator count in the page header does not include the hidden creator
+
+### Test 2: Hidden creator not in search results
+1. Navigate to http://ub01:8096
+2. Type "TestCreator" in the search bar
+3. **Expected:** No creator result for TestCreator appears (technique pages referencing test content may still appear — that's acceptable)
+
+### Test 3: Search results have no yellow banner
+1. Navigate to http://ub01:8096
+2. Search for any term (e.g., "compression")
+3. **Expected:** Results appear with no yellow "semantic search unavailable" banner, regardless of whether Qdrant is available or not
+4. If Qdrant is down, results should still appear via keyword fallback — just without the banner
+
+### Test 4: Footer version display
+1. Navigate to any page on http://ub01:8096
+2. Scroll to the footer
+3. **Expected:** Footer shows "v0.8.0" with no "dev" commit hash text visible
+4. When deployed with a real GIT_COMMIT build arg, footer should show the commit as a clickable GitHub link
+
+### Test 5: Frontend build integrity
+1. On the dev machine, run `cd frontend && npm run build`
+2. **Expected:** Build succeeds with zero errors and zero warnings about unused variables
+
+### Edge Cases
+- **Direct URL to hidden creator:** Navigate to http://ub01:8096/creators/testcreator — the creator detail page may still load (the hidden filter is only on the list endpoint). This is acceptable for M008; a future milestone could add hidden filtering to the detail endpoint.
+- **New creators default visible:** Any creator added after migration 009 should have hidden=false by default and appear normally in the list.
diff --git a/.gsd/milestones/M008/slices/S02/tasks/T02-VERIFY.json b/.gsd/milestones/M008/slices/S02/tasks/T02-VERIFY.json
new file mode 100644
index 0000000..21076af
--- /dev/null
+++ b/.gsd/milestones/M008/slices/S02/tasks/T02-VERIFY.json
@@ -0,0 +1,24 @@
+{
+ "schemaVersion": 1,
+ "taskId": "T02",
+ "unitId": "M008/S02/T02",
+ "timestamp": 1774934098914,
+ "passed": false,
+ "discoverySource": "task-plan",
+ "checks": [
+ {
+ "command": "cd frontend",
+ "exitCode": 0,
+ "durationMs": 6,
+ "verdict": "pass"
+ },
+ {
+ "command": "grep -q 'search-fallback-banner' src/pages/SearchResults.tsx",
+ "exitCode": 2,
+ "durationMs": 8,
+ "verdict": "fail"
+ }
+ ],
+ "retryAttempt": 1,
+ "maxRetries": 2
+}
diff --git a/.gsd/milestones/M008/slices/S03/S03-PLAN.md b/.gsd/milestones/M008/slices/S03/S03-PLAN.md
index f885094..1eda69c 100644
--- a/.gsd/milestones/M008/slices/S03/S03-PLAN.md
+++ b/.gsd/milestones/M008/slices/S03/S03-PLAN.md
@@ -1,6 +1,44 @@
# S03: Homepage Cards & Creator Metric Polish
-**Goal:** Richer information density on cards and honest metrics
+**Goal:** Homepage technique cards show sub-topic tag pills and key moment count. Creator detail pages show technique count by topic category instead of '0 views'.
**Demo:** After this: Homepage technique cards show sub-topic tags and key moment count. Creator pages show technique count by topic instead of '0 views'.
## Tasks
+- [x] **T01: Added key_moment_count correlated subquery to technique list API and rendered topic tag pills + moment count on homepage cards** — Two coupled changes: (1) Backend — add `key_moment_count: int = 0` field to `TechniquePageRead` schema and populate it via a correlated COUNT subquery in the `list_techniques` endpoint, matching the existing pattern in `creators.py`. (2) Frontend — add `key_moment_count: number` to the `TechniqueListItem` TypeScript interface, then render `topic_tags` as pill badges and `key_moment_count` as a small inline label in the homepage "Recently Added" cards.
+
+Backend steps:
+1. In `backend/schemas.py`, add `key_moment_count: int = 0` to `TechniquePageRead` class
+2. In `backend/routers/techniques.py` `list_techniques`, add a correlated subquery: `key_moment_count_sq = select(func.count()).where(KeyMoment.technique_page_id == TechniquePage.id).correlate(TechniquePage).scalar_subquery()` — import KeyMoment from models
+3. Add `.add_columns(key_moment_count_sq.label('key_moment_count'))` to the main query
+4. Update the result processing loop to set `item.key_moment_count` from the subquery column
+5. 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'`
+6. Verify: `curl http://ub01:8096/api/v1/techniques?limit=3 | python3 -m json.tool | grep key_moment_count`
+
+Frontend steps:
+1. In `frontend/src/api/public-client.ts`, add `key_moment_count: number` to `TechniqueListItem` interface
+2. In `frontend/src/pages/Home.tsx`, inside the `recent-card__meta` span:
+ - After the category badge, render `topic_tags` as pills: `{t.topic_tags && t.topic_tags.length > 0 && t.topic_tags.map(tag => {tag})}`
+ - After the summary, render key moment count: `{t.key_moment_count > 0 && {t.key_moment_count} moment{t.key_moment_count !== 1 ? 's' : ''}}`
+3. In `frontend/src/App.css`, add minimal styles for `.recent-card__moments` (small, muted text)
+4. Run `npx tsc --noEmit` and `npm run build` to verify
+5. Rebuild web container: `ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && docker compose build chrysopedia-web-8096 && docker compose up -d chrysopedia-web-8096'`
+
+Note on the subquery approach: The `list_techniques` endpoint currently uses `select(TechniquePage)` and then `.options(selectinload(...))`. Adding `.add_columns()` changes the result shape from scalar `TechniquePage` objects to `Row` tuples of `(TechniquePage, int)`. The loop that processes results needs to unpack accordingly: `for row in result: p = row[0]; count = row[1]`. Check the existing pattern in `routers/creators.py` lines 37-65 for reference.
+ - Estimate: 45m
+ - Files: backend/schemas.py, backend/routers/techniques.py, frontend/src/api/public-client.ts, frontend/src/pages/Home.tsx, frontend/src/App.css
+ - Verify: curl -s http://ub01:8096/api/v1/techniques?limit=3 | python3 -c "import sys,json; items=json.load(sys.stdin)['items']; assert all('key_moment_count' in i for i in items), 'missing key_moment_count'; print('API OK')" && cd frontend && npx tsc --noEmit && npm run build
+- [ ] **T02: Replace 'views' stat with topic-category breakdown on creator detail page** — Replace the meaningless '0 views' display on the creator detail page with a compact topic-category breakdown derived from the already-fetched techniques array.
+
+Steps:
+1. In `frontend/src/pages/CreatorDetail.tsx`, find the stats span showing `{creator.view_count.toLocaleString()} views` (around line 110)
+2. Compute a topic category map from the techniques array: `const topicCounts = techniques.reduce((acc, t) => { const cat = t.topic_category || 'Uncategorized'; acc[cat] = (acc[cat] || 0) + 1; return acc; }, {} as Record);`
+3. Replace the views span with the category breakdown. Format: pills or dot-separated like "Mixing: 4 · Synthesis: 2". If techniques array is empty, show nothing or a subtle 'No techniques' note.
+4. In `frontend/src/App.css`, add minimal styling for the topic breakdown display if needed — the existing `.creator-detail__stats` flex layout should accommodate pills or inline text.
+5. Handle edge case: if techniques haven't loaded yet (loading state), don't show stale '0 views' — wait for techniques or show nothing.
+6. Run `npx tsc --noEmit` and `npm run build`
+7. Rebuild web container on ub01
+
+This task is independent of T01 — all data needed (techniques with topic_category) is already fetched by the existing `fetchTechniques` call.
+ - Estimate: 25m
+ - Files: frontend/src/pages/CreatorDetail.tsx, frontend/src/App.css
+ - Verify: cd frontend && npx tsc --noEmit && npm run build && grep -q 'topic' frontend/src/pages/CreatorDetail.tsx && ! grep -q 'view_count' frontend/src/pages/CreatorDetail.tsx
diff --git a/.gsd/milestones/M008/slices/S03/S03-RESEARCH.md b/.gsd/milestones/M008/slices/S03/S03-RESEARCH.md
new file mode 100644
index 0000000..5efd1b4
--- /dev/null
+++ b/.gsd/milestones/M008/slices/S03/S03-RESEARCH.md
@@ -0,0 +1,82 @@
+# S03 Research: Homepage Cards & Creator Metric Polish
+
+## Summary
+
+Straightforward UI polish slice with two changes:
+1. **Homepage technique cards**: Add sub-topic tag pills and key moment count to the "Recently Added" cards on `Home.tsx`
+2. **Creator detail page**: Replace the meaningless "0 views" stat with technique count grouped by topic category
+
+## Recommendation
+
+Light frontend work + one small backend addition. No new libraries, no architectural changes, no risky integration.
+
+## Implementation Landscape
+
+### Change 1: Homepage cards — sub-topic tags + key moment count
+
+**Frontend** (`frontend/src/pages/Home.tsx`, lines ~166-185):
+- `recent-card` already renders `topic_category` badge and `summary`
+- `TechniqueListItem` type already has `topic_tags: string[] | null` — it's just not rendered in the card
+- **Missing**: `key_moment_count` is not in `TechniqueListItem` or `TechniquePageRead` schema
+
+**Backend** (`backend/schemas.py`, `backend/routers/techniques.py`):
+- `TechniquePageRead` (line 132) needs a new `key_moment_count: int = 0` field
+- `list_techniques` endpoint (line 33) currently does `selectinload(TechniquePage.creator)` — needs to also count key moments
+- Two approaches for count:
+ - **Subquery** (preferred): `select(func.count()).where(KeyMoment.technique_page_id == TechniquePage.id).correlate(TechniquePage).scalar_subquery()` — same pattern already used in `creators.py` for `technique_count_sq`
+ - **selectinload + len**: Loads all key moments just to count them — wasteful
+- The subquery approach matches the existing codebase pattern in `routers/creators.py` lines 42-47
+
+**Frontend type** (`frontend/src/api/public-client.ts`):
+- Add `key_moment_count: number` to `TechniqueListItem` interface (line ~87)
+
+**CSS** (`frontend/src/App.css`):
+- `recent-card__meta` (line 1100) already uses flex + gap + wrap — tag pills will flow naturally
+- Existing `.pill` class can be reused for sub-topic tags
+- Key moment count needs a small inline element — pattern: `3 moments`
+
+### Change 2: Creator detail — technique count by topic instead of "0 views"
+
+**Frontend** (`frontend/src/pages/CreatorDetail.tsx`, lines ~90-96):
+- Currently shows: `{creator.view_count.toLocaleString()} views`
+- The `techniques` array is already fetched (line 39: `fetchTechniques({ creator_slug: slug, limit: 100 })`)
+- Each `TechniqueListItem` has `topic_category` — group with a simple reduce to get `{category: count}` map
+- Replace the views span with pills like `"Mixing: 4 · Synthesis: 2 · Sound Design: 1"` or similar compact display
+
+**Backend**: No backend change needed — all data already present in the frontend.
+
+**CSS**: Minimal — the existing `.creator-detail__stats` (line 1830) styles work. May need small additions for the category breakdown display.
+
+### Files to modify
+
+| File | Change |
+|------|--------|
+| `backend/schemas.py` | Add `key_moment_count: int = 0` to `TechniquePageRead` |
+| `backend/routers/techniques.py` | Add key_moment count subquery to `list_techniques`, populate field in loop |
+| `frontend/src/api/public-client.ts` | Add `key_moment_count: number` to `TechniqueListItem` |
+| `frontend/src/pages/Home.tsx` | Render `topic_tags` pills and `key_moment_count` in recent cards |
+| `frontend/src/pages/CreatorDetail.tsx` | Replace `view_count` display with topic-category breakdown derived from `techniques` array |
+| `frontend/src/App.css` | Small additions for key moment count display and any creator topic breakdown styling |
+
+### Natural task seams
+
+1. **Backend: Add key_moment_count to technique list endpoint** — schema change + subquery + verify via curl. Independent, unblocks T2.
+2. **Frontend: Homepage card enhancements** — render topic_tags + key_moment_count in Home.tsx recent cards + CSS. Depends on T1.
+3. **Frontend: Creator detail metric polish** — replace views with topic breakdown in CreatorDetail.tsx + CSS. Independent of T1/T2.
+
+T1 and T3 can run in parallel. T2 depends on T1.
+
+### Verification approach
+
+- `cd backend && python -c "from schemas import TechniquePageRead; print(TechniquePageRead.model_fields.keys())"` — confirm field exists
+- `curl http://ub01:8096/api/v1/techniques?limit=3 | python -m json.tool` — confirm `key_moment_count` appears in response
+- `cd frontend && npx tsc --noEmit` — zero TypeScript errors
+- `cd frontend && npm run build` — production build succeeds
+- Visual: homepage cards show tag pills and "N moments" text
+- Visual: creator detail page shows topic breakdown instead of "0 views"
+
+### Constraints / edge cases
+
+- Some techniques may have 0 key moments — display should handle gracefully (omit or show "0 moments")
+- Some techniques may have null `topic_tags` — already handled by the card pattern on CreatorDetail.tsx (conditional render)
+- The `techniques` fetch on CreatorDetail uses `limit: 100` — sufficient for current data but won't scale to creators with 100+ techniques. Noted but not in scope.
diff --git a/.gsd/milestones/M008/slices/S03/tasks/T01-PLAN.md b/.gsd/milestones/M008/slices/S03/tasks/T01-PLAN.md
new file mode 100644
index 0000000..0c154db
--- /dev/null
+++ b/.gsd/milestones/M008/slices/S03/tasks/T01-PLAN.md
@@ -0,0 +1,49 @@
+---
+estimated_steps: 17
+estimated_files: 5
+skills_used: []
+---
+
+# T01: Add key_moment_count to technique list API and render topic tags + moment count on homepage cards
+
+Two coupled changes: (1) Backend — add `key_moment_count: int = 0` field to `TechniquePageRead` schema and populate it via a correlated COUNT subquery in the `list_techniques` endpoint, matching the existing pattern in `creators.py`. (2) Frontend — add `key_moment_count: number` to the `TechniqueListItem` TypeScript interface, then render `topic_tags` as pill badges and `key_moment_count` as a small inline label in the homepage "Recently Added" cards.
+
+Backend steps:
+1. In `backend/schemas.py`, add `key_moment_count: int = 0` to `TechniquePageRead` class
+2. In `backend/routers/techniques.py` `list_techniques`, add a correlated subquery: `key_moment_count_sq = select(func.count()).where(KeyMoment.technique_page_id == TechniquePage.id).correlate(TechniquePage).scalar_subquery()` — import KeyMoment from models
+3. Add `.add_columns(key_moment_count_sq.label('key_moment_count'))` to the main query
+4. Update the result processing loop to set `item.key_moment_count` from the subquery column
+5. 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'`
+6. Verify: `curl http://ub01:8096/api/v1/techniques?limit=3 | python3 -m json.tool | grep key_moment_count`
+
+Frontend steps:
+1. In `frontend/src/api/public-client.ts`, add `key_moment_count: number` to `TechniqueListItem` interface
+2. In `frontend/src/pages/Home.tsx`, inside the `recent-card__meta` span:
+ - After the category badge, render `topic_tags` as pills: `{t.topic_tags && t.topic_tags.length > 0 && t.topic_tags.map(tag => {tag})}`
+ - After the summary, render key moment count: `{t.key_moment_count > 0 && {t.key_moment_count} moment{t.key_moment_count !== 1 ? 's' : ''}}`
+3. In `frontend/src/App.css`, add minimal styles for `.recent-card__moments` (small, muted text)
+4. Run `npx tsc --noEmit` and `npm run build` to verify
+5. Rebuild web container: `ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && docker compose build chrysopedia-web-8096 && docker compose up -d chrysopedia-web-8096'`
+
+Note on the subquery approach: The `list_techniques` endpoint currently uses `select(TechniquePage)` and then `.options(selectinload(...))`. Adding `.add_columns()` changes the result shape from scalar `TechniquePage` objects to `Row` tuples of `(TechniquePage, int)`. The loop that processes results needs to unpack accordingly: `for row in result: p = row[0]; count = row[1]`. Check the existing pattern in `routers/creators.py` lines 37-65 for reference.
+
+## Inputs
+
+- ``backend/schemas.py` — TechniquePageRead class to extend with key_moment_count field`
+- ``backend/routers/techniques.py` — list_techniques endpoint to add count subquery`
+- ``backend/routers/creators.py` — reference for existing subquery pattern (read-only)`
+- ``frontend/src/api/public-client.ts` — TechniqueListItem interface to extend`
+- ``frontend/src/pages/Home.tsx` — recent card rendering to add tags and count`
+- ``frontend/src/App.css` — styles for new card elements`
+
+## Expected Output
+
+- ``backend/schemas.py` — TechniquePageRead with key_moment_count field`
+- ``backend/routers/techniques.py` — list_techniques with key_moment count subquery`
+- ``frontend/src/api/public-client.ts` — TechniqueListItem with key_moment_count`
+- ``frontend/src/pages/Home.tsx` — recent cards with topic_tags pills and moment count`
+- ``frontend/src/App.css` — .recent-card__moments styling`
+
+## Verification
+
+curl -s http://ub01:8096/api/v1/techniques?limit=3 | python3 -c "import sys,json; items=json.load(sys.stdin)['items']; assert all('key_moment_count' in i for i in items), 'missing key_moment_count'; print('API OK')" && cd frontend && npx tsc --noEmit && npm run build
diff --git a/.gsd/milestones/M008/slices/S03/tasks/T01-SUMMARY.md b/.gsd/milestones/M008/slices/S03/tasks/T01-SUMMARY.md
new file mode 100644
index 0000000..9e970b0
--- /dev/null
+++ b/.gsd/milestones/M008/slices/S03/tasks/T01-SUMMARY.md
@@ -0,0 +1,85 @@
+---
+id: T01
+parent: S03
+milestone: M008
+provides: []
+requires: []
+affects: []
+key_files: ["backend/schemas.py", "backend/routers/techniques.py", "frontend/src/api/public-client.ts", "frontend/src/pages/Home.tsx", "frontend/src/App.css"]
+key_decisions: ["Used correlated COUNT subquery for key_moment_count matching creators.py pattern", "Built separate base_stmt for count query to handle join filters"]
+patterns_established: []
+drill_down_paths: []
+observability_surfaces: []
+duration: ""
+verification_result: "API verification: curl confirmed all technique items include key_moment_count with real values (4, 4, 2). Frontend: tsc --noEmit passes, npm run build succeeds. Browser screenshot confirmed tag pills and moment counts render on homepage cards."
+completed_at: 2026-03-31T05:23:21.747Z
+blocker_discovered: false
+---
+
+# T01: Added key_moment_count correlated subquery to technique list API and rendered topic tag pills + moment count on homepage cards
+
+> Added key_moment_count correlated subquery to technique list API and rendered topic tag pills + moment count on homepage cards
+
+## What Happened
+---
+id: T01
+parent: S03
+milestone: M008
+key_files:
+ - backend/schemas.py
+ - backend/routers/techniques.py
+ - frontend/src/api/public-client.ts
+ - frontend/src/pages/Home.tsx
+ - frontend/src/App.css
+key_decisions:
+ - Used correlated COUNT subquery for key_moment_count matching creators.py pattern
+ - Built separate base_stmt for count query to handle join filters
+duration: ""
+verification_result: passed
+completed_at: 2026-03-31T05:23:21.748Z
+blocker_discovered: false
+---
+
+# T01: Added key_moment_count correlated subquery to technique list API and rendered topic tag pills + moment count on homepage cards
+
+**Added key_moment_count correlated subquery to technique list API and rendered topic tag pills + moment count on homepage cards**
+
+## What Happened
+
+Backend: Added key_moment_count field to TechniquePageRead schema and correlated COUNT subquery in list_techniques endpoint following the creators.py pattern. Frontend: Added key_moment_count to TechniqueListItem interface, rendered topic_tags as pill badges and moment count on homepage recent cards. Deployed to ub01 and verified API returns real counts and homepage renders correctly.
+
+## Verification
+
+API verification: curl confirmed all technique items include key_moment_count with real values (4, 4, 2). Frontend: tsc --noEmit passes, npm run build succeeds. Browser screenshot confirmed tag pills and moment counts render on homepage cards.
+
+## Verification Evidence
+
+| # | Command | Exit Code | Verdict | Duration |
+|---|---------|-----------|---------|----------|
+| 1 | `curl -s http://ub01:8096/api/v1/techniques?limit=3 | python3 -c "...assert all('key_moment_count' in i for i in items)..."` | 0 | ✅ pass | 1000ms |
+| 2 | `npx tsc --noEmit` | 0 | ✅ pass | 8000ms |
+| 3 | `npm run build` | 0 | ✅ pass | 1000ms |
+
+
+## Deviations
+
+Used separate base_stmt for total count query instead of stmt.subquery() to handle join filters cleanly. Docker service name is chrysopedia-web not chrysopedia-web-8096.
+
+## Known Issues
+
+None.
+
+## Files Created/Modified
+
+- `backend/schemas.py`
+- `backend/routers/techniques.py`
+- `frontend/src/api/public-client.ts`
+- `frontend/src/pages/Home.tsx`
+- `frontend/src/App.css`
+
+
+## Deviations
+Used separate base_stmt for total count query instead of stmt.subquery() to handle join filters cleanly. Docker service name is chrysopedia-web not chrysopedia-web-8096.
+
+## Known Issues
+None.
diff --git a/.gsd/milestones/M008/slices/S03/tasks/T02-PLAN.md b/.gsd/milestones/M008/slices/S03/tasks/T02-PLAN.md
new file mode 100644
index 0000000..d5fb308
--- /dev/null
+++ b/.gsd/milestones/M008/slices/S03/tasks/T02-PLAN.md
@@ -0,0 +1,34 @@
+---
+estimated_steps: 10
+estimated_files: 2
+skills_used: []
+---
+
+# T02: Replace 'views' stat with topic-category breakdown on creator detail page
+
+Replace the meaningless '0 views' display on the creator detail page with a compact topic-category breakdown derived from the already-fetched techniques array.
+
+Steps:
+1. In `frontend/src/pages/CreatorDetail.tsx`, find the stats span showing `{creator.view_count.toLocaleString()} views` (around line 110)
+2. Compute a topic category map from the techniques array: `const topicCounts = techniques.reduce((acc, t) => { const cat = t.topic_category || 'Uncategorized'; acc[cat] = (acc[cat] || 0) + 1; return acc; }, {} as Record);`
+3. Replace the views span with the category breakdown. Format: pills or dot-separated like "Mixing: 4 · Synthesis: 2". If techniques array is empty, show nothing or a subtle 'No techniques' note.
+4. In `frontend/src/App.css`, add minimal styling for the topic breakdown display if needed — the existing `.creator-detail__stats` flex layout should accommodate pills or inline text.
+5. Handle edge case: if techniques haven't loaded yet (loading state), don't show stale '0 views' — wait for techniques or show nothing.
+6. Run `npx tsc --noEmit` and `npm run build`
+7. Rebuild web container on ub01
+
+This task is independent of T01 — all data needed (techniques with topic_category) is already fetched by the existing `fetchTechniques` call.
+
+## Inputs
+
+- ``frontend/src/pages/CreatorDetail.tsx` — creator detail page with view_count display to replace`
+- ``frontend/src/App.css` — existing styles for creator detail stats`
+
+## Expected Output
+
+- ``frontend/src/pages/CreatorDetail.tsx` — shows topic-category breakdown instead of views`
+- ``frontend/src/App.css` — any additional styling for topic breakdown display`
+
+## Verification
+
+cd frontend && npx tsc --noEmit && npm run build && grep -q 'topic' frontend/src/pages/CreatorDetail.tsx && ! grep -q 'view_count' frontend/src/pages/CreatorDetail.tsx
diff --git a/backend/routers/techniques.py b/backend/routers/techniques.py
index c4a761f..6f0944b 100644
--- a/backend/routers/techniques.py
+++ b/backend/routers/techniques.py
@@ -38,34 +38,53 @@ async def list_techniques(
db: AsyncSession = Depends(get_session),
) -> PaginatedResponse:
"""List technique pages with optional category/creator filtering."""
- stmt = select(TechniquePage)
+ # Correlated subquery for key moment count (same pattern as creators.py)
+ key_moment_count_sq = (
+ select(func.count())
+ .where(KeyMoment.technique_page_id == TechniquePage.id)
+ .correlate(TechniquePage)
+ .scalar_subquery()
+ )
+ # Build base query with filters
+ base_stmt = select(TechniquePage.id)
if category:
- stmt = stmt.where(TechniquePage.topic_category == category)
-
+ base_stmt = base_stmt.where(TechniquePage.topic_category == category)
if creator_slug:
- # Join to Creator to filter by slug
- stmt = stmt.join(Creator, TechniquePage.creator_id == Creator.id).where(
+ base_stmt = base_stmt.join(Creator, TechniquePage.creator_id == Creator.id).where(
Creator.slug == creator_slug
)
# Count total before pagination
- from sqlalchemy import func
-
- count_stmt = select(func.count()).select_from(stmt.subquery())
+ count_stmt = select(func.count()).select_from(base_stmt.subquery())
count_result = await db.execute(count_stmt)
total = count_result.scalar() or 0
+ # Main query with subquery column
+ stmt = select(
+ TechniquePage,
+ key_moment_count_sq.label("key_moment_count"),
+ )
+ if category:
+ stmt = stmt.where(TechniquePage.topic_category == category)
+ if creator_slug:
+ stmt = stmt.join(Creator, TechniquePage.creator_id == Creator.id).where(
+ Creator.slug == creator_slug
+ )
+
stmt = stmt.options(selectinload(TechniquePage.creator)).order_by(TechniquePage.created_at.desc()).offset(offset).limit(limit)
result = await db.execute(stmt)
- pages = result.scalars().all()
+ rows = result.all()
items = []
- for p in pages:
+ for row in rows:
+ p = row[0]
+ km_count = row[1] or 0
item = TechniquePageRead.model_validate(p)
if p.creator:
item.creator_name = p.creator.name
item.creator_slug = p.creator.slug
+ item.key_moment_count = km_count
items.append(item)
return PaginatedResponse(
diff --git a/backend/schemas.py b/backend/schemas.py
index 50de0aa..c46b417 100644
--- a/backend/schemas.py
+++ b/backend/schemas.py
@@ -138,6 +138,7 @@ class TechniquePageRead(TechniquePageBase):
creator_slug: str = ""
source_quality: str | None = None
view_count: int = 0
+ key_moment_count: int = 0
created_at: datetime
updated_at: datetime
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 2ff3c25..8b9a806 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1110,6 +1110,17 @@ a.app-footer__repo:hover {
line-height: 1.4;
}
+.recent-card__moments {
+ font-size: 0.75rem;
+ color: var(--color-text-tertiary);
+ white-space: nowrap;
+}
+
+.pill--tag {
+ font-size: 0.625rem;
+ padding: 0 0.375rem;
+}
+
/* ── Search results page ──────────────────────────────────────────────────── */
.search-results-page {
diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts
index 5fd03b7..254ea2a 100644
--- a/frontend/src/api/public-client.ts
+++ b/frontend/src/api/public-client.ts
@@ -102,6 +102,7 @@ export interface TechniqueListItem {
creator_slug: string;
source_quality: string | null;
view_count: number;
+ key_moment_count: number;
created_at: string;
updated_at: string;
}
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index ff188e5..c89d91f 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -210,6 +210,9 @@ export default function Home() {
{t.topic_category}
+ {t.topic_tags && t.topic_tags.length > 0 && t.topic_tags.map(tag => (
+ {tag}
+ ))}
{t.summary && (
{t.summary.length > 100
@@ -217,6 +220,11 @@ export default function Home() {
: t.summary}
)}
+ {t.key_moment_count > 0 && (
+
+ {t.key_moment_count} moment{t.key_moment_count !== 1 ? 's' : ''}
+
+ )}
))}