feat: Added Head/Tail segmented toggle to EventLog with order param wir…

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

GSD-Task: S02/T02
This commit is contained in:
jlightner 2026-03-30 11:15:21 +00:00
parent bf126f4825
commit ee24731e59
7 changed files with 148 additions and 5 deletions

View file

@ -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

View file

@ -0,0 +1,9 @@
{
"schemaVersion": 1,
"taskId": "T01",
"unitId": "M006/S02/T01",
"timestamp": 1774869044959,
"passed": true,
"discoverySource": "none",
"checks": []
}

View file

@ -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.

View file

@ -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);

View file

@ -439,13 +439,14 @@ export async function fetchPipelineVideos(): Promise<PipelineVideoListResponse>
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<PipelineEventListResponse> {
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<PipelineEventListResponse>(
`${BASE}/admin/pipeline/events/${videoId}${query ? `?${query}` : ""}`,

View file

@ -103,13 +103,18 @@ function EventLog({ videoId }: { videoId: string }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 }) {
<div className="pipeline-events">
<div className="pipeline-events__header">
<span className="pipeline-events__count">{total} event{total !== 1 ? "s" : ""}</span>
<div className="pipeline-events__view-toggle">
<button
className={`pipeline-events__view-btn${viewMode === "head" ? " pipeline-events__view-btn--active" : ""}`}
onClick={() => { setViewMode("head"); setOffset(0); }}
>
Head
</button>
<button
className={`pipeline-events__view-btn${viewMode === "tail" ? " pipeline-events__view-btn--active" : ""}`}
onClick={() => { setViewMode("tail"); setOffset(0); }}
>
Tail
</button>
</div>
<button className="btn btn--small btn--secondary" onClick={() => void load()}> Refresh</button>
</div>

View file

@ -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"}
{"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"}