diff --git a/.gsd/KNOWLEDGE.md b/.gsd/KNOWLEDGE.md index f875c07..c0da99e 100644 --- a/.gsd/KNOWLEDGE.md +++ b/.gsd/KNOWLEDGE.md @@ -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". diff --git a/.gsd/PROJECT.md b/.gsd/PROJECT.md index eed90cf..b9dcfde 100644 --- a/.gsd/PROJECT.md +++ b/.gsd/PROJECT.md @@ -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. diff --git a/.gsd/milestones/M007/M007-ROADMAP.md b/.gsd/milestones/M007/M007-ROADMAP.md index fd4e585..fb56c0a 100644 --- a/.gsd/milestones/M007/M007-ROADMAP.md +++ b/.gsd/milestones/M007/M007-ROADMAP.md @@ -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 | diff --git a/.gsd/milestones/M007/slices/S03/S03-SUMMARY.md b/.gsd/milestones/M007/slices/S03/S03-SUMMARY.md new file mode 100644 index 0000000..c81b2a5 --- /dev/null +++ b/.gsd/milestones/M007/slices/S03/S03-SUMMARY.md @@ -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 diff --git a/.gsd/milestones/M007/slices/S03/S03-UAT.md b/.gsd/milestones/M007/slices/S03/S03-UAT.md new file mode 100644 index 0000000..410c0db --- /dev/null +++ b/.gsd/milestones/M007/slices/S03/S03-UAT.md @@ -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. diff --git a/.gsd/milestones/M007/slices/S03/tasks/T02-VERIFY.json b/.gsd/milestones/M007/slices/S03/tasks/T02-VERIFY.json new file mode 100644 index 0000000..342e1b0 --- /dev/null +++ b/.gsd/milestones/M007/slices/S03/tasks/T02-VERIFY.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": 1, + "taskId": "T02", + "unitId": "M007/S03/T02", + "timestamp": 1774898679946, + "passed": true, + "discoverySource": "none", + "checks": [] +} diff --git a/.gsd/milestones/M007/slices/S04/S04-PLAN.md b/.gsd/milestones/M007/slices/S04/S04-PLAN.md index 7533e08..66a2da5 100644 --- a/.gsd/milestones/M007/slices/S04/S04-PLAN.md +++ b/.gsd/milestones/M007/slices/S04/S04-PLAN.md @@ -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 `` 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 diff --git a/.gsd/milestones/M007/slices/S04/S04-RESEARCH.md b/.gsd/milestones/M007/slices/S04/S04-RESEARCH.md new file mode 100644 index 0000000..41d4bc6 --- /dev/null +++ b/.gsd/milestones/M007/slices/S04/S04-RESEARCH.md @@ -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()` 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 diff --git a/.gsd/milestones/M007/slices/S04/tasks/T01-PLAN.md b/.gsd/milestones/M007/slices/S04/tasks/T01-PLAN.md new file mode 100644 index 0000000..bf4d9cc --- /dev/null +++ b/.gsd/milestones/M007/slices/S04/tasks/T01-PLAN.md @@ -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 diff --git a/.gsd/milestones/M007/slices/S04/tasks/T01-SUMMARY.md b/.gsd/milestones/M007/slices/S04/tasks/T01-SUMMARY.md new file mode 100644 index 0000000..b566161 --- /dev/null +++ b/.gsd/milestones/M007/slices/S04/tasks/T01-SUMMARY.md @@ -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. diff --git a/.gsd/milestones/M007/slices/S04/tasks/T02-PLAN.md b/.gsd/milestones/M007/slices/S04/tasks/T02-PLAN.md new file mode 100644 index 0000000..acd5538 --- /dev/null +++ b/.gsd/milestones/M007/slices/S04/tasks/T02-PLAN.md @@ -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 `` 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 diff --git a/frontend/src/App.css b/frontend/src/App.css index ba54afb..c641cf9 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -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 { diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts index 4f6817b..8e9133f 100644 --- a/frontend/src/api/public-client.ts +++ b/frontend/src/api/public-client.ts @@ -471,3 +471,20 @@ export async function revokePipeline(videoId: string): Promise { method: "POST", }); } + +// ── Debug Mode ────────────────────────────────────────────────────────────── + +export interface DebugModeResponse { + debug_mode: boolean; +} + +export async function fetchDebugMode(): Promise { + return request(`${BASE}/admin/pipeline/debug-mode`); +} + +export async function setDebugMode(enabled: boolean): Promise { + return request(`${BASE}/admin/pipeline/debug-mode`, { + method: "PUT", + body: JSON.stringify({ debug_mode: enabled }), + }); +} diff --git a/frontend/src/pages/AdminPipeline.tsx b/frontend/src/pages/AdminPipeline.tsx index 2a1ac72..f8f9c73 100644 --- a/frontend/src/pages/AdminPipeline.tsx +++ b/frontend/src/pages/AdminPipeline.tsx @@ -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(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 ( +
+ + Debug {debugMode ? "On" : "Off"} + +
+ ); +} + +// ── 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 ( +
+ + {statuses.map((status) => ( + + ))} +
+ ); +} + // ── Main Page ──────────────────────────────────────────────────────────────── export default function AdminPipeline() { @@ -373,6 +462,7 @@ export default function AdminPipeline() { const [expandedId, setExpandedId] = useState(null); const [actionLoading, setActionLoading] = useState(null); const [actionMessage, setActionMessage] = useState<{ id: string; text: string; ok: boolean } | null>(null); + const [activeFilter, setActiveFilter] = useState(null); const load = useCallback(async () => { setLoading(true); @@ -448,6 +538,7 @@ export default function AdminPipeline() {

+