feat: admin-api-and-dashboard-transcript-management#13
Conversation
- Implement EditTranscriptModal for editing transcript details. - Create ChannelsPage and VideosPage for channel and video management. - Add DashboardPage to display transcript statistics and recent activity. - Introduce HealthPage to monitor API and database health. - Develop LoginPage for admin authentication. - Create TranscriptsPage for listing and managing transcripts with search and pagination. - Update About page to dynamically display transcript statistics. - Adjust TypeScript configuration files for improved module resolution.
There was a problem hiding this comment.
Pull request overview
This PR adds an admin panel and supporting backend admin API for transcript management, plus updates the public About page to display dynamic transcript statistics.
Changes:
- Added admin UI (login, dashboard, transcripts CRUD, health monitor, placeholder channels/videos pages) and wired
/admin/*routes in the frontend. - Introduced backend admin namespace (
/api/v1/admin) with JWT-based auth, rate limiting, transcript CRUD endpoints, and admin health endpoints. - Improved DB pooling/timeout configuration and made About page stats dynamic via transcript meta.
Reviewed changes
Copilot reviewed 32 out of 34 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.node.tsbuildinfo | Adds TS build info artifact (should be ignored, not committed). |
| tsconfig.json | Alters TS path alias configuration (currently removes baseUrl). |
| tsconfig.app.tsbuildinfo | Adds TS build info artifact (should be ignored, not committed). |
| tsconfig.app.json | Alters app TS path alias configuration (currently removes baseUrl). |
| src/pages/About.tsx | Replaces static stats with dynamic meta-derived stats and adjusts CTA. |
| src/admin/pages/VideosPage.tsx | Adds placeholder admin videos page. |
| src/admin/pages/TranscriptsPage.tsx | Adds admin transcript listing with search, pagination, edit/delete flows. |
| src/admin/pages/LoginPage.tsx | Adds admin login page using password → token flow. |
| src/admin/pages/HealthPage.tsx | Adds admin health monitor UI consuming admin health endpoints. |
| src/admin/pages/DashboardPage.tsx | Adds admin dashboard summary + recent activity list. |
| src/admin/pages/ChannelsPage.tsx | Adds placeholder admin channels page. |
| src/admin/components/EditTranscriptModal.tsx | Adds modal to edit transcript metadata and text fields. |
| src/admin/api.ts | Adds admin API client wrapper (login, transcripts CRUD, health). |
| src/admin/AuthContext.tsx | Adds auth context for token storage and auth state. |
| src/admin/AdminLayout.tsx | Adds authenticated admin shell with sidebar navigation + logout. |
| src/App.tsx | Wires admin routes and wraps app in AuthProvider; introduces PublicLayout/Outlet. |
| backend/src/services/supabaseService.js | Makes pool settings configurable and adds retry/timeouts behavior to DB queries. |
| backend/src/services/admin/transcriptService.js | Adds admin transcript list/get/update/delete service logic. |
| backend/src/services/admin/authService.js | Adds admin password validation and JWT issuance. |
| backend/src/routes/index.js | Mounts /admin routes under /api/v1. |
| backend/src/routes/adminRoutes.js | Defines admin route grouping with auth + rate limiting. |
| backend/src/routes/admin/transcriptRoutes.js | Adds admin transcript endpoints with validation + permissions. |
| backend/src/routes/admin/healthRoutes.js | Adds admin health endpoints (basic + detailed). |
| backend/src/routes/admin/authRoutes.js | Adds admin login endpoint with validation + auth rate limiter. |
| backend/src/middleware/validation.js | Adds validation rules for admin login and transcript list/update. |
| backend/src/middleware/index.js | Re-exports new admin auth + admin rate limiter middleware. |
| backend/src/middleware/adminRateLimiter.js | Adds admin and admin-auth rate limiters. |
| backend/src/middleware/adminAuth.js | Adds JWT verification middleware and permission guard. |
| backend/src/controllers/admin/transcriptController.js | Adds admin transcript controller actions. |
| backend/src/controllers/admin/authController.js | Adds admin login controller action. |
| backend/src/config/index.js | Adds DB pool/timeout config + admin config + production env validation. |
| backend/package.json | Adds jsonwebtoken dependency. |
| backend/package-lock.json | Locks jsonwebtoken and its transitive deps. |
| backend/.env.example | Documents new DB tuning and admin env vars. |
Files not reviewed (1)
- backend/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -0,0 +1 @@ | |||
| {"root":["./vite.config.ts"],"version":"5.9.3"} No newline at end of file | |||
There was a problem hiding this comment.
This .tsbuildinfo file is a build artifact and should not be committed. Add a *.tsbuildinfo (or the specific filename) entry to .gitignore and remove this file from the repo to avoid noisy diffs and merge conflicts.
| try { | ||
| return await request<HealthResponse>("/api/v1/admin/health/detailed"); | ||
| } catch { | ||
| return await request<HealthResponse>("/api/v1/admin/health"); | ||
| } |
There was a problem hiding this comment.
getHealth() falls back to the basic health endpoint for any error from /health/detailed. If /health/detailed returns 503 in production to indicate degraded health, this fallback will hide the detailed dependency status. Consider only falling back on network/parse failures, or handling 503 responses by still consuming the detailed JSON payload.
| api.getTranscripts(1, 100) | ||
| .then((res) => { | ||
| const all = res.transcripts || []; | ||
| setTranscripts(all); | ||
| const known = all.filter((t) => t.status === "pending" || t.status === "processing" || t.status === "done").length; | ||
| setStats({ | ||
| total: res.total || all.length, | ||
| withKnownStatus: known, | ||
| unknownStatus: all.length - known, | ||
| }); | ||
| }) | ||
| .catch((error) => { | ||
| toast.error(getErrorMessage(error, "Failed to load dashboard data")); | ||
| }) | ||
| .finally(() => setLoading(false)); |
There was a problem hiding this comment.
Dashboard stats are computed from api.getTranscripts(1, 100), so withKnownStatus/unknownStatus only reflect the first 100 transcripts, not the whole dataset (while total uses res.total). This will report incorrect counts when there are more than 100 transcripts. Consider adding a dedicated stats endpoint (or a query that returns aggregated status counts) instead of sampling the first page.
| api.getTranscripts(1, 100) | |
| .then((res) => { | |
| const all = res.transcripts || []; | |
| setTranscripts(all); | |
| const known = all.filter((t) => t.status === "pending" || t.status === "processing" || t.status === "done").length; | |
| setStats({ | |
| total: res.total || all.length, | |
| withKnownStatus: known, | |
| unknownStatus: all.length - known, | |
| }); | |
| }) | |
| .catch((error) => { | |
| toast.error(getErrorMessage(error, "Failed to load dashboard data")); | |
| }) | |
| .finally(() => setLoading(false)); | |
| let cancelled = false; | |
| const loadDashboardData = async () => { | |
| const pageSize = 100; | |
| let page = 1; | |
| let total = 0; | |
| let all: Transcript[] = []; | |
| try { | |
| while (true) { | |
| const res = await api.getTranscripts(page, pageSize); | |
| const batch = res.transcripts || []; | |
| if (page === 1) { | |
| total = res.total || batch.length; | |
| } | |
| all = all.concat(batch); | |
| if (batch.length === 0 || all.length >= total || batch.length < pageSize) { | |
| break; | |
| } | |
| page += 1; | |
| } | |
| if (cancelled) return; | |
| const known = all.filter( | |
| (t) => t.status === "pending" || t.status === "processing" || t.status === "done" | |
| ).length; | |
| setTranscripts(all); | |
| setStats({ | |
| total: total || all.length, | |
| withKnownStatus: known, | |
| unknownStatus: all.length - known, | |
| }); | |
| } catch (error) { | |
| if (cancelled) return; | |
| toast.error(getErrorMessage(error, "Failed to load dashboard data")); | |
| } finally { | |
| if (!cancelled) { | |
| setLoading(false); | |
| } | |
| } | |
| }; | |
| loadDashboardData(); | |
| return () => { | |
| cancelled = true; | |
| }; |
| import ChannelsPage from "./admin/pages/ChannelsPage.tsx"; | ||
| import VideosPage from "./admin/pages/VideosPage.tsx"; |
There was a problem hiding this comment.
These imports include the .tsx extension, while other imports in the same file omit it (e.g., ./admin/pages/LoginPage on lines 20-23). For consistency and to avoid tooling edge-cases, prefer extensionless imports (e.g. ./admin/pages/ChannelsPage).
| import ChannelsPage from "./admin/pages/ChannelsPage.tsx"; | |
| import VideosPage from "./admin/pages/VideosPage.tsx"; | |
| import ChannelsPage from "./admin/pages/ChannelsPage"; | |
| import VideosPage from "./admin/pages/VideosPage"; |
| <button onClick={() => setEditingId(t.id)} className="p-1.5 rounded hover:bg-secondary transition-colors text-muted-foreground hover:text-foreground"> | ||
| <Pencil className="w-3.5 h-3.5" /> | ||
| </button> | ||
| <button onClick={() => setDeletingId(t.id)} className="p-1.5 rounded hover:bg-destructive/10 transition-colors text-muted-foreground hover:text-destructive"> |
There was a problem hiding this comment.
These icon-only edit/delete buttons are missing accessible names. Add aria-label values (and consider type="button") so they’re usable with screen readers and don’t accidentally act as submit buttons if placed inside a form later.
| <button onClick={() => setEditingId(t.id)} className="p-1.5 rounded hover:bg-secondary transition-colors text-muted-foreground hover:text-foreground"> | |
| <Pencil className="w-3.5 h-3.5" /> | |
| </button> | |
| <button onClick={() => setDeletingId(t.id)} className="p-1.5 rounded hover:bg-destructive/10 transition-colors text-muted-foreground hover:text-destructive"> | |
| <button | |
| type="button" | |
| aria-label={`Edit transcript ${t.title}`} | |
| onClick={() => setEditingId(t.id)} | |
| className="p-1.5 rounded hover:bg-secondary transition-colors text-muted-foreground hover:text-foreground" | |
| > | |
| <Pencil className="w-3.5 h-3.5" /> | |
| </button> | |
| <button | |
| type="button" | |
| aria-label={`Delete transcript ${t.title}`} | |
| onClick={() => setDeletingId(t.id)} | |
| className="p-1.5 rounded hover:bg-destructive/10 transition-colors text-muted-foreground hover:text-destructive" | |
| > |
|
|
||
| "baseUrl": ".", | ||
| "paths": { | ||
| "@/*": ["./src/*"] | ||
| } |
There was a problem hiding this comment.
paths is configured but baseUrl was removed. TypeScript requires baseUrl when using paths, otherwise alias imports like @/… will fail to typecheck/compile. Reintroduce baseUrl (typically ".") or drop paths if aliases aren’t intended.
| if (!res.ok) { | ||
| let message = `Request failed (${res.status})`; | ||
|
|
||
| try { | ||
| const json = (await res.json()) as ApiErrorEnvelope; |
There was a problem hiding this comment.
On non-2xx responses, request() only attempts to read error.message and otherwise throws away the body. For health endpoints it’s common to return 503 while still returning useful data about which dependency is unhealthy. Consider parsing and surfacing that data (or special-casing health calls) so the UI can show degraded state instead of a generic failure.
| placeholder="Search by title..." | ||
| value={search} | ||
| onChange={(e) => { setSearch(e.target.value); setPage(1); }} | ||
| className="w-full pl-8 pr-3 py-2 rounded-md border border-input bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary" | ||
| /> |
There was a problem hiding this comment.
Because search is part of fetchData’s dependencies, typing in this input will trigger a fetch on every keystroke via the effect. Add a debounce (or a submit button) to reduce API load and improve responsiveness.
| <button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page === 1} | ||
| className="p-1.5 rounded hover:bg-secondary disabled:opacity-30 transition-colors"> | ||
| <ChevronLeft className="w-4 h-4" /> | ||
| </button> | ||
| <span className="text-sm text-muted-foreground">Page {page} of {totalPages}</span> | ||
| <button onClick={() => setPage((p) => Math.min(totalPages, p + 1))} disabled={page === totalPages} | ||
| className="p-1.5 rounded hover:bg-secondary disabled:opacity-30 transition-colors"> |
There was a problem hiding this comment.
Pagination controls are icon-only buttons without accessible names. Add aria-label text (e.g., “Previous page” / “Next page”) so assistive tech can identify them.
| <button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page === 1} | |
| className="p-1.5 rounded hover:bg-secondary disabled:opacity-30 transition-colors"> | |
| <ChevronLeft className="w-4 h-4" /> | |
| </button> | |
| <span className="text-sm text-muted-foreground">Page {page} of {totalPages}</span> | |
| <button onClick={() => setPage((p) => Math.min(totalPages, p + 1))} disabled={page === totalPages} | |
| className="p-1.5 rounded hover:bg-secondary disabled:opacity-30 transition-colors"> | |
| <button | |
| onClick={() => setPage((p) => Math.max(1, p - 1))} | |
| disabled={page === 1} | |
| aria-label="Previous page" | |
| className="p-1.5 rounded hover:bg-secondary disabled:opacity-30 transition-colors" | |
| > | |
| <ChevronLeft className="w-4 h-4" /> | |
| </button> | |
| <span className="text-sm text-muted-foreground">Page {page} of {totalPages}</span> | |
| <button | |
| onClick={() => setPage((p) => Math.min(totalPages, p + 1))} | |
| disabled={page === totalPages} | |
| aria-label="Next page" | |
| className="p-1.5 rounded hover:bg-secondary disabled:opacity-30 transition-colors" | |
| > |
| </span> | ||
| )} | ||
| <button | ||
| onClick={() => setSidebarOpen(!sidebarOpen)} |
There was a problem hiding this comment.
The sidebar toggle is an icon-only button without an accessible name. Add an aria-label (e.g., “Collapse sidebar” / “Expand sidebar”) so it’s usable with screen readers.
| onClick={() => setSidebarOpen(!sidebarOpen)} | |
| onClick={() => setSidebarOpen(!sidebarOpen)} | |
| aria-label={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"} |
|
@cubic-dev-ai review this pull request |
@Sansh2356 I have started the AI code review. It will take a few minutes to complete. |
|
@CodeRabbit-ai review . |
|
Tip For best results, initiate chat on the files or code changes.
[review] |
-Overall made a basic admin panel