release: 2901262102
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
172
frontend/app/admin/requests-all/page.tsx
Normal file
172
frontend/app/admin/requests-all/page.tsx
Normal file
@@ -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<RequestRow[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<AdminShell
|
||||
title="All requests"
|
||||
subtitle="Paginated view of every cached request."
|
||||
actions={
|
||||
<button type="button" onClick={() => router.push('/admin')}>
|
||||
Back to settings
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<section className="admin-section">
|
||||
<div className="admin-toolbar">
|
||||
<div className="admin-toolbar-info">
|
||||
<span>{total.toLocaleString()} total</span>
|
||||
</div>
|
||||
<div className="admin-toolbar-actions">
|
||||
<label className="admin-select">
|
||||
<span>Per page</span>
|
||||
<select value={pageSize} onChange={(e) => setPageSize(Number(e.target.value))}>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={200}>200</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="status-banner">Loading requests…</div>
|
||||
) : error ? (
|
||||
<div className="error-banner">{error}</div>
|
||||
) : rows.length === 0 ? (
|
||||
<div className="status-banner">No requests found.</div>
|
||||
) : (
|
||||
<div className="admin-table">
|
||||
<div className="admin-table-head">
|
||||
<span>Request</span>
|
||||
<span>Status</span>
|
||||
<span>Requested by</span>
|
||||
<span>Created</span>
|
||||
</div>
|
||||
{rows.map((row) => (
|
||||
<button
|
||||
key={row.id}
|
||||
type="button"
|
||||
className="admin-table-row"
|
||||
onClick={() => router.push(`/requests/${row.id}`)}
|
||||
>
|
||||
<span>
|
||||
{row.title || `Request #${row.id}`}
|
||||
{row.year ? ` (${row.year})` : ''}
|
||||
</span>
|
||||
<span>{row.statusLabel || 'Unknown'}</span>
|
||||
<span>{row.requestedBy || 'Unknown'}</span>
|
||||
<span>{formatDateTime(row.createdAt)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="admin-pagination">
|
||||
<button type="button" onClick={() => setPage(1)} disabled={page <= 1}>
|
||||
First
|
||||
</button>
|
||||
<button type="button" onClick={() => setPage(page - 1)} disabled={page <= 1}>
|
||||
Previous
|
||||
</button>
|
||||
<span>
|
||||
Page {page} of {pageCount}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={page >= pageCount}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPage(pageCount)}
|
||||
disabled={page >= pageCount}
|
||||
>
|
||||
Last
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</AdminShell>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "magent-frontend",
|
||||
"private": true,
|
||||
"version": "2901262044",
|
||||
"version": "2901262102",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
||||
Reference in New Issue
Block a user