# 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.
