diff --git a/.gsd/DECISIONS.md b/.gsd/DECISIONS.md index a3d5613..8d7d896 100644 --- a/.gsd/DECISIONS.md +++ b/.gsd/DECISIONS.md @@ -42,3 +42,4 @@ | D034 | | architecture | Documentation strategy for Phase 2 | Forgejo wiki at forgejo.xpltd.co populated incrementally — KB slice at end of every milestone | KB stays current by documenting what just shipped at each milestone boundary. Final comprehensive pass in M025. Newcomers can onboard at any point during Phase 2 development. | Yes | collaborative | | D035 | | architecture | File/object storage for creator posts, shorts, and file distribution | MinIO (S3-compatible) self-hosted on ub01 home server stack | Docker-native, S3-compatible API for signed URLs with expiration. Already fits the self-hosted infrastructure model. Handles presets, sample packs, shorts output, and gated downloads. | Yes | collaborative | | D036 | M019/S02 | architecture | JWT auth configuration for creator authentication | HS256 with existing app_secret_key, 24-hour expiry, OAuth2PasswordBearer at /api/v1/auth/login | Reuses existing secret from config.py settings. 24-hour expiry balances convenience with security for a single-admin/invite-only tool. OAuth2PasswordBearer integrates with FastAPI's dependency injection and auto-generates OpenAPI security schemes. | Yes | agent | +| D037 | | architecture | Search impressions query strategy for creator dashboard | Exact case-insensitive title match via EXISTS subquery against SearchLog | MVP approach — counts SearchLog rows where query exactly matches (case-insensitive) any of the creator's technique page titles. Sufficient for initial dashboard. Can be expanded to ILIKE partial matching or full-text search later when more search data accumulates. | Yes | agent | diff --git a/.gsd/milestones/M020/M020-ROADMAP.md b/.gsd/milestones/M020/M020-ROADMAP.md index 1186eff..e2cbddb 100644 --- a/.gsd/milestones/M020/M020-ROADMAP.md +++ b/.gsd/milestones/M020/M020-ROADMAP.md @@ -7,7 +7,7 @@ Creators can log in, see analytics, play video in a custom player, and manage co | ID | Slice | Risk | Depends | Done | After this | |----|-------|------|---------|------|------------| | S01 | [A] Web Media Player MVP | high | — | ✅ | Custom video player with HLS playback, speed controls (0.5x-2x), and synchronized transcript sidebar | -| S02 | [A] Creator Dashboard with Real Analytics | medium | — | ⬜ | Dashboard shows upload count, technique pages generated, search impressions, content library | +| S02 | [A] Creator Dashboard with Real Analytics | medium | — | ✅ | Dashboard shows upload count, technique pages generated, search impressions, content library | | S03 | [A] Consent Dashboard UI | low | — | ⬜ | Creator can toggle per-video consent settings (KB, AI training, shorts, embedding) through the dashboard | | S04 | [A] Admin Impersonation | high | — | ⬜ | Admin clicks View As next to any creator → sees the site as that creator with amber warning banner. Read-only. Full audit log. | | S05 | [B] LightRAG Validation & A/B Testing | medium | — | ⬜ | Side-by-side comparison of top 20 queries: current Qdrant search vs LightRAG results with quality scoring | diff --git a/.gsd/milestones/M020/slices/S02/S02-SUMMARY.md b/.gsd/milestones/M020/slices/S02/S02-SUMMARY.md new file mode 100644 index 0000000..419dd62 --- /dev/null +++ b/.gsd/milestones/M020/slices/S02/S02-SUMMARY.md @@ -0,0 +1,94 @@ +--- +id: S02 +parent: M020 +milestone: M020 +provides: + - GET /api/v1/creator/dashboard endpoint with auth + - CreatorDashboard UI with real analytics +requires: + [] +affects: + - S07 +key_files: + - backend/routers/creator_dashboard.py + - backend/schemas.py + - backend/main.py + - frontend/src/api/creator-dashboard.ts + - frontend/src/pages/CreatorDashboard.tsx + - frontend/src/pages/CreatorDashboard.module.css +key_decisions: + - Search impressions use exact case-insensitive title match via EXISTS subquery (D037) + - Alembic migration 016 rewritten to raw SQL to avoid asyncpg enum double-creation bug + - Separate desktop table and mobile card views toggled via CSS media queries +patterns_established: + - Authenticated creator endpoint pattern: resolve creator_id from user → 404 if null → aggregate queries → content lists + - CSS module ?? '' fallback pattern for noUncheckedIndexedAccess strict mode +observability_surfaces: + - chrysopedia.creator_dashboard logger with per-request stats (video_count, technique_count, moments, impressions) +drill_down_paths: + - .gsd/milestones/M020/slices/S02/tasks/T01-SUMMARY.md + - .gsd/milestones/M020/slices/S02/tasks/T02-SUMMARY.md +duration: "" +verification_result: passed +completed_at: 2026-04-04T00:15:19.382Z +blocker_discovered: false +--- + +# S02: [A] Creator Dashboard with Real Analytics + +**Added authenticated creator dashboard endpoint and replaced placeholder UI with real analytics — 4 stat cards, techniques table, videos table, responsive layout.** + +## What Happened + +Two tasks delivered the full creator dashboard vertical slice. + +**T01 (Backend):** Created `GET /api/v1/creator/dashboard` behind `get_current_user` auth. The endpoint resolves `creator_id` from the authenticated user, runs aggregate count queries (videos, techniques, key moments), computes search impressions via an EXISTS subquery against SearchLog, and returns content lists (techniques with per-page key moment counts, videos with processing status). Added Pydantic response schemas to `schemas.py` and registered the router in `main.py`. Fixed Alembic migration 016 to use raw SQL for enum/table creation (asyncpg double-creation bug workaround). Verified on ub01 via direct API and nginx proxy — authenticated requests return correct JSON, unauthenticated returns 401. + +**T02 (Frontend):** Created `frontend/src/api/creator-dashboard.ts` with TypeScript types matching the backend response and a `fetchCreatorDashboard()` function using the shared `request()` helper. Rewrote `CreatorDashboard.tsx` from 3 placeholder cards to a full dashboard: 4 stat cards (uploads, techniques, key moments, search impressions), techniques table with linked titles and category badges, videos table with processing status badges. Handles loading skeleton, error state, and not-linked empty state. Responsive layout — tables on desktop, stacked cards on mobile. TypeScript strict mode satisfied with `?? ''` fallback on CSS module class lookups. + +## Verification + +- `npx tsc --noEmit` — zero errors (frontend type check) +- `npm run build` — Vite production build succeeds, 88 modules transformed +- Backend endpoint verified on ub01: authenticated returns full JSON payload (video_count=3, technique_count=2, key_moment_count=30, search_impressions=0, techniques array, videos array) +- Unauthenticated request returns 401 +- Works through both direct API (:8000) and nginx proxy (:8096) + +## Requirements Advanced + +None. + +## Requirements Validated + +None. + +## New Requirements Surfaced + +None. + +## Requirements Invalidated or Re-scoped + +None. + +## Deviations + +Migration 016 rewritten to raw SQL to avoid asyncpg enum double-creation bug. Entire backend synced to ub01 (was behind). CSS module class lookups needed ?? '' fallback for noUncheckedIndexedAccess strict mode. + +## Known Limitations + +search_impressions returns 0 on current data — exact title match finds no hits in SearchLog. Will become useful as search traffic grows. Can be expanded to ILIKE partial matching later. + +## Follow-ups + +Consider ILIKE partial matching for search impressions when there's enough search data to evaluate. Add GET /api/v1/creator/dashboard integration test with a seeded creator. + +## Files Created/Modified + +- `backend/routers/creator_dashboard.py` — New authenticated creator dashboard endpoint with aggregate queries and content lists +- `backend/schemas.py` — Added CreatorDashboardResponse, CreatorDashboardTechnique, CreatorDashboardVideo Pydantic schemas +- `backend/main.py` — Registered creator_dashboard router at /api/v1/creator +- `alembic/versions/016_add_users_and_invite_codes.py` — Rewritten to raw SQL for enum/table creation (asyncpg bug workaround) +- `frontend/src/api/creator-dashboard.ts` — New API module with TS types and fetchCreatorDashboard() function +- `frontend/src/api/index.ts` — Re-exported creator-dashboard module +- `frontend/src/pages/CreatorDashboard.tsx` — Replaced placeholders with real dashboard: stat cards, techniques table, videos table, loading/error/empty states +- `frontend/src/pages/CreatorDashboard.module.css` — Styles for stat cards, data tables, badges, responsive mobile layout diff --git a/.gsd/milestones/M020/slices/S02/S02-UAT.md b/.gsd/milestones/M020/slices/S02/S02-UAT.md new file mode 100644 index 0000000..4c09b4a --- /dev/null +++ b/.gsd/milestones/M020/slices/S02/S02-UAT.md @@ -0,0 +1,64 @@ +# S02: [A] Creator Dashboard with Real Analytics — UAT + +**Milestone:** M020 +**Written:** 2026-04-04T00:15:19.382Z + +## UAT: Creator Dashboard with Real Analytics + +### Preconditions +- Chrysopedia stack running on ub01 (API + Web + DB) +- A user account exists with a linked `creator_id` (e.g., test creator with videos/techniques in DB) +- A second user account exists with `creator_id = NULL` (or no creator link) +- Valid JWT tokens for both users + +### Test Cases + +#### TC1: Authenticated dashboard returns correct stats +1. Obtain a JWT for a user with a linked creator +2. `curl -s -H 'Authorization: Bearer $TOKEN' http://ub01:8096/api/v1/creator/dashboard | python3 -m json.tool` +3. **Expected:** JSON response with `video_count`, `technique_count`, `key_moment_count`, `search_impressions` (integers), `techniques` (array of objects with title, slug, topic_category, created_at, key_moment_count), `videos` (array of objects with filename, processing_status, created_at) +4. Verify counts match actual DB content for that creator + +#### TC2: Unauthenticated request rejected +1. `curl -s -o /dev/null -w '%{http_code}' http://ub01:8096/api/v1/creator/dashboard` +2. **Expected:** 401 + +#### TC3: User without creator_id gets 404 +1. Obtain a JWT for a user with no linked creator (creator_id is NULL) +2. `curl -s -o /dev/null -w '%{http_code}' -H 'Authorization: Bearer $TOKEN' http://ub01:8096/api/v1/creator/dashboard` +3. **Expected:** 404 with message "No creator profile linked to this account" + +#### TC4: Frontend renders stat cards +1. Log in as a creator user at http://ub01:8096 +2. Navigate to the creator dashboard page +3. **Expected:** 4 stat cards visible — Uploads, Techniques, Key Moments, Search Impressions — each showing a number and label + +#### TC5: Frontend techniques table populated +1. On the dashboard page (logged in as creator with techniques) +2. **Expected:** Techniques section shows a table (desktop) or cards (mobile) with columns: title (clickable link), category badge, key moments count, date +3. Click a technique title +4. **Expected:** Navigates to `/techniques/{slug}` for that technique + +#### TC6: Frontend videos table populated +1. On the dashboard page (logged in as creator with videos) +2. **Expected:** Videos section shows filename, processing status badge (color-coded), created date + +#### TC7: Loading state +1. Throttle network to slow 3G in browser DevTools +2. Navigate to dashboard +3. **Expected:** Loading skeleton or spinner shown while data fetches + +#### TC8: Empty/not-linked state +1. Log in as a user with no linked creator +2. Navigate to dashboard +3. **Expected:** Friendly message indicating no creator profile is linked — not a raw error + +#### TC9: Mobile responsive layout +1. Set viewport to 375px width +2. Navigate to dashboard (logged in as creator) +3. **Expected:** Stat cards stack in a single column. Content tables render as stacked cards, not a horizontal table + +#### TC10: Search impressions accuracy +1. Add entries to `search_log` table where `LOWER(query)` exactly matches a technique page title for the test creator +2. Refresh dashboard +3. **Expected:** Search Impressions stat card shows the count of matching search log entries diff --git a/.gsd/milestones/M020/slices/S02/tasks/T02-VERIFY.json b/.gsd/milestones/M020/slices/S02/tasks/T02-VERIFY.json new file mode 100644 index 0000000..23d37b4 --- /dev/null +++ b/.gsd/milestones/M020/slices/S02/tasks/T02-VERIFY.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "taskId": "T02", + "unitId": "M020/S02/T02", + "timestamp": 1775261628865, + "passed": false, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd frontend", + "exitCode": 0, + "durationMs": 4, + "verdict": "pass" + }, + { + "command": "npx tsc --noEmit", + "exitCode": 1, + "durationMs": 803, + "verdict": "fail" + }, + { + "command": "npm run build — both pass with zero errors. Visual check: dashboard page renders stat cards with numbers and content library table.", + "exitCode": 254, + "durationMs": 95, + "verdict": "fail" + } + ], + "retryAttempt": 1, + "maxRetries": 2 +} diff --git a/.gsd/milestones/M020/slices/S03/S03-PLAN.md b/.gsd/milestones/M020/slices/S03/S03-PLAN.md index 4f53bfe..03b0673 100644 --- a/.gsd/milestones/M020/slices/S03/S03-PLAN.md +++ b/.gsd/milestones/M020/slices/S03/S03-PLAN.md @@ -1,6 +1,61 @@ # S03: [A] Consent Dashboard UI -**Goal:** Build the frontend for consent management using the M019 consent API +**Goal:** Creator can view and toggle per-video consent settings (kb_inclusion, training_usage, public_display) through the creator dashboard, with audit history visible per video. **Demo:** After this: Creator can toggle per-video consent settings (KB, AI training, shorts, embedding) through the dashboard ## Tasks +- [x] **T01: Created TypeScript consent API client with 5 fetch functions and a reusable accessible ToggleSwitch component with CSS module styling** — Create the TypeScript API client with types mirroring backend consent schemas, and a reusable ToggleSwitch component. These are independent building blocks that T02 consumes. + +## Steps + +1. Create `frontend/src/api/consent.ts` with TypeScript interfaces matching backend schemas: `VideoConsentRead` (id, video_id, video_title, kb_inclusion, training_usage, public_display, updated_at, updated_by), `ConsentAuditEntry` (id, video_id, field_name, old_value, new_value, changed_by, changed_at), `ConsentListResponse` (items: VideoConsentRead[], total: number). Add fetch functions: `fetchConsentList(token)` calling GET /api/v1/consent/videos, `fetchVideoConsent(token, videoId)` calling GET /api/v1/consent/videos/{videoId}, `updateVideoConsent(token, videoId, updates)` calling PUT /api/v1/consent/videos/{videoId}, `fetchConsentHistory(token, videoId)` calling GET /api/v1/consent/videos/{videoId}/history. Use `request()` from `api/client.ts` with `BASE` prefix. + +2. Create `frontend/src/components/ToggleSwitch.tsx` — a reusable toggle component with props: `checked: boolean`, `onChange: (checked: boolean) => void`, `label: string`, `disabled?: boolean`, `id: string`. Render an accessible checkbox input visually styled as a sliding toggle. Use a CSS module `ToggleSwitch.module.css` with a track (40×22px), sliding circle thumb, checked state colors using existing CSS custom properties (--color-accent for on, --color-surface-2 for off), smooth 200ms transition, and disabled opacity. The input must have aria-label and the visual label must be associated. + +3. Export consent API functions from `frontend/src/api/index.ts`. + +## Must-Haves + +- [ ] TypeScript types match the 3 consent boolean fields (kb_inclusion, training_usage, public_display) +- [ ] API functions use request() from client.ts with proper Authorization header +- [ ] ToggleSwitch is accessible (checkbox input, aria-label, label association) +- [ ] ToggleSwitch has disabled state with visual feedback + - Estimate: 30m + - Files: frontend/src/api/consent.ts, frontend/src/components/ToggleSwitch.tsx, frontend/src/components/ToggleSwitch.module.css, frontend/src/api/index.ts + - Verify: cd frontend && npx tsc --noEmit && echo 'Types OK' +- [ ] **T02: Build ConsentDashboard page with route wiring and sidebar nav** — Build the main consent page, wire it into the app router and sidebar navigation. This is the user-facing deliverable. + +## Steps + +1. Create `frontend/src/pages/ConsentDashboard.module.css` with styles for the consent page. Follow CreatorSettings.module.css patterns. Key classes: `.consentPage` container, `.videoRow` for each video's consent card, `.toggleRow` for label + toggle pairs, `.historyToggle` button to expand audit trail, `.historyList` for audit entries, `.emptyState` for no-videos message, `.loading` and `.error` states. Use CSS custom properties from the existing theme. + +2. Create `frontend/src/pages/ConsentDashboard.tsx`: + - Import `SidebarNav` from `CreatorDashboard`, `dashStyles` from `CreatorDashboard.module.css`, `useAuth` from context, `useDocumentTitle` hook + - Import consent API functions from `../api/consent` and `ToggleSwitch` from `../components/ToggleSwitch` + - Layout: `
` (matches CreatorSettings pattern) + - On mount, call `fetchConsentList(token)` to get all videos with consent state + - Render each video as a card with title, 3 ToggleSwitch components (KB Inclusion, Training Usage, Public Display) + - On toggle change, call `updateVideoConsent()` and update local state optimistically + - Each video card has an expandable "History" section that lazy-loads audit trail via `fetchConsentHistory()` on first expand + - Handle loading, error, and empty states + - Call `useDocumentTitle('Consent Settings')` + +3. Update `frontend/src/pages/CreatorDashboard.tsx` SidebarNav to add a "Consent" link pointing to `/creator/consent`. Add a shield/lock SVG icon consistent with existing sidebar icons. Mark active when pathname is `/creator/consent`. + +4. Update `frontend/src/App.tsx`: + - Add lazy import: `const ConsentDashboard = lazy(() => import('./pages/ConsentDashboard'))` + - Add route: `}>} />` + +## Must-Haves + +- [ ] ConsentDashboard uses shared SidebarNav + dashStyles.layout +- [ ] All 3 consent fields rendered as toggles per video +- [ ] Toggle changes call PUT endpoint and update UI +- [ ] Audit history expandable per video +- [ ] Route registered at /creator/consent with ProtectedRoute + Suspense +- [ ] Sidebar nav has Consent link with active state +- [ ] useDocumentTitle called +- [ ] Loading, error, and empty states handled + - Estimate: 1h + - Files: frontend/src/pages/ConsentDashboard.tsx, frontend/src/pages/ConsentDashboard.module.css, frontend/src/pages/CreatorDashboard.tsx, frontend/src/App.tsx + - Verify: cd frontend && npx tsc --noEmit && npm run build && echo 'Build OK' diff --git a/.gsd/milestones/M020/slices/S03/S03-RESEARCH.md b/.gsd/milestones/M020/slices/S03/S03-RESEARCH.md new file mode 100644 index 0000000..9115c5c --- /dev/null +++ b/.gsd/milestones/M020/slices/S03/S03-RESEARCH.md @@ -0,0 +1,81 @@ +# S03 Research — Consent Dashboard UI + +## Summary + +This is a **frontend-only** slice. The entire backend — model, migration, router, schemas, 21 integration tests — is already built and tested. The work is: build a React page that calls the existing consent API and integrate it into the creator dashboard sidebar. + +## Recommendation + +Light implementation. Follow the CreatorSettings page pattern (shared sidebar layout, CSS modules, `useAuth` for token). Build a toggle switch component and a consent page. No backend changes needed. + +## Implementation Landscape + +### What Exists (Backend — Complete) + +| Layer | File | Status | +|-------|------|--------| +| Model | `backend/models.py` lines 573-655 | `VideoConsent`, `ConsentAuditLog`, `ConsentField` enum | +| Migration | `alembic/versions/017_add_consent_tables.py` | Tables: `video_consents`, `consent_audit_log` | +| Router | `backend/routers/consent.py` | 5 endpoints, ownership-gated, audit logged | +| Schemas | `backend/schemas.py` lines 578-625 | `VideoConsentUpdate`, `VideoConsentRead`, `ConsentAuditEntry`, `ConsentListResponse`, `ConsentSummary` | +| Tests | `backend/tests/test_consent.py` | 21 tests covering auth, CRUD, audit, pagination, admin | +| Registration | `backend/main.py:82` | `app.include_router(consent.router, prefix="/api/v1")` | + +**API Endpoints:** +- `GET /api/v1/consent/videos` — list consent for current creator's videos (paginated) +- `GET /api/v1/consent/videos/{video_id}` — single video consent +- `PUT /api/v1/consent/videos/{video_id}` — upsert consent (partial update, audit logged) +- `GET /api/v1/consent/videos/{video_id}/history` — audit trail +- `GET /api/v1/consent/admin/summary` — aggregate counts (admin only) + +### Consent Flags (3, Not 4) + +The roadmap description says "(KB, AI training, shorts, embedding)" — 4 toggles. The actual schema has **3 boolean fields**: +- `kb_inclusion` (default false) — include in knowledge base +- `training_usage` (default false) — allow AI training usage +- `public_display` (default true) — show publicly + +No `shorts_clips` or `embedding` columns exist. The planner should build the UI for the 3 fields that exist, not the 4 the roadmap aspirationally describes. + +### What Exists (Frontend — Dashboard Shell) + +| File | Purpose | +|------|---------| +| `frontend/src/pages/CreatorDashboard.tsx` | Main dashboard with `SidebarNav` (exported), stats, techniques table, videos table | +| `frontend/src/pages/CreatorDashboard.module.css` | 377-line module with layout, sidebar, stat cards, tables, mobile cards, badges | +| `frontend/src/pages/CreatorSettings.tsx` | Settings page — **pattern to follow** — uses shared `SidebarNav` + `dashStyles.layout` | +| `frontend/src/pages/CreatorSettings.module.css` | 115-line module with form styles | +| `frontend/src/api/creator-dashboard.ts` | API client — `request()` from `client.ts` with Bearer token auto-injection | +| `frontend/src/api/client.ts` | Shared `request()` helper, `BASE="/api/v1"`, `ApiError` class, auto `Authorization` header | + +**Sidebar nav** currently has: Dashboard (active), Content (disabled/coming-soon), Settings. The consent page needs a new nav link. + +**Routing** is in `frontend/src/App.tsx` — ProtectedRoute + Suspense pattern at `/creator/dashboard` and `/creator/settings`. + +### What Needs Building + +1. **API client** (`frontend/src/api/consent.ts`) — types + fetch functions for the 5 consent endpoints +2. **Toggle switch component** (`frontend/src/components/ToggleSwitch.tsx` + CSS module) — no existing toggle in the codebase +3. **Consent page** (`frontend/src/pages/ConsentDashboard.tsx` + CSS module) — list videos with toggle rows, expandable audit history +4. **Sidebar nav update** — add "Consent" link to `SidebarNav` in `CreatorDashboard.tsx` +5. **Route registration** — add `/creator/consent` route in `App.tsx` + +### Patterns to Follow + +- **Layout**: `
` (see CreatorSettings) +- **Data fetching**: `useEffect` + `useState` + `request()` from `api/client.ts` +- **Error handling**: `ApiError` class with `.status` and `.detail` +- **Document title**: `useDocumentTitle("Consent Settings")` +- **Lazy loading**: `const ConsentDashboard = lazy(() => import("./pages/ConsentDashboard"))` in App.tsx + +### Natural Task Seams + +1. **T01: API client + types** — `frontend/src/api/consent.ts` with TS interfaces mirroring the backend schemas and fetch functions. Small, independent, unblocks T03. +2. **T02: Toggle switch component** — Reusable `ToggleSwitch` with label, checked, onChange, disabled props. CSS module. Independent of consent-specific code. +3. **T03: Consent page + route wiring** — The main page component, CSS module, sidebar nav link, App.tsx route. Depends on T01 and T02. + +### Verification + +- `cd frontend && npx tsc --noEmit` — zero type errors +- `cd frontend && npm run build` — production build succeeds +- Visual verification: navigate to `/creator/consent` in browser, see video list with toggles, toggle a flag, verify it persists on reload, check audit history expands diff --git a/.gsd/milestones/M020/slices/S03/tasks/T01-PLAN.md b/.gsd/milestones/M020/slices/S03/tasks/T01-PLAN.md new file mode 100644 index 0000000..10724b5 --- /dev/null +++ b/.gsd/milestones/M020/slices/S03/tasks/T01-PLAN.md @@ -0,0 +1,39 @@ +--- +estimated_steps: 10 +estimated_files: 4 +skills_used: [] +--- + +# T01: Build consent API client and ToggleSwitch component + +Create the TypeScript API client with types mirroring backend consent schemas, and a reusable ToggleSwitch component. These are independent building blocks that T02 consumes. + +## Steps + +1. Create `frontend/src/api/consent.ts` with TypeScript interfaces matching backend schemas: `VideoConsentRead` (id, video_id, video_title, kb_inclusion, training_usage, public_display, updated_at, updated_by), `ConsentAuditEntry` (id, video_id, field_name, old_value, new_value, changed_by, changed_at), `ConsentListResponse` (items: VideoConsentRead[], total: number). Add fetch functions: `fetchConsentList(token)` calling GET /api/v1/consent/videos, `fetchVideoConsent(token, videoId)` calling GET /api/v1/consent/videos/{videoId}, `updateVideoConsent(token, videoId, updates)` calling PUT /api/v1/consent/videos/{videoId}, `fetchConsentHistory(token, videoId)` calling GET /api/v1/consent/videos/{videoId}/history. Use `request()` from `api/client.ts` with `BASE` prefix. + +2. Create `frontend/src/components/ToggleSwitch.tsx` — a reusable toggle component with props: `checked: boolean`, `onChange: (checked: boolean) => void`, `label: string`, `disabled?: boolean`, `id: string`. Render an accessible checkbox input visually styled as a sliding toggle. Use a CSS module `ToggleSwitch.module.css` with a track (40×22px), sliding circle thumb, checked state colors using existing CSS custom properties (--color-accent for on, --color-surface-2 for off), smooth 200ms transition, and disabled opacity. The input must have aria-label and the visual label must be associated. + +3. Export consent API functions from `frontend/src/api/index.ts`. + +## Must-Haves + +- [ ] TypeScript types match the 3 consent boolean fields (kb_inclusion, training_usage, public_display) +- [ ] API functions use request() from client.ts with proper Authorization header +- [ ] ToggleSwitch is accessible (checkbox input, aria-label, label association) +- [ ] ToggleSwitch has disabled state with visual feedback + +## Inputs + +- `frontend/src/api/client.ts` +- `backend/schemas.py` + +## Expected Output + +- `frontend/src/api/consent.ts` +- `frontend/src/components/ToggleSwitch.tsx` +- `frontend/src/components/ToggleSwitch.module.css` + +## Verification + +cd frontend && npx tsc --noEmit && echo 'Types OK' diff --git a/.gsd/milestones/M020/slices/S03/tasks/T01-SUMMARY.md b/.gsd/milestones/M020/slices/S03/tasks/T01-SUMMARY.md new file mode 100644 index 0000000..0d61c8f --- /dev/null +++ b/.gsd/milestones/M020/slices/S03/tasks/T01-SUMMARY.md @@ -0,0 +1,81 @@ +--- +id: T01 +parent: S03 +milestone: M020 +provides: [] +requires: [] +affects: [] +key_files: ["frontend/src/api/consent.ts", "frontend/src/components/ToggleSwitch.tsx", "frontend/src/components/ToggleSwitch.module.css", "frontend/src/api/index.ts"] +key_decisions: ["Followed existing pattern of no explicit token param — request() auto-injects from localStorage", "Used data-attributes for state styling (data-checked, data-disabled) for cleaner CSS selectors"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "TypeScript compilation passed with zero errors: cd frontend && npx tsc --noEmit" +completed_at: 2026-04-04T00:21:09.776Z +blocker_discovered: false +--- + +# T01: Created TypeScript consent API client with 5 fetch functions and a reusable accessible ToggleSwitch component with CSS module styling + +> Created TypeScript consent API client with 5 fetch functions and a reusable accessible ToggleSwitch component with CSS module styling + +## What Happened +--- +id: T01 +parent: S03 +milestone: M020 +key_files: + - frontend/src/api/consent.ts + - frontend/src/components/ToggleSwitch.tsx + - frontend/src/components/ToggleSwitch.module.css + - frontend/src/api/index.ts +key_decisions: + - Followed existing pattern of no explicit token param — request() auto-injects from localStorage + - Used data-attributes for state styling (data-checked, data-disabled) for cleaner CSS selectors +duration: "" +verification_result: passed +completed_at: 2026-04-04T00:21:09.776Z +blocker_discovered: false +--- + +# T01: Created TypeScript consent API client with 5 fetch functions and a reusable accessible ToggleSwitch component with CSS module styling + +**Created TypeScript consent API client with 5 fetch functions and a reusable accessible ToggleSwitch component with CSS module styling** + +## What Happened + +Built two independent building blocks for the consent dashboard: (1) consent.ts API client with types mirroring backend schemas and 5 fetch functions using the established request() pattern with auto-injected auth, (2) ToggleSwitch component with accessible checkbox (role=switch, aria-label, label association), CSS module styling using project color tokens, 200ms slide transition, and disabled state. Exported consent module from the api barrel. + +## Verification + +TypeScript compilation passed with zero errors: cd frontend && npx tsc --noEmit + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd frontend && npx tsc --noEmit` | 0 | ✅ pass | 8000ms | + + +## Deviations + +Added ConsentSummary type and fetchConsentSummary() not in original plan — backend schema defines it. Used data-attributes for state styling instead of className toggling. + +## Known Issues + +None. + +## Files Created/Modified + +- `frontend/src/api/consent.ts` +- `frontend/src/components/ToggleSwitch.tsx` +- `frontend/src/components/ToggleSwitch.module.css` +- `frontend/src/api/index.ts` + + +## Deviations +Added ConsentSummary type and fetchConsentSummary() not in original plan — backend schema defines it. Used data-attributes for state styling instead of className toggling. + +## Known Issues +None. diff --git a/.gsd/milestones/M020/slices/S03/tasks/T02-PLAN.md b/.gsd/milestones/M020/slices/S03/tasks/T02-PLAN.md new file mode 100644 index 0000000..005d7d6 --- /dev/null +++ b/.gsd/milestones/M020/slices/S03/tasks/T02-PLAN.md @@ -0,0 +1,63 @@ +--- +estimated_steps: 26 +estimated_files: 4 +skills_used: [] +--- + +# T02: Build ConsentDashboard page with route wiring and sidebar nav + +Build the main consent page, wire it into the app router and sidebar navigation. This is the user-facing deliverable. + +## Steps + +1. Create `frontend/src/pages/ConsentDashboard.module.css` with styles for the consent page. Follow CreatorSettings.module.css patterns. Key classes: `.consentPage` container, `.videoRow` for each video's consent card, `.toggleRow` for label + toggle pairs, `.historyToggle` button to expand audit trail, `.historyList` for audit entries, `.emptyState` for no-videos message, `.loading` and `.error` states. Use CSS custom properties from the existing theme. + +2. Create `frontend/src/pages/ConsentDashboard.tsx`: + - Import `SidebarNav` from `CreatorDashboard`, `dashStyles` from `CreatorDashboard.module.css`, `useAuth` from context, `useDocumentTitle` hook + - Import consent API functions from `../api/consent` and `ToggleSwitch` from `../components/ToggleSwitch` + - Layout: `
` (matches CreatorSettings pattern) + - On mount, call `fetchConsentList(token)` to get all videos with consent state + - Render each video as a card with title, 3 ToggleSwitch components (KB Inclusion, Training Usage, Public Display) + - On toggle change, call `updateVideoConsent()` and update local state optimistically + - Each video card has an expandable "History" section that lazy-loads audit trail via `fetchConsentHistory()` on first expand + - Handle loading, error, and empty states + - Call `useDocumentTitle('Consent Settings')` + +3. Update `frontend/src/pages/CreatorDashboard.tsx` SidebarNav to add a "Consent" link pointing to `/creator/consent`. Add a shield/lock SVG icon consistent with existing sidebar icons. Mark active when pathname is `/creator/consent`. + +4. Update `frontend/src/App.tsx`: + - Add lazy import: `const ConsentDashboard = lazy(() => import('./pages/ConsentDashboard'))` + - Add route: `}>} />` + +## Must-Haves + +- [ ] ConsentDashboard uses shared SidebarNav + dashStyles.layout +- [ ] All 3 consent fields rendered as toggles per video +- [ ] Toggle changes call PUT endpoint and update UI +- [ ] Audit history expandable per video +- [ ] Route registered at /creator/consent with ProtectedRoute + Suspense +- [ ] Sidebar nav has Consent link with active state +- [ ] useDocumentTitle called +- [ ] Loading, error, and empty states handled + +## Inputs + +- `frontend/src/api/consent.ts` +- `frontend/src/components/ToggleSwitch.tsx` +- `frontend/src/components/ToggleSwitch.module.css` +- `frontend/src/pages/CreatorDashboard.tsx` +- `frontend/src/pages/CreatorDashboard.module.css` +- `frontend/src/pages/CreatorSettings.tsx` +- `frontend/src/App.tsx` +- `frontend/src/context/AuthContext.tsx` + +## Expected Output + +- `frontend/src/pages/ConsentDashboard.tsx` +- `frontend/src/pages/ConsentDashboard.module.css` +- `frontend/src/pages/CreatorDashboard.tsx` +- `frontend/src/App.tsx` + +## Verification + +cd frontend && npx tsc --noEmit && npm run build && echo 'Build OK' diff --git a/frontend/src/api/consent.ts b/frontend/src/api/consent.ts new file mode 100644 index 0000000..40fa562 --- /dev/null +++ b/frontend/src/api/consent.ts @@ -0,0 +1,78 @@ +/** + * Consent API client — per-video consent settings and audit history. + */ + +import { request, BASE } from "./client"; + +// ── Types ──────────────────────────────────────────────────────────────────── + +export interface VideoConsentRead { + source_video_id: string; + video_filename: string; + creator_id: string; + kb_inclusion: boolean; + training_usage: boolean; + public_display: boolean; + updated_at: string; +} + +export interface VideoConsentUpdate { + kb_inclusion?: boolean; + training_usage?: boolean; + public_display?: boolean; +} + +export interface ConsentAuditEntry { + version: number; + field_name: string; + old_value: boolean | null; + new_value: boolean; + changed_by: string; + created_at: string; +} + +export interface ConsentListResponse { + items: VideoConsentRead[]; + total: number; +} + +export interface ConsentSummary { + total_videos: number; + kb_inclusion_granted: number; + training_usage_granted: number; + public_display_granted: number; +} + +// ── Functions ──────────────────────────────────────────────────────────────── + +export async function fetchConsentList(): Promise { + return request(`${BASE}/consent/videos`); +} + +export async function fetchVideoConsent( + videoId: string, +): Promise { + return request(`${BASE}/consent/videos/${videoId}`); +} + +export async function updateVideoConsent( + videoId: string, + updates: VideoConsentUpdate, +): Promise { + return request(`${BASE}/consent/videos/${videoId}`, { + method: "PUT", + body: JSON.stringify(updates), + }); +} + +export async function fetchConsentHistory( + videoId: string, +): Promise { + return request( + `${BASE}/consent/videos/${videoId}/history`, + ); +} + +export async function fetchConsentSummary(): Promise { + return request(`${BASE}/consent/summary`); +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index a4b2d58..e7e27c4 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -14,3 +14,4 @@ export * from "./admin-pipeline"; export * from "./admin-techniques"; export * from "./auth"; export * from "./creator-dashboard"; +export * from "./consent"; diff --git a/frontend/src/components/ToggleSwitch.module.css b/frontend/src/components/ToggleSwitch.module.css new file mode 100644 index 0000000..98cb6f6 --- /dev/null +++ b/frontend/src/components/ToggleSwitch.module.css @@ -0,0 +1,71 @@ +/* ToggleSwitch — sliding toggle with accessible checkbox */ + +.wrapper { + display: inline-flex; + align-items: center; + gap: 0.625rem; + cursor: pointer; + user-select: none; +} + +.wrapper[data-disabled] { + opacity: 0.45; + cursor: not-allowed; + pointer-events: none; +} + +.label { + font-size: 0.875rem; + color: var(--color-text-primary); +} + +/* Track (pill shape) */ +.track { + position: relative; + display: inline-flex; + align-items: center; + width: 40px; + height: 22px; + border-radius: 11px; + background: var(--color-border); + transition: background 200ms ease; + flex-shrink: 0; +} + +.track[data-checked] { + background: var(--color-accent); +} + +/* Visually hidden native checkbox */ +.input { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Sliding thumb */ +.thumb { + position: absolute; + left: 2px; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--color-text-primary); + transition: transform 200ms ease; + pointer-events: none; +} + +.track[data-checked] .thumb { + transform: translateX(18px); +} + +/* Focus ring on the track when input is focused */ +.input:focus-visible ~ .thumb { + box-shadow: 0 0 0 2px var(--color-accent-focus); +} diff --git a/frontend/src/components/ToggleSwitch.tsx b/frontend/src/components/ToggleSwitch.tsx new file mode 100644 index 0000000..aa7018b --- /dev/null +++ b/frontend/src/components/ToggleSwitch.tsx @@ -0,0 +1,44 @@ +/** + * Reusable toggle switch — accessible checkbox styled as a sliding toggle. + */ + +import { useId } from "react"; +import styles from "./ToggleSwitch.module.css"; + +export interface ToggleSwitchProps { + checked: boolean; + onChange: (checked: boolean) => void; + label: string; + disabled?: boolean; + id?: string; +} + +export function ToggleSwitch({ + checked, + onChange, + label, + disabled = false, + id, +}: ToggleSwitchProps) { + const autoId = useId(); + const inputId = id ?? autoId; + + return ( + + ); +}