diff --git a/.gsd/milestones/M014/slices/S03/S03-PLAN.md b/.gsd/milestones/M014/slices/S03/S03-PLAN.md index 23018e8..d51ec5f 100644 --- a/.gsd/milestones/M014/slices/S03/S03-PLAN.md +++ b/.gsd/milestones/M014/slices/S03/S03-PLAN.md @@ -45,7 +45,7 @@ This is the data layer foundation — migration file, model changes, and schema - Estimate: 30m - Files: alembic/versions/012_multi_source_format.py, backend/models.py, backend/schemas.py - Verify: cd backend && python -c "from models import TechniquePageVideo, TechniquePage; assert hasattr(TechniquePage, 'body_sections_format'); print('models OK')" && python -c "from schemas import SourceVideoSummary, TechniquePageDetail; print('schemas OK')" -- [ ] **T02: Wire source_videos into technique detail endpoint** — Update the technique detail endpoint to eagerly load source_video_links and build the source_videos list on the response. Verify end-to-end with an API call. +- [x] **T02: Wired source_videos and body_sections_format into technique detail API response with eager-loaded association table** — Update the technique detail endpoint to eagerly load source_video_links and build the source_videos list on the response. Verify end-to-end with an API call. ## Steps diff --git a/.gsd/milestones/M014/slices/S03/tasks/T01-VERIFY.json b/.gsd/milestones/M014/slices/S03/tasks/T01-VERIFY.json new file mode 100644 index 0000000..6961fa6 --- /dev/null +++ b/.gsd/milestones/M014/slices/S03/tasks/T01-VERIFY.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "taskId": "T01", + "unitId": "M014/S03/T01", + "timestamp": 1775178991056, + "passed": true, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd backend", + "exitCode": 0, + "durationMs": 5, + "verdict": "pass" + } + ] +} diff --git a/.gsd/milestones/M014/slices/S03/tasks/T02-SUMMARY.md b/.gsd/milestones/M014/slices/S03/tasks/T02-SUMMARY.md new file mode 100644 index 0000000..0f388e6 --- /dev/null +++ b/.gsd/milestones/M014/slices/S03/tasks/T02-SUMMARY.md @@ -0,0 +1,77 @@ +--- +id: T02 +parent: S03 +milestone: M014 +provides: [] +requires: [] +affects: [] +key_files: ["backend/routers/techniques.py"] +key_decisions: [] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "Alembic migration 012 ran clean. API response for existing technique shows body_sections_format: "v1" and source_videos: []. Tested via both Docker internal curl and external ub01:8096 URL." +completed_at: 2026-04-03T01:19:28.974Z +blocker_discovered: false +--- + +# T02: Wired source_videos and body_sections_format into technique detail API response with eager-loaded association table + +> Wired source_videos and body_sections_format into technique detail API response with eager-loaded association table + +## What Happened +--- +id: T02 +parent: S03 +milestone: M014 +key_files: + - backend/routers/techniques.py +key_decisions: + - (none) +duration: "" +verification_result: passed +completed_at: 2026-04-03T01:19:28.974Z +blocker_discovered: false +--- + +# T02: Wired source_videos and body_sections_format into technique detail API response with eager-loaded association table + +**Wired source_videos and body_sections_format into technique detail API response with eager-loaded association table** + +## What Happened + +Updated get_technique() in backend/routers/techniques.py to eagerly load source_video_links relationship chained to source_video via selectinload. Built source_videos list from association table rows with content_type enum handling. Added imports for TechniquePageVideo and SourceVideoSummary. Deployed to ub01, ran Alembic migration 012, and verified API response includes both new fields with correct defaults. + +## Verification + +Alembic migration 012 ran clean. API response for existing technique shows body_sections_format: "v1" and source_videos: []. Tested via both Docker internal curl and external ub01:8096 URL. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd backend && python -c "from models import TechniquePageVideo, TechniquePage; assert hasattr(TechniquePage, 'body_sections_format'); print('models OK')"` | 0 | ✅ pass | 500ms | +| 2 | `cd backend && python -c "from schemas import SourceVideoSummary, TechniquePageDetail; print('schemas OK')"` | 0 | ✅ pass | 400ms | +| 3 | `ssh ub01 'docker exec chrysopedia-api alembic upgrade head'` | 0 | ✅ pass | 2000ms | +| 4 | `ssh ub01 'curl -s http://ub01:8096/api/v1/techniques/parallel-stereo-processing-keota | python3 -m json.tool | grep -E body_sections_format|source_videos'` | 0 | ✅ pass | 1000ms | + + +## Deviations + +None. + +## Known Issues + +None. + +## Files Created/Modified + +- `backend/routers/techniques.py` + + +## Deviations +None. + +## Known Issues +None. diff --git a/backend/routers/techniques.py b/backend/routers/techniques.py index a5eeb48..63a6b39 100644 --- a/backend/routers/techniques.py +++ b/backend/routers/techniques.py @@ -11,12 +11,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from database import get_session -from models import Creator, KeyMoment, RelatedTechniqueLink, SourceVideo, TechniquePage, TechniquePageVersion +from models import Creator, KeyMoment, RelatedTechniqueLink, SourceVideo, TechniquePage, TechniquePageVersion, TechniquePageVideo from schemas import ( CreatorInfo, KeyMomentSummary, PaginatedResponse, RelatedLinkItem, + SourceVideoSummary, TechniquePageDetail, TechniquePageRead, TechniquePageVersionDetail, @@ -223,6 +224,9 @@ async def get_technique( selectinload(TechniquePage.incoming_links).selectinload( RelatedTechniqueLink.source_page ), + selectinload(TechniquePage.source_video_links).selectinload( + TechniquePageVideo.source_video + ), ) ) result = await db.execute(stmt) @@ -295,12 +299,25 @@ async def get_technique( version_count_result = await db.execute(version_count_stmt) version_count = version_count_result.scalar() or 0 + # Build source video list from association table + source_videos = [ + SourceVideoSummary( + id=link.source_video.id, + filename=link.source_video.filename, + content_type=link.source_video.content_type.value if hasattr(link.source_video.content_type, 'value') else str(link.source_video.content_type), + added_at=link.added_at, + ) + for link in page.source_video_links + if link.source_video is not None + ] + return TechniquePageDetail( **base.model_dump(), key_moments=key_moment_items, creator_info=creator_info, related_links=related_links, version_count=version_count, + source_videos=source_videos, )