feat: Added DebugModeToggle component and StatusFilter pill bar to Admi…

- "frontend/src/pages/AdminPipeline.tsx"
- "frontend/src/api/public-client.ts"
- "frontend/src/App.css"

GSD-Task: S04/T01
This commit is contained in:
jlightner 2026-03-30 19:34:11 +00:00
parent 97b9f7234a
commit 44c0df6e08
14 changed files with 646 additions and 5 deletions

View file

@ -1,5 +1,11 @@
# KNOWLEDGE
## Healthcheck in slim Python Docker images: use os.kill(1,0) not pgrep
**Context:** The `python:3.x-slim` Docker image doesn't include `procps`, so `pgrep -f watcher.py` fails in healthcheck commands. This applies to any slim-based image where a healthcheck needs to verify the main process is alive.
**Fix:** Use `python -c "import os; os.kill(1, 0)"` as the healthcheck command. PID 1 is always the container's entrypoint process. `os.kill(pid, 0)` checks if the process exists without sending a signal — it raises `ProcessLookupError` if the process is dead, causing the healthcheck to fail. No external tools needed.
## SQLAlchemy column names that shadow ORM functions
**Context:** If a model has a column named `relationship` (or any other ORM function name like `query`, `metadata`), Python resolves the name to the column's `MappedColumn` descriptor within the class body. This causes `relationship(...)` calls below it to fail with "MappedColumn object is not callable".

View file

@ -13,7 +13,8 @@ Five milestones complete plus a sixth refinement milestone. The system is deploy
- **Article versioning** — Pre-overwrite snapshots with pipeline metadata (prompt SHA-256 hashes, model config) for benchmarking extraction quality across prompt iterations.
- **Review queue UI** — Admin interface for approving/editing/rejecting extracted key moments.
- **Search-first web UI** — Dark theme with cyan accents. Semantic search (Qdrant), topic/creator browse pages, technique detail pages with signal chain blocks and video source metadata.
- **Docker Compose deployment** — API, worker, web, PostgreSQL, Redis, Qdrant, Ollama on ub01 with bind mounts at `/vmPool/r/services/chrysopedia_*`.
- **Docker Compose deployment** — API, worker, web, watcher, PostgreSQL, Redis, Qdrant, Ollama on ub01 with bind mounts at `/vmPool/r/services/chrysopedia_*`.
- **Transcript folder watcher** — Standalone service monitors `/vmPool/r/services/chrysopedia_watch/` for new transcript JSON files, validates structure, POSTs to ingest API. Processed files move to `processed/`, failures to `failed/` with `.error` sidecar. Uses watchdog PollingObserver for ZFS reliability.
- **Prompt template system** — Editable per-stage prompt files with SHA-256 tracking for reproducibility.
- **Pipeline admin dashboard** — Admin page at `/admin/pipeline` with video list, status badges, retrigger/revoke controls, expandable event log with token usage, collapsible JSON response viewer, and live worker status indicator. Pipeline events instrumented via `PipelineEvent` model (stage, event_type, token counts, JSONB payload).
- **Technique page 2-column layout** — Prose content (summary + study guide) on left, sidebar (key moments, signal chains, plugins, related techniques) on right at desktop widths. Sticky sidebar. Collapses to single column at ≤768px.

View file

@ -8,7 +8,7 @@ Make the pipeline fully transparent — every LLM call's full prompt and respons
|----|-------|------|---------|------|------------|
| S01 | Pipeline Debug Mode — Full LLM I/O Capture and Token Accounting | medium | — | ✅ | Toggle debug mode on for a video, trigger pipeline, see full prompt + response stored for every LLM call with per-stage token breakdown |
| S02 | Debug Payload Viewer — Inline View, Copy, and Export in Admin UI | low | S01 | ✅ | Admin clicks an LLM call event, sees full prompt and response in a readable viewer, copies to clipboard, or downloads as JSON |
| S03 | Transcript Folder Watcher — Auto-Ingest Service | medium | — | | Drop a transcript JSON into the watch folder on ub01 → file is detected, validated, POSTed to /ingest, pipeline triggers automatically |
| S03 | Transcript Folder Watcher — Auto-Ingest Service | medium | — | | Drop a transcript JSON into the watch folder on ub01 → file is detected, validated, POSTed to /ingest, pipeline triggers automatically |
| S04 | Admin UX Audit — Prune, Streamline, and Polish | low | S02 | ⬜ | Admin pipeline page is cleaner and more efficient for daily content management. Dead UI elements removed. Workflow improvements visible. |
| S05 | Key Moment Card Text Overflow Fix | low | — | ⬜ | Key moment cards with long filenames and timestamp ranges display cleanly — text truncated with ellipsis or wrapped within card bounds, no horizontal bleed |
| S06 | Mobile Viewport Overflow Fix — Technique Pages and Global Content | low | S05 | ⬜ | Technique page on Galaxy S25 Ultra renders cleanly — body text wraps, tags don't overflow, metadata stays within viewport, no horizontal scroll |

