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:
parent
deb060cfa3
commit
95b11ae5bc
14 changed files with 505 additions and 12 deletions
|
|
@ -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'. |
|
||||
|
|
|
|||
100
.gsd/milestones/M008/slices/S02/S02-SUMMARY.md
Normal file
100
.gsd/milestones/M008/slices/S02/S02-SUMMARY.md
Normal 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
|
||||
41
.gsd/milestones/M008/slices/S02/S02-UAT.md
Normal file
41
.gsd/milestones/M008/slices/S02/S02-UAT.md
Normal 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.
|
||||
24
.gsd/milestones/M008/slices/S02/tasks/T02-VERIFY.json
Normal file
24
.gsd/milestones/M008/slices/S02/tasks/T02-VERIFY.json
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
82
.gsd/milestones/M008/slices/S03/S03-RESEARCH.md
Normal file
82
.gsd/milestones/M008/slices/S03/S03-RESEARCH.md
Normal 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.
|
||||
49
.gsd/milestones/M008/slices/S03/tasks/T01-PLAN.md
Normal file
49
.gsd/milestones/M008/slices/S03/tasks/T01-PLAN.md
Normal 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
|
||||
85
.gsd/milestones/M008/slices/S03/tasks/T01-SUMMARY.md
Normal file
85
.gsd/milestones/M008/slices/S03/tasks/T01-SUMMARY.md
Normal 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.
|
||||
34
.gsd/milestones/M008/slices/S03/tasks/T02-PLAN.md
Normal file
34
.gsd/milestones/M008/slices/S03/tasks/T02-PLAN.md
Normal 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
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue