From 7d805b80e3b63af819fa1d30a5f2428441197bf9 Mon Sep 17 00:00:00 2001 From: jlightner Date: Fri, 3 Apr 2026 01:59:49 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Built=20AdminTechniquePages=20page=20at?= =?UTF-8?q?=20/admin/techniques=20with=20table,=20e=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "frontend/src/pages/AdminTechniquePages.tsx" - "frontend/src/api/public-client.ts" - "frontend/src/App.tsx" - "frontend/src/components/AdminDropdown.tsx" GSD-Task: S06/T02 --- .gsd/milestones/M014/slices/S06/S06-PLAN.md | 2 +- .../M014/slices/S06/tasks/T01-VERIFY.json | 18 ++ .../M014/slices/S06/tasks/T02-SUMMARY.md | 82 ++++++ frontend/src/App.tsx | 2 + frontend/src/api/public-client.ts | 44 ++++ frontend/src/components/AdminDropdown.tsx | 8 + frontend/src/pages/AdminTechniquePages.tsx | 244 ++++++++++++++++++ frontend/tsconfig.app.tsbuildinfo | 2 +- 8 files changed, 400 insertions(+), 2 deletions(-) create mode 100644 .gsd/milestones/M014/slices/S06/tasks/T01-VERIFY.json create mode 100644 .gsd/milestones/M014/slices/S06/tasks/T02-SUMMARY.md create mode 100644 frontend/src/pages/AdminTechniquePages.tsx diff --git a/.gsd/milestones/M014/slices/S06/S06-PLAN.md b/.gsd/milestones/M014/slices/S06/S06-PLAN.md index 4a9bdec..a2706bd 100644 --- a/.gsd/milestones/M014/slices/S06/S06-PLAN.md +++ b/.gsd/milestones/M014/slices/S06/S06-PLAN.md @@ -31,7 +31,7 @@ The endpoint lives in `backend/routers/pipeline.py` alongside other admin endpoi - Estimate: 45m - Files: backend/routers/pipeline.py, backend/schemas.py - Verify: ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && PYTHONPATH=backend python -c "from schemas import AdminTechniquePageItem; print(AdminTechniquePageItem.model_fields.keys())"' && curl -sf http://ub01:8096/api/v1/admin/pipeline/technique-pages | python3 -c 'import json,sys; d=json.load(sys.stdin); assert "items" in d; print(f"OK: {d["total"]} pages")' -- [ ] **T02: Build AdminTechniquePages page with route and dropdown entry** — Create the frontend page for admin technique page management. Table showing technique pages with source video counts, version counts, format badges, and creator. Expandable rows show source videos list (from existing `/techniques/{slug}` detail endpoint) and link to version history. Filters for multi-source only and creator. Cross-links to pipeline admin and public technique pages. +- [x] **T02: Built AdminTechniquePages page at /admin/techniques with table, expandable source video rows, filters, format badges, and admin dropdown entry** — Create the frontend page for admin technique page management. Table showing technique pages with source video counts, version counts, format badges, and creator. Expandable rows show source videos list (from existing `/techniques/{slug}` detail endpoint) and link to version history. Filters for multi-source only and creator. Cross-links to pipeline admin and public technique pages. Follows patterns from `AdminPipeline.tsx` (table layout, expandable rows, badges, fetch pattern). diff --git a/.gsd/milestones/M014/slices/S06/tasks/T01-VERIFY.json b/.gsd/milestones/M014/slices/S06/tasks/T01-VERIFY.json new file mode 100644 index 0000000..6e0961f --- /dev/null +++ b/.gsd/milestones/M014/slices/S06/tasks/T01-VERIFY.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "taskId": "T01", + "unitId": "M014/S06/T01", + "timestamp": 1775181335296, + "passed": false, + "discoverySource": "task-plan", + "checks": [ + { + "command": "ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia", + "exitCode": 2, + "durationMs": 7, + "verdict": "fail" + } + ], + "retryAttempt": 1, + "maxRetries": 2 +} diff --git a/.gsd/milestones/M014/slices/S06/tasks/T02-SUMMARY.md b/.gsd/milestones/M014/slices/S06/tasks/T02-SUMMARY.md new file mode 100644 index 0000000..f474ab0 --- /dev/null +++ b/.gsd/milestones/M014/slices/S06/tasks/T02-SUMMARY.md @@ -0,0 +1,82 @@ +--- +id: T02 +parent: S06 +milestone: M014 +provides: [] +requires: [] +affects: [] +key_files: ["frontend/src/pages/AdminTechniquePages.tsx", "frontend/src/api/public-client.ts", "frontend/src/App.tsx", "frontend/src/components/AdminDropdown.tsx"] +key_decisions: [] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "Frontend build passes clean (tsc + vite, 0 errors). Schema import verified via docker exec. API endpoint returns valid JSON with 20 pages. Browser verification: page renders table with all columns, row expansion works showing source videos section, admin dropdown shows all three entries (Reports, Pipeline, Techniques)." +completed_at: 2026-04-03T01:59:32.720Z +blocker_discovered: false +--- + +# T02: Built AdminTechniquePages page at /admin/techniques with table, expandable source video rows, filters, format badges, and admin dropdown entry + +> Built AdminTechniquePages page at /admin/techniques with table, expandable source video rows, filters, format badges, and admin dropdown entry + +## What Happened +--- +id: T02 +parent: S06 +milestone: M014 +key_files: + - frontend/src/pages/AdminTechniquePages.tsx + - frontend/src/api/public-client.ts + - frontend/src/App.tsx + - frontend/src/components/AdminDropdown.tsx +key_decisions: + - (none) +duration: "" +verification_result: passed +completed_at: 2026-04-03T01:59:32.720Z +blocker_discovered: false +--- + +# T02: Built AdminTechniquePages page at /admin/techniques with table, expandable source video rows, filters, format badges, and admin dropdown entry + +**Built AdminTechniquePages page at /admin/techniques with table, expandable source video rows, filters, format badges, and admin dropdown entry** + +## What Happened + +Added AdminTechniquePageItem interface and fetchAdminTechniquePages function to public-client.ts matching the backend schema from T01. Created AdminTechniquePages.tsx with a table showing title, creator, category, format badge (v1/v2), source count, version count, and updated date. Rows expand on click to show source videos fetched from the existing technique detail endpoint, with links to pipeline admin via video ID query param. Filter bar includes multi-source-only checkbox, creator text filter, and sort dropdown. Added /admin/techniques route in App.tsx and Techniques entry in AdminDropdown.tsx. Deployed to ub01 and verified in browser. + +## Verification + +Frontend build passes clean (tsc + vite, 0 errors). Schema import verified via docker exec. API endpoint returns valid JSON with 20 pages. Browser verification: page renders table with all columns, row expansion works showing source videos section, admin dropdown shows all three entries (Reports, Pipeline, Techniques). + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd frontend && npm run build` | 0 | ✅ pass | 3000ms | +| 2 | `docker exec chrysopedia-api python -c 'from schemas import AdminTechniquePageItem; print(AdminTechniquePageItem.model_fields.keys())'` | 0 | ✅ pass | 1000ms | +| 3 | `curl -sf http://ub01:8096/api/v1/admin/pipeline/technique-pages | python3 -c 'assert items in d; print(OK)'` | 0 | ✅ pass | 500ms | + + +## Deviations + +None. + +## Known Issues + +Source video and version counts are all 0 because association tables are empty. UI displays correctly and will show real counts when populated. + +## Files Created/Modified + +- `frontend/src/pages/AdminTechniquePages.tsx` +- `frontend/src/api/public-client.ts` +- `frontend/src/App.tsx` +- `frontend/src/components/AdminDropdown.tsx` + + +## Deviations +None. + +## Known Issues +Source video and version counts are all 0 because association tables are empty. UI displays correctly and will show real counts when populated. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5ea450c..0c44003 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,7 @@ import TopicsBrowse from "./pages/TopicsBrowse"; import SubTopicPage from "./pages/SubTopicPage"; import AdminReports from "./pages/AdminReports"; import AdminPipeline from "./pages/AdminPipeline"; +import AdminTechniquePages from "./pages/AdminTechniquePages"; import About from "./pages/About"; import AdminDropdown from "./components/AdminDropdown"; import AppFooter from "./components/AppFooter"; @@ -128,6 +129,7 @@ export default function App() { {/* Admin routes */} } /> } /> + } /> {/* Info routes */} } /> diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts index c5bf25c..2e436fd 100644 --- a/frontend/src/api/public-client.ts +++ b/frontend/src/api/public-client.ts @@ -739,3 +739,47 @@ export async function setDebugMode(enabled: boolean): Promise body: JSON.stringify({ debug_mode: enabled }), }); } + +// ── Admin: Technique Pages ───────────────────────────────────────────────── + +export interface AdminTechniquePageItem { + id: string; + title: string; + slug: string; + creator_name: string; + creator_slug: string; + topic_category: string; + body_sections_format: string; + source_video_count: number; + version_count: number; + created_at: string; + updated_at: string; +} + +export interface AdminTechniquePageListResponse { + items: AdminTechniquePageItem[]; + total: number; + offset: number; + limit: number; +} + +export async function fetchAdminTechniquePages( + params: { + multi_source_only?: boolean; + creator?: string; + sort?: string; + offset?: number; + limit?: number; + } = {}, +): Promise { + const qs = new URLSearchParams(); + if (params.multi_source_only) qs.set("multi_source_only", "true"); + if (params.creator) qs.set("creator", params.creator); + if (params.sort) qs.set("sort", params.sort); + if (params.offset !== undefined) qs.set("offset", String(params.offset)); + if (params.limit !== undefined) qs.set("limit", String(params.limit)); + const query = qs.toString(); + return request( + `${BASE}/admin/pipeline/technique-pages${query ? `?${query}` : ""}`, + ); +} diff --git a/frontend/src/components/AdminDropdown.tsx b/frontend/src/components/AdminDropdown.tsx index eb8d8c0..081c096 100644 --- a/frontend/src/components/AdminDropdown.tsx +++ b/frontend/src/components/AdminDropdown.tsx @@ -58,6 +58,14 @@ export default function AdminDropdown() { > Pipeline + setOpen(false)} + > + Techniques + )} diff --git a/frontend/src/pages/AdminTechniquePages.tsx b/frontend/src/pages/AdminTechniquePages.tsx new file mode 100644 index 0000000..c5e8eaa --- /dev/null +++ b/frontend/src/pages/AdminTechniquePages.tsx @@ -0,0 +1,244 @@ +/** + * Admin view for technique pages — list with source/version counts, + * expandable rows showing source videos, cross-links to pipeline admin. + */ + +import React, { useCallback, useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; +import { + fetchAdminTechniquePages, + fetchTechnique, + type AdminTechniquePageItem, + type SourceVideoSummary, +} from "../api/public-client"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +function formatDate(iso: string | null): string { + if (!iso) return "—"; + return new Date(iso).toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +function formatBadge(format: string): string { + if (format === "v2_sections") return "v2"; + if (format === "v1_markdown") return "v1"; + return format; +} + +// ── Main Component ─────────────────────────────────────────────────────────── + +export default function AdminTechniquePages() { + useDocumentTitle("Admin — Technique Pages"); + + // List state + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Filters + const [multiSourceOnly, setMultiSourceOnly] = useState(false); + const [creatorFilter, setCreatorFilter] = useState(""); + const [sort, setSort] = useState("recent"); + + // Expand state + const [expandedSlug, setExpandedSlug] = useState(null); + const [expandedVideos, setExpandedVideos] = useState([]); + const [expandLoading, setExpandLoading] = useState(false); + + // Fetch list + const fetchList = useCallback(async () => { + setLoading(true); + setError(null); + try { + const data = await fetchAdminTechniquePages({ + multi_source_only: multiSourceOnly || undefined, + creator: creatorFilter || undefined, + sort, + limit: 100, + }); + setItems(data.items); + setTotal(data.total); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); + } + }, [multiSourceOnly, creatorFilter, sort]); + + useEffect(() => { + fetchList(); + }, [fetchList]); + + // Expand row — fetch technique detail for source videos + const handleExpand = useCallback( + async (slug: string) => { + if (expandedSlug === slug) { + setExpandedSlug(null); + return; + } + setExpandedSlug(slug); + setExpandLoading(true); + try { + const detail = await fetchTechnique(slug); + setExpandedVideos(detail.source_videos ?? []); + } catch { + setExpandedVideos([]); + } finally { + setExpandLoading(false); + } + }, + [expandedSlug], + ); + + return ( +
+
+

