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' : ''} + + )} ))}