View file

@ -0,0 +1,93 @@
---
id: S03
parent: M007
milestone: M007
provides:
- chrysopedia-watcher Docker service running on ub01
- Auto-ingest via file drop to /vmPool/r/services/chrysopedia_watch/
- backend/watcher.py standalone script
requires:
[]
affects:
[]
key_files:
- backend/watcher.py
- backend/requirements.txt
- docker-compose.yml
key_decisions:
- Used httpx sync client in watcher (runs in synchronous watchdog callback threads)
- PollingObserver over inotify Observer for ZFS/NFS reliability
- os.kill(1,0) healthcheck instead of pgrep for slim Python images
- SCP'd files to ub01 directly instead of git push to avoid outward-facing GitHub action
- Ignored files in processed/ and failed/ subdirs to prevent re-processing loops
patterns_established:
- Standalone watcher service pattern: watchdog PollingObserver + file stability check + validate + POST + disposition (processed/failed with error sidecar)
- Reusing Dockerfile.api with command override for lightweight companion services
observability_surfaces:
- docker logs chrysopedia-watcher — shows all watcher activity (pickup, stability wait, validation, POST result, file moves)
- .error sidecar files in failed/ directory contain HTTP status, response body, or exception traceback
drill_down_paths:
- .gsd/milestones/M007/slices/S03/tasks/T01-SUMMARY.md
- .gsd/milestones/M007/slices/S03/tasks/T02-SUMMARY.md
duration: ""
verification_result: passed
completed_at: 2026-03-30T19:26:20.063Z
blocker_discovered: false
---
# S03: Transcript Folder Watcher — Auto-Ingest Service
**Built and deployed a watchdog-based folder watcher service that auto-ingests transcript JSON files dropped into a monitored directory on ub01, replacing manual curl/upload for pipeline input.**
## What Happened
This slice delivered a new Docker service (`chrysopedia-watcher`) that monitors `/vmPool/r/services/chrysopedia_watch/` on ub01 for new transcript JSON files and automatically POSTs them to the ingest API.
**T01** built `backend/watcher.py` — a standalone Python script using watchdog's `PollingObserver` (chosen over inotify for ZFS/NFS reliability). The watcher handles file stability detection (waits for size to stabilize over 2 seconds to handle partial SCP/rsync writes), validates JSON structure against required keys (`source_file`, `creator_folder`, `duration_seconds`, `segments`), POSTs valid files as multipart upload via httpx to `/api/v1/ingest`, and moves files to `processed/` or `failed/` subdirectories. Failed files get an `.error` sidecar with the failure details (HTTP status, response body, or exception traceback). Files inside `processed/` and `failed/` subdirectories are ignored to prevent re-processing loops.
**T02** wired the watcher into the Docker Compose stack as `chrysopedia-watcher`, reusing the existing `Dockerfile.api` image with a command override. The healthcheck was changed from `pgrep` (unavailable in slim Python image) to `python -c "import os; os.kill(1, 0)"`. Deployed to ub01 and verified end-to-end: valid transcript JSON auto-ingested (HTTP 200, file moved to `processed/`), invalid JSON moved to `failed/` with `.error` sidecar.
## Verification
All slice-level verification checks pass:
1. `python -m py_compile backend/watcher.py` — exits 0
2. `grep -q 'watchdog' backend/requirements.txt` — exits 0
3. `grep -q 'PollingObserver' backend/watcher.py` — exits 0
4. `grep -q 'processed' backend/watcher.py && grep -q 'failed' backend/watcher.py` — exits 0
5. `ssh ub01 "docker ps --filter name=chrysopedia-watcher --format '{{.Status}}'"` — shows "Up N minutes (healthy)"
6. End-to-end valid JSON ingestion verified on ub01 (HTTP 200, file in processed/)
7. End-to-end invalid JSON handling verified on ub01 (file in failed/ with .error sidecar)
## Requirements Advanced
- R002 — Transcript ingestion now automated via folder watcher — no manual curl/upload needed
## Requirements Validated
None.
## New Requirements Surfaced
None.
## Requirements Invalidated or Re-scoped
None.
## Deviations
Healthcheck changed from `pgrep -f watcher.py` to `python -c "import os; os.kill(1, 0)"` because procps is not available in the slim Python Docker image.
## Known Limitations
None.
## Follow-ups
None.
## Files Created/Modified
- `backend/watcher.py` — New standalone folder watcher script using watchdog PollingObserver
- `backend/requirements.txt` — Added watchdog>=4.0,<5.0 dependency
- `docker-compose.yml` — Added chrysopedia-watcher service definition

View file