Technique Pages

+ {total} pages +
+ + {/* Filter bar */} +
+ + setCreatorFilter(e.target.value)} + /> + +
+ + {error &&
{error}
} + + {loading ? ( +
Loading…
+ ) : items.length === 0 ? ( +
No technique pages found.
+ ) : ( +
+ + + + + + + + + + + + + + + {items.map((item) => ( + + handleExpand(item.slug)} + style={{ cursor: "pointer" }} + > + + + + + + + + + + {expandedSlug === item.slug && ( + + + + )} + + ))} + +
TitleCreatorCategoryFormatSourcesVersionsUpdated
+ {expandedSlug === item.slug ? "▾" : "▸"} + + e.stopPropagation()} + title="View public page" + > + {item.title} + + + e.stopPropagation()} + > + {item.creator_name} + + {item.topic_category} + + {formatBadge(item.body_sections_format)} + + {item.source_video_count}{item.version_count}{formatDate(item.updated_at)}
+
+
+ + View public page → + +
+

Source Videos

+ {expandLoading ? ( +

Loading source videos…

+ ) : expandedVideos.length === 0 ? ( +

+ No source videos linked. +

+ ) : ( +
    + {expandedVideos.map((v) => ( +
  • + + {v.filename} + + {v.added_at && ( + + {" "} + — added {formatDate(v.added_at)} + + )} +
  • + ))} +
+ )} +
+
+
+ )} +
+ ); +} diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index 9d3e49a..6c151ce 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/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/hooks/useDocumentTitle.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts","./src/utils/citations.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/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/hooks/useDocumentTitle.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"} \ No newline at end of file