feat: Created TypeScript consent API client with 5 fetch functions and…

- "frontend/src/api/consent.ts"
- "frontend/src/components/ToggleSwitch.tsx"
- "frontend/src/components/ToggleSwitch.module.css"
- "frontend/src/api/index.ts"

GSD-Task: S03/T01
This commit is contained in:
jlightner 2026-04-04 00:21:13 +00:00
parent da29a2a723
commit 31638b5a3a
14 changed files with 704 additions and 2 deletions

View file

@ -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 |

View file

@ -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 |

View file

@ -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<T>()` 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

View file

@ -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

View file

@ -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
}

View file

@ -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<T>()` 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<T>() 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: `<div className={dashStyles.layout}><SidebarNav /><div className={dashStyles.content}>` (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: `<Route path='/creator/consent' element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><ConsentDashboard /></Suspense></ProtectedRoute>} />`
## 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'

View file

@ -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<T>()` 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**: `<div className={dashStyles.layout}><SidebarNav /><div className={dashStyles.content}>` (see CreatorSettings)
- **Data fetching**: `useEffect` + `useState` + `request<T>()` 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

View file

@ -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<T>()` 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<T>() 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'

View file

@ -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<T>() 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.

View file

@ -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: `<div className={dashStyles.layout}><SidebarNav /><div className={dashStyles.content}>` (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: `<Route path='/creator/consent' element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><ConsentDashboard /></Suspense></ProtectedRoute>} />`
## 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'

View file

@ -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<ConsentListResponse> {
return request<ConsentListResponse>(`${BASE}/consent/videos`);
}
export async function fetchVideoConsent(
videoId: string,
): Promise<VideoConsentRead> {
return request<VideoConsentRead>(`${BASE}/consent/videos/${videoId}`);
}
export async function updateVideoConsent(
videoId: string,
updates: VideoConsentUpdate,
): Promise<VideoConsentRead> {
return request<VideoConsentRead>(`${BASE}/consent/videos/${videoId}`, {
method: "PUT",
body: JSON.stringify(updates),
});
}
export async function fetchConsentHistory(
videoId: string,
): Promise<ConsentAuditEntry[]> {
return request<ConsentAuditEntry[]>(
`${BASE}/consent/videos/${videoId}/history`,
);
}
export async function fetchConsentSummary(): Promise<ConsentSummary> {
return request<ConsentSummary>(`${BASE}/consent/summary`);
}

View file

@ -14,3 +14,4 @@ export * from "./admin-pipeline";
export * from "./admin-techniques";
export * from "./auth";
export * from "./creator-dashboard";
export * from "./consent";

View file

@ -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);
}

View file

@ -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 (
<label className={styles.wrapper} htmlFor={inputId} data-disabled={disabled || undefined}>
<span className={styles.label}>{label}</span>
<span className={styles.track} data-checked={checked || undefined}>
<input
id={inputId}
type="checkbox"
role="switch"
className={styles.input}
checked={checked}
disabled={disabled}
aria-label={label}
onChange={(e) => onChange(e.target.checked)}
/>
<span className={styles.thumb} />
</span>
</label>
);
}