diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 2892257..987a764 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -20,10 +20,10 @@ export default tseslint.config( rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ - 'warn', + 'error', { allowConstantExport: true }, ], - '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 43e38cb..508902c 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -1,3 +1,5 @@ +import type { User } from "../lib/types/user"; + export class APIError extends Error { status: number; constructor(message: string, status: number) { @@ -16,7 +18,75 @@ export async function fetchAboutInfo(): Promise { return r.data.message; } -export async function fetchLeaderBoard(id: string): Promise { +export interface LeaderboardDetail { + deadline: string; + description: string; + name: string; + reference: string; + gpu_types: string[]; + rankings: Record< + string, + Array<{ + file_name: string; + prev_score: number; + rank: number; + score: number; + user_name: string; + submission_id: number; + }> + >; +} + +export interface CodesResponse { + results: Array<{ + submission_id: number; + code: string; + }>; +} + +export interface NewsPost { + id: string; + title: string; + content: string; + [key: string]: unknown; +} + +export interface LeaderboardSummary { + id: number; + name: string; + deadline: string; + gpu_types: string[]; + priority_gpu_type: string; + top_users: Array<{ rank: number; score: number; user_name: string }> | null; +} + +export interface LeaderboardSummariesResponse { + leaderboards: LeaderboardSummary[]; + now: string; +} + +export interface UserSubmissionsResponse { + items: Array<{ + submission_id: number; + file_name?: string | null; + submitted_at: string; + status?: string | null; + submission_done: boolean; + runs?: Array<{ + start_time: string; + end_time: string | null; + mode: string; + passed: boolean; + score: number | null; + meta: Record | null; + report: Record | null; + }>; + }>; + total: number; + limit: number; +} + +export async function fetchLeaderBoard(id: string): Promise { const start = performance.now(); const res = await fetch(`/api/leaderboard/${id}`); const fetchTime = performance.now() - start; @@ -41,7 +111,7 @@ export async function fetchLeaderBoard(id: string): Promise { export async function fetchCodes( leaderboardId: number | string, submissionIds: (number | string)[], -): Promise { +): Promise { const res = await fetch("/api/codes", { method: "POST", headers: { @@ -62,7 +132,7 @@ export async function fetchCodes( return r.data; } -export async function fetchAllNews(): Promise { +export async function fetchAllNews(): Promise { const res = await fetch("/api/news"); if (!res.ok) { const json = await res.json(); @@ -73,7 +143,7 @@ export async function fetchAllNews(): Promise { return r.data; } -export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise { +export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise { const start = performance.now(); const url = useV1 ? "/api/leaderboard-summaries?v1_query" @@ -102,7 +172,7 @@ export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise return r.data; } -export async function getMe(): Promise { +export async function getMe(): Promise { const res = await fetch("/api/me"); if (!res.ok) { const json = await res.json(); @@ -113,15 +183,14 @@ export async function getMe(): Promise { return r.data; } -export async function logout(): Promise { +export async function logout(): Promise { const res = await fetch("/api/logout"); if (!res.ok) { const json = await res.json(); const message = json?.message || "Unknown error"; throw new APIError(`Failed to fetch news contents: ${message}`, res.status); } - const r = await res.json(); - return r.data; + await res.json(); } export async function submitFile(form: FormData) { @@ -131,7 +200,7 @@ export async function submitFile(form: FormData) { }); const text = await resp.text(); - let data: any; + let data: Record; try { data = JSON.parse(text); } catch { @@ -139,7 +208,7 @@ export async function submitFile(form: FormData) { } if (!resp.ok) { - const msg = data?.detail || data?.message || "Submission failed"; + const msg = (data?.detail as string) || (data?.message as string) || "Submission failed"; throw new Error(msg); } @@ -151,7 +220,7 @@ export async function fetchUserSubmissions( userId: number | string, page: number = 1, pageSize: number = 10, -): Promise { +): Promise { const offset = (page - 1) * pageSize; const res = await fetch( `/api/submissions?leaderboard_id=${leaderboardId}&offset=${offset}&limit=${pageSize}`, diff --git a/frontend/src/components/common/styles/shared_style.tsx b/frontend/src/components/common/styles/shared_style.tsx index 6c12f4a..7a0f266 100644 --- a/frontend/src/components/common/styles/shared_style.tsx +++ b/frontend/src/components/common/styles/shared_style.tsx @@ -1,18 +1,21 @@ import styled from "@emotion/styled"; import { Container } from "@mui/material"; +// eslint-disable-next-line react-refresh/only-export-components export const flexRowCenter = { display: "flex", alignItems: "center", gap: 0.5, }; +// eslint-disable-next-line react-refresh/only-export-components export const flexRowCenterMediumGap = { display: "flex", alignItems: "center", gap: 5, }; +// eslint-disable-next-line react-refresh/only-export-components export const mediumText = { fontSize: "1.25rem", }; diff --git a/frontend/src/lib/hooks/useApi.ts b/frontend/src/lib/hooks/useApi.ts index d2727e3..d34707f 100644 --- a/frontend/src/lib/hooks/useApi.ts +++ b/frontend/src/lib/hooks/useApi.ts @@ -1,7 +1,7 @@ import { useCallback, useState } from "react"; import { useNavigate } from "react-router-dom"; -type Fetcher = (...args: Args) => Promise; +type Fetcher = (...args: Args) => Promise; export const defaultRedirectMap: Record = { 401: "/401", @@ -49,7 +49,7 @@ export const defaultRedirectMap: Record = { * ``` * @returns An object with `data`, `loading`, `error`, `errorStatus`, and `call` */ -export function fetcherApiCallback( +export function fetcherApiCallback( fetcher: Fetcher, redirectMap: Record = defaultRedirectMap, ) { @@ -71,9 +71,10 @@ export function fetcherApiCallback( const result = await fetcher(...params); setData(result); return result; - } catch (e: any) { - const status = e.status ? e.status : 0; - const msg = e.message ? e.message : ""; + } catch (e: unknown) { + const err = e as { status?: number; message?: string }; + const status = err.status ? err.status : 0; + const msg = err.message ? err.message : ""; // set and logging the error if any setError(status); @@ -84,7 +85,7 @@ export function fetcherApiCallback( // navigate to config page if the status is found // otherwise passes - const redirectPath = redirectMap[e.status]; + const redirectPath = redirectMap[err.status ?? 0]; if (redirectPath) { navigate(redirectPath); } diff --git a/frontend/src/lib/store/authStore.ts b/frontend/src/lib/store/authStore.ts index afaa4d2..be5886b 100644 --- a/frontend/src/lib/store/authStore.ts +++ b/frontend/src/lib/store/authStore.ts @@ -9,7 +9,7 @@ type AuthState = { inFlight: boolean; setMe: (me: User | null) => void; fetchMe: () => Promise; - logoutAndRefresh: () => Promise; + logoutAndRefresh: () => Promise<{ ok: boolean; error?: unknown }>; }; export const useAuthStore = create((set, get) => ({ @@ -33,9 +33,9 @@ export const useAuthStore = create((set, get) => ({ inFlight: false, error: null, })); - } catch (e: any) { + } catch (e: unknown) { set((_s) => ({ - error: e?.message ?? "Failed to fetch user", + error: e instanceof Error ? e.message : "Failed to fetch user", loading: false, inFlight: false, me: null, @@ -48,7 +48,7 @@ export const useAuthStore = create((set, get) => ({ await logout(); await get().fetchMe(); return { ok: true }; - } catch (e: any) { + } catch (e: unknown) { return { ok: false, error: e }; } }, diff --git a/frontend/src/pages/home/components/LeaderboardTile.tsx b/frontend/src/pages/home/components/LeaderboardTile.tsx index a6a2325..f365f01 100644 --- a/frontend/src/pages/home/components/LeaderboardTile.tsx +++ b/frontend/src/pages/home/components/LeaderboardTile.tsx @@ -1,4 +1,4 @@ -import { Box, Card, CardContent, Chip, Typography } from "@mui/material"; +import { Box, Card, CardContent, Chip, type Theme, Typography } from "@mui/material"; import { Link } from "react-router-dom"; import { getMedalIcon } from "../../../components/common/medal.tsx"; import { getTimeLeft } from "../../../lib/date/utils.ts"; @@ -17,7 +17,7 @@ const styles = { "&:hover": { transform: "translateY(-2px)", boxShadow: "0 4px 12px rgba(0,0,0,0.15)", - borderColor: (theme: any) => theme.palette.custom.secondary, + borderColor: (theme: Theme) => theme.palette.custom.secondary, }, padding: 1, "& .MuiCardContent-root:last-child": { diff --git a/frontend/src/pages/leaderboard/Leaderboard.test.tsx b/frontend/src/pages/leaderboard/Leaderboard.test.tsx index a02d6f4..ff6e1f5 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.test.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.test.tsx @@ -18,7 +18,7 @@ let currentAuth: AuthState = { me: null }; vi.mock("../../lib/store/authStore", () => { return { // Simulate Zustand's selector pattern - useAuthStore: (selector: any) => + useAuthStore: (selector: (state: { me: AuthState["me"] }) => unknown) => selector({ me: currentAuth.me, }), diff --git a/frontend/src/pages/leaderboard/Leaderboard.tsx b/frontend/src/pages/leaderboard/Leaderboard.tsx index 1144b28..8c84933 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.tsx @@ -109,12 +109,12 @@ export default function Leaderboard() { next.set("tab", tab); setSearchParams(next, { replace: true }); } - }, [tab]); + }, [tab, searchParams, setSearchParams]); // Fetch leaderboard data useEffect(() => { if (id) call(id); - }, [id]); + }, [id, call]); // Fetch top user (strongest submission) when rankings are available // Select from the GPU with the most unique users diff --git a/frontend/src/pages/leaderboard/components/AiTrendChart.tsx b/frontend/src/pages/leaderboard/components/AiTrendChart.tsx index 8406da1..6bfd795 100644 --- a/frontend/src/pages/leaderboard/components/AiTrendChart.tsx +++ b/frontend/src/pages/leaderboard/components/AiTrendChart.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import ReactECharts from "echarts-for-react"; import { Box, @@ -45,7 +45,10 @@ export default function AiTrendChart({ leaderboardId, rankings }: AiTrendChartPr const isDark = resolvedMode === "dark"; const textColor = isDark ? "#e0e0e0" : "#333"; - const gpuTypes = data?.time_series ? Object.keys(data.time_series) : []; + const gpuTypes = useMemo( + () => (data?.time_series ? Object.keys(data.time_series) : []), + [data?.time_series], + ); useEffect(() => { if (gpuTypes.length > 0 && !selectedGpuType && data?.time_series) { @@ -115,8 +118,8 @@ export default function AiTrendChart({ leaderboardId, rankings }: AiTrendChartPr try { const result = await fetchAiTrend(leaderboardId); setData(result); - } catch (err: any) { - setError(err.message || "Failed to load data"); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to load data"); } finally { setLoading(false); } @@ -203,7 +206,7 @@ export default function AiTrendChart({ leaderboardId, rankings }: AiTrendChartPr } // Build series for ECharts - const series: any[] = []; + const series: Array> = []; Object.entries(selectedData).forEach(([model, dataPoints]) => { const color = hashStringToColor(model); @@ -249,7 +252,7 @@ export default function AiTrendChart({ leaderboardId, rankings }: AiTrendChartPr }, tooltip: { trigger: "item", - formatter: (params: any) => { + formatter: (params: { value: [number, number]; data: { gpu_type?: string }; seriesName: string }) => { const date = new Date(params.value[0]); const score = formatMicroseconds(params.value[1]); const gpuType = params.data.gpu_type || "Unknown"; diff --git a/frontend/src/pages/leaderboard/components/CodeDialog.tsx b/frontend/src/pages/leaderboard/components/CodeDialog.tsx index df04e71..17f2897 100644 --- a/frontend/src/pages/leaderboard/components/CodeDialog.tsx +++ b/frontend/src/pages/leaderboard/components/CodeDialog.tsx @@ -18,7 +18,7 @@ export function CodeDialog({ userName, problemName, }: { - code: any; + code: string | null | undefined; fileName?: string; isActive?: boolean; rank?: number; diff --git a/frontend/src/pages/leaderboard/components/LeaderboardSubmit.test.tsx b/frontend/src/pages/leaderboard/components/LeaderboardSubmit.test.tsx index 28a7dc0..7f8c223 100644 --- a/frontend/src/pages/leaderboard/components/LeaderboardSubmit.test.tsx +++ b/frontend/src/pages/leaderboard/components/LeaderboardSubmit.test.tsx @@ -17,7 +17,7 @@ vi.mock("../../../api/api", () => ({ // Make AlertBar deterministic and closable vi.mock("../../../components/alert/AlertBar", () => ({ __esModule: true, - default: ({ notice, onClose }: any) => + default: ({ notice, onClose }: { notice: { open: boolean; title?: string; message?: string }; onClose: () => void }) => notice?.open ? (
{notice.title}
@@ -79,7 +79,7 @@ function getHiddenFileInput(): HTMLInputElement { } async function formDataToObject(fd: FormData) { - const out: Record = {}; + const out: Record = {}; fd.forEach((v, k) => (out[k] = v)); return out; } diff --git a/frontend/src/pages/leaderboard/components/LeaderboardSubmit.tsx b/frontend/src/pages/leaderboard/components/LeaderboardSubmit.tsx index 26a222e..4c1b681 100644 --- a/frontend/src/pages/leaderboard/components/LeaderboardSubmit.tsx +++ b/frontend/src/pages/leaderboard/components/LeaderboardSubmit.tsx @@ -126,8 +126,8 @@ export default function LeaderboardSubmit({ setOpen(false); resetForm(); }, 100); - } catch (e: any) { - setStatus({ kind: "error", msg: e?.message || "Submission failed" }); + } catch (e: unknown) { + setStatus({ kind: "error", msg: e instanceof Error ? e.message : "Submission failed" }); } } diff --git a/frontend/src/pages/leaderboard/components/RankingLists.tsx b/frontend/src/pages/leaderboard/components/RankingLists.tsx index 9fc534c..0a286d6 100644 --- a/frontend/src/pages/leaderboard/components/RankingLists.tsx +++ b/frontend/src/pages/leaderboard/components/RankingLists.tsx @@ -94,7 +94,7 @@ export default function RankingsList({ if (!rankings) return []; const ids: number[] = []; Object.entries(rankings).forEach(([_key, value]) => { - const li = value as any[]; + const li = value as RankingItem[]; if (Array.isArray(li) && li.length > 0) { li.forEach((item) => { if (item?.submission_id) { diff --git a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx index e552feb..ac7cd44 100644 --- a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx +++ b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback } from "react"; +import React, { useEffect, useState, useCallback } from "react"; import ReactECharts from "echarts-for-react"; import { Box, @@ -86,8 +86,8 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT } setSelectedGpuType(defaultGpu); } - } catch (err: any) { - setError(err.message || "Failed to load data"); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to load data"); } finally { setLoading(false); } @@ -142,7 +142,7 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT }, [inputValue, leaderboardId]); const handleUserSelectionChange = ( - _event: any, + _event: React.SyntheticEvent, newValue: UserSearchResult[] ) => { setSelectedUsers(newValue); @@ -325,7 +325,7 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT ); } - const series: any[] = []; + const series: Array> = []; Object.entries(gpuData).forEach(([userId, dataPoints]) => { const sortedData = [...dataPoints].sort( @@ -375,7 +375,7 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT }, tooltip: { trigger: "item", - formatter: (params: any) => { + formatter: (params: { value: [number, number]; data: { gpu_type?: string; user_name?: string }; seriesName: string }) => { const date = new Date(params.value[0]); const score = formatMicroseconds(params.value[1]); const gpuType = params.data.gpu_type || "Unknown"; diff --git a/frontend/src/pages/leaderboard/components/submission-history/ReportCell.tsx b/frontend/src/pages/leaderboard/components/submission-history/ReportCell.tsx index 269aab9..026fb7e 100644 --- a/frontend/src/pages/leaderboard/components/submission-history/ReportCell.tsx +++ b/frontend/src/pages/leaderboard/components/submission-history/ReportCell.tsx @@ -8,7 +8,7 @@ import { TableCell, } from "@mui/material"; -export function ReportCell({ report }: { report: any }) { +export function ReportCell({ report }: { report: string | null | undefined }) { const [open, setOpen] = useState(false); return ( diff --git a/frontend/src/pages/leaderboard/components/submission-history/SubmissionHistorySection.tsx b/frontend/src/pages/leaderboard/components/submission-history/SubmissionHistorySection.tsx index cc07fad..7ac1cb3 100644 --- a/frontend/src/pages/leaderboard/components/submission-history/SubmissionHistorySection.tsx +++ b/frontend/src/pages/leaderboard/components/submission-history/SubmissionHistorySection.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Box, Typography, @@ -107,6 +107,12 @@ export default function SubmissionHistorySection({ const { data, loading, error, errorStatus, call } = fetcherApiCallback(fetchUserSubmissions); + const refresh = useCallback(() => { + if (!leaderboardId || !userId) return; + call(leaderboardId, userId, page, pageSize); + setLastRefresh(new Date()); + }, [leaderboardId, userId, page, pageSize, call]); + useEffect(() => { const id = setInterval(() => setNow(Date.now()), 30_000); return () => clearInterval(id); @@ -119,7 +125,7 @@ export default function SubmissionHistorySection({ useEffect(() => { refresh(); - }, [refreshFlag]); + }, [refreshFlag, refresh]); // fetch when inputs or page change useEffect(() => { @@ -130,7 +136,7 @@ export default function SubmissionHistorySection({ const totalPages = data?.limit && data?.total ? Math.ceil(data?.total / data?.limit) : 1; - const items: Submission[] = data?.items ?? []; + const items: Submission[] = useMemo(() => data?.items ?? [], [data?.items]); const total: number = data?.total ?? 0; const tooOld = lastRefresh && now - lastRefresh.getTime() > 10 * 60 * 1000; @@ -152,12 +158,6 @@ export default function SubmissionHistorySection({ setOpenMap((m) => ({ ...m, [id]: !m[id] })); }; - const refresh = () => { - if (!leaderboardId || !userId) return; - call(leaderboardId, userId, page, pageSize); - setLastRefresh(new Date()); - }; - const stabelItems = useMemo( () => [...items].sort( diff --git a/frontend/src/pages/leaderboard/components/submission-history/SubmissionRunsTable.tsx b/frontend/src/pages/leaderboard/components/submission-history/SubmissionRunsTable.tsx index 2d0a9c1..c117605 100644 --- a/frontend/src/pages/leaderboard/components/submission-history/SubmissionRunsTable.tsx +++ b/frontend/src/pages/leaderboard/components/submission-history/SubmissionRunsTable.tsx @@ -22,8 +22,8 @@ export type SubmissionRun = { mode: string; passed: boolean; score: number | null; - meta: any | null; // present, but null when passed = true - report: any | null; // present, but null when passed = true + meta: Record | null; + report: Record | null; }; // --- Child table for runs (rendered inside Collapse) --- @@ -44,16 +44,16 @@ export function SubmissionRunsTable({ runs }: { runs: SubmissionRun[] }) { {runs.map((r, idx) => { const debug_info = { - stderr: r.meta?.stderr, - success: r.meta?.success, - exit_code: r.meta?.exit_code, - exit_code_info: r.meta?.exit_code - ? getExitCodeMessage(r.meta?.exit_code) + stderr: (r.meta as Record)?.stderr, + success: (r.meta as Record)?.success, + exit_code: (r.meta as Record)?.exit_code, + exit_code_info: (r.meta as Record)?.exit_code + ? getExitCodeMessage((r.meta as Record)?.exit_code as number) : null, - duration: r.meta?.duration, + duration: (r.meta as Record)?.duration, }; - const report = r.report?.log; + const report = (r.report as Record)?.log as string | undefined; return ( diff --git a/frontend/src/pages/leaderboard/components/submission-history/SubmissionStatusChip.tsx b/frontend/src/pages/leaderboard/components/submission-history/SubmissionStatusChip.tsx index 17258bc..2da80fa 100644 --- a/frontend/src/pages/leaderboard/components/submission-history/SubmissionStatusChip.tsx +++ b/frontend/src/pages/leaderboard/components/submission-history/SubmissionStatusChip.tsx @@ -29,7 +29,7 @@ function SubmissionStatusChip({ status }: { status?: string | null }) { diff --git a/frontend/src/pages/login/login.test.tsx b/frontend/src/pages/login/login.test.tsx index ae52d29..c5a9b94 100644 --- a/frontend/src/pages/login/login.test.tsx +++ b/frontend/src/pages/login/login.test.tsx @@ -4,7 +4,7 @@ import Login from "./login"; import { describe, expect, it, vi } from "vitest"; vi.mock("../../components/alert/AlertBar", () => ({ - default: ({ notice }: { notice: any }) => ( + default: ({ notice }: { notice: { open: boolean; title?: string; message?: string; status?: number | null } }) => (
diff --git a/frontend/src/pages/news/News.tsx b/frontend/src/pages/news/News.tsx index 1c56290..2da20c7 100644 --- a/frontend/src/pages/news/News.tsx +++ b/frontend/src/pages/news/News.tsx @@ -1,3 +1,4 @@ +import type { NewsPost } from "../../api/api"; import { ErrorAlert } from "../../components/alert/ErrorAlert"; import { fetcherApiCallback } from "../../lib/hooks/useApi"; import { fetchAllNews } from "../../api/api"; @@ -14,14 +15,14 @@ export default function News() { useEffect(() => { call(); - }, []); + }, [call]); if (loading) return ; if (error) return ; // If slug is provided, find and show that specific post if (slug) { - const post = data?.find((item: any) => item.id === slug); + const post = data?.find((item: NewsPost) => item.id === slug); if (!post) { return ; } diff --git a/test_notification.py b/test_notification.py new file mode 100644 index 0000000..403a2d2 --- /dev/null +++ b/test_notification.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Temporarily fake a snapshot row to test Discord webhook notifications.""" +import psycopg2 +import os +import sys + +conn = psycopg2.connect(os.environ["DATABASE_URL"]) +cur = conn.cursor() + +if "--restore" in sys.argv: + # Just re-run the worker with --test to restore real data + print("Run: python3 ranking_worker.py --test") +else: + cur.execute(""" + UPDATE leaderboard.ranking_snapshot + SET user_id = 'fake_user_123', user_name = 'FakeUser' + WHERE (leaderboard_id, gpu_type, rank) = ( + SELECT leaderboard_id, gpu_type, rank + FROM leaderboard.ranking_snapshot + WHERE rank = 1 + LIMIT 1 + ) + """) + conn.commit() + print(f"Updated {cur.rowcount} row(s) to fake user") + print("Now run: python3 ranking_worker.py --test") + +conn.close()