From 24f5e07f61397e8ae2209f3be004b41d673b5b57 Mon Sep 17 00:00:00 2001 From: Mark Saroufim Date: Wed, 18 Feb 2026 00:18:32 -0800 Subject: [PATCH 1/2] Make description section collapsible by default Wrap the description and benchmark shapes in a collapsed
/ so the leaderboard tabs are immediately visible without scrolling. --- .../src/pages/leaderboard/Leaderboard.tsx | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/frontend/src/pages/leaderboard/Leaderboard.tsx b/frontend/src/pages/leaderboard/Leaderboard.tsx index d81349d..52e3376 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.tsx @@ -191,22 +191,26 @@ export default function Leaderboard() { - Description - - {data.benchmarks && data.benchmarks.length > 0 && ( -
- - Benchmark Shapes - -
    - {data.benchmarks.map((b, i) => ( -
  • - {JSON.stringify(Object.fromEntries(Object.entries(b).filter(([k]) => k !== "seed")))} -
  • - ))} -
-
- )} +
+ + Description + + + {data.benchmarks && data.benchmarks.length > 0 && ( +
+ + Benchmark Shapes + +
    + {data.benchmarks.map((b, i) => ( +
  • + {JSON.stringify(Object.fromEntries(Object.entries(b).filter(([k]) => k !== "seed")))} +
  • + ))} +
+
+ )} +
From f23a2745bb7423f94bda6aae8355da9a73fba613 Mon Sep 17 00:00:00 2001 From: Mark Saroufim Date: Wed, 18 Feb 2026 08:34:27 -0800 Subject: [PATCH 2/2] Add admin submission view and delete from rankings page Admins can click a submission ID in the rankings list to open a dialog showing the submitted code, with a two-step delete confirmation. The delete proxies through kernelbot's existing DELETE /admin/submissions/{id} endpoint using the shared ADMIN_TOKEN (Bearer auth). --- frontend/src/api/api.ts | 11 ++ .../src/pages/leaderboard/Leaderboard.tsx | 3 + .../leaderboard/components/RankingLists.tsx | 125 +++++++++++++++++- kernelboard/api/submission.py | 67 ++++++++++ kernelboard/lib/env.py | 1 + 5 files changed, 204 insertions(+), 3 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 283aaa2..ccafc47 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -216,6 +216,17 @@ export async function submitFile(form: FormData) { return data; // e.g. { submission_id, message, ... } } +export async function deleteSubmission(submissionId: number): Promise { + const res = await fetch(`/api/submission/${submissionId}`, { + method: "DELETE", + }); + if (!res.ok) { + const json = await res.json(); + const message = json?.message || "Failed to delete submission"; + throw new APIError(message, res.status); + } +} + export async function fetchUserSubmissions( leaderboardId: number | string, userId: number | string, diff --git a/frontend/src/pages/leaderboard/Leaderboard.tsx b/frontend/src/pages/leaderboard/Leaderboard.tsx index 52e3376..eb8db8b 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.tsx @@ -240,6 +240,9 @@ export default function Leaderboard() { rankings={data.rankings} leaderboardId={id} deadline={data.deadline} + onRefresh={() => { + if (id) call(id); + }} /> diff --git a/frontend/src/pages/leaderboard/components/RankingLists.tsx b/frontend/src/pages/leaderboard/components/RankingLists.tsx index 47c0887..df226a7 100644 --- a/frontend/src/pages/leaderboard/components/RankingLists.tsx +++ b/frontend/src/pages/leaderboard/components/RankingLists.tsx @@ -2,7 +2,12 @@ import { useEffect, useMemo, useState } from "react"; import { Box, Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, Grid, + Link as MuiLink, Stack, type SxProps, type Theme, @@ -12,7 +17,8 @@ import RankingTitleBadge from "./RankingTitleBadge"; import { formatMicroseconds } from "../../../lib/utils/ranking.ts"; import { getMedalIcon } from "../../../components/common/medal.tsx"; -import { fetchCodes } from "../../../api/api.ts"; +import { deleteSubmission, fetchCodes } from "../../../api/api.ts"; +import CodeBlock from "../../../components/codeblock/CodeBlock"; import { CodeDialog } from "./CodeDialog.tsx"; import { isExpired } from "../../../lib/date/utils.ts"; import { useAuthStore } from "../../../lib/store/authStore.ts"; @@ -31,6 +37,7 @@ interface RankingsListProps { rankings: Record; leaderboardId?: string; deadline?: string; + onRefresh?: () => void; } const styles: Record> = { @@ -86,6 +93,7 @@ export default function RankingsList({ rankings, leaderboardId, deadline, + onRefresh, }: RankingsListProps) { const expired = !!deadline && isExpired(deadline); const me = useAuthStore((s) => s.me); @@ -95,6 +103,30 @@ export default function RankingsList({ Math.random().toString(36).slice(2, 8), ); const [codes, setCodes] = useState>(new Map()); + const [selectedSubmission, setSelectedSubmission] = useState<{ + id: number; + userName: string; + } | null>(null); + const [confirmDelete, setConfirmDelete] = useState(false); + const [deleting, setDeleting] = useState(false); + const [deleteError, setDeleteError] = useState(null); + + const handleDelete = async () => { + if (!selectedSubmission) return; + setDeleting(true); + setDeleteError(null); + try { + await deleteSubmission(selectedSubmission.id); + setSelectedSubmission(null); + setConfirmDelete(false); + if (onRefresh) onRefresh(); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : "Delete failed"; + setDeleteError(msg); + } finally { + setDeleting(false); + } + }; const submissionIds = useMemo(() => { if (!rankings) return []; @@ -225,9 +257,23 @@ export default function RankingsList({ )} {isAdmin && ( - + + setSelectedSubmission({ + id: item.submission_id, + userName: item.user_name, + }) + } + > ID: {item.submission_id} - + )} @@ -236,6 +282,79 @@ export default function RankingsList({ ); })} + + {/* Admin submission detail + delete dialog */} + {isAdmin && selectedSubmission && ( + { + setSelectedSubmission(null); + setConfirmDelete(false); + setDeleteError(null); + }} + maxWidth="md" + fullWidth + > + + Submission #{selectedSubmission.id} by {selectedSubmission.userName} + + + {codes.get(selectedSubmission.id) ? ( + + ) : ( + + No code available for this submission. + + )} + {deleteError && ( + + Error: {deleteError} + + )} + + + {!confirmDelete ? ( + <> + + + + ) : ( + <> + + + + )} + + + )} ); } diff --git a/kernelboard/api/submission.py b/kernelboard/api/submission.py index 3e2520b..3599ce0 100644 --- a/kernelboard/api/submission.py +++ b/kernelboard/api/submission.py @@ -187,6 +187,73 @@ def list_codes_route(): ) +@submission_bp.route("/submission/", methods=["DELETE"]) +@login_required +def delete_submission(submission_id): + """ + DELETE /api/submission/ + Admin-only: deletes a submission by proxying to the cluster-manager admin endpoint. + """ + logger.info("[delete_submission] request for submission_id=%s", submission_id) + + user_id, _ = get_id_and_username_from_session() + if not user_id: + return http_error( + message="user is not logged in", + status_code=http.HTTPStatus.UNAUTHORIZED, + ) + + whitelist = get_whitelist() + if user_id not in whitelist: + logger.warning( + "[delete_submission] non-admin user %s attempted delete on %s", + user_id, + submission_id, + ) + return http_error( + message="forbidden: admin access required", + status_code=http.HTTPStatus.FORBIDDEN, + ) + + admin_token = os.getenv("ADMIN_TOKEN", "") + if not admin_token: + logger.error("[delete_submission] ADMIN_TOKEN is not set") + return http_error( + message="admin API not configured", + status_code=http.HTTPStatus.INTERNAL_SERVER_ERROR, + ) + + base = get_cluster_manager_endpoint() + url = f"{base}/admin/submissions/{submission_id}" + headers = {"Authorization": f"Bearer {admin_token}"} + + try: + resp = requests.delete(url, headers=headers, timeout=30) + except requests.RequestException as e: + logger.error("[delete_submission] forward failed: %s", e) + return http_error( + message=f"forward failed: {e}", + status_code=http.HTTPStatus.BAD_GATEWAY, + ) + + try: + payload = resp.json() + message = payload.get("message") or payload.get("detail") or resp.reason + if resp.status_code == 200: + return http_success(message=message, data=payload) + else: + return http_error( + message=message, + status_code=http.HTTPStatus(resp.status_code), + ) + except Exception as e: + logger.error("[delete_submission] failed: %s", e) + return http_error( + message=str(e), + status_code=http.HTTPStatus.INTERNAL_SERVER_ERROR, + ) + + def check_admin_access_codes( user_id: str, leaderboard_id: int, submission_ids: List[int] ): diff --git a/kernelboard/lib/env.py b/kernelboard/lib/env.py index e65f943..26cd54b 100644 --- a/kernelboard/lib/env.py +++ b/kernelboard/lib/env.py @@ -23,6 +23,7 @@ def check_env_vars(): "DISCORD_CLIENT_ID": "preview-disabled", "DISCORD_CLIENT_SECRET": "preview-disabled", "DISCORD_CLUSTER_MANAGER_API_BASE_URL": "http://localhost:8080", + "ADMIN_TOKEN": "", } for var, default in optional_with_defaults.items():