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:
parent
97b9f7234a
commit
44c0df6e08
14 changed files with 646 additions and 5 deletions
|
|
@ -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".
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
93
.gsd/milestones/M007/slices/S03/S03-SUMMARY.md
Normal file
93
.gsd/milestones/M007/slices/S03/S03-SUMMARY.md
Normal 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
|
||||
97
.gsd/milestones/M007/slices/S03/S03-UAT.md
Normal file
97
.gsd/milestones/M007/slices/S03/S03-UAT.md
Normal 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.
|
||||
9
.gsd/milestones/M007/slices/S03/tasks/T02-VERIFY.json
Normal file
9
.gsd/milestones/M007/slices/S03/tasks/T02-VERIFY.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T02",
|
||||
"unitId": "M007/S03/T02",
|
||||
"timestamp": 1774898679946,
|
||||
"passed": true,
|
||||
"discoverySource": "none",
|
||||
"checks": []
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
101
.gsd/milestones/M007/slices/S04/S04-RESEARCH.md
Normal file
101
.gsd/milestones/M007/slices/S04/S04-RESEARCH.md
Normal 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
|
||||
33
.gsd/milestones/M007/slices/S04/tasks/T01-PLAN.md
Normal file
33
.gsd/milestones/M007/slices/S04/tasks/T01-PLAN.md
Normal 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
|
||||
79
.gsd/milestones/M007/slices/S04/tasks/T01-SUMMARY.md
Normal file
79
.gsd/milestones/M007/slices/S04/tasks/T01-SUMMARY.md
Normal 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.
|
||||
31
.gsd/milestones/M007/slices/S04/tasks/T02-PLAN.md
Normal file
31
.gsd/milestones/M007/slices/S04/tasks/T02-PLAN.md
Normal 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
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue