From 06e07977229189e7ef9827daef75c175e3fb7678 Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Thu, 29 Jan 2026 21:03:32 +1300 Subject: [PATCH] release: 2901262102 --- backend/app/db.py | 26 ++++ backend/app/routers/admin.py | 39 ++++- frontend/app/admin/requests-all/page.tsx | 172 +++++++++++++++++++++++ frontend/app/globals.css | 79 +++++++++++ frontend/app/ui/AdminSidebar.tsx | 1 + frontend/package.json | 2 +- 6 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 frontend/app/admin/requests-all/page.tsx diff --git a/backend/app/db.py b/backend/app/db.py index 0de4a56..f5d81cf 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -986,6 +986,32 @@ def get_cached_requests( return results +def get_cached_requests_count( + requested_by_norm: Optional[str] = None, + requested_by_id: Optional[int] = None, + since_iso: Optional[str] = None, +) -> int: + query = "SELECT COUNT(*) FROM requests_cache" + params: list[Any] = [] + conditions = [] + if requested_by_id is not None: + conditions.append("requested_by_id = ?") + params.append(requested_by_id) + elif requested_by_norm: + conditions.append("requested_by_norm = ?") + params.append(requested_by_norm) + if since_iso: + conditions.append("created_at >= ?") + params.append(since_iso) + if conditions: + query += " WHERE " + " AND ".join(conditions) + with _connect() as conn: + row = conn.execute(query, tuple(params)).fetchone() + if not row: + return 0 + return int(row[0]) + + def get_request_cache_overview(limit: int = 50) -> list[Dict[str, Any]]: limit = max(1, min(limit, 200)) with _connect() as conn: diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py index 1f1beeb..ba972a3 100644 --- a/backend/app/routers/admin.py +++ b/backend/app/routers/admin.py @@ -1,13 +1,16 @@ from typing import Any, Dict, List, Optional +from datetime import datetime, timedelta, timezone import os from fastapi import APIRouter, HTTPException, Depends, UploadFile, File -from ..auth import require_admin +from ..auth import require_admin, get_current_user from ..config import settings as env_settings from ..db import ( delete_setting, get_all_users, + get_cached_requests, + get_cached_requests_count, get_request_cache_overview, get_request_cache_missing_titles, get_request_cache_stats, @@ -480,6 +483,40 @@ async def requests_cache(limit: int = 50) -> Dict[str, Any]: return {"rows": rows} +@router.get("/requests/all") +async def requests_all( + take: int = 50, + skip: int = 0, + days: Optional[int] = None, + user: Dict[str, str] = Depends(get_current_user), +) -> Dict[str, Any]: + if user.get("role") != "admin": + raise HTTPException(status_code=403, detail="Forbidden") + take = max(1, min(int(take or 50), 200)) + skip = max(0, int(skip or 0)) + since_iso = None + if days is not None and int(days) > 0: + since_iso = (datetime.now(timezone.utc) - timedelta(days=int(days))).isoformat() + rows = get_cached_requests(limit=take, offset=skip, since_iso=since_iso) + total = get_cached_requests_count(since_iso=since_iso) + results = [] + for row in rows: + status = row.get("status") + results.append( + { + "id": row.get("request_id"), + "title": row.get("title"), + "year": row.get("year"), + "type": row.get("media_type"), + "status": status, + "statusLabel": requests_router._status_label(status), + "requestedBy": row.get("requested_by"), + "createdAt": row.get("created_at"), + } + ) + return {"results": results, "total": total, "take": take, "skip": skip} + + @router.post("/branding/logo") async def upload_branding_logo(file: UploadFile = File(...)) -> Dict[str, Any]: return await save_branding_image(file) diff --git a/frontend/app/admin/requests-all/page.tsx b/frontend/app/admin/requests-all/page.tsx new file mode 100644 index 0000000..cd2e685 --- /dev/null +++ b/frontend/app/admin/requests-all/page.tsx @@ -0,0 +1,172 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { useRouter } from 'next/navigation' +import { authFetch, clearToken, getApiBase, getToken } from '../../lib/auth' +import AdminShell from '../../ui/AdminShell' + +type RequestRow = { + id: number + title?: string | null + year?: number | null + type?: string | null + statusLabel?: string | null + requestedBy?: string | null + createdAt?: string | null +} + +const formatDateTime = (value?: string | null) => { + if (!value) return 'Unknown' + const date = new Date(value) + if (Number.isNaN(date.valueOf())) return value + return date.toLocaleString() +} + +export default function AdminRequestsAllPage() { + const router = useRouter() + const [rows, setRows] = useState([]) + const [total, setTotal] = useState(0) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [pageSize, setPageSize] = useState(50) + const [page, setPage] = useState(1) + + const pageCount = useMemo(() => { + if (!total || pageSize <= 0) return 1 + return Math.max(1, Math.ceil(total / pageSize)) + }, [total, pageSize]) + + const load = async () => { + if (!getToken()) { + router.push('/login') + return + } + setLoading(true) + setError(null) + try { + const baseUrl = getApiBase() + const skip = (page - 1) * pageSize + const response = await authFetch( + `${baseUrl}/admin/requests/all?take=${pageSize}&skip=${skip}` + ) + if (!response.ok) { + if (response.status === 401) { + clearToken() + router.push('/login') + return + } + if (response.status === 403) { + router.push('/') + return + } + throw new Error(`Load failed: ${response.status}`) + } + const data = await response.json() + setRows(Array.isArray(data?.results) ? data.results : []) + setTotal(Number(data?.total ?? 0)) + } catch (err) { + console.error(err) + setError('Unable to load requests.') + } finally { + setLoading(false) + } + } + + useEffect(() => { + void load() + }, [page, pageSize]) + + useEffect(() => { + if (page > pageCount) { + setPage(pageCount) + } + }, [pageCount, page]) + + return ( + router.push('/admin')}> + Back to settings + + } + > +
+
+
+ {total.toLocaleString()} total +
+
+ +
+
+ {loading ? ( +
Loading requests…
+ ) : error ? ( +
{error}
+ ) : rows.length === 0 ? ( +
No requests found.
+ ) : ( +
+
+ Request + Status + Requested by + Created +
+ {rows.map((row) => ( + + ))} +
+ )} +
+ + + + Page {page} of {pageCount} + + + +
+
+
+ ) +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 60e01eb..384e710 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1027,6 +1027,85 @@ button span { gap: 12px; } +.admin-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + flex-wrap: wrap; +} + +.admin-toolbar-info { + color: var(--ink-muted); + font-size: 13px; +} + +.admin-toolbar-actions { + display: flex; + gap: 12px; + align-items: center; +} + +.admin-select { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--ink-muted); + font-size: 13px; +} + +.admin-table { + display: grid; + gap: 8px; +} + +.admin-table-head { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 12px; + font-size: 12px; + color: var(--ink-muted); + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 0 12px; +} + +.admin-table-row { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 12px; + align-items: center; + text-align: left; + background: rgba(255, 255, 255, 0.04); + border-radius: 16px; + padding: 12px; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.admin-table-row:hover { + transform: translateY(-1px); + box-shadow: 0 12px 24px rgba(15, 20, 45, 0.18); +} + +.admin-pagination { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + justify-content: flex-end; + color: var(--ink-muted); + font-size: 13px; +} + +.admin-pagination button { + background: rgba(255, 255, 255, 0.08); + color: var(--ink); +} + +.admin-pagination span { + padding: 0 6px; +} + .section-header { display: flex; justify-content: space-between; diff --git a/frontend/app/ui/AdminSidebar.tsx b/frontend/app/ui/AdminSidebar.tsx index a3bcd2f..69c2ab2 100644 --- a/frontend/app/ui/AdminSidebar.tsx +++ b/frontend/app/ui/AdminSidebar.tsx @@ -18,6 +18,7 @@ const NAV_GROUPS = [ title: 'Requests', items: [ { href: '/admin/requests', label: 'Request sync' }, + { href: '/admin/requests-all', label: 'All requests' }, { href: '/admin/cache', label: 'Cache Control' }, ], }, diff --git a/frontend/package.json b/frontend/package.json index 307ce27..fadced5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "magent-frontend", "private": true, - "version": "2901262044", + "version": "2901262102", "scripts": { "dev": "next dev", "build": "next build",