@ -0,0 +1,97 @@
# S03: Transcript Folder Watcher — Auto-Ingest Service — UAT
**Milestone:** M007
**Written:** 2026-03-30T19:26:20.063Z
## UAT: Transcript Folder Watcher — Auto-Ingest Service
### Preconditions
- Chrysopedia stack running on ub01 (`docker ps --filter name=chrysopedia` shows all services healthy)
- Watch folder exists: `/vmPool/r/services/chrysopedia_watch/`
- Watcher container running: `docker ps --filter name=chrysopedia-watcher` shows healthy
---
### Test 1: Valid Transcript Auto-Ingest (Happy Path)
1. SSH to ub01
2. Create a valid transcript JSON file:
```bash
cat > /vmPool/r/services/chrysopedia_watch/uat_valid.json << 'EOF'
{
"source_file": "uat_test_video.mp4",
"creator_folder": "UAT Creator",
"duration_seconds": 120.5,
"segments": [{"start": 0.0, "end": 5.0, "text": "Hello world"}]
}
EOF
```
3. Wait 10 seconds for watcher to detect and process
4. **Expected:** `docker logs chrysopedia-watcher --tail 20` shows:
- File pickup log line mentioning `uat_valid.json`
- Stability check pass
- POST to ingest API with HTTP 200
- File moved to processed/
5. **Expected:** `ls /vmPool/r/services/chrysopedia_watch/processed/uat_valid.json` — file exists
6. **Expected:** `ls /vmPool/r/services/chrysopedia_watch/uat_valid.json` — file no longer in root
### Test 2: Invalid JSON File Handling
1. Create an invalid JSON file (missing required keys):
```bash
echo '{"foo": "bar"}' > /vmPool/r/services/chrysopedia_watch/uat_invalid.json
```
2. Wait 10 seconds
3. **Expected:** `ls /vmPool/r/services/chrysopedia_watch/failed/uat_invalid.json` — file exists
4. **Expected:** `cat /vmPool/r/services/chrysopedia_watch/failed/uat_invalid.json.error` — contains validation error mentioning missing keys
5. **Expected:** File no longer in root watch folder
### Test 3: Malformed JSON (Parse Error)
1. Create a file with broken JSON:
```bash
echo 'not json at all' > /vmPool/r/services/chrysopedia_watch/uat_broken.json
```
2. Wait 10 seconds
3. **Expected:** File moved to `failed/uat_broken.json` with `.error` sidecar containing JSON parse error
### Test 4: Non-JSON File Ignored
1. Create a non-JSON file:
```bash
echo 'hello' > /vmPool/r/services/chrysopedia_watch/readme.txt
```
2. Wait 10 seconds
3. **Expected:** File remains in root watch folder — not moved to processed/ or failed/
4. **Expected:** No log entry in watcher for this file
### Test 5: File Stability (Simulated Partial Write)
1. Start writing a large file slowly:
```bash
(echo '{"source_file":"test.mp4",' ; sleep 3 ; echo '"creator_folder":"Test","duration_seconds":60,"segments":[]}') > /vmPool/r/services/chrysopedia_watch/uat_slow.json
```
2. **Expected:** Watcher waits for file stability before processing (logs show stability check)
3. **Expected:** File ultimately processed (valid JSON) and moved to processed/
### Test 6: Watcher Survives API Downtime
1. Stop the API: `docker stop chrysopedia-api`
2. Drop a valid JSON file into watch folder
3. **Expected:** File moves to `failed/` with `.error` sidecar containing connection error
4. Restart the API: `docker start chrysopedia-api`
5. **Expected:** Watcher continues running (check `docker ps --filter name=chrysopedia-watcher`)
### Cleanup
```bash
rm -f /vmPool/r/services/chrysopedia_watch/processed/uat_*.json
rm -f /vmPool/r/services/chrysopedia_watch/failed/uat_*.json
rm -f /vmPool/r/services/chrysopedia_watch/failed/uat_*.json.error
rm -f /vmPool/r/services/chrysopedia_watch/readme.txt
```
### Operational Readiness (Q8)
- **Health signal:** `docker ps --filter name=chrysopedia-watcher` shows healthy; `docker logs chrysopedia-watcher --tail 5` shows recent activity or idle polling
- **Failure signal:** Container exits or becomes unhealthy; `.error` sidecar files accumulate in `failed/` directory
- **Recovery procedure:** `docker compose restart chrysopedia-watcher` — no state to lose, watcher re-scans on startup
- **Monitoring gaps:** No alerting on failed/ file accumulation; no metrics endpoint. Admin must manually check `failed/` directory or watcher logs.

View file

@ -0,0 +1,9 @@
{
"schemaVersion": 1,
"taskId": "T02",
"unitId": "M007/S03/T02",
"timestamp": 1774898679946,
"passed": true,
"discoverySource": "none",
"checks": []
}

View file

@ -1,6 +1,29 @@
# S04: Admin UX Audit — Prune, Streamline, and Polish
**Goal:** Audit all admin pages (Pipeline, Reports, Review). Remove unhelpful elements. Improve pipeline page layout for the primary workflow: check status, investigate issues, retrigger. Add clear visual hierarchy for the content management workflow.
**Goal:** Admin pipeline page is cleaner and more efficient for daily content management. Debug mode toggle, status filter, workflow streamlining, and dead UI removal all deployed.
**Demo:** After this: Admin pipeline page is cleaner and more efficient for daily content management. Dead UI elements removed. Workflow improvements visible.
## Tasks
- [x] **T01: Added DebugModeToggle component and StatusFilter pill bar to AdminPipeline page with API client functions for debug mode GET/PUT endpoints** — Add two new interactive features to AdminPipeline.tsx: (1) a debug mode toggle in the page header that reads/writes via GET/PUT /admin/pipeline/debug-mode, and (2) a status filter pill bar that filters the video list client-side by processing_status.
Steps:
1. Add `fetchDebugMode()` and `setDebugMode(enabled: boolean)` functions to `public-client.ts`. The backend endpoints are `GET /admin/pipeline/debug-mode``{enabled: boolean}` and `PUT /admin/pipeline/debug-mode` with body `{enabled: boolean}`.
2. In AdminPipeline.tsx, add state for `debugMode` (boolean) and `debugLoading` (boolean). Fetch debug mode on mount. Add a toggle switch in the header-right area next to WorkerStatus — use the same visual pattern as the ModeToggle component on ReviewQueue (a labeled toggle with on/off state). Clicking the toggle calls `setDebugMode(!debugMode)` then updates local state.
3. Add a status filter pill bar below the page header. Extract unique statuses from the `videos` array. Add `activeFilter` state (string | null, default null = show all). Render filter pills for each status plus an 'All' pill. Filter the `videos` array before rendering when `activeFilter` is set. Use the existing `filter-tab` / `filter-tab--active` CSS classes from ReviewQueue.
4. Add CSS for the debug mode toggle (`.debug-toggle`, `.debug-toggle__switch`, `.debug-toggle__label`) in App.css. Style it to match the dark theme using existing `var(--color-*)` custom properties.
5. Verify: Docker build succeeds, toggle visible and functional, filter pills render and filter correctly.
- Estimate: 45m
- Files: frontend/src/api/public-client.ts, frontend/src/pages/AdminPipeline.tsx, frontend/src/App.css
- Verify: ssh ub01 "cd /vmPool/r/repos/xpltdco/chrysopedia && docker compose build chrysopedia-web" succeeds with exit 0
- [ ] **T02: Prune dead UI, rename view toggle, add debug indicator on trigger, add review queue cross-link** — Clean up the pipeline page: remove low-value UI elements, rename confusing labels, add debug mode context to trigger button, and add cross-navigation to review queue.
Steps:
1. In EventLog component, rename 'Head'/'Tail' buttons to 'Oldest first'/'Newest first'. The buttons are in the `.pipeline-events__view-toggle` div. Just change the button text.
2. In EventLog component, remove the duplicate '↻ Refresh' button from `.pipeline-events__header`. The page-level refresh in the main header is sufficient.
3. In the expanded video detail section (the `pipeline-video__detail` div), remove the `ID: {video.id.slice(0, 8)}…` span. Keep Created and Updated dates.
4. In the video header actions area, add a '(debug)' indicator next to the Trigger button when debug mode is active. Pass `debugMode` state from the parent AdminPipeline component down to or alongside the trigger button. When debugMode is true, the trigger button text changes to '▶ Trigger (debug)' to indicate the run will capture full LLM I/O.
5. Add a cross-link button/icon on each pipeline video card that navigates to the review queue filtered by that video. Use `<a href='/admin/review'>` or React Router Link with the video's creator name as context. A small '→ Moments' link in the video meta area works. Since the review queue doesn't currently support URL-based video filtering, just link to `/admin/review` with the video filename as a visual cue.
6. Verify: Docker build succeeds, view toggle says 'Oldest first'/'Newest first', no event-level refresh button, no event ID in detail, debug indicator on trigger when debug mode is on, cross-link visible.
- Estimate: 30m
- Files: frontend/src/pages/AdminPipeline.tsx, frontend/src/App.css
- Verify: ssh ub01 "cd /vmPool/r/repos/xpltdco/chrysopedia && docker compose build chrysopedia-web" succeeds with exit 0

View file

@ -0,0 +1,101 @@
# S04 Research: Admin UX Audit — Prune, Streamline, and Polish
## Summary
This is a **light research** slice — straightforward UX cleanup using established patterns in an already well-structured codebase. The admin pages (Pipeline, Reports, Review) are functional but have accumulated some gaps and could benefit from streamlining for the single-admin daily workflow.
## Recommendation
Three tasks: (1) Add debug mode toggle + video status filter to Pipeline page, (2) Cross-link admin pages and add workflow shortcuts, (3) CSS/UX polish pass. All tasks are independent and could run in parallel.
## Implementation Landscape
### Admin Pages Inventory (canonical on ub01)
| Page | File | Lines | Role |
|------|------|-------|------|
| Pipeline Management | `frontend/src/pages/AdminPipeline.tsx` | 535 | Video list, status, trigger/revoke, event log, worker status, debug payload viewer |
| Content Reports | `frontend/src/pages/AdminReports.tsx` | 246 | User-submitted issue reports, triage workflow |
| Review Queue | `frontend/src/pages/ReviewQueue.tsx` | 189 | Key moment review with stats, filters, pagination |
| Moment Detail | `frontend/src/pages/MomentDetail.tsx` | 454 | Single moment review with approve/reject/edit/split/merge |
### Supporting Components
| Component | File | Lines | Used By |
|-----------|------|-------|---------|
| AdminDropdown | `components/AdminDropdown.tsx` | 73 | App.tsx header nav |
| ModeToggle | `components/ModeToggle.tsx` | 59 | ReviewQueue.tsx |
| AppFooter | `components/AppFooter.tsx` | 47 | App.tsx |
| StatusBadge | `components/StatusBadge.tsx` | 19 | ReviewQueue, MomentDetail |
| ReportIssueModal | `components/ReportIssueModal.tsx` | 135 | TechniquePage.tsx only |
### API Clients
- `api/client.ts` — Review queue endpoints only (BASE = `/api/v1/review`)
- `api/public-client.ts` — Everything else: search, techniques, creators, topics, pipeline admin, reports
Both have independent `request<T>()` helpers with identical error-handling logic. This duplication is mild but noted.
### CSS
Single `App.css` at 3082 lines. Uses 77 CSS custom properties (D017). Admin-specific styles start around line 2265 (reports) and 2577 (pipeline). BEM naming throughout.
## Concrete Findings
### 1. Missing Debug Mode Toggle on Pipeline Page
Backend has `GET/PUT /admin/pipeline/debug-mode` endpoints (pipeline.py lines 244-278, using Redis key `chrysopedia:debug_mode`). **No frontend UI exposes this toggle.** The `ModeToggle` component on the Review Queue page is a good pattern to follow, but the debug mode toggle serves a different purpose (controls whether pipeline captures full LLM I/O per S01).
**Action:** Add a debug mode toggle to the Pipeline page header, next to WorkerStatus. Reuse the ModeToggle visual pattern. Add `fetchDebugMode` / `setDebugMode` functions to `public-client.ts`.
### 2. No Video Status Filter on Pipeline Page
The pipeline page shows all videos in a flat list. With growing content, the admin needs to filter by `processing_status` (pending, processing, extracted, completed, failed, etc.). The `statusBadgeClass()` function already maps all statuses to visual styles.
**Action:** Add a status filter pill bar (same pattern as ReviewQueue's FILTERS and AdminReports's STATUS_OPTIONS). Filter client-side — the video list is small enough (currently fetched all at once via `fetchPipelineVideos()`).
### 3. No Cross-Links Between Admin Pages
- Pipeline video → no link to its technique pages or review queue moments
- Review queue → no link back to pipeline for the source video
- No way to jump from a pipeline event to the related technique page
**Action:** Add subtle cross-link buttons/icons where natural. E.g., a "View moments" link on each pipeline video card that navigates to `/admin/review?video_id=...` (would need review queue to support URL-based filtering, or just use the video filename as visual context). Keep this simple — a link to review queue filtered to that video's moments is the most valuable.
### 4. Dead/Low-Value UI Elements
- **"Head/Tail" view toggle** on event log — the distinction is confusing for an admin ("head" means oldest-first, "tail" means newest-first). Consider renaming to "Oldest first" / "Newest first" or just defaulting to newest-first and removing the toggle.
- **Event ID display** in expanded video detail (`ID: {video.id.slice(0, 8)}…`) — not useful for the admin workflow. Created/Updated dates are marginally useful.
- **"↻ Refresh" button** appears twice on pipeline page — once in the header, once per event log. The header one is sufficient; auto-refresh on a timer (like WorkerStatus does every 15s) would be better.
### 5. Pipeline Page Doesn't Show Debug Mode State Visually
When debug mode is on, the admin should see a visual indicator so they know pipeline runs will be capturing full prompts. Without this, they might wonder why events have debug payloads or not.
### 6. Trigger Button Lacks Debug Mode Context
When triggering a pipeline run, there's no indication whether it will run in debug mode or not. A small "(debug)" label next to the trigger button when debug mode is active would clarify.
## Files That Will Change
| File | What Changes |
|------|-------------|
| `frontend/src/pages/AdminPipeline.tsx` | Add debug mode toggle, status filter, rename head/tail, prune low-value elements, add cross-links |
| `frontend/src/api/public-client.ts` | Add `fetchDebugMode()` / `setDebugMode()` API functions |
| `frontend/src/App.css` | Add debug-mode toggle styles, status filter styles, polish tweaks |
## Constraints
- **All changes on ub01** — canonical dev directory is `/vmPool/r/repos/xpltdco/chrysopedia`
- **Deploy via Docker Compose rebuild**`docker compose build chrysopedia-web && docker compose up -d chrysopedia-web-8096`
- **Container name vs service name** — build target is `chrysopedia-web`, container is `chrysopedia-web-8096`
- **No backend changes needed** — all required API endpoints already exist
- **Frontend-only slice** — TypeScript + CSS changes only
## Verification Strategy
1. `docker compose build chrysopedia-web` succeeds (TypeScript compiles)
2. Container healthy: `curl -s http://localhost:8096/ | grep -q Chrysopedia`
3. Browser verification of pipeline page: debug toggle visible, status filter functional, head/tail renamed, cross-links present
4. Debug mode toggle: click toggle → API call succeeds → visual state updates
5. Status filter: click filter pill → video list filters correctly

