feat: Added last_technique_at correlated subquery to creators API endpo…
- "backend/schemas.py" - "backend/routers/creators.py" GSD-Task: S02/T01
This commit is contained in:
parent
8c81c472ea
commit
18c76dd8ec
10 changed files with 498 additions and 2 deletions
|
|
@ -46,6 +46,7 @@ Fourteen milestones complete. The system is deployed and running on ub01 at `htt
|
|||
- **Multi-field composite search** — Search tokenizes multi-word queries, AND-matches each token across creator/title/tags/category/body fields. Partial matches fallback when no exact cross-field match exists. Qdrant embeddings enriched with creator names and topic tags. Admin reindex-all endpoint for re-embedding after changes.
|
||||
- **Sort controls on all list views** — Reusable SortDropdown component on SearchResults, SubTopicPage, and CreatorDetail. Sort options: relevance/newest/oldest/alpha/creator (context-appropriate per page). Preference persists in sessionStorage across navigation.
|
||||
- **Prompt quality toolkit** — CLI tool (`python -m pipeline.quality`) with: LLM fitness suite (9 tests across Mandelbrot reasoning, JSON compliance, instruction following, diverse battery), 5-dimension quality scorer with voice preservation dial (3-band prompt modification), automated prompt A/B optimization loop (LLM-powered variant generation, iterative scoring, leaderboard/trajectory reporting), multi-stage support for pipeline stages 2-5 with per-stage rubrics and fixtures.
|
||||
- **Search query logging** — All non-empty searches logged to PostgreSQL search_log table via fire-and-forget async pattern. GET /api/v1/search/popular returns top 10 queries from last 7 days with Redis read-through cache (5-min TTL).
|
||||
- **Multi-source technique pages** — Technique pages restructured to support multiple source videos per page. Nested H2/H3 body sections with table of contents and inline [N] citation markers linking prose claims to source key moments. Composition pipeline merges new video moments into existing pages with offset-based citation re-indexing and deduplication. Format-discriminated rendering (v1 dict / v2 list-of-objects) preserves backward compatibility. Per-section Qdrant embeddings with deterministic UUIDs enable section-level search results with deep-link scrolling. Admin view at /admin/techniques for multi-source page management.
|
||||
|
||||
### Stack
|
||||
|
|
@ -73,3 +74,4 @@ Fourteen milestones complete. The system is deployed and running on ub01 at `htt
|
|||
| M012 | Multi-Field Composite Search & Sort Controls | ✅ Complete |
|
||||
| M013 | Prompt Quality Toolkit — LLM Fitness, Scoring, and Automated Optimization | ✅ Complete |
|
||||
| M014 | Multi-Source Technique Pages — Nested Sections, Composition, Citations, and Section Search | ✅ Complete |
|
||||
| M015 | Social Proof & Freshness Signals | 🔧 In Progress |
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Add social proof and freshness signals to the public site: search query logging
|
|||
## Slice Overview
|
||||
| ID | Slice | Risk | Depends | Done | After this |
|
||||
|----|-------|------|---------|------|------------|
|
||||
| S01 | Search Query Logging + Popular Searches API | medium | — | ⬜ | GET /api/v1/search/popular returns top 10 queries from last 7 days, cached in Redis. search_log table has rows from real searches. |
|
||||
| S01 | Search Query Logging + Popular Searches API | medium | — | ✅ | GET /api/v1/search/popular returns top 10 queries from last 7 days, cached in Redis. search_log table has rows from real searches. |
|
||||
| S02 | Creator Freshness + Homepage Card Dates | low | — | ⬜ | Creators browse page shows 'Last updated: Apr 3' per creator. Homepage recently-added cards show a subtle date. |
|
||||
| S03 | Homepage Stats Scorecard | low | — | ⬜ | Homepage shows a metric block with article count, creator count in scorecard style matching the dark/cyan design. |
|
||||
| S04 | Trending Searches Homepage Block | low | S01 | ⬜ | Homepage shows a 'Trending Searches' section with real-time terms people are searching. |
|
||||
|
|
|
|||
88
.gsd/milestones/M015/slices/S01/S01-SUMMARY.md
Normal file
88
.gsd/milestones/M015/slices/S01/S01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
id: S01
|
||||
parent: M015
|
||||
milestone: M015
|
||||
provides:
|
||||
- GET /api/v1/search/popular endpoint returning PopularSearchesResponse JSON
|
||||
- search_log PostgreSQL table with query logging from all non-empty searches
|
||||
requires:
|
||||
[]
|
||||
affects:
|
||||
- S04
|
||||
key_files:
|
||||
- backend/models.py
|
||||
- backend/schemas.py
|
||||
- backend/routers/search.py
|
||||
- alembic/versions/013_add_search_log.py
|
||||
key_decisions:
|
||||
- D025: PostgreSQL search_log + Redis read-through cache with 5-min TTL (collaborative, already recorded)
|
||||
- Fire-and-forget asyncio.create_task with independent session for non-blocking logging
|
||||
- Integer PK for SearchLog (append-only analytics, no need for UUID)
|
||||
patterns_established:
|
||||
- Fire-and-forget async logging via asyncio.create_task with independent session and catch-all exception handling
|
||||
- Redis read-through cache pattern: check Redis → cache miss → DB query → SET with TTL → return
|
||||
observability_surfaces:
|
||||
- GET /search/popular cached flag indicates whether result came from Redis or DB
|
||||
- search_log table queryable for analytics: zero-result queries, time-of-day patterns, query frequency
|
||||
drill_down_paths:
|
||||
- .gsd/milestones/M015/slices/S01/tasks/T01-SUMMARY.md
|
||||
- .gsd/milestones/M015/slices/S01/tasks/T02-SUMMARY.md
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-04-03T04:08:38.241Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# S01: Search Query Logging + Popular Searches API
|
||||
|
||||
**Backend search query logging to PostgreSQL with fire-and-forget pattern, and GET /search/popular endpoint with Redis read-through cache (5-min TTL, 7-day window, LOWER normalization).**
|
||||
|
||||
## What Happened
|
||||
|
||||
Added SearchLog model (Integer PK, indexed query + created_at columns) with Alembic migration 013 creating the search_log table. Added PopularSearchItem and PopularSearchesResponse Pydantic schemas. Implemented non-blocking _log_search async helper that opens an independent session and catches all exceptions — hooked into the search handler via asyncio.create_task for non-empty queries only. Added GET /search/popular endpoint with Redis read-through cache (key: chrysopedia:popular_searches, 300s TTL). Cache miss queries search_log with LOWER(query) normalization and 7-day window, returning top 10. Redis failures fall through gracefully to the DB query.
|
||||
|
||||
Deployed to ub01: pushed to origin, rebuilt API/worker containers, applied Alembic migration 013, restarted nginx for stale DNS. Verified end-to-end: real searches create search_log rows, empty queries are excluded, GET /search/popular returns valid JSON with items array and cached flag. Three search_log rows confirmed after test queries.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `cd backend && python -c "from models import SearchLog; print(SearchLog.__tablename__)"` → search_log ✅
|
||||
2. `cd backend && python -c "from schemas import PopularSearchItem, PopularSearchesResponse; print('OK')"` → OK ✅
|
||||
3. `cd backend && python -c "from routers.search import router; routes = [r.path for r in router.routes]; assert '/search/popular' in routes; print('OK')"` → OK ✅
|
||||
4. `curl -sf 'http://ub01:8096/api/v1/search/popular'` → {"items":[{"query":"reverb","count":1}],"cached":true} ✅
|
||||
5. `ssh ub01 "docker exec chrysopedia-db psql -U chrysopedia -t -c 'SELECT count(*) FROM search_log;'"` → 3 ✅
|
||||
6. Empty query guard: sent empty and whitespace-only queries, count stayed at 3 ✅
|
||||
|
||||
## Requirements Advanced
|
||||
|
||||
- R035 — Backend half delivered: search queries logged to PostgreSQL, popular endpoint returns cached top-N from last 7 days. Frontend display deferred to S04.
|
||||
|
||||
## Requirements Validated
|
||||
|
||||
None.
|
||||
|
||||
## New Requirements Surfaced
|
||||
|
||||
None.
|
||||
|
||||
## Requirements Invalidated or Re-scoped
|
||||
|
||||
None.
|
||||
|
||||
## Deviations
|
||||
|
||||
Slice plan verification checks '/popular' in router routes but APIRouter stores full prefix paths ('/search/popular'). Minor spec mismatch — adjusted assertions. Docker Compose service name is 'chrysopedia-web' not 'chrysopedia-web-8096' as referenced in CLAUDE.md.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
Popular searches endpoint returns results from a single-node PostgreSQL — no read replicas. At current scale (single-admin tool) this is fine. Redis cache key is global — no per-scope popular queries yet.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
S04 (Trending Searches Homepage Block) will consume GET /search/popular from the frontend. No backend changes needed for S04.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `backend/models.py` — Added SearchLog model with Integer PK, query (indexed), scope, result_count, created_at (indexed)
|
||||
- `backend/schemas.py` — Added PopularSearchItem and PopularSearchesResponse Pydantic schemas
|
||||
- `backend/routers/search.py` — Added _log_search fire-and-forget helper and GET /search/popular endpoint with Redis cache
|
||||
- `alembic/versions/013_add_search_log.py` — Alembic migration creating search_log table with indexes on query and created_at
|
||||
55
.gsd/milestones/M015/slices/S01/S01-UAT.md
Normal file
55
.gsd/milestones/M015/slices/S01/S01-UAT.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# S01: Search Query Logging + Popular Searches API — UAT
|
||||
|
||||
**Milestone:** M015
|
||||
**Written:** 2026-04-03T04:08:38.241Z
|
||||
|
||||
## UAT: Search Query Logging + Popular Searches API
|
||||
|
||||
### Preconditions
|
||||
- Chrysopedia deployed on ub01 with containers healthy
|
||||
- Alembic migration 013 applied (search_log table exists)
|
||||
- Redis running and accessible
|
||||
|
||||
### Test 1: Search logging — normal query
|
||||
1. Run: `curl -s 'http://ub01:8096/api/v1/search?q=compression&scope=all'`
|
||||
2. Expected: 200 OK with search results JSON
|
||||
3. Run: `ssh ub01 "docker exec chrysopedia-db psql -U chrysopedia -t -c \"SELECT query, scope, result_count FROM search_log WHERE query='compression' ORDER BY created_at DESC LIMIT 1;\""`
|
||||
4. Expected: Row with `compression | all | <some count>`
|
||||
|
||||
### Test 2: Empty query not logged
|
||||
1. Note current count: `ssh ub01 "docker exec chrysopedia-db psql -U chrysopedia -t -c 'SELECT count(*) FROM search_log;'"`
|
||||
2. Run: `curl -s 'http://ub01:8096/api/v1/search?q=&scope=all'`
|
||||
3. Run: `curl -s 'http://ub01:8096/api/v1/search?q=%20&scope=all'`
|
||||
4. Re-check count (after 2s delay)
|
||||
5. Expected: Count unchanged — empty/whitespace queries are not logged
|
||||
|
||||
### Test 3: Popular searches — cache miss (fresh)
|
||||
1. Clear Redis cache: `ssh ub01 "docker exec chrysopedia-redis redis-cli DEL chrysopedia:popular_searches"`
|
||||
2. Run: `curl -s 'http://ub01:8096/api/v1/search/popular'`
|
||||
3. Expected: JSON with `{"items": [...], "cached": false}` — items array (may be empty if no recent searches)
|
||||
|
||||
### Test 4: Popular searches — cache hit
|
||||
1. Run: `curl -s 'http://ub01:8096/api/v1/search/popular'`
|
||||
2. Expected: JSON with `{"items": [...], "cached": true}` — same items, cached flag true
|
||||
|
||||
### Test 5: Popular searches — LOWER normalization
|
||||
1. Clear cache: `ssh ub01 "docker exec chrysopedia-redis redis-cli DEL chrysopedia:popular_searches"`
|
||||
2. Search with mixed case: `curl -s 'http://ub01:8096/api/v1/search?q=Reverb&scope=all'`
|
||||
3. Search lowercase: `curl -s 'http://ub01:8096/api/v1/search?q=reverb&scope=all'`
|
||||
4. Wait 2s, then: `curl -s 'http://ub01:8096/api/v1/search/popular'`
|
||||
5. Expected: "reverb" appears once in items with combined count (not two separate entries)
|
||||
|
||||
### Test 6: Popular searches — no data (empty table scenario)
|
||||
1. If possible to test on a clean DB: GET /search/popular should return `{"items": [], "cached": false}`
|
||||
2. Expected: Empty items array, no 500 error
|
||||
|
||||
### Test 7: Redis down fallback
|
||||
1. Stop Redis: `ssh ub01 "docker compose stop chrysopedia-redis"`
|
||||
2. Run: `curl -s 'http://ub01:8096/api/v1/search/popular'`
|
||||
3. Expected: JSON with items from DB (cached: false), no 500 error
|
||||
4. Restart Redis: `ssh ub01 "docker compose start chrysopedia-redis"`
|
||||
|
||||
### Edge Cases
|
||||
- Scope values: searches with scope=techniques, scope=creators should all log correctly
|
||||
- Very long query strings: should log without error (VARCHAR default limit)
|
||||
- Concurrent searches: fire-and-forget pattern should not block or corrupt other requests
|
||||
16
.gsd/milestones/M015/slices/S01/tasks/T02-VERIFY.json
Normal file
16
.gsd/milestones/M015/slices/S01/tasks/T02-VERIFY.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T02",
|
||||
"unitId": "M015/S01/T02",
|
||||
"timestamp": 1775189230294,
|
||||
"passed": true,
|
||||
"discoverySource": "task-plan",
|
||||
"checks": [
|
||||
{
|
||||
"command": "echo 'LOGGING OK'",
|
||||
"exitCode": 0,
|
||||
"durationMs": 17,
|
||||
"verdict": "pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +1,73 @@
|
|||
# S02: Creator Freshness + Homepage Card Dates
|
||||
|
||||
**Goal:** Add last_technique_at to creator API responses and display freshness dates on creator browse and homepage cards.
|
||||
**Goal:** Creators browse page shows "Last updated: Mon D" per creator. Homepage recently-added cards show a subtle date.
|
||||
**Demo:** After this: Creators browse page shows 'Last updated: Apr 3' per creator. Homepage recently-added cards show a subtle date.
|
||||
|
||||
## Tasks
|
||||
- [x] **T01: Added last_technique_at correlated subquery to creators API endpoint, returning MAX(technique_pages.created_at) per creator** — Add a correlated subquery for MAX(technique_pages.created_at) to the list_creators endpoint, expose it via the CreatorBrowseItem schema, and rebuild+deploy the API container.
|
||||
|
||||
## Steps
|
||||
|
||||
1. In `backend/schemas.py`, add `last_technique_at: datetime | None = None` to `CreatorBrowseItem` (after `video_count`).
|
||||
2. In `backend/routers/creators.py`:
|
||||
- Add a `last_technique_sq` correlated subquery: `select(func.max(TechniquePage.created_at)).where(TechniquePage.creator_id == Creator.id).correlate(Creator).scalar_subquery()`
|
||||
- Add it to the `select()` call with `.label("last_technique_at")`
|
||||
- Update the row unpacking to read index [3] as `last_technique_at`
|
||||
- Pass `last_technique_at=last_technique_at` to the `CreatorBrowseItem(...)` constructor
|
||||
3. SSH to ub01, rebuild and restart the API container:
|
||||
```
|
||||
ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && git pull && docker compose build chrysopedia-api && docker compose up -d chrysopedia-api'
|
||||
```
|
||||
4. Verify the field appears in the API response.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] `last_technique_at` field present on every item in `/api/v1/creators` response
|
||||
- [ ] Field is a datetime string for creators with techniques, null for creators with 0 techniques
|
||||
- [ ] Existing sort/filter/pagination behavior unchanged
|
||||
|
||||
## Verification
|
||||
|
||||
- `ssh ub01 'curl -s http://localhost:8096/api/v1/creators?limit=5' | python3 -m json.tool | grep last_technique_at` shows the field on each item
|
||||
- At least one item has a non-null datetime value
|
||||
- At least one item (if any creator has 0 techniques) has null
|
||||
- Estimate: 20m
|
||||
- Files: backend/schemas.py, backend/routers/creators.py
|
||||
- Verify: ssh ub01 'curl -s http://localhost:8096/api/v1/creators?limit=5' | python3 -m json.tool | grep last_technique_at
|
||||
- [ ] **T02: Render dates on creators browse page and homepage cards** — Add the `last_technique_at` field to the frontend TS interface, render it on the creators browse page, render `created_at` on homepage recently-added cards, and rebuild+deploy the web container.
|
||||
|
||||
## Steps
|
||||
|
||||
1. In `frontend/src/api/public-client.ts`, add `last_technique_at: string | null;` to the `CreatorBrowseItem` interface.
|
||||
2. In `frontend/src/pages/CreatorsBrowse.tsx`:
|
||||
- After the existing stats (technique count · video count · view count), add a separator and "Last updated: Mon D" when `creator.last_technique_at` is non-null.
|
||||
- Format: `new Date(creator.last_technique_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })`
|
||||
- Skip rendering entirely when null (no "No articles" text — just omit).
|
||||
3. In `frontend/src/pages/Home.tsx`:
|
||||
- Inside each `.recent-card`, after the creator name span, add a small date element showing `created_at`.
|
||||
- Use the same short date format (Mon D).
|
||||
- Style with a muted color and small font to keep it subtle.
|
||||
4. In `frontend/src/App.css`:
|
||||
- Add `.creator-row__updated` style: muted color (`var(--text-muted)` or similar), small font-size.
|
||||
- Add `.recent-card__date` style: muted, small, positioned after creator name.
|
||||
5. SSH to ub01, rebuild and restart the web container:
|
||||
```
|
||||
ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && git pull && docker compose build chrysopedia-web-8096 && docker compose up -d chrysopedia-web-8096'
|
||||
```
|
||||
6. Visually verify both pages in the browser.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] Creators browse page shows "Last updated: Mon D" per creator (when non-null)
|
||||
- [ ] Creators with null `last_technique_at` show no date element
|
||||
- [ ] Homepage recently-added cards show a subtle date
|
||||
- [ ] Date styling is muted and consistent with existing secondary text
|
||||
|
||||
## Verification
|
||||
|
||||
- Browse http://ub01:8096/creators — each creator row with techniques shows a last-updated date
|
||||
- Browse http://ub01:8096 — each recently-added card shows a small date
|
||||
- Dates use short format (e.g., "Apr 3" not "April 3, 2026" or "2026-04-03")
|
||||
- Estimate: 25m
|
||||
- Files: frontend/src/api/public-client.ts, frontend/src/pages/CreatorsBrowse.tsx, frontend/src/pages/Home.tsx, frontend/src/App.css
|
||||
- Verify: ssh ub01 'curl -s http://ub01:8096/creators' | grep -o 'Last updated' | head -3
|
||||
|
|
|
|||
79
.gsd/milestones/M015/slices/S02/S02-RESEARCH.md
Normal file
79
.gsd/milestones/M015/slices/S02/S02-RESEARCH.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# S02 Research — Creator Freshness + Homepage Card Dates
|
||||
|
||||
## Summary
|
||||
|
||||
Straightforward slice. Two independent changes using established patterns — no new endpoints, no migrations, no new dependencies.
|
||||
|
||||
**Requirements targeted:** R033 (Creator Last Updated Date)
|
||||
|
||||
## Recommendation
|
||||
|
||||
Wire `last_technique_at` through existing creator list endpoint (backend + frontend), and render `created_at` on homepage recently-added cards (frontend only). Two tasks, fully independent.
|
||||
|
||||
## Implementation Landscape
|
||||
|
||||
### 1. Creator "Last Updated" on Creators Browse Page
|
||||
|
||||
**Backend change — `backend/routers/creators.py`:**
|
||||
- The `list_creators` endpoint already uses correlated subqueries for `technique_count` and `video_count`. Add a third:
|
||||
```python
|
||||
last_technique_sq = (
|
||||
select(func.max(TechniquePage.created_at))
|
||||
.where(TechniquePage.creator_id == Creator.id)
|
||||
.correlate(Creator)
|
||||
.scalar_subquery()
|
||||
)
|
||||
```
|
||||
- Add it to the `select()` call and read it from the result row.
|
||||
- The field should be `datetime | None` (null if creator has 0 technique pages).
|
||||
|
||||
**Backend schema — `backend/schemas.py`:**
|
||||
- Add `last_technique_at: datetime | None = None` to `CreatorBrowseItem` (line ~358).
|
||||
|
||||
**Frontend type — `frontend/src/api/public-client.ts`:**
|
||||
- Add `last_technique_at: string | null` to the `CreatorBrowseItem` interface (line ~156).
|
||||
|
||||
**Frontend display — `frontend/src/pages/CreatorsBrowse.tsx`:**
|
||||
- In the `creator-row__stats` span (around line 152), add a "Last updated: Mon D" display after the existing stats when `last_technique_at` is non-null.
|
||||
- Format with `new Date(creator.last_technique_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })`.
|
||||
- Use muted styling consistent with other secondary text.
|
||||
|
||||
### 2. Date on Homepage Recently-Added Cards
|
||||
|
||||
**No backend change needed.** `TechniqueListItem` already includes `created_at` (confirmed in schema and TS interface).
|
||||
|
||||
**Frontend display — `frontend/src/pages/Home.tsx`:**
|
||||
- Inside the `.recent-card` Link (around line 237), render `t.created_at` as a small date badge.
|
||||
- Position: after the creator name in `.recent-card__header`, or as a subtle tag in `.recent-card__meta`.
|
||||
- Same date formatting as creators page.
|
||||
|
||||
**Frontend styling — `frontend/src/App.css`:**
|
||||
- Add minimal CSS for the date elements (muted color, small font-size, consistent with existing secondary text patterns).
|
||||
|
||||
### File Inventory
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `backend/routers/creators.py` | Add `last_technique_sq` subquery, include in select + row reading |
|
||||
| `backend/schemas.py` | Add `last_technique_at` field to `CreatorBrowseItem` |
|
||||
| `frontend/src/api/public-client.ts` | Add `last_technique_at` to TS interface |
|
||||
| `frontend/src/pages/CreatorsBrowse.tsx` | Render last updated date in creator row |
|
||||
| `frontend/src/pages/Home.tsx` | Render created_at on recently-added cards |
|
||||
| `frontend/src/App.css` | Minimal date styling |
|
||||
|
||||
### Natural Task Seams
|
||||
|
||||
**Task 1: Backend — Add `last_technique_at` to creators endpoint**
|
||||
- Files: `backend/routers/creators.py`, `backend/schemas.py`
|
||||
- Verify: `curl http://ub01:8096/api/v1/creators | python3 -m json.tool | grep last_technique_at`
|
||||
|
||||
**Task 2: Frontend — Creator dates + homepage card dates**
|
||||
- Files: `frontend/src/api/public-client.ts`, `frontend/src/pages/CreatorsBrowse.tsx`, `frontend/src/pages/Home.tsx`, `frontend/src/App.css`
|
||||
- Verify: Visual check at http://ub01:8096/creators and http://ub01:8096
|
||||
- Depends on Task 1 (needs the API field to exist)
|
||||
|
||||
### Constraints
|
||||
|
||||
- Date formatting should use short month format ("Apr 3") not full dates — keeps the UI compact.
|
||||
- `last_technique_at` will be null for creators with 0 techniques — frontend must handle this gracefully (hide or show "No articles yet").
|
||||
- The correlated subquery adds negligible cost — creator count is <100 and `technique_pages.creator_id` is already indexed via FK.
|
||||
50
.gsd/milestones/M015/slices/S02/tasks/T01-PLAN.md
Normal file
50
.gsd/milestones/M015/slices/S02/tasks/T01-PLAN.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
estimated_steps: 21
|
||||
estimated_files: 2
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T01: Add last_technique_at to creators API endpoint
|
||||
|
||||
Add a correlated subquery for MAX(technique_pages.created_at) to the list_creators endpoint, expose it via the CreatorBrowseItem schema, and rebuild+deploy the API container.
|
||||
|
||||
## Steps
|
||||
|
||||
1. In `backend/schemas.py`, add `last_technique_at: datetime | None = None` to `CreatorBrowseItem` (after `video_count`).
|
||||
2. In `backend/routers/creators.py`:
|
||||
- Add a `last_technique_sq` correlated subquery: `select(func.max(TechniquePage.created_at)).where(TechniquePage.creator_id == Creator.id).correlate(Creator).scalar_subquery()`
|
||||
- Add it to the `select()` call with `.label("last_technique_at")`
|
||||
- Update the row unpacking to read index [3] as `last_technique_at`
|
||||
- Pass `last_technique_at=last_technique_at` to the `CreatorBrowseItem(...)` constructor
|
||||
3. SSH to ub01, rebuild and restart the API container:
|
||||
```
|
||||
ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && git pull && docker compose build chrysopedia-api && docker compose up -d chrysopedia-api'
|
||||
```
|
||||
4. Verify the field appears in the API response.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] `last_technique_at` field present on every item in `/api/v1/creators` response
|
||||
- [ ] Field is a datetime string for creators with techniques, null for creators with 0 techniques
|
||||
- [ ] Existing sort/filter/pagination behavior unchanged
|
||||
|
||||
## Verification
|
||||
|
||||
- `ssh ub01 'curl -s http://localhost:8096/api/v1/creators?limit=5' | python3 -m json.tool | grep last_technique_at` shows the field on each item
|
||||
- At least one item has a non-null datetime value
|
||||
- At least one item (if any creator has 0 techniques) has null
|
||||
|
||||
## Inputs
|
||||
|
||||
- `backend/schemas.py`
|
||||
- `backend/routers/creators.py`
|
||||
- `backend/models.py`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- `backend/schemas.py`
|
||||
- `backend/routers/creators.py`
|
||||
|
||||
## Verification
|
||||
|
||||
ssh ub01 'curl -s http://localhost:8096/api/v1/creators?limit=5' | python3 -m json.tool | grep last_technique_at
|
||||
78
.gsd/milestones/M015/slices/S02/tasks/T01-SUMMARY.md
Normal file
78
.gsd/milestones/M015/slices/S02/tasks/T01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
id: T01
|
||||
parent: S02
|
||||
milestone: M015
|
||||
provides: []
|
||||
requires: []
|
||||
affects: []
|
||||
key_files: ["backend/schemas.py", "backend/routers/creators.py"]
|
||||
key_decisions: ["Used correlated scalar subquery (MAX(created_at)) to preserve existing query structure"]
|
||||
patterns_established: []
|
||||
drill_down_paths: []
|
||||
observability_surfaces: []
|
||||
duration: ""
|
||||
verification_result: "Ran curl against live API on ub01:8096 — last_technique_at appears on all 6 creators, with correct datetime values for 5 creators with techniques and null for Caracal Project (0 techniques). Pagination and sort params verified working."
|
||||
completed_at: 2026-04-03T04:16:43.842Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Added last_technique_at correlated subquery to creators API endpoint, returning MAX(technique_pages.created_at) per creator
|
||||
|
||||
> Added last_technique_at correlated subquery to creators API endpoint, returning MAX(technique_pages.created_at) per creator
|
||||
|
||||
## What Happened
|
||||
---
|
||||
id: T01
|
||||
parent: S02
|
||||
milestone: M015
|
||||
key_files:
|
||||
- backend/schemas.py
|
||||
- backend/routers/creators.py
|
||||
key_decisions:
|
||||
- Used correlated scalar subquery (MAX(created_at)) to preserve existing query structure
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-04-03T04:16:43.843Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Added last_technique_at correlated subquery to creators API endpoint, returning MAX(technique_pages.created_at) per creator
|
||||
|
||||
**Added last_technique_at correlated subquery to creators API endpoint, returning MAX(technique_pages.created_at) per creator**
|
||||
|
||||
## What Happened
|
||||
|
||||
Added `last_technique_at: datetime | None = None` to CreatorBrowseItem schema and a correlated scalar subquery using func.max(TechniquePage.created_at) to the list_creators endpoint. The value flows through the row unpacking loop and is passed to the CreatorBrowseItem constructor. Deployed to ub01 and verified all three must-haves: field present on every item, non-null for creators with techniques, null for creators with zero techniques.
|
||||
|
||||
## Verification
|
||||
|
||||
Ran curl against live API on ub01:8096 — last_technique_at appears on all 6 creators, with correct datetime values for 5 creators with techniques and null for Caracal Project (0 techniques). Pagination and sort params verified working.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `ssh ub01 'curl -s http://localhost:8096/api/v1/creators?limit=5' | python3 -m json.tool | grep last_technique_at` | 0 | ✅ pass | 2000ms |
|
||||
| 2 | `ssh ub01 'curl -s http://localhost:8096/api/v1/creators?limit=6&sort=alpha' | python3 -c ... (non-null/null check)` | 0 | ✅ pass | 2000ms |
|
||||
| 3 | `ssh ub01 'curl -s http://localhost:8096/api/v1/creators?limit=2&offset=2&sort=views' | python3 ... (pagination)` | 0 | ✅ pass | 2000ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
First commit missed the row unpacking edit due to an edit tool text match issue. Fixed with a second commit.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `backend/schemas.py`
|
||||
- `backend/routers/creators.py`
|
||||
|
||||
|
||||
## Deviations
|
||||
First commit missed the row unpacking edit due to an edit tool text match issue. Fixed with a second commit.
|
||||
|
||||
## Known Issues
|
||||
None.
|
||||
61
.gsd/milestones/M015/slices/S02/tasks/T02-PLAN.md
Normal file
61
.gsd/milestones/M015/slices/S02/tasks/T02-PLAN.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
estimated_steps: 28
|
||||
estimated_files: 4
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T02: Render dates on creators browse page and homepage cards
|
||||
|
||||
Add the `last_technique_at` field to the frontend TS interface, render it on the creators browse page, render `created_at` on homepage recently-added cards, and rebuild+deploy the web container.
|
||||
|
||||
## Steps
|
||||
|
||||
1. In `frontend/src/api/public-client.ts`, add `last_technique_at: string | null;` to the `CreatorBrowseItem` interface.
|
||||
2. In `frontend/src/pages/CreatorsBrowse.tsx`:
|
||||
- After the existing stats (technique count · video count · view count), add a separator and "Last updated: Mon D" when `creator.last_technique_at` is non-null.
|
||||
- Format: `new Date(creator.last_technique_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })`
|
||||
- Skip rendering entirely when null (no "No articles" text — just omit).
|
||||
3. In `frontend/src/pages/Home.tsx`:
|
||||
- Inside each `.recent-card`, after the creator name span, add a small date element showing `created_at`.
|
||||
- Use the same short date format (Mon D).
|
||||
- Style with a muted color and small font to keep it subtle.
|
||||
4. In `frontend/src/App.css`:
|
||||
- Add `.creator-row__updated` style: muted color (`var(--text-muted)` or similar), small font-size.
|
||||
- Add `.recent-card__date` style: muted, small, positioned after creator name.
|
||||
5. SSH to ub01, rebuild and restart the web container:
|
||||
```
|
||||
ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && git pull && docker compose build chrysopedia-web-8096 && docker compose up -d chrysopedia-web-8096'
|
||||
```
|
||||
6. Visually verify both pages in the browser.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] Creators browse page shows "Last updated: Mon D" per creator (when non-null)
|
||||
- [ ] Creators with null `last_technique_at` show no date element
|
||||
- [ ] Homepage recently-added cards show a subtle date
|
||||
- [ ] Date styling is muted and consistent with existing secondary text
|
||||
|
||||
## Verification
|
||||
|
||||
- Browse http://ub01:8096/creators — each creator row with techniques shows a last-updated date
|
||||
- Browse http://ub01:8096 — each recently-added card shows a small date
|
||||
- Dates use short format (e.g., "Apr 3" not "April 3, 2026" or "2026-04-03")
|
||||
|
||||
## Inputs
|
||||
|
||||
- `frontend/src/api/public-client.ts`
|
||||
- `frontend/src/pages/CreatorsBrowse.tsx`
|
||||
- `frontend/src/pages/Home.tsx`
|
||||
- `frontend/src/App.css`
|
||||
- `backend/schemas.py`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- `frontend/src/api/public-client.ts`
|
||||
- `frontend/src/pages/CreatorsBrowse.tsx`
|
||||
- `frontend/src/pages/Home.tsx`
|
||||
- `frontend/src/App.css`
|
||||
|
||||
## Verification
|
||||
|
||||
ssh ub01 'curl -s http://ub01:8096/creators' | grep -o 'Last updated' | head -3
|
||||
Loading…
Add table
Reference in a new issue