feat: ship minecraft theme updates and platform workflow improvements

这个提交包含在:
Codex CLI
2026-02-15 17:36:56 +08:00
父节点 cd7540ab9d
当前提交 37266bb846
修改 32 个文件,包含 5297 行新增119 行删除

查看文件

@@ -59,6 +59,20 @@ type TriggerMissingResp = {
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();
@@ -81,17 +95,24 @@ export default function BackendLogsPage() {
const [queuedIds, setQueuedIds] = useState<number[]>([]);
const [triggerLoading, setTriggerLoading] = useState(false);
const [triggerMsg, setTriggerMsg] = useState("");
const [users, setUsers] = useState<AdminUser[]>([]);
const [deleteUserId, setDeleteUserId] = useState<number | null>(null);
const [userMsg, setUserMsg] = useState("");
const refresh = async () => {
if (!isAdmin || !token) return;
setLoading(true);
setError("");
try {
const data = await apiFetch<BackendLogsResp>(
`/api/v1/backend/logs?limit=${limit}&running_limit=20&queued_limit=100`,
{},
token
);
const [data, usersData] = await Promise.all([
apiFetch<BackendLogsResp>(
`/api/v1/backend/logs?limit=${limit}&running_limit=20&queued_limit=100`,
{},
token
),
apiFetch<AdminUsersResp>("/api/v1/admin/users?page=1&page_size=200", {}, token),
]);
setPendingJobs(data.pending_jobs ?? 0);
setMissingProblems(data.missing_problems ?? 0);
setItems(data.items ?? []);
@@ -99,6 +120,7 @@ export default function BackendLogsPage() {
setQueuedJobs(data.queued_jobs ?? []);
setRunningIds(data.running_problem_ids ?? []);
setQueuedIds(data.queued_problem_ids ?? []);
setUsers(usersData.items ?? []);
} catch (e: unknown) {
setError(String(e));
} finally {
@@ -185,6 +207,45 @@ export default function BackendLogsPage() {
}
};
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(() => {
@@ -250,12 +311,94 @@ export default function BackendLogsPage() {
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
{triggerMsg && <p className="mt-3 text-sm text-emerald-700">{triggerMsg}</p>}
{userMsg && <p className="mt-3 text-sm text-emerald-700">{userMsg}</p>}
<p className="mt-3 text-xs text-zinc-500">
{tx(
"系统已自动单线程异步处理待队列任务,无需手工点击;上方按钮仅用于立即手动补全。",
"System auto-processes queued jobs in single-thread async mode; the button above is only for manual trigger."
)}
</p>
<section className="mt-4 rounded-xl border bg-white p-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<h2 className="text-sm font-medium">{tx("用户管理(可删除)", "User Management (Delete Supported)")}</h2>
<p className="text-xs text-zinc-600">
{tx("总用户", "Total users")} {users.length}
</p>
</div>
<div className="mt-3 divide-y md:hidden">
{users.map((user) => (
<article key={user.id} className="space-y-1 py-2 text-xs">
<p className="font-medium">
#{user.id} · {user.username}
</p>
<p className="text-zinc-600">
Rating {user.rating} · {tx("创建", "Created")} {fmtTs(user.created_at)}
</p>
<button
className="rounded border px-2 py-1 text-xs text-red-700 hover:bg-red-50 disabled:opacity-60"
disabled={deleteUserId === user.id || user.username === "admin"}
onClick={() => void deleteUser(user)}
>
{user.username === "admin"
? tx("保留账号", "Reserved")
: deleteUserId === user.id
? tx("删除中...", "Deleting...")
: tx("删除用户", "Delete User")}
</button>
</article>
))}
{!loading && users.length === 0 && (
<p className="py-4 text-center text-sm text-zinc-500">{tx("暂无用户数据", "No users found")}</p>
)}
</div>
<div className="mt-2 hidden overflow-x-auto md:block">
<table className="min-w-full text-xs">
<thead className="bg-zinc-100 text-left">
<tr>
<th className="px-2 py-2">ID</th>
<th className="px-2 py-2">{tx("用户名", "Username")}</th>
<th className="px-2 py-2">Rating</th>
<th className="px-2 py-2">{tx("创建时间", "Created At")}</th>
<th className="px-2 py-2">{tx("操作", "Action")}</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id} className="border-t">
<td className="px-2 py-2">{user.id}</td>
<td className="px-2 py-2">{user.username}</td>
<td className="px-2 py-2">{user.rating}</td>
<td className="px-2 py-2 text-zinc-600">{fmtTs(user.created_at)}</td>
<td className="px-2 py-2">
<button
className="rounded border px-2 py-1 text-xs text-red-700 hover:bg-red-50 disabled:opacity-60"
disabled={deleteUserId === user.id || user.username === "admin"}
onClick={() => void deleteUser(user)}
>
{user.username === "admin"
? tx("保留账号", "Reserved")
: deleteUserId === user.id
? tx("删除中...", "Deleting...")
: tx("删除用户", "Delete User")}
</button>
</td>
</tr>
))}
{!loading && users.length === 0 && (
<tr>
<td className="px-2 py-4 text-center text-zinc-500" colSpan={5}>
{tx("暂无用户数据", "No users found")}
</td>
</tr>
)}
</tbody>
</table>
</div>
</section>
<section className="mt-4 grid gap-3 md:grid-cols-2">
<article className="rounded-xl border bg-white p-3">
<h2 className="text-sm font-medium">{tx("正在处理Running", "Running Jobs")}</h2>