diff --git a/.gsd/DECISIONS.md b/.gsd/DECISIONS.md index ab0bb17..01f2eba 100644 --- a/.gsd/DECISIONS.md +++ b/.gsd/DECISIONS.md @@ -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 | | 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 | +| 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 | diff --git a/.gsd/milestones/M004/M004-ROADMAP.md b/.gsd/milestones/M004/M004-ROADMAP.md index 0f1a1f6..949cd94 100644 --- a/.gsd/milestones/M004/M004-ROADMAP.md +++ b/.gsd/milestones/M004/M004-ROADMAP.md @@ -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 | |----|-------|------|---------|------|------------| | 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 | | S04 | Article Versioning + Pipeline Tuning Metadata | high | S03 | ⬜ | Technique pages track version history with pipeline metadata; API returns version list | diff --git a/.gsd/milestones/M004/slices/S02/S02-SUMMARY.md b/.gsd/milestones/M004/slices/S02/S02-SUMMARY.md new file mode 100644 index 0000000..fc32f50 --- /dev/null +++ b/.gsd/milestones/M004/slices/S02/S02-SUMMARY.md @@ -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 `` 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 +- `Chrysopedia` confirmed in index.html +- `` 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 diff --git a/.gsd/milestones/M004/slices/S02/S02-UAT.md b/.gsd/milestones/M004/slices/S02/S02-UAT.md new file mode 100644 index 0000000..8554a51 --- /dev/null +++ b/.gsd/milestones/M004/slices/S02/S02-UAT.md @@ -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 `` +2. **Expected:** `` 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 diff --git a/.gsd/milestones/M004/slices/S02/tasks/T02-VERIFY.json b/.gsd/milestones/M004/slices/S02/tasks/T02-VERIFY.json new file mode 100644 index 0000000..ba9901e --- /dev/null +++ b/.gsd/milestones/M004/slices/S02/tasks/T02-VERIFY.json @@ -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' 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 +} diff --git a/.gsd/milestones/M004/slices/S03/S03-PLAN.md b/.gsd/milestones/M004/slices/S03/S03-PLAN.md index 5ac076e..dd99bed 100644 --- a/.gsd/milestones/M004/slices/S03/S03-PLAN.md +++ b/.gsd/milestones/M004/slices/S03/S03-PLAN.md @@ -1,6 +1,89 @@ # 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 ## 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: `
Compiled from {N} source{s} · {M} key moment{s} · Last updated {date}
` + +3. In the key moments section, add the video filename between the title and timestamp: + ```tsx + {km.video_filename && ( + {km.video_filename} + )} + ``` + +4. Redesign the signal chains section. Replace the `
    ` with a monospace flow block: + ```tsx +
    + {steps.map((step, j) => ( + + {j > 0 && } + {String(step)} + + ))} +
    + ``` + +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 `
      ` 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' diff --git a/.gsd/milestones/M004/slices/S03/S03-RESEARCH.md b/.gsd/milestones/M004/slices/S03/S03-RESEARCH.md new file mode 100644 index 0000000..a9ed65d --- /dev/null +++ b/.gsd/milestones/M004/slices/S03/S03-RESEARCH.md @@ -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 `
        ` 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 `
          ` 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` — keys are section titles, values are prose paragraphs. Current rendering handles this correctly. + +**signal_chains:** `Array<{name: string, steps: string[]}>` — currently rendered as `

          ` + `
            `. 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. diff --git a/.gsd/milestones/M004/slices/S03/tasks/T01-PLAN.md b/.gsd/milestones/M004/slices/S03/tasks/T01-PLAN.md new file mode 100644 index 0000000..b1a60b0 --- /dev/null +++ b/.gsd/milestones/M004/slices/S03/tasks/T01-PLAN.md @@ -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\")"' diff --git a/.gsd/milestones/M004/slices/S03/tasks/T01-SUMMARY.md b/.gsd/milestones/M004/slices/S03/tasks/T01-SUMMARY.md new file mode 100644 index 0000000..f3a47ad --- /dev/null +++ b/.gsd/milestones/M004/slices/S03/tasks/T01-SUMMARY.md @@ -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. diff --git a/.gsd/milestones/M004/slices/S03/tasks/T02-PLAN.md b/.gsd/milestones/M004/slices/S03/tasks/T02-PLAN.md new file mode 100644 index 0000000..4b6a3cc --- /dev/null +++ b/.gsd/milestones/M004/slices/S03/tasks/T02-PLAN.md @@ -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: `
            Compiled from {N} source{s} · {M} key moment{s} · Last updated {date}
            ` + +3. In the key moments section, add the video filename between the title and timestamp: + ```tsx + {km.video_filename && ( + {km.video_filename} + )} + ``` + +4. Redesign the signal chains section. Replace the `
              ` with a monospace flow block: + ```tsx +
              + {steps.map((step, j) => ( + + {j > 0 && } + {String(step)} + + ))} +
              + ``` + +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 `
                ` 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' diff --git a/backend/routers/techniques.py b/backend/routers/techniques.py index 5ca68c6..7d2e676 100644 --- a/backend/routers/techniques.py +++ b/backend/routers/techniques.py @@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from database import get_session -from models import Creator, KeyMoment, RelatedTechniqueLink, TechniquePage +from models import Creator, KeyMoment, RelatedTechniqueLink, SourceVideo, TechniquePage from schemas import ( CreatorInfo, KeyMomentSummary, @@ -75,7 +75,7 @@ async def get_technique( select(TechniquePage) .where(TechniquePage.slug == slug) .options( - selectinload(TechniquePage.key_moments), + selectinload(TechniquePage.key_moments).selectinload(KeyMoment.source_video), selectinload(TechniquePage.creator), selectinload(TechniquePage.outgoing_links).selectinload( RelatedTechniqueLink.target_page @@ -93,7 +93,11 @@ async def get_technique( # Build key moments (ordered by 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 creator_info = None diff --git a/backend/schemas.py b/backend/schemas.py index d989758..7afe140 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -286,6 +286,7 @@ class KeyMomentSummary(BaseModel): end_time: float content_type: str plugins: list[str] | None = None + video_filename: str = "" class RelatedLinkItem(BaseModel):