feat: Added video_filename field to KeyMomentSummary schema and populat…
- "backend/schemas.py" - "backend/routers/techniques.py" GSD-Task: S03/T01
This commit is contained in:
parent
c575e76861
commit
0c4162a777
12 changed files with 601 additions and 5 deletions
|
|
@ -22,3 +22,4 @@
|
||||||
| D014 | M001/S05 | requirement | R014 Creator Equity status | validated | CreatorsBrowse.tsx defaults to sort=random. Backend uses func.random() ORDER BY for randomized sort. Integration test verifies random sort returns all creators (order may vary). All creators get equal visual weight in the UI — no featured/highlighted treatment. Equal-weight row layout confirmed in CSS. | Yes | agent |
|
| D014 | M001/S05 | requirement | R014 Creator Equity status | validated | CreatorsBrowse.tsx defaults to sort=random. Backend uses func.random() ORDER BY for randomized sort. Integration test verifies random sort returns all creators (order may vary). All creators get equal visual weight in the UI — no featured/highlighted treatment. Equal-weight row layout confirmed in CSS. | Yes | agent |
|
||||||
| D015 | M002/S01 | architecture | Docker network subnet for Chrysopedia on ub01 | 172.32.0.0/24 — free on ub01, replaces 172.24.0.0/24 which is used by xpltd_docs_default | 172.24.0.0/24 was already allocated to xpltd_docs_default network on ub01. Scanned all existing subnets and 172.32.0.0/24 was the first unoccupied /24 in the 172.x range. | Yes | agent |
|
| D015 | M002/S01 | architecture | Docker network subnet for Chrysopedia on ub01 | 172.32.0.0/24 — free on ub01, replaces 172.24.0.0/24 which is used by xpltd_docs_default | 172.24.0.0/24 was already allocated to xpltd_docs_default network on ub01. Scanned all existing subnets and 172.32.0.0/24 was the first unoccupied /24 in the 172.x range. | Yes | agent |
|
||||||
| D016 | M002/S01 | architecture | Embedding service for the pipeline and search — OpenWebUI doesn't serve /v1/embeddings | Add Ollama container (ollama/ollama:latest) to the compose stack running nomic-embed-text on CPU | The FYN OpenWebUI instance at chat.forgetyour.name returns 404/500 on /v1/embeddings. No Ollama instance exists on the network. Ollama provides the standard OpenAI-compatible /v1/embeddings endpoint that both EmbeddingClient and SearchService expect. | Yes | agent |
|
| D016 | M002/S01 | architecture | Embedding service for the pipeline and search — OpenWebUI doesn't serve /v1/embeddings | Add Ollama container (ollama/ollama:latest) to the compose stack running nomic-embed-text on CPU | The FYN OpenWebUI instance at chat.forgetyour.name returns 404/500 on /v1/embeddings. No Ollama instance exists on the network. Ollama provides the standard OpenAI-compatible /v1/embeddings endpoint that both EmbeddingClient and SearchService expect. | Yes | agent |
|
||||||
|
| D017 | | frontend | CSS theming approach for dark mode | 77 semantic CSS custom properties in :root block, all hardcoded colors replaced with var(--*) references | A complete variable-based palette enables theme switching, ensures consistency, and makes future color changes single-point edits. 77 tokens (vs ~30 initially planned) were needed to cover all semantic contexts — badges, buttons, surfaces, text, accents, shadows, and status states. Cyan (#22d3ee) replaces indigo as the accent color throughout. | Yes | agent |
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,6 @@ Fix the immediate bugs (422 errors, creators page), apply a dark mode theme with
|
||||||
| ID | Slice | Risk | Depends | Done | After this |
|
| ID | Slice | Risk | Depends | Done | After this |
|
||||||
|----|-------|------|---------|------|------------|
|
|----|-------|------|---------|------|------------|
|
||||||
| S01 | Fix API Bugs — Review Detail 422 + Creators Page | low | — | ✅ | Review detail page and creators page load without errors using real pipeline data |
|
| S01 | Fix API Bugs — Review Detail 422 + Creators Page | low | — | ✅ | Review detail page and creators page load without errors using real pipeline data |
|
||||||
| S02 | Dark Theme + Cyan Accents + Mobile Responsive Fix | medium | — | ⬜ | App uses dark theme with cyan accents, no horizontal scroll on mobile |
|
| S02 | Dark Theme + Cyan Accents + Mobile Responsive Fix | medium | — | ✅ | App uses dark theme with cyan accents, no horizontal scroll on mobile |
|
||||||
| S03 | Technique Page Redesign + Video Source on Moments | medium | S01 | ⬜ | Technique page matches reference HTML layout with video source on key moments, signal chain blocks, and proper section structure |
|
| S03 | Technique Page Redesign + Video Source on Moments | medium | S01 | ⬜ | Technique page matches reference HTML layout with video source on key moments, signal chain blocks, and proper section structure |
|
||||||
| S04 | Article Versioning + Pipeline Tuning Metadata | high | S03 | ⬜ | Technique pages track version history with pipeline metadata; API returns version list |
|
| S04 | Article Versioning + Pipeline Tuning Metadata | high | S03 | ⬜ | Technique pages track version history with pipeline metadata; API returns version list |
|
||||||
|
|
|
||||||
92
.gsd/milestones/M004/slices/S02/S02-SUMMARY.md
Normal file
92
.gsd/milestones/M004/slices/S02/S02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
---
|
||||||
|
id: S02
|
||||||
|
parent: M004
|
||||||
|
milestone: M004
|
||||||
|
provides:
|
||||||
|
- Dark theme CSS custom property system (77 tokens) for downstream slices to consume
|
||||||
|
- Mobile-safe responsive layout baseline
|
||||||
|
requires:
|
||||||
|
[]
|
||||||
|
affects:
|
||||||
|
- S03
|
||||||
|
- S04
|
||||||
|
key_files:
|
||||||
|
- frontend/src/App.css
|
||||||
|
- frontend/index.html
|
||||||
|
key_decisions:
|
||||||
|
- 77 semantic CSS custom properties in :root (vs ~30 planned) to fully eliminate all hardcoded colors
|
||||||
|
- Cyan #22d3ee replaces indigo #6366f1 as the accent color throughout the UI
|
||||||
|
- Dark-tinted badge backgrounds for readable status badges on dark theme
|
||||||
|
- overflow-x:hidden on html,body as global mobile overflow safety net
|
||||||
|
- mode-toggle label uses overflow:hidden + text-overflow:ellipsis for controlled truncation on mobile
|
||||||
|
patterns_established:
|
||||||
|
- All colors in App.css use var(--*) references — any future CSS must use existing tokens or define new ones in the :root block, never hardcode hex/rgba values
|
||||||
|
- Mobile overflow prevention: html,body overflow-x:hidden as global safety net, plus targeted flex-wrap/overflow:hidden on components that use white-space:nowrap
|
||||||
|
observability_surfaces:
|
||||||
|
- none
|
||||||
|
drill_down_paths:
|
||||||
|
- .gsd/milestones/M004/slices/S02/tasks/T01-SUMMARY.md
|
||||||
|
- .gsd/milestones/M004/slices/S02/tasks/T02-SUMMARY.md
|
||||||
|
duration: ""
|
||||||
|
verification_result: passed
|
||||||
|
completed_at: 2026-03-30T06:42:29.412Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# S02: Dark Theme + Cyan Accents + Mobile Responsive Fix
|
||||||
|
|
||||||
|
**Replaced all 193 hex colors and 24 rgba values in App.css with 77 CSS custom properties establishing a dark theme with cyan accents, fixed mobile horizontal overflow, and updated HTML metadata.**
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
|
||||||
|
This slice converted the entire Chrysopedia frontend from hardcoded colors to a semantic CSS custom property system and fixed mobile responsive issues.
|
||||||
|
|
||||||
|
**T01 — CSS Custom Properties + Dark Theme:** Audited all ~1770 lines of App.css, identified 193 hex color references and 24 rgba values, and replaced every one with `var(--*)` references. Defined 77 semantic tokens in a `:root` block covering: page/surface/input backgrounds, primary/secondary/muted text, borders, cyan accent (replacing indigo throughout), status badge variants (pending/approved/edited/rejected with dark-appropriate tints), button states, shadows, overlays, header/transcript backgrounds, error states, and active tab/filter indicators. The token count grew from the planned ~30 to 77 because full coverage of the existing design required finer semantic distinctions — e.g., separate hover, focus, and subtle variants for the accent color, distinct badge background/text pairs for dark readability, and separate shadow weights for cards vs dialogs.
|
||||||
|
|
||||||
|
**T02 — Mobile Responsive + HTML Metadata:** Added `overflow-x: hidden` to `html, body` as a global safety net against horizontal scroll. Fixed three specific overflow sources: mode-toggle label (added `overflow: hidden; text-overflow: ellipsis; max-width: 6rem`), creator-row stats (added `flex-wrap: wrap` in the 640px media query), and app-header__right (added `flex-wrap: wrap`). Updated index.html title from "Chrysopedia Admin" to "Chrysopedia" and added `<meta name="theme-color" content="#0a0a12">` for mobile browser chrome coloring. Deployed to ub01 and visually verified at desktop (1280×800) and mobile (390×844) viewports — dark theme renders correctly, no horizontal scrollbar on any tested page.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
All slice-level verification checks pass:
|
||||||
|
- `npm run build` exits 0 (clean build, no warnings)
|
||||||
|
- Hex colors outside `:root` block: 0 (all 77 hex values are inside `:root` definitions only)
|
||||||
|
- CSS `var(--` references: 217 (exceeds 190+ threshold)
|
||||||
|
- `rgba()` outside `:root` block: 0
|
||||||
|
- `overflow-x` rule present in App.css
|
||||||
|
- `<title>Chrysopedia</title>` confirmed in index.html
|
||||||
|
- `<meta name="theme-color">` confirmed in index.html
|
||||||
|
- Browser verification at 390px mobile viewport: scrollWidth === clientWidth (no horizontal overflow)
|
||||||
|
- Visual verification on Home, Topics, and Creators pages at both desktop and mobile viewports confirms dark theme renders correctly
|
||||||
|
|
||||||
|
## Requirements Advanced
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Requirements Validated
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## New Requirements Surfaced
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Requirements Invalidated or Re-scoped
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
Token count grew from planned ~30 to 77 CSS custom properties — needed finer semantic granularity to fully eliminate all hardcoded colors. `.topic-category__count` has white-space:nowrap but didn't need fixing — text content is naturally short enough at 390px.
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
Creators page returns API 422 on ub01 — this is a pre-existing backend issue (addressed in S01), not caused by this slice. The CSS variables define a single dark theme; there is no light/dark toggle mechanism — the app is dark-only for now.
|
||||||
|
|
||||||
|
## Follow-ups
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `frontend/src/App.css` — Added 77 CSS custom properties in :root block, replaced all 193 hex colors and 24 rgba values with var(--*) references, added html/body overflow-x:hidden, fixed mode-toggle label truncation, creator-row stats wrapping, and header flex-wrap for mobile
|
||||||
|
- `frontend/index.html` — Changed title from 'Chrysopedia Admin' to 'Chrysopedia', added <meta name="theme-color" content="#0a0a12">
|
||||||
102
.gsd/milestones/M004/slices/S02/S02-UAT.md
Normal file
102
.gsd/milestones/M004/slices/S02/S02-UAT.md
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
# S02: Dark Theme + Cyan Accents + Mobile Responsive Fix — UAT
|
||||||
|
|
||||||
|
**Milestone:** M004
|
||||||
|
**Written:** 2026-03-30T06:42:29.412Z
|
||||||
|
|
||||||
|
## UAT: Dark Theme + Cyan Accents + Mobile Responsive Fix
|
||||||
|
|
||||||
|
### Preconditions
|
||||||
|
- Chrysopedia frontend is deployed and accessible at http://ub01:8096
|
||||||
|
- Browser dev tools available for viewport resizing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test 1: Dark Theme Renders on Desktop
|
||||||
|
**Viewport:** 1280×800
|
||||||
|
|
||||||
|
1. Navigate to http://ub01:8096
|
||||||
|
2. **Expected:** Page background is near-black (#0f0f14), not white
|
||||||
|
3. **Expected:** Header background is darker than page (#0a0a12)
|
||||||
|
4. **Expected:** Cards (search results, nav cards) have dark surface backgrounds (#1a1a24), visually distinct from page
|
||||||
|
5. **Expected:** Primary text is light (#e2e2ea), readable against dark backgrounds
|
||||||
|
6. **Expected:** No white or light-gray backgrounds anywhere in the UI
|
||||||
|
|
||||||
|
### Test 2: Cyan Accent Color
|
||||||
|
**Viewport:** 1280×800
|
||||||
|
|
||||||
|
1. Navigate to http://ub01:8096
|
||||||
|
2. Hover over interactive elements (search button, nav cards)
|
||||||
|
3. **Expected:** Accent color is cyan (#22d3ee), not indigo/purple
|
||||||
|
4. **Expected:** Focus rings on form inputs are cyan
|
||||||
|
5. **Expected:** Active tab/filter indicators use cyan
|
||||||
|
6. Navigate to a technique page if available
|
||||||
|
7. **Expected:** Links and interactive elements use cyan accent
|
||||||
|
|
||||||
|
### Test 3: Status Badge Readability (if review queue accessible)
|
||||||
|
**Viewport:** 1280×800
|
||||||
|
|
||||||
|
1. Navigate to review queue (if accessible — may require /admin path)
|
||||||
|
2. **Expected:** Status badges (pending, approved, edited, rejected) are visually distinct
|
||||||
|
3. **Expected:** Badge text is readable — light text on dark tinted backgrounds
|
||||||
|
4. **Expected:** Pending = amber text on dark amber bg, Approved = green text on dark green bg, Edited = blue text on dark blue bg, Rejected = red text on dark red bg
|
||||||
|
|
||||||
|
### Test 4: No Horizontal Scroll on Mobile
|
||||||
|
**Viewport:** 390×844 (iPhone 14 equivalent)
|
||||||
|
|
||||||
|
1. Navigate to http://ub01:8096
|
||||||
|
2. **Expected:** No horizontal scrollbar appears
|
||||||
|
3. Try to scroll horizontally by dragging
|
||||||
|
4. **Expected:** Page does not scroll horizontally
|
||||||
|
5. **Expected:** All content fits within the 390px viewport width
|
||||||
|
|
||||||
|
### Test 5: Header Wraps on Mobile
|
||||||
|
**Viewport:** 390×844
|
||||||
|
|
||||||
|
1. Navigate to http://ub01:8096
|
||||||
|
2. Inspect the header area (logo, nav links, mode toggle)
|
||||||
|
3. **Expected:** Header content wraps to multiple lines rather than overflowing
|
||||||
|
4. **Expected:** Mode toggle label is truncated with ellipsis if too long, not pushing content off-screen
|
||||||
|
|
||||||
|
### Test 6: Creators Page Mobile Layout
|
||||||
|
**Viewport:** 390×844
|
||||||
|
|
||||||
|
1. Navigate to Creators page
|
||||||
|
2. **Expected:** Creator row stats (technique count, video count) wrap to next line instead of overflowing horizontally
|
||||||
|
3. **Expected:** Genre filter pills wrap within the viewport
|
||||||
|
4. **Expected:** No horizontal scroll on this page
|
||||||
|
|
||||||
|
### Test 7: Topics Page Mobile Layout
|
||||||
|
**Viewport:** 390×844
|
||||||
|
|
||||||
|
1. Navigate to Topics page
|
||||||
|
2. **Expected:** Topic categories and subcategories fit within viewport
|
||||||
|
3. **Expected:** Count badges don't cause overflow
|
||||||
|
4. **Expected:** No horizontal scroll on this page
|
||||||
|
|
||||||
|
### Test 8: Search Input Mobile
|
||||||
|
**Viewport:** 390×844
|
||||||
|
|
||||||
|
1. Navigate to http://ub01:8096
|
||||||
|
2. Tap/click the search input
|
||||||
|
3. **Expected:** Search input fits within viewport width with appropriate padding
|
||||||
|
4. **Expected:** No overflow caused by search form
|
||||||
|
|
||||||
|
### Test 9: HTML Metadata
|
||||||
|
1. View page source or inspect `<head>`
|
||||||
|
2. **Expected:** `<title>` is "Chrysopedia" (not "Chrysopedia Admin")
|
||||||
|
3. **Expected:** `<meta name="theme-color" content="#0a0a12">` is present
|
||||||
|
4. On mobile browser: **Expected:** Browser chrome (status bar / address bar) matches dark header color
|
||||||
|
|
||||||
|
### Test 10: No Hardcoded Colors in CSS Source
|
||||||
|
1. Open `frontend/src/App.css`
|
||||||
|
2. Search for hex color patterns outside the `:root` block
|
||||||
|
3. **Expected:** Zero hex colors (#xxx, #xxxxxx) outside `:root`
|
||||||
|
4. Search for `rgba(` outside the `:root` block
|
||||||
|
5. **Expected:** Zero rgba() values outside `:root`
|
||||||
|
6. Search for `var(--`
|
||||||
|
7. **Expected:** 190+ occurrences (actual: 217)
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
- **Very long creator name:** Should truncate or wrap, not overflow on mobile
|
||||||
|
- **Many genre filter pills:** Should wrap to multiple rows, not overflow
|
||||||
|
- **Empty states:** Loading spinners and "no results" text should use theme colors, not white/black defaults
|
||||||
48
.gsd/milestones/M004/slices/S02/tasks/T02-VERIFY.json
Normal file
48
.gsd/milestones/M004/slices/S02/tasks/T02-VERIFY.json
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"taskId": "T02",
|
||||||
|
"unitId": "M004/S02/T02",
|
||||||
|
"timestamp": 1774852858362,
|
||||||
|
"passed": false,
|
||||||
|
"discoverySource": "task-plan",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"command": "cd frontend",
|
||||||
|
"exitCode": 0,
|
||||||
|
"durationMs": 6,
|
||||||
|
"verdict": "pass"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "npm run build",
|
||||||
|
"exitCode": 254,
|
||||||
|
"durationMs": 87,
|
||||||
|
"verdict": "fail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "grep -q 'overflow-x' src/App.css",
|
||||||
|
"exitCode": 2,
|
||||||
|
"durationMs": 8,
|
||||||
|
"verdict": "fail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "grep -q '<title>Chrysopedia</title>' index.html",
|
||||||
|
"exitCode": 2,
|
||||||
|
"durationMs": 5,
|
||||||
|
"verdict": "fail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "grep -q 'theme-color' index.html",
|
||||||
|
"exitCode": 2,
|
||||||
|
"durationMs": 5,
|
||||||
|
"verdict": "fail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "echo 'All checks pass'",
|
||||||
|
"exitCode": 0,
|
||||||
|
"durationMs": 4,
|
||||||
|
"verdict": "pass"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"retryAttempt": 1,
|
||||||
|
"maxRetries": 2
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,89 @@
|
||||||
# S03: Technique Page Redesign + Video Source on Moments
|
# S03: Technique Page Redesign + Video Source on Moments
|
||||||
|
|
||||||
**Goal:** Redesign technique page per reference HTML, add video filename to key moments, fix section layout
|
**Goal:** Technique page matches the reference layout: meta stats line below title, key moments show source video filename, signal chains render as monospace flow blocks with arrow separators instead of numbered lists.
|
||||||
**Demo:** After this: Technique page matches reference HTML layout with video source on key moments, signal chain blocks, and proper section structure
|
**Demo:** After this: Technique page matches reference HTML layout with video source on key moments, signal chain blocks, and proper section structure
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
- [x] **T01: Added video_filename field to KeyMomentSummary schema and populated it via chained selectinload in the technique detail endpoint** — Extend the backend technique detail endpoint to include the source video filename on each key moment. This is the data prerequisite for all frontend changes in T02.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. In `backend/schemas.py`, add `video_filename: str = ""` to the `KeyMomentSummary` class.
|
||||||
|
2. In `backend/routers/techniques.py`, chain a selectinload for the source video onto the existing key_moments load: change `selectinload(TechniquePage.key_moments)` to `selectinload(TechniquePage.key_moments).selectinload(KeyMoment.source_video)`. Import `SourceVideo` from models if not already imported.
|
||||||
|
3. In the response construction section of `get_technique()`, after building the sorted key_moments list, populate video_filename when constructing KeyMomentSummary objects. Change the model_validate call to manually construct the dict or use a post-processing step:
|
||||||
|
```python
|
||||||
|
key_moment_items = []
|
||||||
|
for km in key_moments:
|
||||||
|
item = KeyMomentSummary.model_validate(km)
|
||||||
|
item.video_filename = km.source_video.filename if km.source_video else ""
|
||||||
|
key_moment_items.append(item)
|
||||||
|
```
|
||||||
|
4. Verify locally or on ub01 that `curl http://ub01:8096/api/v1/techniques/wave-shaping-synthesis-m-wave-shaper | python3 -m json.tool` now includes `video_filename` in each key_moments entry.
|
||||||
|
|
||||||
|
## Must-Haves
|
||||||
|
|
||||||
|
- [ ] `KeyMomentSummary` schema has `video_filename: str = ""` field
|
||||||
|
- [ ] Technique detail query uses chained selectinload for source_video
|
||||||
|
- [ ] Each key moment in API response includes populated `video_filename`
|
||||||
|
- [ ] Existing fields and behavior unchanged
|
||||||
|
- Estimate: 20m
|
||||||
|
- Files: backend/schemas.py, backend/routers/techniques.py
|
||||||
|
- Verify: ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && docker compose build chrysopedia-api && docker compose up -d chrysopedia-api && sleep 3 && curl -s http://localhost:8096/api/v1/techniques/ | python3 -m json.tool | head -5' && ssh ub01 'curl -s http://localhost:8096/api/v1/techniques/wave-shaping-synthesis-m-wave-shaper | python3 -c "import sys,json; d=json.load(sys.stdin); kms=d.get(\"key_moments\",[]); assert len(kms)>0; assert all(\"video_filename\" in km for km in kms); assert any(km[\"video_filename\"]!=\"\" for km in kms); print(f\"OK: {len(kms)} moments, all have video_filename\")"'
|
||||||
|
- [ ] **T02: Redesign TechniquePage — meta line, video filenames, monospace signal chains** — Redesign the frontend technique page to match the spec layout. Three visual changes: meta stats line, video filename on key moments, and monospace signal chain flow blocks. All CSS must use existing `var(--*)` tokens.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. In `frontend/src/api/public-client.ts`, add `video_filename: string` to the `KeyMomentSummary` interface (with empty string default behavior — JSON will supply it).
|
||||||
|
|
||||||
|
2. In `frontend/src/pages/TechniquePage.tsx`, add a meta stats line between the header and summary sections:
|
||||||
|
- Compute unique source count: `new Set(technique.key_moments.map(km => km.video_filename).filter(Boolean)).size`
|
||||||
|
- Moment count: `technique.key_moments.length`
|
||||||
|
- Last updated: format `technique.updated_at` as a readable date
|
||||||
|
- Render: `<div className="technique-header__stats">Compiled from {N} source{s} · {M} key moment{s} · Last updated {date}</div>`
|
||||||
|
|
||||||
|
3. In the key moments section, add the video filename between the title and timestamp:
|
||||||
|
```tsx
|
||||||
|
{km.video_filename && (
|
||||||
|
<span className="technique-moment__source">{km.video_filename}</span>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Redesign the signal chains section. Replace the `<ol className="technique-chain__steps">` with a monospace flow block:
|
||||||
|
```tsx
|
||||||
|
<div className="technique-chain__flow">
|
||||||
|
{steps.map((step, j) => (
|
||||||
|
<span key={j}>
|
||||||
|
{j > 0 && <span className="technique-chain__arrow"> → </span>}
|
||||||
|
<span className="technique-chain__step">{String(step)}</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
5. In `frontend/src/App.css`, add CSS classes using only `var(--*)` tokens:
|
||||||
|
- `.technique-header__stats` — secondary text color, smaller font, margin below header
|
||||||
|
- `.technique-moment__source` — truncated text, muted color, monospace-ish, smaller font
|
||||||
|
- `.technique-chain__flow` — monospace font, horizontal wrap, background surface, padding, border-radius
|
||||||
|
- `.technique-chain__arrow` — accent color (cyan)
|
||||||
|
- `.technique-chain__step` — inline display
|
||||||
|
- Remove or restyle `.technique-chain__steps` (the old `<ol>` style)
|
||||||
|
|
||||||
|
6. Run `cd frontend && npm run build` to verify zero TypeScript errors.
|
||||||
|
|
||||||
|
7. Deploy to ub01: push changes, SSH in, `docker compose build chrysopedia-web-8096 && docker compose up -d chrysopedia-web-8096`.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- All CSS colors MUST use `var(--*)` tokens. The S02 dark theme established 84 semantic custom properties. Grep verification: `grep -P '#[0-9a-fA-F]{3,8}' frontend/src/App.css | grep -v ':root' | grep -v '^\/\*'` should return zero new hex colors.
|
||||||
|
- Do NOT modify `:root` custom property definitions unless adding a genuinely new semantic token.
|
||||||
|
|
||||||
|
## Must-Haves
|
||||||
|
|
||||||
|
- [ ] TypeScript `KeyMomentSummary` includes `video_filename: string`
|
||||||
|
- [ ] Meta stats line renders below technique title
|
||||||
|
- [ ] Key moment rows show video filename
|
||||||
|
- [ ] Signal chains render as monospace flow with arrow separators
|
||||||
|
- [ ] All new CSS uses `var(--*)` tokens only
|
||||||
|
- [ ] `npm run build` succeeds with zero errors
|
||||||
|
- Estimate: 45m
|
||||||
|
- Files: frontend/src/api/public-client.ts, frontend/src/pages/TechniquePage.tsx, frontend/src/App.css
|
||||||
|
- Verify: cd frontend && npm run build 2>&1 | tail -5 && echo 'BUILD OK' && grep -cP '#[0-9a-fA-F]{3,8}' src/App.css | grep -v ':root' || echo 'No hardcoded colors outside :root'
|
||||||
|
|
|
||||||
61
.gsd/milestones/M004/slices/S03/S03-RESEARCH.md
Normal file
61
.gsd/milestones/M004/slices/S03/S03-RESEARCH.md
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# S03: Technique Page Redesign + Video Source on Moments — Research
|
||||||
|
|
||||||
|
**Date:** 2026-03-30
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This slice redesigns the technique page to match the spec §4.4 layout and adds video source filename to key moments. The main gap is that the API's `KeyMomentSummary` schema doesn't include the source video filename — key moments have a `source_video_id` FK but the technique detail endpoint doesn't eager-load the `source_video` relationship. No schema migration is needed — the `key_moments.source_video_id` FK already exists and the `SourceVideo.filename` column has the data (confirmed with real pipeline data on ub01).
|
||||||
|
|
||||||
|
Secondary gaps: missing meta line ("Compiled from N sources · M key moments · Last updated [date]"), signal chains rendered as plain `<ol>` lists instead of styled monospace blocks, and the key moments section doesn't show the video filename per spec §4.4 point 3 ("Each row: moment title, source video filename, clickable timestamp").
|
||||||
|
|
||||||
|
All changes are within the existing patterns: backend query/schema additions follow the `selectinload` + Pydantic pattern already used for creator_info; frontend CSS must use `var(--*)` tokens per the S02 dark theme system (77 custom properties in `:root`, zero hardcoded colors allowed outside `:root`).
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Single backend+frontend task is the cleanest approach. The backend change is small (add `selectinload(KeyMoment.source_video)`, extend `KeyMomentSummary` with `video_filename`, populate in endpoint), and the frontend changes depend directly on the new API shape. Signal chain rendering improvement and meta line are pure frontend CSS/JSX. All can be verified together with one deploy cycle.
|
||||||
|
|
||||||
|
## Implementation Landscape
|
||||||
|
|
||||||
|
### Key Files
|
||||||
|
|
||||||
|
- `backend/routers/techniques.py` — The `get_technique()` endpoint builds the detail response. Needs: add `selectinload(TechniquePage.key_moments).selectinload(KeyMoment.source_video)` to the query (chained selectinload for nested eager-load). In the response construction, populate `video_filename` from `km.source_video.filename`.
|
||||||
|
- `backend/schemas.py` — `KeyMomentSummary` class. Add `video_filename: str = ""` field. Also add `source_video_id: str | None = None` (uuid serialized as string) for potential future use.
|
||||||
|
- `frontend/src/api/public-client.ts` — `KeyMomentSummary` TypeScript interface. Add `video_filename: string` field.
|
||||||
|
- `frontend/src/pages/TechniquePage.tsx` — Main redesign target. Changes needed:
|
||||||
|
1. Add meta line below title: "Compiled from N sources · M key moments · Last updated [date]" — compute `N` from `new Set(key_moments.map(km => km.video_filename)).size`, `M` from `key_moments.length`, date from `technique.updated_at`.
|
||||||
|
2. Key moments: show `km.video_filename` in each row, between title and timestamp.
|
||||||
|
3. Signal chains: replace `<ol>` list rendering with a styled monospace flow block (arrows between steps: `Step1 → Step2 → Step3`).
|
||||||
|
- `frontend/src/App.css` — Add/modify CSS classes for: signal chain monospace blocks (`.technique-chain__flow`), video filename badge on key moments (`.technique-moment__source`), meta line (`.technique-header__stats`). All colors must use existing `var(--*)` tokens.
|
||||||
|
|
||||||
|
### Existing Data Shape (confirmed on ub01)
|
||||||
|
|
||||||
|
**body_sections:** `Record<string, string>` — keys are section titles, values are prose paragraphs. Current rendering handles this correctly.
|
||||||
|
|
||||||
|
**signal_chains:** `Array<{name: string, steps: string[]}>` — currently rendered as `<h3>` + `<ol>`. Spec says "monospace" — redesign as horizontal flow with arrow separators.
|
||||||
|
|
||||||
|
**key_moments join path:** `technique_pages → key_moments → source_videos`. The `KeyMoment.source_video` relationship already exists in models.py. Just needs `selectinload` in the query.
|
||||||
|
|
||||||
|
**Real data:** 2 technique pages exist with 2 and 13 key moments respectively. All moments come from 1 source video each. Filename format: `"Skope - Understanding Waveshapers (2160p).mp4"`.
|
||||||
|
|
||||||
|
### Build Order
|
||||||
|
|
||||||
|
1. **Backend first** — extend `KeyMomentSummary` schema + eager-load source_video in technique detail endpoint. This unblocks frontend work and is independently verifiable via `curl`.
|
||||||
|
2. **Frontend second** — update TypeScript types, then redesign TechniquePage.tsx with meta line, video filename on moments, monospace signal chains. CSS additions use existing dark theme tokens.
|
||||||
|
3. **Deploy + verify** — rebuild containers on ub01 and verify with real data.
|
||||||
|
|
||||||
|
### Verification Approach
|
||||||
|
|
||||||
|
1. `curl http://ub01:8096/api/v1/techniques/wave-shaping-synthesis-m-wave-shaper | python3 -m json.tool` — key_moments items should include `video_filename` field
|
||||||
|
2. `cd frontend && npm run build` — zero TypeScript errors
|
||||||
|
3. Deploy to ub01: `docker compose build && docker compose up -d`
|
||||||
|
4. Browser verification at `http://ub01:8096/techniques/wave-shaping-synthesis-m-wave-shaper`:
|
||||||
|
- Meta line visible below title showing source count, moment count, last updated
|
||||||
|
- Each key moment row shows video filename
|
||||||
|
- Signal chains render as monospace flow blocks with arrows, not numbered lists
|
||||||
|
- All new CSS uses `var(--*)` tokens (grep verification: zero new hex colors outside `:root`)
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- All CSS colors must use `var(--*)` tokens — S02 established 77 semantic custom properties and eliminated all hardcoded colors. Any new color must be defined as a new token in `:root` or use an existing one.
|
||||||
|
- The chained `selectinload` pattern (`selectinload(TechniquePage.key_moments).selectinload(KeyMoment.source_video)`) is required because the current query already uses `selectinload(TechniquePage.key_moments)`. Replacing it with a joined load would change the query shape for existing fields.
|
||||||
|
- No Alembic migration needed — all data already exists in the DB schema.
|
||||||
45
.gsd/milestones/M004/slices/S03/tasks/T01-PLAN.md
Normal file
45
.gsd/milestones/M004/slices/S03/tasks/T01-PLAN.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
estimated_steps: 18
|
||||||
|
estimated_files: 2
|
||||||
|
skills_used: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# T01: Add video_filename to technique detail API response
|
||||||
|
|
||||||
|
Extend the backend technique detail endpoint to include the source video filename on each key moment. This is the data prerequisite for all frontend changes in T02.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. In `backend/schemas.py`, add `video_filename: str = ""` to the `KeyMomentSummary` class.
|
||||||
|
2. In `backend/routers/techniques.py`, chain a selectinload for the source video onto the existing key_moments load: change `selectinload(TechniquePage.key_moments)` to `selectinload(TechniquePage.key_moments).selectinload(KeyMoment.source_video)`. Import `SourceVideo` from models if not already imported.
|
||||||
|
3. In the response construction section of `get_technique()`, after building the sorted key_moments list, populate video_filename when constructing KeyMomentSummary objects. Change the model_validate call to manually construct the dict or use a post-processing step:
|
||||||
|
```python
|
||||||
|
key_moment_items = []
|
||||||
|
for km in key_moments:
|
||||||
|
item = KeyMomentSummary.model_validate(km)
|
||||||
|
item.video_filename = km.source_video.filename if km.source_video else ""
|
||||||
|
key_moment_items.append(item)
|
||||||
|
```
|
||||||
|
4. Verify locally or on ub01 that `curl http://ub01:8096/api/v1/techniques/wave-shaping-synthesis-m-wave-shaper | python3 -m json.tool` now includes `video_filename` in each key_moments entry.
|
||||||
|
|
||||||
|
## Must-Haves
|
||||||
|
|
||||||
|
- [ ] `KeyMomentSummary` schema has `video_filename: str = ""` field
|
||||||
|
- [ ] Technique detail query uses chained selectinload for source_video
|
||||||
|
- [ ] Each key moment in API response includes populated `video_filename`
|
||||||
|
- [ ] Existing fields and behavior unchanged
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- `backend/schemas.py`
|
||||||
|
- `backend/routers/techniques.py`
|
||||||
|
- `backend/models.py`
|
||||||
|
|
||||||
|
## Expected Output
|
||||||
|
|
||||||
|
- `backend/schemas.py`
|
||||||
|
- `backend/routers/techniques.py`
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && docker compose build chrysopedia-api && docker compose up -d chrysopedia-api && sleep 3 && curl -s http://localhost:8096/api/v1/techniques/ | python3 -m json.tool | head -5' && ssh ub01 'curl -s http://localhost:8096/api/v1/techniques/wave-shaping-synthesis-m-wave-shaper | python3 -c "import sys,json; d=json.load(sys.stdin); kms=d.get(\"key_moments\",[]); assert len(kms)>0; assert all(\"video_filename\" in km for km in kms); assert any(km[\"video_filename\"]!=\"\" for km in kms); print(f\"OK: {len(kms)} moments, all have video_filename\")"'
|
||||||
79
.gsd/milestones/M004/slices/S03/tasks/T01-SUMMARY.md
Normal file
79
.gsd/milestones/M004/slices/S03/tasks/T01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
---
|
||||||
|
id: T01
|
||||||
|
parent: S03
|
||||||
|
milestone: M004
|
||||||
|
provides: []
|
||||||
|
requires: []
|
||||||
|
affects: []
|
||||||
|
key_files: ["backend/schemas.py", "backend/routers/techniques.py"]
|
||||||
|
key_decisions: ["Populate video_filename via post-processing loop rather than from_attributes auto-mapping, since the field comes from a joined relation not a direct column"]
|
||||||
|
patterns_established: []
|
||||||
|
drill_down_paths: []
|
||||||
|
observability_surfaces: []
|
||||||
|
duration: ""
|
||||||
|
verification_result: "Rebuilt chrysopedia-api container on ub01 and verified: (1) wave-shaping-synthesis-m-wave-shaper returns 13 key moments all with video_filename populated, (2) second technique page returns 2 moments with video_filename, (3) all existing fields unchanged, (4) assertion script confirms all moments have video_filename and at least one is non-empty."
|
||||||
|
completed_at: 2026-03-30T06:49:59.009Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T01: Added video_filename field to KeyMomentSummary schema and populated it via chained selectinload in the technique detail endpoint
|
||||||
|
|
||||||
|
> Added video_filename field to KeyMomentSummary schema and populated it via chained selectinload in the technique detail endpoint
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
---
|
||||||
|
id: T01
|
||||||
|
parent: S03
|
||||||
|
milestone: M004
|
||||||
|
key_files:
|
||||||
|
- backend/schemas.py
|
||||||
|
- backend/routers/techniques.py
|
||||||
|
key_decisions:
|
||||||
|
- Populate video_filename via post-processing loop rather than from_attributes auto-mapping, since the field comes from a joined relation not a direct column
|
||||||
|
duration: ""
|
||||||
|
verification_result: passed
|
||||||
|
completed_at: 2026-03-30T06:49:59.009Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T01: Added video_filename field to KeyMomentSummary schema and populated it via chained selectinload in the technique detail endpoint
|
||||||
|
|
||||||
|
**Added video_filename field to KeyMomentSummary schema and populated it via chained selectinload in the technique detail endpoint**
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
|
||||||
|
Added `video_filename: str = ""` to the KeyMomentSummary Pydantic schema. Chained `.selectinload(KeyMoment.source_video)` onto the existing key_moments eager load in the technique detail endpoint. Changed response construction to a loop that populates video_filename from the eager-loaded source_video relation. Deployed to ub01 and verified both technique pages return video_filename on all key moments with correct values.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Rebuilt chrysopedia-api container on ub01 and verified: (1) wave-shaping-synthesis-m-wave-shaper returns 13 key moments all with video_filename populated, (2) second technique page returns 2 moments with video_filename, (3) all existing fields unchanged, (4) assertion script confirms all moments have video_filename and at least one is non-empty.
|
||||||
|
|
||||||
|
## Verification Evidence
|
||||||
|
|
||||||
|
| # | Command | Exit Code | Verdict | Duration |
|
||||||
|
|---|---------|-----------|---------|----------|
|
||||||
|
| 1 | `ssh ub01 'docker compose build chrysopedia-api'` | 0 | ✅ pass | 8000ms |
|
||||||
|
| 2 | `ssh ub01 'docker compose up -d chrysopedia-api'` | 0 | ✅ pass | 5000ms |
|
||||||
|
| 3 | `ssh ub01 'curl -s .../techniques/wave-shaping-synthesis-m-wave-shaper | python3 assert video_filename'` | 0 | ✅ pass | 1000ms |
|
||||||
|
| 4 | `ssh ub01 'curl -s .../techniques/balancing-softness-... | python3 check video_filename'` | 0 | ✅ pass | 1000ms |
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `backend/schemas.py`
|
||||||
|
- `backend/routers/techniques.py`
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
None.
|
||||||
80
.gsd/milestones/M004/slices/S03/tasks/T02-PLAN.md
Normal file
80
.gsd/milestones/M004/slices/S03/tasks/T02-PLAN.md
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
---
|
||||||
|
estimated_steps: 44
|
||||||
|
estimated_files: 3
|
||||||
|
skills_used: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# T02: Redesign TechniquePage — meta line, video filenames, monospace signal chains
|
||||||
|
|
||||||
|
Redesign the frontend technique page to match the spec layout. Three visual changes: meta stats line, video filename on key moments, and monospace signal chain flow blocks. All CSS must use existing `var(--*)` tokens.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. In `frontend/src/api/public-client.ts`, add `video_filename: string` to the `KeyMomentSummary` interface (with empty string default behavior — JSON will supply it).
|
||||||
|
|
||||||
|
2. In `frontend/src/pages/TechniquePage.tsx`, add a meta stats line between the header and summary sections:
|
||||||
|
- Compute unique source count: `new Set(technique.key_moments.map(km => km.video_filename).filter(Boolean)).size`
|
||||||
|
- Moment count: `technique.key_moments.length`
|
||||||
|
- Last updated: format `technique.updated_at` as a readable date
|
||||||
|
- Render: `<div className="technique-header__stats">Compiled from {N} source{s} · {M} key moment{s} · Last updated {date}</div>`
|
||||||
|
|
||||||
|
3. In the key moments section, add the video filename between the title and timestamp:
|
||||||
|
```tsx
|
||||||
|
{km.video_filename && (
|
||||||
|
<span className="technique-moment__source">{km.video_filename}</span>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Redesign the signal chains section. Replace the `<ol className="technique-chain__steps">` with a monospace flow block:
|
||||||
|
```tsx
|
||||||
|
<div className="technique-chain__flow">
|
||||||
|
{steps.map((step, j) => (
|
||||||
|
<span key={j}>
|
||||||
|
{j > 0 && <span className="technique-chain__arrow"> → </span>}
|
||||||
|
<span className="technique-chain__step">{String(step)}</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
5. In `frontend/src/App.css`, add CSS classes using only `var(--*)` tokens:
|
||||||
|
- `.technique-header__stats` — secondary text color, smaller font, margin below header
|
||||||
|
- `.technique-moment__source` — truncated text, muted color, monospace-ish, smaller font
|
||||||
|
- `.technique-chain__flow` — monospace font, horizontal wrap, background surface, padding, border-radius
|
||||||
|
- `.technique-chain__arrow` — accent color (cyan)
|
||||||
|
- `.technique-chain__step` — inline display
|
||||||
|
- Remove or restyle `.technique-chain__steps` (the old `<ol>` style)
|
||||||
|
|
||||||
|
6. Run `cd frontend && npm run build` to verify zero TypeScript errors.
|
||||||
|
|
||||||
|
7. Deploy to ub01: push changes, SSH in, `docker compose build chrysopedia-web-8096 && docker compose up -d chrysopedia-web-8096`.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- All CSS colors MUST use `var(--*)` tokens. The S02 dark theme established 84 semantic custom properties. Grep verification: `grep -P '#[0-9a-fA-F]{3,8}' frontend/src/App.css | grep -v ':root' | grep -v '^\/\*'` should return zero new hex colors.
|
||||||
|
- Do NOT modify `:root` custom property definitions unless adding a genuinely new semantic token.
|
||||||
|
|
||||||
|
## Must-Haves
|
||||||
|
|
||||||
|
- [ ] TypeScript `KeyMomentSummary` includes `video_filename: string`
|
||||||
|
- [ ] Meta stats line renders below technique title
|
||||||
|
- [ ] Key moment rows show video filename
|
||||||
|
- [ ] Signal chains render as monospace flow with arrow separators
|
||||||
|
- [ ] All new CSS uses `var(--*)` tokens only
|
||||||
|
- [ ] `npm run build` succeeds with zero errors
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- `frontend/src/api/public-client.ts`
|
||||||
|
- `frontend/src/pages/TechniquePage.tsx`
|
||||||
|
- `frontend/src/App.css`
|
||||||
|
- `backend/schemas.py`
|
||||||
|
|
||||||
|
## Expected Output
|
||||||
|
|
||||||
|
- `frontend/src/api/public-client.ts`
|
||||||
|
- `frontend/src/pages/TechniquePage.tsx`
|
||||||
|
- `frontend/src/App.css`
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
cd frontend && npm run build 2>&1 | tail -5 && echo 'BUILD OK' && grep -cP '#[0-9a-fA-F]{3,8}' src/App.css | grep -v ':root' || echo 'No hardcoded colors outside :root'
|
||||||
|
|
@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from database import get_session
|
from database import get_session
|
||||||
from models import Creator, KeyMoment, RelatedTechniqueLink, TechniquePage
|
from models import Creator, KeyMoment, RelatedTechniqueLink, SourceVideo, TechniquePage
|
||||||
from schemas import (
|
from schemas import (
|
||||||
CreatorInfo,
|
CreatorInfo,
|
||||||
KeyMomentSummary,
|
KeyMomentSummary,
|
||||||
|
|
@ -75,7 +75,7 @@ async def get_technique(
|
||||||
select(TechniquePage)
|
select(TechniquePage)
|
||||||
.where(TechniquePage.slug == slug)
|
.where(TechniquePage.slug == slug)
|
||||||
.options(
|
.options(
|
||||||
selectinload(TechniquePage.key_moments),
|
selectinload(TechniquePage.key_moments).selectinload(KeyMoment.source_video),
|
||||||
selectinload(TechniquePage.creator),
|
selectinload(TechniquePage.creator),
|
||||||
selectinload(TechniquePage.outgoing_links).selectinload(
|
selectinload(TechniquePage.outgoing_links).selectinload(
|
||||||
RelatedTechniqueLink.target_page
|
RelatedTechniqueLink.target_page
|
||||||
|
|
@ -93,7 +93,11 @@ async def get_technique(
|
||||||
|
|
||||||
# Build key moments (ordered by start_time)
|
# Build key moments (ordered by start_time)
|
||||||
key_moments = sorted(page.key_moments, key=lambda km: km.start_time)
|
key_moments = sorted(page.key_moments, key=lambda km: km.start_time)
|
||||||
key_moment_items = [KeyMomentSummary.model_validate(km) for km in key_moments]
|
key_moment_items = []
|
||||||
|
for km in key_moments:
|
||||||
|
item = KeyMomentSummary.model_validate(km)
|
||||||
|
item.video_filename = km.source_video.filename if km.source_video else ""
|
||||||
|
key_moment_items.append(item)
|
||||||
|
|
||||||
# Build creator info
|
# Build creator info
|
||||||
creator_info = None
|
creator_info = None
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,7 @@ class KeyMomentSummary(BaseModel):
|
||||||
end_time: float
|
end_time: float
|
||||||
content_type: str
|
content_type: str
|
||||||
plugins: list[str] | None = None
|
plugins: list[str] | None = None
|
||||||
|
video_filename: str = ""
|
||||||
|
|
||||||
|
|
||||||
class RelatedLinkItem(BaseModel):
|
class RelatedLinkItem(BaseModel):
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue