feat: Added key_moment_count correlated subquery to technique list API…

- "backend/schemas.py"
- "backend/routers/techniques.py"
- "frontend/src/api/public-client.ts"
- "frontend/src/pages/Home.tsx"
- "frontend/src/App.css"

GSD-Task: S03/T01
This commit is contained in:
jlightner 2026-03-31 05:23:37 +00:00
parent deb060cfa3
commit 95b11ae5bc
14 changed files with 505 additions and 12 deletions

View file

@ -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'. |

View file

@ -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

View file

@ -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.

View file

@ -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
}

View file

@ -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 => <span key={tag} className="pill">{tag}</span>)}`
- After the summary, render key moment count: `{t.key_moment_count > 0 && <span className="recent-card__moments">{t.key_moment_count} moment{t.key_moment_count !== 1 ? 's' : ''}</span>}`
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<string, number>);`
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

View file

@ -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: `<span className="recent-card__moments">3 moments</span>`
### 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.

View file

@ -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 => <span key={tag} className="pill">{tag}</span>)}`
- After the summary, render key moment count: `{t.key_moment_count > 0 && <span className="recent-card__moments">{t.key_moment_count} moment{t.key_moment_count !== 1 ? 's' : ''}</span>}`
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

View file

@ -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.

View file

@ -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<string, number>);`
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

View file

@ -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(

View file

@ -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

View file

@ -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 {

View file

@ -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;
}

View file

@ -210,6 +210,9 @@ export default function Home() {
<span className="badge badge--category">
{t.topic_category}
</span>
{t.topic_tags && t.topic_tags.length > 0 && t.topic_tags.map(tag => (
<span key={tag} className="pill pill--tag">{tag}</span>
))}
{t.summary && (
<span className="recent-card__summary">
{t.summary.length > 100
@ -217,6 +220,11 @@ export default function Home() {
: t.summary}
</span>
)}
{t.key_moment_count > 0 && (
<span className="recent-card__moments">
{t.key_moment_count} moment{t.key_moment_count !== 1 ? 's' : ''}
</span>
)}
</span>
</Link>
))}