View file

@ -0,0 +1,33 @@
---
estimated_steps: 7
estimated_files: 3
skills_used: []
---
# T01: Add debug mode toggle and video status filter to Pipeline page
Add two new interactive features to AdminPipeline.tsx: (1) a debug mode toggle in the page header that reads/writes via GET/PUT /admin/pipeline/debug-mode, and (2) a status filter pill bar that filters the video list client-side by processing_status.
Steps:
1. Add `fetchDebugMode()` and `setDebugMode(enabled: boolean)` functions to `public-client.ts`. The backend endpoints are `GET /admin/pipeline/debug-mode``{enabled: boolean}` and `PUT /admin/pipeline/debug-mode` with body `{enabled: boolean}`.
2. In AdminPipeline.tsx, add state for `debugMode` (boolean) and `debugLoading` (boolean). Fetch debug mode on mount. Add a toggle switch in the header-right area next to WorkerStatus — use the same visual pattern as the ModeToggle component on ReviewQueue (a labeled toggle with on/off state). Clicking the toggle calls `setDebugMode(!debugMode)` then updates local state.
3. Add a status filter pill bar below the page header. Extract unique statuses from the `videos` array. Add `activeFilter` state (string | null, default null = show all). Render filter pills for each status plus an 'All' pill. Filter the `videos` array before rendering when `activeFilter` is set. Use the existing `filter-tab` / `filter-tab--active` CSS classes from ReviewQueue.
4. Add CSS for the debug mode toggle (`.debug-toggle`, `.debug-toggle__switch`, `.debug-toggle__label`) in App.css. Style it to match the dark theme using existing `var(--color-*)` custom properties.
5. Verify: Docker build succeeds, toggle visible and functional, filter pills render and filter correctly.
## Inputs
- ``frontend/src/api/public-client.ts` — existing API client, needs debug mode functions added`
- ``frontend/src/pages/AdminPipeline.tsx` — main pipeline page component (535 lines)`
- ``frontend/src/App.css` — global stylesheet with CSS custom properties`
- ``frontend/src/pages/ReviewQueue.tsx` — reference for filter pill pattern and ModeToggle visual pattern`
## Expected Output
- ``frontend/src/api/public-client.ts` — added fetchDebugMode() and setDebugMode() functions`
- ``frontend/src/pages/AdminPipeline.tsx` — added DebugModeToggle component and status filter pill bar with state management`
- ``frontend/src/App.css` — added debug-toggle and status-filter CSS styles`
## Verification
ssh ub01 "cd /vmPool/r/repos/xpltdco/chrysopedia && docker compose build chrysopedia-web" succeeds with exit 0

View file

