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:
parent
da29a2a723
commit
31638b5a3a
14 changed files with 704 additions and 2 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
94
.gsd/milestones/M020/slices/S02/S02-SUMMARY.md
Normal file
94
.gsd/milestones/M020/slices/S02/S02-SUMMARY.md
Normal 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
|
||||
64
.gsd/milestones/M020/slices/S02/S02-UAT.md
Normal file
64
.gsd/milestones/M020/slices/S02/S02-UAT.md
Normal 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
|
||||
30
.gsd/milestones/M020/slices/S02/tasks/T02-VERIFY.json
Normal file
30
.gsd/milestones/M020/slices/S02/tasks/T02-VERIFY.json
Normal 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
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
81
.gsd/milestones/M020/slices/S03/S03-RESEARCH.md
Normal file
81
.gsd/milestones/M020/slices/S03/S03-RESEARCH.md
Normal 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
|
||||
39
.gsd/milestones/M020/slices/S03/tasks/T01-PLAN.md
Normal file
39
.gsd/milestones/M020/slices/S03/tasks/T01-PLAN.md
Normal 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'
|
||||
81
.gsd/milestones/M020/slices/S03/tasks/T01-SUMMARY.md
Normal file
81
.gsd/milestones/M020/slices/S03/tasks/T01-SUMMARY.md
Normal 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.
|
||||
63
.gsd/milestones/M020/slices/S03/tasks/T02-PLAN.md
Normal file
63
.gsd/milestones/M020/slices/S03/tasks/T02-PLAN.md
Normal 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'
|
||||
78
frontend/src/api/consent.ts
Normal file
78
frontend/src/api/consent.ts
Normal 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`);
|
||||
}
|
||||
|
|
@ -14,3 +14,4 @@ export * from "./admin-pipeline";
|
|||
export * from "./admin-techniques";
|
||||
export * from "./auth";
|
||||
export * from "./creator-dashboard";
|
||||
export * from "./consent";
|
||||
|
|
|
|||
71
frontend/src/components/ToggleSwitch.module.css
Normal file
71
frontend/src/components/ToggleSwitch.module.css
Normal 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);
|
||||
}
|
||||
44
frontend/src/components/ToggleSwitch.tsx
Normal file
44
frontend/src/components/ToggleSwitch.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue