From ee24731e596edffe7cfa17def6e56329c646d983 Mon Sep 17 00:00:00 2001 From: jlightner Date: Mon, 30 Mar 2026 11:15:21 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Added=20Head/Tail=20segmented=20toggle?= =?UTF-8?q?=20to=20EventLog=20with=20order=20param=20wir=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "frontend/src/api/public-client.ts" - "frontend/src/pages/AdminPipeline.tsx" - "frontend/src/App.css" GSD-Task: S02/T02 --- .gsd/milestones/M006/slices/S02/S02-PLAN.md | 2 +- .../M006/slices/S02/tasks/T01-VERIFY.json | 9 +++ .../M006/slices/S02/tasks/T02-SUMMARY.md | 80 +++++++++++++++++++ frontend/src/App.css | 34 ++++++++ frontend/src/api/public-client.ts | 3 +- frontend/src/pages/AdminPipeline.tsx | 23 +++++- frontend/tsconfig.app.tsbuildinfo | 2 +- 7 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 .gsd/milestones/M006/slices/S02/tasks/T01-VERIFY.json create mode 100644 .gsd/milestones/M006/slices/S02/tasks/T02-SUMMARY.md diff --git a/.gsd/milestones/M006/slices/S02/S02-PLAN.md b/.gsd/milestones/M006/slices/S02/S02-PLAN.md index 3e44219..28cde41 100644 --- a/.gsd/milestones/M006/slices/S02/S02-PLAN.md +++ b/.gsd/milestones/M006/slices/S02/S02-PLAN.md @@ -34,7 +34,7 @@ - Estimate: 20m - Files: backend/routers/pipeline.py (on ub01: /vmPool/r/repos/xpltdco/chrysopedia/backend/routers/pipeline.py) - Verify: curl -s 'http://ub01:8096/api/v1/admin/pipeline/events/{video_id}?order=asc&limit=3' returns events in ascending timestamp order; curl with order=invalid returns HTTP 400 -- [ ] **T02: Add Head/Tail toggle to EventLog component with API wiring and CSS** — Add a Head/Tail segmented toggle to the EventLog component in AdminPipeline.tsx. Wire it to pass the `order` param through the API client. Add compact segmented-button CSS. +- [x] **T02: Added Head/Tail segmented toggle to EventLog with order param wiring — Head shows oldest-first (asc), Tail shows newest-first (desc), switching resets pagination** — Add a Head/Tail segmented toggle to the EventLog component in AdminPipeline.tsx. Wire it to pass the `order` param through the API client. Add compact segmented-button CSS. ## Steps diff --git a/.gsd/milestones/M006/slices/S02/tasks/T01-VERIFY.json b/.gsd/milestones/M006/slices/S02/tasks/T01-VERIFY.json new file mode 100644 index 0000000..2c36f7f --- /dev/null +++ b/.gsd/milestones/M006/slices/S02/tasks/T01-VERIFY.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": 1, + "taskId": "T01", + "unitId": "M006/S02/T01", + "timestamp": 1774869044959, + "passed": true, + "discoverySource": "none", + "checks": [] +} diff --git a/.gsd/milestones/M006/slices/S02/tasks/T02-SUMMARY.md b/.gsd/milestones/M006/slices/S02/tasks/T02-SUMMARY.md new file mode 100644 index 0000000..30c5765 --- /dev/null +++ b/.gsd/milestones/M006/slices/S02/tasks/T02-SUMMARY.md @@ -0,0 +1,80 @@ +--- +id: T02 +parent: S02 +milestone: M006 +provides: [] +requires: [] +affects: [] +key_files: ["frontend/src/api/public-client.ts", "frontend/src/pages/AdminPipeline.tsx", "frontend/src/App.css"] +key_decisions: ["Placed segmented toggle between event count and refresh button in the header row for natural scan order"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "Frontend build passed with zero TypeScript errors. Browser verification confirmed Head shows oldest events first (ascending timestamps), Tail shows newest first (descending). Token counts visible on all event rows and video summary. Pager functional in both modes. Slice-level curl checks: order=asc returns ascending timestamps, order=invalid returns HTTP 400." +completed_at: 2026-03-30T11:15:10.303Z +blocker_discovered: false +--- + +# T02: Added Head/Tail segmented toggle to EventLog with order param wiring — Head shows oldest-first (asc), Tail shows newest-first (desc), switching resets pagination + +> Added Head/Tail segmented toggle to EventLog with order param wiring — Head shows oldest-first (asc), Tail shows newest-first (desc), switching resets pagination + +## What Happened +--- +id: T02 +parent: S02 +milestone: M006 +key_files: + - frontend/src/api/public-client.ts + - frontend/src/pages/AdminPipeline.tsx + - frontend/src/App.css +key_decisions: + - Placed segmented toggle between event count and refresh button in the header row for natural scan order +duration: "" +verification_result: passed +completed_at: 2026-03-30T11:15:10.304Z +blocker_discovered: false +--- + +# T02: Added Head/Tail segmented toggle to EventLog with order param wiring — Head shows oldest-first (asc), Tail shows newest-first (desc), switching resets pagination + +**Added Head/Tail segmented toggle to EventLog with order param wiring — Head shows oldest-first (asc), Tail shows newest-first (desc), switching resets pagination** + +## What Happened + +Added `order` param to `fetchPipelineEvents` in the API client. In the EventLog component, added `viewMode` state with Head/Tail toggle buttons wired to pass `order=asc` or `order=desc`. Switching mode resets offset to 0. Added compact segmented-button CSS using existing CSS custom properties. Built and deployed to ub01 — verified both modes work correctly with ascending/descending event ordering, token counts remain visible, and the pager still functions. + +## Verification + +Frontend build passed with zero TypeScript errors. Browser verification confirmed Head shows oldest events first (ascending timestamps), Tail shows newest first (descending). Token counts visible on all event rows and video summary. Pager functional in both modes. Slice-level curl checks: order=asc returns ascending timestamps, order=invalid returns HTTP 400. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd frontend && npm run build` | 0 | ✅ pass | 4200ms | +| 2 | `curl -sf 'http://ub01:8096/api/v1/admin/pipeline/events/{vid}?order=asc&limit=3'` | 0 | ✅ pass | 800ms | +| 3 | `curl -s -w '%{http_code}' 'http://ub01:8096/api/v1/admin/pipeline/events/{vid}?order=invalid'` | 0 | ✅ pass (400) | 500ms | + + +## Deviations + +None. + +## Known Issues + +None. + +## Files Created/Modified + +- `frontend/src/api/public-client.ts` +- `frontend/src/pages/AdminPipeline.tsx` +- `frontend/src/App.css` + + +## Deviations +None. + +## Known Issues +None. diff --git a/frontend/src/App.css b/frontend/src/App.css index e1faa94..75ec723 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -2722,6 +2722,40 @@ body { font-weight: 500; } +.pipeline-events__view-toggle { + display: inline-flex; + border: 1px solid var(--color-border); + border-radius: 6px; + overflow: hidden; +} + +.pipeline-events__view-btn { + background: transparent; + border: none; + color: var(--color-text-secondary); + font-size: 0.75rem; + font-weight: 500; + padding: 0.25rem 0.625rem; + cursor: pointer; + font-family: inherit; + transition: background 0.15s, color 0.15s; +} + +.pipeline-events__view-btn:hover { + color: var(--color-text-primary); + background: var(--color-accent-subtle); +} + +.pipeline-events__view-btn--active { + background: var(--color-accent); + color: var(--color-bg-page); +} + +.pipeline-events__view-btn--active:hover { + background: var(--color-accent-hover); + color: var(--color-bg-page); +} + .pipeline-events__empty { font-size: 0.85rem; color: var(--color-text-muted); diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts index b94f0a7..650da87 100644 --- a/frontend/src/api/public-client.ts +++ b/frontend/src/api/public-client.ts @@ -439,13 +439,14 @@ export async function fetchPipelineVideos(): Promise export async function fetchPipelineEvents( videoId: string, - params: { offset?: number; limit?: number; stage?: string; event_type?: string } = {}, + params: { offset?: number; limit?: number; stage?: string; event_type?: string; order?: "asc" | "desc" } = {}, ): Promise { const qs = new URLSearchParams(); if (params.offset !== undefined) qs.set("offset", String(params.offset)); if (params.limit !== undefined) qs.set("limit", String(params.limit)); if (params.stage) qs.set("stage", params.stage); if (params.event_type) qs.set("event_type", params.event_type); + if (params.order) qs.set("order", params.order); const query = qs.toString(); return request( `${BASE}/admin/pipeline/events/${videoId}${query ? `?${query}` : ""}`, diff --git a/frontend/src/pages/AdminPipeline.tsx b/frontend/src/pages/AdminPipeline.tsx index e38f2a0..f903ba0 100644 --- a/frontend/src/pages/AdminPipeline.tsx +++ b/frontend/src/pages/AdminPipeline.tsx @@ -103,13 +103,18 @@ function EventLog({ videoId }: { videoId: string }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [offset, setOffset] = useState(0); + const [viewMode, setViewMode] = useState<"head" | "tail">("tail"); const limit = 50; const load = useCallback(async () => { setLoading(true); setError(null); try { - const res = await fetchPipelineEvents(videoId, { offset, limit }); + const res = await fetchPipelineEvents(videoId, { + offset, + limit, + order: viewMode === "head" ? "asc" : "desc", + }); setEvents(res.items); setTotal(res.total); } catch (err) { @@ -117,7 +122,7 @@ function EventLog({ videoId }: { videoId: string }) { } finally { setLoading(false); } - }, [videoId, offset]); + }, [videoId, offset, viewMode]); useEffect(() => { void load(); @@ -134,6 +139,20 @@ function EventLog({ videoId }: { videoId: string }) {
{total} event{total !== 1 ? "s" : ""} +
+ + +
diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index 997c8ec..c9f1734 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/public-client.ts","./src/components/ModeToggle.tsx","./src/components/ReportIssueModal.tsx","./src/components/StatusBadge.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/MomentDetail.tsx","./src/pages/ReviewQueue.tsx","./src/pages/SearchResults.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/ModeToggle.tsx","./src/components/ReportIssueModal.tsx","./src/components/StatusBadge.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/MomentDetail.tsx","./src/pages/ReviewQueue.tsx","./src/pages/SearchResults.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx"],"version":"5.6.3"} \ No newline at end of file