@ -0,0 +1,79 @@
---
id: T01
parent: S04
milestone: M007
provides: []
requires: []
affects: []
key_files: ["frontend/src/pages/AdminPipeline.tsx", "frontend/src/api/public-client.ts", "frontend/src/App.css"]
key_decisions: ["Reused mode-toggle CSS pattern for debug toggle", "StatusFilter shows counts in pill labels and hides when only 1 status exists"]
patterns_established: []
drill_down_paths: []
observability_surfaces: []
duration: ""
verification_result: "Docker build of chrysopedia-web service succeeded with exit 0. TypeScript compiled cleanly, Vite bundled 48 modules, nginx image produced."
completed_at: 2026-03-30T19:34:04.184Z
blocker_discovered: false
---
# T01: Added DebugModeToggle component and StatusFilter pill bar to AdminPipeline page with API client functions for debug mode GET/PUT endpoints
> Added DebugModeToggle component and StatusFilter pill bar to AdminPipeline page with API client functions for debug mode GET/PUT endpoints
## What Happened
---
id: T01
parent: S04
milestone: M007
key_files:
- frontend/src/pages/AdminPipeline.tsx
- frontend/src/api/public-client.ts
- frontend/src/App.css
key_decisions:
- Reused mode-toggle CSS pattern for debug toggle
- StatusFilter shows counts in pill labels and hides when only 1 status exists
duration: ""
verification_result: passed
completed_at: 2026-03-30T19:34:04.184Z
blocker_discovered: false
---
# T01: Added DebugModeToggle component and StatusFilter pill bar to AdminPipeline page with API client functions for debug mode GET/PUT endpoints
**Added DebugModeToggle component and StatusFilter pill bar to AdminPipeline page with API client functions for debug mode GET/PUT endpoints**
## What Happened
Added fetchDebugMode() and setDebugMode() API client functions to public-client.ts for the existing backend debug-mode endpoints. Created DebugModeToggle component (fetch on mount, toggle switch with active state) placed in the header-right area next to WorkerStatus. Created StatusFilter component using the existing filter-tab CSS pattern, with per-status counts and an All pill. Videos are filtered client-side via activeFilter state. Added debug-toggle CSS matching the dark theme toggle pattern.
## Verification
Docker build of chrysopedia-web service succeeded with exit 0. TypeScript compiled cleanly, Vite bundled 48 modules, nginx image produced.
## Verification Evidence
| # | Command | Exit Code | Verdict | Duration |
|---|---------|-----------|---------|----------|
| 1 | `ssh ub01 "cd /vmPool/r/repos/xpltdco/chrysopedia && docker compose build chrysopedia-web"` | 0 | ✅ pass | 8100ms |
## Deviations
Docker service name is chrysopedia-web not chrysopedia-web-8096. Backend response uses debug_mode (snake_case) not enabled as task plan implied.
## Known Issues
None.
## Files Created/Modified
- `frontend/src/pages/AdminPipeline.tsx`
- `frontend/src/api/public-client.ts`
- `frontend/src/App.css`
## Deviations
Docker service name is chrysopedia-web not chrysopedia-web-8096. Backend response uses debug_mode (snake_case) not enabled as task plan implied.
## Known Issues
None.

View file

@ -0,0 +1,31 @@
---
estimated_steps: 8
estimated_files: 2
skills_used: []
---
# T02: Prune dead UI, rename view toggle, add debug indicator on trigger, add review queue cross-link
Clean up the pipeline page: remove low-value UI elements, rename confusing labels, add debug mode context to trigger button, and add cross-navigation to review queue.
Steps:
1. In EventLog component, rename 'Head'/'Tail' buttons to 'Oldest first'/'Newest first'. The buttons are in the `.pipeline-events__view-toggle` div. Just change the button text.
2. In EventLog component, remove the duplicate '↻ Refresh' button from `.pipeline-events__header`. The page-level refresh in the main header is sufficient.
3. In the expanded video detail section (the `pipeline-video__detail` div), remove the `ID: {video.id.slice(0, 8)}…` span. Keep Created and Updated dates.
4. In the video header actions area, add a '(debug)' indicator next to the Trigger button when debug mode is active. Pass `debugMode` state from the parent AdminPipeline component down to or alongside the trigger button. When debugMode is true, the trigger button text changes to '▶ Trigger (debug)' to indicate the run will capture full LLM I/O.
5. Add a cross-link button/icon on each pipeline video card that navigates to the review queue filtered by that video. Use `<a href='/admin/review'>` or React Router Link with the video's creator name as context. A small '→ Moments' link in the video meta area works. Since the review queue doesn't currently support URL-based video filtering, just link to `/admin/review` with the video filename as a visual cue.
6. Verify: Docker build succeeds, view toggle says 'Oldest first'/'Newest first', no event-level refresh button, no event ID in detail, debug indicator on trigger when debug mode is on, cross-link visible.
## Inputs
- ``frontend/src/pages/AdminPipeline.tsx` — pipeline page with debug toggle and status filter from T01`
- ``frontend/src/App.css` — global stylesheet with debug-toggle styles from T01`
## Expected Output
- ``frontend/src/pages/AdminPipeline.tsx` — head/tail renamed, duplicate refresh removed, event ID removed, debug indicator on trigger, review queue cross-link added`
- ``frontend/src/App.css` — any minor style additions for cross-link`
## Verification
ssh ub01 "cd /vmPool/r/repos/xpltdco/chrysopedia && docker compose build chrysopedia-web" succeeds with exit 0

View file

@ -541,6 +541,57 @@ a.app-footer__repo:hover {
cursor: not-allowed;
}
/* ── Debug Mode Toggle ────────────────────────────────────────────────────── */
.debug-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8125rem;
}
.debug-toggle__label {
color: var(--color-text-on-header-label);
white-space: nowrap;
}
.debug-toggle__switch {
position: relative;
width: 2.5rem;
height: 1.25rem;
background: var(--color-toggle-track);
border: none;
border-radius: 9999px;
cursor: pointer;
transition: background 0.2s;
flex-shrink: 0;
}
.debug-toggle__switch--active {
background: var(--color-toggle-review);
}
.debug-toggle__switch::after {
content: "";
position: absolute;
top: 0.125rem;
left: 0.125rem;
width: 1rem;
height: 1rem;
background: var(--color-toggle-thumb);
border-radius: 50%;
transition: transform 0.2s;
}
.debug-toggle__switch--active::after {
transform: translateX(1.25rem);
}
.debug-toggle__switch:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ── Pagination ───────────────────────────────────────────────────────────── */
.pagination {

View file

@ -471,3 +471,20 @@ export async function revokePipeline(videoId: string): Promise<RevokeResponse> {
method: "POST",
});
}
// ── Debug Mode ──────────────────────────────────────────────────────────────
export interface DebugModeResponse {
debug_mode: boolean;
}
export async function fetchDebugMode(): Promise<DebugModeResponse> {
return request<DebugModeResponse>(`${BASE}/admin/pipeline/debug-mode`);
}
export async function setDebugMode(enabled: boolean): Promise<DebugModeResponse> {
return request<DebugModeResponse>(`${BASE}/admin/pipeline/debug-mode`, {
method: "PUT",
body: JSON.stringify({ debug_mode: enabled }),
});
}

View file

@ -8,6 +8,8 @@ import {
fetchPipelineVideos,
fetchPipelineEvents,
fetchWorkerStatus,
fetchDebugMode,
setDebugMode,
triggerPipeline,
revokePipeline,
type PipelineVideoItem,
@ -364,6 +366,93 @@ function WorkerStatus() {
);
}
// ── Debug Mode Toggle ────────────────────────────────────────────────────────
function DebugModeToggle() {
const [debugMode, setDebugModeState] = useState<boolean | null>(null);
const [debugLoading, setDebugLoading] = useState(false);
useEffect(() => {
let cancelled = false;
fetchDebugMode()
.then((res) => {
if (!cancelled) setDebugModeState(res.debug_mode);
})
.catch(() => {
// silently fail — toggle stays hidden
});
return () => { cancelled = true; };
}, []);
async function handleToggle() {
if (debugMode === null || debugLoading) return;
setDebugLoading(true);
try {
const res = await setDebugMode(!debugMode);
setDebugModeState(res.debug_mode);
} catch {
// swallow — leave previous state
} finally {
setDebugLoading(false);
}
}
if (debugMode === null) return null;
return (
<div className="debug-toggle">
<span className="debug-toggle__label">
Debug {debugMode ? "On" : "Off"}
</span>
<button
type="button"
className={`debug-toggle__switch ${debugMode ? "debug-toggle__switch--active" : ""}`}
onClick={handleToggle}
disabled={debugLoading}
aria-label={`Turn debug mode ${debugMode ? "off" : "on"}`}
/>
</div>
);
}
// ── Status Filter ────────────────────────────────────────────────────────────
function StatusFilter({
videos,
activeFilter,
onFilterChange,
}: {
videos: PipelineVideoItem[];
activeFilter: string | null;
onFilterChange: (filter: string | null) => void;
}) {
const statuses = Array.from(new Set(videos.map((v) => v.processing_status))).sort();
if (statuses.length <= 1) return null;
return (
<div className="filter-tabs">
<button
type="button"
className={`filter-tab ${activeFilter === null ? "filter-tab--active" : ""}`}
onClick={() => onFilterChange(null)}
>
All
</button>
{statuses.map((status) => (
<button
key={status}
type="button"
className={`filter-tab ${activeFilter === status ? "filter-tab--active" : ""}`}
onClick={() => onFilterChange(status)}
>
{status} ({videos.filter((v) => v.processing_status === status).length})
</button>
))}
</div>
);
}
// ── Main Page ────────────────────────────────────────────────────────────────
export default function AdminPipeline() {
@ -373,6 +462,7 @@ export default function AdminPipeline() {
const [expandedId, setExpandedId] = useState<string | null>(null);
const [actionLoading, setActionLoading] = useState<string | null>(null);
const [actionMessage, setActionMessage] = useState<{ id: string; text: string; ok: boolean } | null>(null);
const [activeFilter, setActiveFilter] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
@ -448,6 +538,7 @@ export default function AdminPipeline() {
</p>
</div>
<div className="admin-pipeline__header-right">
<DebugModeToggle />
<WorkerStatus />
<button className="btn btn--secondary" onClick={() => void load()} disabled={loading}>
Refresh
@ -462,8 +553,16 @@ export default function AdminPipeline() {
) : videos.length === 0 ? (
<div className="empty-state">No videos in pipeline.</div>
) : (
<div className="admin-pipeline__list">
{videos.map((video) => (
<>
<StatusFilter
videos={videos}
activeFilter={activeFilter}
onFilterChange={setActiveFilter}
/>
<div className="admin-pipeline__list">
{videos
.filter((v) => activeFilter === null || v.processing_status === activeFilter)
.map((video) => (
<div key={video.id} className="pipeline-video">
<div
className="pipeline-video__header"
@ -530,6 +629,7 @@ export default function AdminPipeline() {
</div>
))}
</div>
</>
)}
</div>
);