docs: add Player, Impersonation pages; update LightRAG eval results (M020/S07)

Chrysopedia Bot 2026-04-03 22:41:16 -05:00
parent e0a52757b0
commit 4f3de1b1c7
4 changed files with 249 additions and 1 deletions

@ -154,6 +154,42 @@ LightRAG is a graph-based RAG (Retrieval-Augmented Generation) service added to
LightRAG provides graph-structured knowledge retrieval as a complement to Qdrant's vector similarity search. It runs as a standalone service within the Docker Compose stack, sharing Qdrant and Ollama with the main application. LightRAG provides graph-structured knowledge retrieval as a complement to Qdrant's vector similarity search. It runs as a standalone service within the Docker Compose stack, sharing Qdrant and Ollama with the main application.
## Admin Impersonation
Admins can view the site as any creator. Full details on the dedicated [[Impersonation]] page.
- **Mechanism:** Impersonation JWT with `original_user_id` claim and 1h expiry
- **Safety:** Write endpoints blocked via `reject_impersonation` dependency
- **Audit:** All start/stop actions logged to `impersonation_log` table with admin ID, target ID, IP, timestamp
### Evaluation Results (M020/S05)
A/B comparison of 25 queries (13 real user queries, 12 curated) against Qdrant search and LightRAG hybrid mode:
- **LightRAG wins:** 23/25 queries on relevance scoring
- **Qdrant wins:** 2/25 (short query rejected by LightRAG, ambiguous incomplete query)
- **Avg relevance:** Qdrant 2.09/5, LightRAG 4.52/5
- **Avg latency:** Qdrant 99ms, LightRAG 86s
**Key finding:** LightRAG is a RAG system producing synthesized multi-paragraph answers, not a search engine. The two serve different interaction patterns.
### Recommended Hybrid Routing
- **Autocomplete / typeahead** → Qdrant (sub-100ms required)
- **Search results list** → Qdrant (users expect instant ranked results)
- **"Ask a question" / chat** → LightRAG (synthesized answers are the core value)
- **Deep-dive / explore** → Both (LightRAG answer + Qdrant related pages sidebar)
### Creator-Scoped Retrieval
LightRAG has no metadata-based filtering. Creator scoping uses `ll_keywords` to bias retrieval toward a specific creator name (soft bias, not hard filter). Each document's text includes structured provenance: Creator name, Creator ID, Source Videos, and per-key-moment source attribution.
Query utility: `backend/scripts/lightrag_query.py --query "snare design" --creator "COPYCATT"`
### Data Coverage
LightRAG reindex in progress. Entity types configured: Creator, Technique, Plugin, Synthesizer, Effect, Genre, DAW, SamplePack, SignalChain, Concept, Frequency, SoundDesignElement.
--- ---
*See also: [[Architecture]], [[API-Surface]], [[Data-Model]]* *See also: [[Architecture]], [[API-Surface]], [[Data-Model]], [[Impersonation]], [[Player]]*

124
Impersonation.md Normal file

@ -0,0 +1,124 @@
# Admin Impersonation
Admins can view the site as any creator to debug permission issues, verify consent settings, and test the creator experience. Added in M020/S04.
## Overview
```
Admin clicks "View As" on /admin/users
POST /api/v1/admin/impersonate/{user_id}
├─ Verify caller is admin
├─ Create impersonation JWT (1h expiry, original_user_id claim)
├─ Log to impersonation_log table
Frontend swaps JWT, stashes admin token in sessionStorage
Amber banner: "Viewing as {Creator Name} — Exit"
All API calls use target user's identity
Write endpoints blocked by reject_impersonation guard
Click "Exit" → POST /api/v1/admin/impersonate/stop
├─ Log stop action to impersonation_log
├─ Restore admin token from sessionStorage
Back to normal admin session
```
## Security Model
- **Read-only:** Write endpoints are guarded by `reject_impersonation` FastAPI dependency. Returns 403 if the current token has `original_user_id` claim
- **Short-lived:** Impersonation tokens expire in 1 hour (vs 24h for normal tokens)
- **Transparent:** The impersonation JWT uses `sub=target_user_id`, so existing `get_current_user` loads the target user transparently. No special-casing in endpoint handlers
- **Admin-only:** Only users with `admin` role can call the impersonate endpoint
## Audit Trail
Every impersonation action is logged to the `impersonation_log` table:
- `admin_user_id` — Who initiated
- `target_user_id` — Who is being impersonated
- `action` — "start" or "stop"
- `ip_address` — Client IP
- `created_at` — Timestamp
Indexes on `admin_user_id` and `target_user_id` for efficient querying.
## Token Structure
Normal JWT:
```json
{
"sub": "user-uuid",
"role": "admin",
"iat": 1234567890,
"exp": 1234654290
}
```
Impersonation JWT:
```json
{
"sub": "target-user-uuid",
"role": "creator",
"original_user_id": "admin-user-uuid",
"iat": 1234567890,
"exp": 1234571490
}
```
The `original_user_id` claim is what `reject_impersonation` checks.
## API Endpoints
- `GET /api/v1/admin/users` — List all users (admin only)
- `POST /api/v1/admin/impersonate/{user_id}` — Start impersonation, returns new JWT
- `POST /api/v1/admin/impersonate/stop` — Stop impersonation, logs action
## Frontend Integration
### AuthContext Extensions
- `startImpersonation(userId)` — Calls impersonate API, saves current admin token to `sessionStorage`, swaps to impersonation token
- `exitImpersonation()` — Calls stop API, restores admin token from `sessionStorage`
- `user.impersonating` (boolean) — True when viewing as another user
### ImpersonationBanner
Fixed amber bar at page top when impersonating. Shows "Viewing as {name}" with Exit button. Rendered in `AppShell` when `user.impersonating` is true.
### AdminUsers Page
Route: `/admin/users`. Table of all users with "View As" buttons for creator-role users. Code-split with `React.lazy`.
## Key Files
- `backend/auth.py``create_impersonation_token()`, `reject_impersonation` dependency
- `backend/routers/admin.py` — Impersonate/stop endpoints, user list
- `backend/models.py``ImpersonationLog` model
- `alembic/versions/018_add_impersonation_log.py` — Migration
- `frontend/src/context/AuthContext.tsx` — Impersonation state management
- `frontend/src/components/ImpersonationBanner.tsx` — Amber warning banner
- `frontend/src/pages/AdminUsers.tsx` — User management page
## Database
**Table: `impersonation_log`** (Migration 018)
- `id` UUID PK
- `admin_user_id` UUID FK → users
- `target_user_id` UUID FK → users
- `action` VARCHAR (start/stop)
- `ip_address` VARCHAR
- `created_at` TIMESTAMP
---
*See also: [[Authentication]], [[API-Surface]]*

86
Player.md Normal file

@ -0,0 +1,86 @@
# Web Media Player
Custom video player with HLS playback, synchronized transcript sidebar, and keyboard shortcuts. Added in M020/S01.
## Architecture
```
┌──────────────────────────────────────────────────────────┐
│ WatchPage (/watch/:videoId) │
│ │
│ ┌─────────────────────┐ ┌────────────────────────────┐ │
│ │ VideoPlayer │ │ TranscriptSidebar │ │
│ │ ┌────────────────┐ │ │ Segments sorted by time │ │
│ │ │ <video> elem │ │ │ Binary search for active │ │
│ │ └────────────────┘ │ │ Auto-scroll on playback │ │
│ │ PlayerControls │ │ Click → seek to timestamp │ │
│ └──────────┬───────────┘ └──────────┬─────────────────┘ │
│ │ useMediaSync() │ │
│ └──────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
```
## Video Source Detection
Three-path fallback for maximum browser compatibility:
1. **HLS via hls.js** — Lazy-loaded (dynamic import). Used when `Hls.isSupported()` returns true (Chrome, Firefox, Edge)
2. **Safari native HLS**`<video>` element handles `.m3u8` natively on Safari/iOS
3. **Direct MP4 fallback** — Plain `<video src="...">` for non-HLS sources
hls.js is loaded only when needed (~200KB), keeping the main bundle small.
## Synchronized Transcript
The `TranscriptSidebar` component displays timestamped transcript segments synchronized to video playback:
- **Active segment detection:** Binary search (O(log n)) on sorted segments by `start_time`
- **Auto-scroll:** Active segment scrolls into view with `scrollIntoView({ block: "center" })`
- **Click to seek:** Each segment is a `<button>` for keyboard accessibility. Click seeks the video to that timestamp
- **Non-blocking:** Transcript fetch failure doesn't break the player — degrades gracefully
### useMediaSync Hook
Shared playback state between VideoPlayer and TranscriptSidebar:
- `currentTime` (number) — Current playback position in seconds
- `duration` (number) — Total video duration
- `isPlaying` (boolean) — Playback state
- `seekTo(time)` (function) — Seek video to specific time
## Keyboard Shortcuts
- **Space** — Play/pause
- **← / →** — Seek ±5 seconds
- **Shift+← / Shift+→** — Seek ±10 seconds
- **↑ / ↓** — Volume ±10%
- **M** — Mute/unmute
- **F** — Toggle fullscreen
- **1-9** — Seek to 10%-90%
## Speed Controls
Playback rate options: 0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x.
## API Endpoints
- `GET /api/v1/videos/{id}` — Video metadata with creator info (`SourceVideoDetail`)
- `GET /api/v1/videos/{id}/transcript` — Ordered transcript segments (`TranscriptForPlayerResponse`)
## Integration Points
- **TechniquePage:** Key moment timestamps are clickable links to `/watch/:videoId?t=<seconds>`
- **Code-splitting:** WatchPage is lazy-loaded via `React.lazy` + `Suspense`
## Key Files
- `frontend/src/pages/WatchPage.tsx` — Page component
- `frontend/src/components/VideoPlayer.tsx` — Video element + HLS setup
- `frontend/src/components/PlayerControls.tsx` — Play/pause, speed, volume, seek bar
- `frontend/src/components/TranscriptSidebar.tsx` — Synchronized transcript display
- `frontend/src/hooks/useMediaSync.ts` — Shared playback state hook
- `backend/routers/videos.py` — Video detail + transcript API
---
*See also: [[Architecture]], [[API-Surface]], [[Frontend]]*

@ -5,8 +5,10 @@
**Architecture** **Architecture**
- [[Architecture]] - [[Architecture]]
- [[Authentication]] - [[Authentication]]
- [[Impersonation]]
- [[Data-Model]] - [[Data-Model]]
- [[Pipeline]] - [[Pipeline]]
- [[Player]]
**Reference** **Reference**
- [[API-Surface]] - [[API-Surface]]