"use client"; import Link from "next/link"; import { useEffect, useState } from "react"; import { apiFetch } from "@/lib/api"; import { readToken } from "@/lib/auth"; import { useI18nText } from "@/lib/i18n"; import { Activity, AlertCircle, List, Play, RefreshCw, Server, Trash2, Zap } from "lucide-react"; type BackendLogItem = { id: number; problem_id: number; problem_title: string; status: string; progress: number; message: string; created_by: number; max_solutions: number; created_at: number; started_at: number | null; finished_at: number | null; updated_at: number; runner_pending: boolean; }; type QueueJobItem = { id: number; problem_id: number; problem_title: string; status: string; progress: number; message: string; updated_at: number; started_at?: number | null; }; type BackendLogsResp = { items: BackendLogItem[]; running_jobs: QueueJobItem[]; queued_jobs: QueueJobItem[]; running_problem_ids: number[]; queued_problem_ids: number[]; running_count: number; queued_count_preview: number; pending_jobs: number; missing_problems: number; limit: number; running_limit: number; queued_limit: number; }; type TriggerMissingResp = { started: boolean; missing_total: number; candidate_count: number; queued_count: number; pending_jobs: number; limit: number; max_solutions: number; }; type AdminUser = { id: number; username: string; rating: number; created_at: number; }; type AdminUsersResp = { items: AdminUser[]; total_count: number; page: number; page_size: number; }; function fmtTs(v: number | null | undefined): string { if (!v) return "-"; return new Date(v * 1000).toLocaleString(); } export default function BackendLogsPage() { const { tx } = useI18nText(); const [token, setToken] = useState(""); const [checkingAdmin, setCheckingAdmin] = useState(true); const [isAdmin, setIsAdmin] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [limit, setLimit] = useState(100); const [pendingJobs, setPendingJobs] = useState(0); const [missingProblems, setMissingProblems] = useState(0); const [items, setItems] = useState([]); const [runningJobs, setRunningJobs] = useState([]); const [queuedJobs, setQueuedJobs] = useState([]); const [runningIds, setRunningIds] = useState([]); const [queuedIds, setQueuedIds] = useState([]); const [triggerLoading, setTriggerLoading] = useState(false); const [triggerMsg, setTriggerMsg] = useState(""); const [users, setUsers] = useState([]); const [deleteUserId, setDeleteUserId] = useState(null); const [userMsg, setUserMsg] = useState(""); const refresh = async () => { if (!isAdmin || !token) return; setLoading(true); setError(""); try { const [data, usersData] = await Promise.all([ apiFetch( `/api/v1/backend/logs?limit=${limit}&running_limit=20&queued_limit=100`, {}, token ), apiFetch("/api/v1/admin/users?page=1&page_size=200", {}, token), ]); setPendingJobs(data.pending_jobs ?? 0); setMissingProblems(data.missing_problems ?? 0); setItems(data.items ?? []); setRunningJobs(data.running_jobs ?? []); setQueuedJobs(data.queued_jobs ?? []); setRunningIds(data.running_problem_ids ?? []); setQueuedIds(data.queued_problem_ids ?? []); setUsers(usersData.items ?? []); } catch (e: unknown) { setError(String(e)); } finally { setLoading(false); } }; useEffect(() => { let canceled = false; const checkAdmin = async () => { setCheckingAdmin(true); const tk = readToken(); if (!canceled) setToken(tk); if (!tk) { if (!canceled) { setIsAdmin(false); setError(tx("请先登录管理员账号", "Please sign in with admin account first")); setCheckingAdmin(false); } return; } try { const me = await apiFetch<{ username?: string }>("/api/v1/me", {}, tk); const allowed = (me?.username ?? "") === "admin"; if (!canceled) { setIsAdmin(allowed); if (!allowed) { setError(tx("仅管理员可查看后台日志", "Backend logs are visible to admin only")); } else { setError(""); } } } catch (e: unknown) { if (!canceled) { setIsAdmin(false); setError(String(e)); } } finally { if (!canceled) setCheckingAdmin(false); } }; void checkAdmin(); return () => { canceled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (!isAdmin || !token) return; void refresh(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAdmin, token, limit]); const triggerMissingSolutions = async () => { if (!isAdmin || !token) { setError(tx("请先登录管理员账号", "Please sign in with admin account first")); return; } setTriggerLoading(true); setTriggerMsg(""); setError(""); try { const data = await apiFetch( "/api/v1/backend/solutions/generate-missing", { method: "POST", body: JSON.stringify({ limit: 50000, max_solutions: 3 }), }, token ); setPendingJobs(data.pending_jobs ?? 0); setTriggerMsg( tx( `已触发异步任务:候选 ${data.candidate_count} 题,入队 ${data.queued_count} 题(当前待处理 ${data.pending_jobs})。`, `Async trigger submitted: candidate ${data.candidate_count}, queued ${data.queued_count} (pending ${data.pending_jobs}).` ) ); await refresh(); } catch (e: unknown) { setError(String(e)); } finally { setTriggerLoading(false); } }; const deleteUser = async (user: AdminUser) => { if (!isAdmin || !token) return; if (user.username === "admin") { setError(tx("保留管理员账号不可删除", "Reserved admin account cannot be deleted")); return; } const ok = window.confirm( tx( `确认删除用户 ${user.username}(#${user.id})?该用户提交记录、错题本、草稿、积分记录会被级联删除。`, `Delete user ${user.username}(#${user.id})? Submissions, wrong-book, drafts, and points records will be removed by cascade.` ) ); if (!ok) return; setDeleteUserId(user.id); setError(""); setUserMsg(""); try { await apiFetch( `/api/v1/admin/users/${user.id}`, { method: "DELETE", }, token ); setUserMsg( tx( `已删除用户 ${user.username}(#${user.id})`, `Deleted user ${user.username}(#${user.id}).` ) ); await refresh(); } catch (e: unknown) { setError(String(e)); } finally { setDeleteUserId(null); } }; useEffect(() => { if (!isAdmin || !token) return; const timer = setInterval(() => { void refresh(); }, 5000); return () => clearInterval(timer); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAdmin, token, limit]); if (checkingAdmin) { return (
{tx("正在校验管理员权限...", "Checking admin access...")}
); } if (!isAdmin) { return (

{tx("后台日志(题解异步队列)", "Backend Logs (Async Solution Queue)")}

{error || tx("仅管理员可查看此页面", "This page is available for admin only")}

); } return (

{tx("后台日志(题解异步队列)", "Backend Logs (Async Solution Queue)")}

0 ? "text-emerald-700" : "text-zinc-600"}> {tx("待处理任务", "Pending jobs")} {pendingJobs} 0 ? "text-amber-700" : "text-zinc-600"}> {tx("缺失答案题目", "Problems missing answers")} {missingProblems}
{error &&

{error}

} {triggerMsg &&

{triggerMsg}

} {userMsg &&

{userMsg}

}

{tx( "系统已自动单线程异步处理待队列任务,无需手工点击;上方按钮仅用于立即手动补全。", "System auto-processes queued jobs in single-thread async mode; the button above is only for manual trigger." )}

{tx("用户管理(可删除)", "User Management (Delete Supported)")}

{tx("总用户", "Total users")} {users.length}

{users.map((user) => (

#{user.id} · {user.username}

Rating {user.rating} · {tx("创建", "Created")} {fmtTs(user.created_at)}

))} {!loading && users.length === 0 && (

{tx("暂无用户数据", "No users found")}

)}
{users.map((user) => ( ))} {!loading && users.length === 0 && ( )}
ID {tx("用户名", "Username")} Rating {tx("创建时间", "Created At")} {tx("操作", "Action")}
{user.id} {user.username} {user.rating} {fmtTs(user.created_at)}
{tx("暂无用户数据", "No users found")}

{tx("正在处理(Running)", "Running Jobs")}

{tx("当前题目 ID:", "Current problem IDs:")} {runningIds.length ? runningIds.join(", ") : tx("无", "None")}

    {runningJobs.map((job) => (
  • {tx("任务", "Job")} #{job.id} · {tx("题目", "Problem")} #{job.problem_id} {job.problem_title || tx("(未命名题目)", "(Untitled)")}

    {tx("状态", "Status")} {job.status} · {tx("进度", "Progress")} {job.progress}% · {tx("开始", "Start")} {fmtTs(job.started_at ?? null)}

    {job.message || "-"}

  • ))} {!runningJobs.length &&
  • {tx("当前无运行中的任务", "No running jobs")}
  • }

{tx("待处理队列(Queued)", "Queued Jobs")}

{tx("待处理题目 ID(预览):", "Queued problem IDs (preview):")} {queuedIds.length ? queuedIds.join(", ") : tx("无", "None")}

    {queuedJobs.map((job) => (
  • {tx("任务", "Job")} #{job.id} · {tx("题目", "Problem")} #{job.problem_id} {job.problem_title || tx("(未命名题目)", "(Untitled)")}

    {tx("状态", "Status")} {job.status} · {tx("进度", "Progress")} {job.progress}% · {tx("更新", "Updated")} {fmtTs(job.updated_at)}

    {job.message || "-"}

  • ))} {!queuedJobs.length &&
  • {tx("当前无待处理任务", "No queued jobs")}
  • }
{items.map((item) => (

{tx("任务", "Job")} #{item.id}

{item.status} · {item.progress}%
#{item.problem_id} {item.problem_title || tx("(未命名题目)", "(Untitled)")}

{item.message || "-"}

{tx("创建", "Created")} {fmtTs(item.created_at)} · {tx("开始", "Start")} {fmtTs(item.started_at)} · {tx("结束", "End")} {fmtTs(item.finished_at)}

))} {!loading && items.length === 0 && (

{tx("暂无后台任务日志", "No backend logs yet")}

)}
{items.map((item) => ( ))} {!loading && items.length === 0 && ( )}
{tx("任务ID", "Job ID")} {tx("题目", "Problem")} {tx("状态", "Status")} {tx("进度", "Progress")} {tx("消息", "Message")} {tx("时间", "Time")}
{item.id} #{item.problem_id} {item.problem_title || tx("(未命名题目)", "(Untitled)")} {item.status} {item.progress}%
{item.message || "-"}
{tx("创建", "Created")} {fmtTs(item.created_at)}
{tx("开始", "Start")} {fmtTs(item.started_at)}
{tx("结束", "End")} {fmtTs(item.finished_at)}
{tx("暂无后台任务日志", "No backend logs yet")}
); }