From 74bbd14425123d76867527afb632a0ec2dd3c90e Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 9 Feb 2026 15:31:56 -0800 Subject: [PATCH 01/26] hidemetadata --- kernelboard/api/leaderboard.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/kernelboard/api/leaderboard.py b/kernelboard/api/leaderboard.py index eb475b7f..a7399408 100644 --- a/kernelboard/api/leaderboard.py +++ b/kernelboard/api/leaderboard.py @@ -281,21 +281,22 @@ def get_ai_trend(leaderboard_id: int): def parse_model_from_filename(file_name: str) -> str: """ - Extract model name from file names ending with ka_submission.py: - - trimul_H100_claude-opus-4.5_ka_submission.py -> claude-opus-4.5 + Extract model name - the segment right before _ka_submission.py + Examples: + - matmul_py_H100_claude-opus-4.5_ka_submission.py -> claude-opus-4.5 - trimul_H100_gpt-52_ka_submission.py -> gpt-52 - - trimul_H100_gpt-5_ka_submission.py -> gpt-5 Returns None if file doesn't match pattern. """ - if not file_name or not file_name.endswith("_ka_submission.py"): + suffix = "_ka_submission.py" + if not file_name or not file_name.endswith(suffix): return None - # Extract model name: everything between last GPU type and _ka_submission - # Pattern: {anything}_{gpu}_{model}_ka_submission.py - pattern = r"^.+_[A-Za-z0-9]+_(.+?)_ka_submission\.py$" - match = re.match(pattern, file_name) - if match: - return match.group(1) + # Remove the suffix and get everything before it + base = file_name[:-len(suffix)] + # Split by underscore and get the last segment + parts = base.rsplit("_", 1) + if len(parts) == 2: + return parts[1] return None From cae3c8a52e3973665710a474104f704bead787a0 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Feb 2026 15:19:12 -0800 Subject: [PATCH 02/26] test1 --- frontend/src/api/api.ts | 21 +- frontend/src/pages/home/Home.tsx | 7 +- kernelboard/api/leaderboard_summaries.py | 458 +++++++++++++++++------ 3 files changed, 357 insertions(+), 129 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 283aaa2d..ab43118b 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -144,11 +144,22 @@ export async function fetchAllNews(): Promise { return r.data; } -export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise { +export async function fetchLeaderboardSummaries( + useV1: boolean = false, + forceRefreshCache: boolean = false, +): Promise { const start = performance.now(); - const url = useV1 - ? "/api/leaderboard-summaries?v1_query" + + // Build URL with query params + const params = new URLSearchParams(); + if (useV1) params.append("v1_query", ""); + if (forceRefreshCache) params.append("force_refresh_cache", ""); + + const queryString = params.toString(); + const url = queryString + ? `/api/leaderboard-summaries?${queryString}` : "/api/leaderboard-summaries"; + const res = await fetch(url); const fetchTime = performance.now() - start; @@ -165,9 +176,9 @@ export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise const parseTime = performance.now() - parseStart; const totalTime = performance.now() - start; - const version = useV1 ? "v1" : "v2"; + const version = useV1 ? "v1" : "original"; console.log( - `[Perf] fetchLeaderboardSummaries (${version}) | fetch=${fetchTime.toFixed(2)}ms | parse=${parseTime.toFixed(2)}ms | total=${totalTime.toFixed(2)}ms`, + `[Perf] fetchLeaderboardSummaries (${version}| ${forceRefreshCache})( | fetch=${fetchTime.toFixed(2)}ms | parse=${parseTime.toFixed(2)}ms | total=${totalTime.toFixed(2)}ms`, ); return r.data; diff --git a/frontend/src/pages/home/Home.tsx b/frontend/src/pages/home/Home.tsx index 4e1939ff..2f028f75 100644 --- a/frontend/src/pages/home/Home.tsx +++ b/frontend/src/pages/home/Home.tsx @@ -43,15 +43,16 @@ export default function Home() { const [searchParams] = useSearchParams(); const [isQuickStartOpen, setIsQuickStartOpen] = useState(false); const useV1 = searchParams.has("v1_query"); + const forceRefresh = searchParams.has("force_refresh"); const { data, loading, error, errorStatus, call } = fetcherApiCallback< LeaderboardSummaries, - [boolean] + [boolean, boolean] >(fetchLeaderboardSummaries); useEffect(() => { - call(useV1); - }, [call, useV1]); + call(useV1, forceRefresh); + }, [call, useV1, forceRefresh]); if (loading) { return ; diff --git a/kernelboard/api/leaderboard_summaries.py b/kernelboard/api/leaderboard_summaries.py index 65b3ad4b..87c9bb59 100644 --- a/kernelboard/api/leaderboard_summaries.py +++ b/kernelboard/api/leaderboard_summaries.py @@ -1,33 +1,220 @@ +import json import logging +import os import time from datetime import datetime, timezone from flask import Blueprint, request from kernelboard.lib.db import get_db_connection +from kernelboard.lib.redis_connection import create_redis_connection from kernelboard.lib.status_code import http_success logger = logging.getLogger(__name__) -leaderboard_summaries_bp = Blueprint( - "leaderboard_summaries_bp", __name__, url_prefix="/leaderboard-summaries" -) +leaderboard_summaries_bp = Blueprint("leaderboard_summaries_bp", __name__, url_prefix="/leaderboard-summaries") + +# Redis cache key prefix for ended leaderboard top_users +CACHE_KEY_PREFIX = "lb_top_users:" + + +# ============================================================================= +# Redis Cache Helpers +# ============================================================================= + + +def _get_redis(): + """Get Redis connection.""" + cert_reqs = os.getenv("REDIS_SSL_CERT_REQS") + return create_redis_connection(cert_reqs=cert_reqs) + + +def _get_cached_top_users(redis_conn, leaderboard_ids: list[int]) -> dict[int, list]: + """Get cached top_users for multiple leaderboards from Redis.""" + if not redis_conn or not leaderboard_ids: + return {} + + keys = [f"{CACHE_KEY_PREFIX}{lb_id}" for lb_id in leaderboard_ids] + try: + values = redis_conn.mget(keys) + result = {} + for lb_id, value in zip(leaderboard_ids, values): + if value: + result[lb_id] = json.loads(value) + return result + except Exception: + logger.warning("Redis cache read failed", exc_info=True) + return {} + + +def _set_cached_top_users(redis_conn, leaderboard_id: int, top_users: list): + """Cache top_users for ended leaderboard (no expiry).""" + if not redis_conn: + return + + try: + key = f"{CACHE_KEY_PREFIX}{leaderboard_id}" + redis_conn.set(key, json.dumps(top_users)) + except Exception: + logger.warning("Redis cache write failed", exc_info=True) + + +# ============================================================================= +# Main API Endpoint +# ============================================================================= @leaderboard_summaries_bp.route("", methods=["GET"]) def index(): + """ + Get leaderboard summaries. + + Query params: + - v1_query: Use legacy v1 query (no caching) + - force_refresh_cache: Clear and refresh cache for ended leaderboards + """ total_start = time.perf_counter() - # Check if legacy v1 query is requested (v2 is now default) use_v1 = request.args.get("v1_query") is not None + force_refresh = request.args.get("force_refresh_cache") is not None + + # Choose strategy based on query params + if use_v1: + return _get_leaderboards_cached(total_start, force_refresh) + else: + return _get_leaderboards_original(total_start) + + +# ============================================================================= +# Strategy 1: Cached (default) - Cache ended leaderboards in Redis +# ============================================================================= + + +def _get_leaderboards_cached(total_start: float, force_refresh: bool = False): + """ + Get leaderboard summaries with Redis caching for ended leaderboards. + + Args: + total_start: Start time for performance logging + force_refresh: If True, ignore cache and recompute all ended leaderboards + + Strategy: + - Ended leaderboards (deadline < NOW): Read from Redis cache + - Active leaderboards (deadline >= NOW): Compute in real-time + - Uncached ended leaderboards: Compute and store in cache + """ + # 1. Database & Redis connection + db_conn_start = time.perf_counter() + conn = get_db_connection() + redis_conn = _get_redis() + db_conn_time = (time.perf_counter() - db_conn_start) * 1000 + + query_start = time.perf_counter() + + # 2. Get all leaderboards and identify ended vs active + with conn.cursor() as cur: + cur.execute(""" + SELECT id, name, deadline, + deadline < NOW() AS is_ended + FROM leaderboard.leaderboard + ORDER BY id DESC + """) + all_leaderboards = cur.fetchall() + + ended_ids = [row[0] for row in all_leaderboards if row[3]] + active_ids = [row[0] for row in all_leaderboards if not row[3]] + + # 3. Try to get cached top_users for ended leaderboards + cache_start = time.perf_counter() + if force_refresh: + # Skip cache, will recompute all + cached_top_users = {} + else: + cached_top_users = _get_cached_top_users(redis_conn, ended_ids) + cache_time = (time.perf_counter() - cache_start) * 1000 + + # Find ended leaderboards not in cache + uncached_ended_ids = [lb_id for lb_id in ended_ids if lb_id not in cached_top_users] + + # 4. Compute top_users for: active + uncached ended leaderboards + ids_to_compute = active_ids + uncached_ended_ids + + compute_start = time.perf_counter() + if ids_to_compute: + query = _get_query_for_ids(ids_to_compute) + with conn.cursor() as cur: + cur.execute(query) + computed_results = {row[0]: row[1] for row in cur.fetchall()} + else: + computed_results = {} + compute_time = (time.perf_counter() - compute_start) * 1000 + + # 5. Cache newly computed ended leaderboards + for lb_id in uncached_ended_ids: + if lb_id in computed_results: + _set_cached_top_users(redis_conn, lb_id, computed_results[lb_id]) + + # 6. Build final response + with conn.cursor() as cur: + cur.execute(_get_leaderboard_metadata_query()) + metadata = {row[0]: row[1] for row in cur.fetchall()} + + leaderboards = [] + for row in all_leaderboards: + lb_id = row[0] + lb_data = metadata.get(lb_id, {}) + + # Get top_users from cache or computed results + lb_data["top_users"] = cached_top_users.get(lb_id, computed_results.get(lb_id)) + + if lb_data.get("gpu_types") is None: + lb_data["gpu_types"] = [] + + leaderboards.append(lb_data) + + query_time = (time.perf_counter() - query_start) * 1000 + total_time = (time.perf_counter() - total_start) * 1000 + + logger.info( + "[Perf] leaderboard_summaries (cached) | " + "db_conn=%.2fms | cache=%.2fms | compute=%.2fms | " + "total_query=%.2fms | total=%.2fms | " + "cached=%d | computed=%d", + db_conn_time, + cache_time, + compute_time, + query_time, + total_time, + len(cached_top_users), + len(computed_results), + ) + + return http_success( + { + "leaderboards": leaderboards, + "now": datetime.now(timezone.utc), + } + ) + + +# ============================================================================= +# Strategy 2: Original - No caching, compute all in one query +# ============================================================================= + + +def _get_leaderboards_original(total_start: float): + """ + Get leaderboard summaries without caching (original implementation). + Use ?v1_query to enable this mode. + """ # 1. Database connection db_conn_start = time.perf_counter() conn = get_db_connection() db_conn_time = (time.perf_counter() - db_conn_start) * 1000 - # 2. Query execution (v2 is default, v1 for legacy) - query = _get_query_v1() if use_v1 else _get_query() + # 2. Query execution + query = _get_query() query_start = time.perf_counter() with conn.cursor() as cur: cur.execute(query) @@ -39,16 +226,12 @@ def index(): for lb in leaderboards: if lb["gpu_types"] is None: lb["gpu_types"] = [] - transform_time = (time.perf_counter() - transform_start) * 1000 + transform_time: float = (time.perf_counter() - transform_start) * 1000 total_time = (time.perf_counter() - total_start) * 1000 - # Log timing breakdown - version = "v1" if use_v1 else "v2" logger.info( - "[Perf] leaderboard_summaries (%s) | " - "db_conn=%.2fms | query=%.2fms | transform=%.2fms | total=%.2fms", - version, + "[Perf] leaderboard_summaries (original) | " "db_conn=%.2fms | query=%.2fms | transform=%.2fms | total=%.2fms", db_conn_time, query_time, transform_time, @@ -56,20 +239,155 @@ def index(): ) return http_success( - {"leaderboards": leaderboards, "now": datetime.now(timezone.utc)} + { + "leaderboards": leaderboards, + "now": datetime.now(timezone.utc), + } ) +# ============================================================================= +# SQL Query Builders +# ============================================================================= + + +def _get_leaderboard_metadata_query(): + """Get leaderboard metadata (id, name, deadline, gpu_types).""" + return """ + WITH + gpu_types_agg AS ( + SELECT + leaderboard_id, + jsonb_agg(DISTINCT gpu_type) AS gpu_types + FROM leaderboard.gpu_type + GROUP BY leaderboard_id + ), + priority_gpu AS ( + SELECT DISTINCT ON (leaderboard_id) + leaderboard_id, + gpu_type + FROM leaderboard.gpu_type + ORDER BY leaderboard_id, + CASE gpu_type + WHEN 'B200' THEN 1 + WHEN 'H100' THEN 2 + WHEN 'MI300' THEN 3 + WHEN 'A100' THEN 4 + WHEN 'L4' THEN 5 + WHEN 'T4' THEN 6 + ELSE 7 + END, + gpu_type + ) + SELECT + l.id, + jsonb_build_object( + 'id', l.id, + 'name', l.name, + 'deadline', l.deadline, + 'gpu_types', COALESCE(g.gpu_types, '[]'::jsonb), + 'priority_gpu_type', p.gpu_type + ) + FROM leaderboard.leaderboard l + LEFT JOIN gpu_types_agg g ON g.leaderboard_id = l.id + LEFT JOIN priority_gpu p ON p.leaderboard_id = l.id + ORDER BY l.id DESC; + """ + + +def _get_query_for_ids(leaderboard_ids: list[int]): + """ + Get top_users for specific leaderboard IDs only. + Returns (leaderboard_id, top_users_json) pairs. + """ + ids_str = ",".join(str(id) for id in leaderboard_ids) + return f""" + WITH + priority_gpu AS ( + SELECT DISTINCT ON (leaderboard_id) + leaderboard_id, + gpu_type + FROM leaderboard.gpu_type + WHERE leaderboard_id IN ({ids_str}) + ORDER BY leaderboard_id, + CASE gpu_type + WHEN 'B200' THEN 1 + WHEN 'H100' THEN 2 + WHEN 'MI300' THEN 3 + WHEN 'A100' THEN 4 + WHEN 'L4' THEN 5 + WHEN 'T4' THEN 6 + ELSE 7 + END, + gpu_type + ), + personal_best_candidates AS ( + SELECT + r.runner, + s.leaderboard_id, + s.user_id, + u.user_name, + r.score, + RANK() OVER ( + PARTITION BY s.leaderboard_id, r.runner, s.user_id + ORDER BY r.score ASC + ) AS personal_submission_rank + FROM leaderboard.runs r + JOIN leaderboard.submission s ON r.submission_id = s.id + JOIN priority_gpu p ON p.leaderboard_id = s.leaderboard_id + AND p.gpu_type = r.runner + LEFT JOIN leaderboard.user_info u ON s.user_id = u.id + WHERE NOT r.secret + AND r.score IS NOT NULL + AND r.passed + AND s.leaderboard_id IN ({ids_str}) + ), + personal_best_runs AS ( + SELECT * FROM personal_best_candidates + WHERE personal_submission_rank = 1 + ), + ranked_users AS ( + SELECT + leaderboard_id, + runner, + user_name, + score, + RANK() OVER ( + PARTITION BY leaderboard_id, runner + ORDER BY score ASC + ) AS user_rank + FROM personal_best_runs + ), + top_users_agg AS ( + SELECT + leaderboard_id, + jsonb_agg( + jsonb_build_object( + 'rank', user_rank, + 'score', score, + 'user_name', user_name + ) + ORDER BY user_rank + ) AS top_users + FROM ranked_users + WHERE user_rank <= 3 + GROUP BY leaderboard_id + ) + SELECT leaderboard_id, top_users + FROM top_users_agg; + """ + + def _get_query(): """ - Optimized query for leaderboard summaries (default). + Optimized query for leaderboard summaries (v2). Performance optimizations: 1. Use DISTINCT ON instead of ROW_NUMBER for priority GPU selection 2. Pre-aggregate GPU types to avoid correlated subqueries 3. Pre-aggregate top users JSON to avoid correlated subqueries """ - query = """ + return """ WITH -- Pre-aggregate GPU types per leaderboard (avoids correlated subquery) gpu_types_agg AS ( @@ -99,7 +417,7 @@ def _get_query(): gpu_type ), - -- Step 1: Get each user's best run per leaderboard+runner (same as v1) + -- Step 1: Get each user's best run per leaderboard+runner personal_best_candidates AS ( SELECT r.runner, @@ -127,7 +445,7 @@ def _get_query(): WHERE personal_submission_rank = 1 ), - -- Step 3: Rank users by score (same as v1) + -- Step 3: Rank users by score ranked_users AS ( SELECT leaderboard_id, @@ -141,7 +459,7 @@ def _get_query(): FROM personal_best_runs ), - -- Pre-aggregate top 3 users JSON (optimization over v1) + -- Pre-aggregate top 3 users JSON top_users_agg AS ( SELECT leaderboard_id, @@ -172,106 +490,4 @@ def _get_query(): LEFT JOIN priority_gpu p ON p.leaderboard_id = l.id LEFT JOIN top_users_agg t ON t.leaderboard_id = l.id ORDER BY l.id DESC; - """ - return query - - -def _get_query_v1(): - """Legacy query (use ?v1 to enable).""" - query = """ - WITH - - -- Get basic information about active leaderboards. - active_leaderboards AS ( - SELECT id, name, deadline FROM leaderboard.leaderboard - ), - - -- Get all the GPU types for each leaderboard. - gpu_types AS ( - SELECT DISTINCT leaderboard_id, gpu_type FROM leaderboard.gpu_type - WHERE leaderboard_id IN (SELECT id FROM active_leaderboards) - ), - - -- Get the "highest priority" GPU type for each leaderboard. - priority_gpu_types AS ( - SELECT leaderboard_id, gpu_type FROM ( - SELECT - leaderboard_id, - gpu_type, - -- Assign priority based on the how "capable" GPT-4o thought - -- various GPU types were. - ROW_NUMBER() OVER ( - PARTITION BY leaderboard_id - ORDER BY - CASE gpu_type - WHEN 'B200' THEN 1 - WHEN 'H100' THEN 2 - WHEN 'MI300' THEN 3 - WHEN 'A100' THEN 4 - WHEN 'L4' THEN 5 - WHEN 'T4' THEN 6 - ELSE 7 -- Lowest priority for any other type. - END ASC, - gpu_type ASC - ) as rn - FROM leaderboard.gpu_type - WHERE leaderboard_id IN (SELECT id FROM active_leaderboards) - ) ranked_gpu_types - WHERE rn = 1 - ), - - -- Get each user's best run for each GPU type (runner) on the active - -- leaderboards. - personal_best_candidates AS ( - SELECT r.runner AS runner, - s.leaderboard_id AS leaderboard_id, - u.user_name AS user_name, - r.score AS score, - RANK() OVER (PARTITION BY s.leaderboard_id, r.runner, u.id - ORDER BY r.score ASC) AS personal_submission_rank - FROM leaderboard.runs r - JOIN leaderboard.submission s ON r.submission_id = s.id - JOIN active_leaderboards a ON s.leaderboard_id = a.id - JOIN priority_gpu_types p on p.leaderboard_id = a.id - AND p.gpu_type = r.runner - LEFT JOIN leaderboard.user_info u ON s.user_id = u.id - WHERE NOT r.secret AND r.score IS NOT NULL AND r.passed - ), - - -- Select only the best run for each user and GPU type. - personal_best_runs AS ( - SELECT * FROM personal_best_candidates WHERE personal_submission_rank = 1 - ), - - -- Order the personal best runs by score for each leaderboard and GPU type. - competitive_rankings AS ( - SELECT leaderboard_id, runner, user_name, score, - RANK() OVER (PARTITION BY leaderboard_id, runner ORDER BY score ASC) AS user_rank - FROM personal_best_runs) - - -- Build the JSON response. - SELECT jsonb_build_object( - 'id', l.id, - 'name', l.name, - 'deadline', l.deadline, - 'gpu_types', (SELECT jsonb_agg(gpu_type) FROM gpu_types g WHERE g.leaderboard_id = l.id), - 'priority_gpu_type', (SELECT g.gpu_type FROM priority_gpu_types g WHERE g.leaderboard_id = l.id), - 'top_users', - - -- For the priority GPU type, get the top 3 users by rank. - (SELECT jsonb_agg( - jsonb_build_object( - 'rank', r.user_rank, - 'score', r.score, - 'user_name', r.user_name - ) - ORDER BY r.user_rank ASC - ) - FROM competitive_rankings r - WHERE r.leaderboard_id = l.id AND r.user_rank <= 3 - ) - ) - FROM active_leaderboards l - ORDER BY l.id DESC; - """ - return query + """ From 8d10af27e2cef1942fafe6721605612c50001b3e Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Feb 2026 16:00:16 -0800 Subject: [PATCH 03/26] test1 --- kernelboard/api/leaderboard_summaries.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kernelboard/api/leaderboard_summaries.py b/kernelboard/api/leaderboard_summaries.py index 87c9bb59..d800a88d 100644 --- a/kernelboard/api/leaderboard_summaries.py +++ b/kernelboard/api/leaderboard_summaries.py @@ -80,6 +80,7 @@ def index(): # Choose strategy based on query params if use_v1: + return _get_leaderboards_cached(total_start, force_refresh) else: return _get_leaderboards_original(total_start) @@ -127,9 +128,11 @@ def _get_leaderboards_cached(total_start: float, force_refresh: bool = False): # 3. Try to get cached top_users for ended leaderboards cache_start = time.perf_counter() if force_refresh: + logger.info("[Perf] leaderboard_summaries (cached) force fresh redis cache") # Skip cache, will recompute all cached_top_users = {} else: + logger.info("[Perf] leaderboard_summaries (cached) read from cache") cached_top_users = _get_cached_top_users(redis_conn, ended_ids) cache_time = (time.perf_counter() - cache_start) * 1000 From f8c74c75133440f2e95cd46cca3cfa42ca7c3f2c Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Feb 2026 16:16:06 -0800 Subject: [PATCH 04/26] test1 --- kernelboard/api/leaderboard_summaries.py | 73 +++++++++++++----------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/kernelboard/api/leaderboard_summaries.py b/kernelboard/api/leaderboard_summaries.py index d800a88d..d714c304 100644 --- a/kernelboard/api/leaderboard_summaries.py +++ b/kernelboard/api/leaderboard_summaries.py @@ -112,8 +112,8 @@ def _get_leaderboards_cached(total_start: float, force_refresh: bool = False): query_start = time.perf_counter() - # 2. Get all leaderboards and identify ended vs active with conn.cursor() as cur: + # 2. Get all leaderboards and identify ended vs active cur.execute(""" SELECT id, name, deadline, deadline < NOW() AS is_ended @@ -122,57 +122,62 @@ def _get_leaderboards_cached(total_start: float, force_refresh: bool = False): """) all_leaderboards = cur.fetchall() - ended_ids = [row[0] for row in all_leaderboards if row[3]] - active_ids = [row[0] for row in all_leaderboards if not row[3]] - - # 3. Try to get cached top_users for ended leaderboards - cache_start = time.perf_counter() - if force_refresh: - logger.info("[Perf] leaderboard_summaries (cached) force fresh redis cache") - # Skip cache, will recompute all - cached_top_users = {} - else: - logger.info("[Perf] leaderboard_summaries (cached) read from cache") - cached_top_users = _get_cached_top_users(redis_conn, ended_ids) - cache_time = (time.perf_counter() - cache_start) * 1000 - - # Find ended leaderboards not in cache - uncached_ended_ids = [lb_id for lb_id in ended_ids if lb_id not in cached_top_users] + ended_ids = [row[0] for row in all_leaderboards if row[3]] + active_ids = [row[0] for row in all_leaderboards if not row[3]] + + # 3. Try to get cached top_users for ended leaderboards + cache_start = time.perf_counter() + if force_refresh: + cached_top_users = {} + else: + cached_top_users = _get_cached_top_users(redis_conn, ended_ids) + cache_time = (time.perf_counter() - cache_start) * 1000 + + # Find ended leaderboards not in cache + uncached_ended_ids = [ + lb_id for lb_id in ended_ids if lb_id not in cached_top_users + ] + logger.info( + "[Cache] cached=%d | uncached=%d | active=%d", + len(cached_top_users), + len(uncached_ended_ids), + len(active_ids), + ) - # 4. Compute top_users for: active + uncached ended leaderboards - ids_to_compute = active_ids + uncached_ended_ids + # 4. Compute top_users for: active + uncached ended leaderboards + ids_to_compute = active_ids + uncached_ended_ids - compute_start = time.perf_counter() - if ids_to_compute: - query = _get_query_for_ids(ids_to_compute) - with conn.cursor() as cur: + compute_start = time.perf_counter() + if ids_to_compute: + query = _get_query_for_ids(ids_to_compute) cur.execute(query) computed_results = {row[0]: row[1] for row in cur.fetchall()} - else: - computed_results = {} - compute_time = (time.perf_counter() - compute_start) * 1000 + else: + computed_results = {} + compute_time = (time.perf_counter() - compute_start) * 1000 - # 5. Cache newly computed ended leaderboards - for lb_id in uncached_ended_ids: - if lb_id in computed_results: - _set_cached_top_users(redis_conn, lb_id, computed_results[lb_id]) + # 5. Cache newly computed ended leaderboards + for lb_id in uncached_ended_ids: + if lb_id in computed_results: + _set_cached_top_users(redis_conn, lb_id, computed_results[lb_id]) - # 6. Build final response - with conn.cursor() as cur: + # 6. Get metadata for all leaderboards cur.execute(_get_leaderboard_metadata_query()) metadata = {row[0]: row[1] for row in cur.fetchall()} + # 7. Build final response leaderboards = [] for row in all_leaderboards: lb_id = row[0] lb_data = metadata.get(lb_id, {}) # Get top_users from cache or computed results - lb_data["top_users"] = cached_top_users.get(lb_id, computed_results.get(lb_id)) + lb_data["top_users"] = cached_top_users.get( + lb_id, computed_results.get(lb_id) + ) if lb_data.get("gpu_types") is None: lb_data["gpu_types"] = [] - leaderboards.append(lb_data) query_time = (time.perf_counter() - query_start) * 1000 From 45b28e8df3cafe18078c75b94b1b7a3691f897f5 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Feb 2026 16:49:14 -0800 Subject: [PATCH 05/26] test1 --- frontend/src/api/api.ts | 6 +-- frontend/src/pages/home/Home.tsx | 6 +-- kernelboard/__init__.py | 4 +- kernelboard/api/leaderboard_summaries.py | 55 ++++++++++++++++-------- kernelboard/health.py | 4 +- kernelboard/lib/redis_connection.py | 23 ++++++++-- tests/test_health.py | 4 +- tests/test_redis.py | 4 +- 8 files changed, 71 insertions(+), 35 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index ab43118b..e8238b56 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -145,14 +145,14 @@ export async function fetchAllNews(): Promise { } export async function fetchLeaderboardSummaries( - useV1: boolean = false, + useBeta: boolean = false, forceRefreshCache: boolean = false, ): Promise { const start = performance.now(); // Build URL with query params const params = new URLSearchParams(); - if (useV1) params.append("v1_query", ""); + if (useBeta) params.append("use_beta", ""); if (forceRefreshCache) params.append("force_refresh_cache", ""); const queryString = params.toString(); @@ -176,7 +176,7 @@ export async function fetchLeaderboardSummaries( const parseTime = performance.now() - parseStart; const totalTime = performance.now() - start; - const version = useV1 ? "v1" : "original"; + const version = useBeta ? "beta" : "original"; console.log( `[Perf] fetchLeaderboardSummaries (${version}| ${forceRefreshCache})( | fetch=${fetchTime.toFixed(2)}ms | parse=${parseTime.toFixed(2)}ms | total=${totalTime.toFixed(2)}ms`, ); diff --git a/frontend/src/pages/home/Home.tsx b/frontend/src/pages/home/Home.tsx index 2f028f75..e0654d4b 100644 --- a/frontend/src/pages/home/Home.tsx +++ b/frontend/src/pages/home/Home.tsx @@ -42,7 +42,7 @@ interface LeaderboardSummaries { export default function Home() { const [searchParams] = useSearchParams(); const [isQuickStartOpen, setIsQuickStartOpen] = useState(false); - const useV1 = searchParams.has("v1_query"); + const useBeta = searchParams.has("use_beta"); const forceRefresh = searchParams.has("force_refresh"); const { data, loading, error, errorStatus, call } = fetcherApiCallback< @@ -51,8 +51,8 @@ export default function Home() { >(fetchLeaderboardSummaries); useEffect(() => { - call(useV1, forceRefresh); - }, [call, useV1, forceRefresh]); + call(useBeta, forceRefresh); + }, [call, useBeta, forceRefresh]); if (loading) { return ; diff --git a/kernelboard/__init__.py b/kernelboard/__init__.py index 14faf79b..77fa72c8 100644 --- a/kernelboard/__init__.py +++ b/kernelboard/__init__.py @@ -18,7 +18,7 @@ from kernelboard.lib import db, env, score, time from kernelboard.lib.logging import configure_logging from kernelboard.lib.rate_limiter import limiter -from kernelboard.lib.redis_connection import create_redis_connection +from kernelboard.lib.redis_connection import get_redis_connection from kernelboard.lib.status_code import http_error from kernelboard.og_tags import get_og_tags_for_path, inject_og_tags, is_social_crawler @@ -55,7 +55,7 @@ def create_app(test_config=None): SESSION_TYPE="redis", # REDIS_SSL_CERT_REQS can be set to override SSL cert verification # for Redis connections (e.g., "none" for self-signed certificates). - SESSION_REDIS=create_redis_connection( + SESSION_REDIS=get_redis_connection( cert_reqs=os.getenv("REDIS_SSL_CERT_REQS") ), OAUTH2_PROVIDERS=providers(), diff --git a/kernelboard/api/leaderboard_summaries.py b/kernelboard/api/leaderboard_summaries.py index d714c304..ff9f14c7 100644 --- a/kernelboard/api/leaderboard_summaries.py +++ b/kernelboard/api/leaderboard_summaries.py @@ -1,3 +1,5 @@ +from kernelboard.lib.auth_utils import get_id_and_username_from_session +from kernelboard.lib.auth_utils import get_whitelist import json import logging import os @@ -7,7 +9,7 @@ from flask import Blueprint, request from kernelboard.lib.db import get_db_connection -from kernelboard.lib.redis_connection import create_redis_connection +from kernelboard.lib.redis_connection import get_redis_connection from kernelboard.lib.status_code import http_success logger = logging.getLogger(__name__) @@ -24,9 +26,9 @@ def _get_redis(): - """Get Redis connection.""" + """Get Redis connection (singleton).""" cert_reqs = os.getenv("REDIS_SSL_CERT_REQS") - return create_redis_connection(cert_reqs=cert_reqs) + return get_redis_connection(cert_reqs=cert_reqs) def _get_cached_top_users(redis_conn, leaderboard_ids: list[int]) -> dict[int, list]: @@ -59,6 +61,17 @@ def _set_cached_top_users(redis_conn, leaderboard_id: int, top_users: list): logger.warning("Redis cache write failed", exc_info=True) +def _delete_cached_top_users(redis_conn, leaderboard_ids: list[int]): + """Delete cached top_users for leaderboards (e.g., when deadline extended).""" + if not redis_conn or not leaderboard_ids: + return + try: + keys = [f"{CACHE_KEY_PREFIX}{lb_id}" for lb_id in leaderboard_ids] + redis_conn.delete(*keys) + except Exception: + logger.warning("Redis cache delete failed", exc_info=True) + + # ============================================================================= # Main API Endpoint # ============================================================================= @@ -70,17 +83,22 @@ def index(): Get leaderboard summaries. Query params: - - v1_query: Use legacy v1 query (no caching) + - use_beta: Use hide beta query - force_refresh_cache: Clear and refresh cache for ended leaderboards """ total_start = time.perf_counter() - use_v1 = request.args.get("v1_query") is not None + use_beta = request.args.get("use_beta") is not None force_refresh = request.args.get("force_refresh_cache") is not None - # Choose strategy based on query params - if use_v1: + # Check if user is admin to force refresh cache + user_id, _ = get_id_and_username_from_session() + whitelist = get_whitelist() + if not user_id or user_id not in whitelist: + force_refresh = False + # Choose strategy based on query params + if use_beta: return _get_leaderboards_cached(total_start, force_refresh) else: return _get_leaderboards_original(total_start) @@ -125,7 +143,11 @@ def _get_leaderboards_cached(total_start: float, force_refresh: bool = False): ended_ids = [row[0] for row in all_leaderboards if row[3]] active_ids = [row[0] for row in all_leaderboards if not row[3]] - # 3. Try to get cached top_users for ended leaderboards + # 3. Delete stale cache for active leaderboards (ex. deadline extended) + if active_ids: + _delete_cached_top_users(redis_conn, active_ids) + + # 4. Try to get cached top_users for ended leaderboards cache_start = time.perf_counter() if force_refresh: cached_top_users = {} @@ -149,8 +171,8 @@ def _get_leaderboards_cached(total_start: float, force_refresh: bool = False): compute_start = time.perf_counter() if ids_to_compute: - query = _get_query_for_ids(ids_to_compute) - cur.execute(query) + ids_tuple = tuple(ids_to_compute) + cur.execute(_get_query_for_ids(), (ids_tuple, ids_tuple)) computed_results = {row[0]: row[1] for row in cur.fetchall()} else: computed_results = {} @@ -213,8 +235,6 @@ def _get_leaderboards_cached(total_start: float, force_refresh: bool = False): def _get_leaderboards_original(total_start: float): """ Get leaderboard summaries without caching (original implementation). - - Use ?v1_query to enable this mode. """ # 1. Database connection db_conn_start = time.perf_counter() @@ -303,20 +323,21 @@ def _get_leaderboard_metadata_query(): """ -def _get_query_for_ids(leaderboard_ids: list[int]): +def _get_query_for_ids(): """ Get top_users for specific leaderboard IDs only. Returns (leaderboard_id, top_users_json) pairs. + + Usage: cur.execute(_get_query_for_ids(), (tuple(leaderboard_ids),) * 2) """ - ids_str = ",".join(str(id) for id in leaderboard_ids) - return f""" + return """ WITH priority_gpu AS ( SELECT DISTINCT ON (leaderboard_id) leaderboard_id, gpu_type FROM leaderboard.gpu_type - WHERE leaderboard_id IN ({ids_str}) + WHERE leaderboard_id IN %s ORDER BY leaderboard_id, CASE gpu_type WHEN 'B200' THEN 1 @@ -348,7 +369,7 @@ def _get_query_for_ids(leaderboard_ids: list[int]): WHERE NOT r.secret AND r.score IS NOT NULL AND r.passed - AND s.leaderboard_id IN ({ids_str}) + AND s.leaderboard_id IN %s ), personal_best_runs AS ( SELECT * FROM personal_best_candidates diff --git a/kernelboard/health.py b/kernelboard/health.py index 0a460f2c..6110d937 100644 --- a/kernelboard/health.py +++ b/kernelboard/health.py @@ -5,7 +5,7 @@ from flask import current_app as app from kernelboard.lib.db import get_db_connection -from kernelboard.lib.redis_connection import create_redis_connection +from kernelboard.lib.redis_connection import get_redis_connection from kernelboard.lib.status_code import ( http_error, http_success, @@ -27,7 +27,7 @@ def health(): all_checks_passed = False cert_reqs = os.getenv("REDIS_SSL_CERT_REQS") - redis_conn = create_redis_connection(cert_reqs=cert_reqs) + redis_conn = get_redis_connection(cert_reqs=cert_reqs) if redis_conn is None: app.logger.error("redis_conn is None. Is REDIS_URL set?") all_checks_passed = False diff --git a/kernelboard/lib/redis_connection.py b/kernelboard/lib/redis_connection.py index 68463450..381ba0a3 100644 --- a/kernelboard/lib/redis_connection.py +++ b/kernelboard/lib/redis_connection.py @@ -2,19 +2,34 @@ import redis +# Singleton Redis connection +_redis_client: redis.Redis | None = None +_redis_initialized: bool = False -def create_redis_connection( + +def get_redis_connection( cert_reqs: str | None = None, ) -> redis.Redis | None: """ - Creates a redis connection using application configuration. + Get a singleton Redis connection. + Reuses the same connection across requests for better performance. """ - url = os.getenv("REDIS_URL") + global _redis_client, _redis_initialized + + # Return cached connection if already initialized + if _redis_initialized: + return _redis_client + + url: str | None = os.getenv("REDIS_URL") if url is None: + _redis_initialized = True + _redis_client = None return None kwargs = {} if cert_reqs: kwargs["ssl_cert_reqs"] = cert_reqs - return redis.from_url(url, **kwargs) + _redis_client = redis.from_url(url, **kwargs) + _redis_initialized = True + return _redis_client diff --git a/tests/test_health.py b/tests/test_health.py index 8d3b6670..be0ea43a 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -36,7 +36,7 @@ def test_health_database_error(client): def test_health_no_redis_config(client): with patch( - "kernelboard.health.create_redis_connection", return_value=None + "kernelboard.health.get_redis_connection", return_value=None ): assert_unhealthy(client.get("/health")) @@ -48,7 +48,7 @@ def test_health_redis_error(client): ) with patch( - "kernelboard.health.create_redis_connection", return_value=mock_conn + "kernelboard.health.get_redis_connection", return_value=mock_conn ): assert_unhealthy(client.get("/health")) mock_conn.ping.assert_called_once() diff --git a/tests/test_redis.py b/tests/test_redis.py index 3ac17351..597f4dc2 100644 --- a/tests/test_redis.py +++ b/tests/test_redis.py @@ -1,7 +1,7 @@ -from kernelboard.lib.redis_connection import create_redis_connection +from kernelboard.lib.redis_connection import get_redis_connection def test_get_and_close_redis_connection(app): with app.app_context(): - conn = create_redis_connection() + conn = get_redis_connection() assert conn is not None From cb1e088db588f1d6955f736d9c1903f07bbaca9c Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Feb 2026 16:54:28 -0800 Subject: [PATCH 06/26] test1 --- kernelboard/api/leaderboard_summaries.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/kernelboard/api/leaderboard_summaries.py b/kernelboard/api/leaderboard_summaries.py index ff9f14c7..2b66e901 100644 --- a/kernelboard/api/leaderboard_summaries.py +++ b/kernelboard/api/leaderboard_summaries.py @@ -1,5 +1,3 @@ -from kernelboard.lib.auth_utils import get_id_and_username_from_session -from kernelboard.lib.auth_utils import get_whitelist import json import logging import os @@ -8,6 +6,7 @@ from flask import Blueprint, request +from kernelboard.lib.auth_utils import get_id_and_username_from_session, get_whitelist from kernelboard.lib.db import get_db_connection from kernelboard.lib.redis_connection import get_redis_connection from kernelboard.lib.status_code import http_success @@ -156,9 +155,7 @@ def _get_leaderboards_cached(total_start: float, force_refresh: bool = False): cache_time = (time.perf_counter() - cache_start) * 1000 # Find ended leaderboards not in cache - uncached_ended_ids = [ - lb_id for lb_id in ended_ids if lb_id not in cached_top_users - ] + uncached_ended_ids = [lb_id for lb_id in ended_ids if lb_id not in cached_top_users] logger.info( "[Cache] cached=%d | uncached=%d | active=%d", len(cached_top_users), @@ -194,9 +191,7 @@ def _get_leaderboards_cached(total_start: float, force_refresh: bool = False): lb_data = metadata.get(lb_id, {}) # Get top_users from cache or computed results - lb_data["top_users"] = cached_top_users.get( - lb_id, computed_results.get(lb_id) - ) + lb_data["top_users"] = cached_top_users.get(lb_id, computed_results.get(lb_id)) if lb_data.get("gpu_types") is None: lb_data["gpu_types"] = [] From 166c94a8a3cd080d11306fb238d96db0971f1d4a Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Feb 2026 16:58:57 -0800 Subject: [PATCH 07/26] test1 --- kernelboard/api/leaderboard_summaries.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kernelboard/api/leaderboard_summaries.py b/kernelboard/api/leaderboard_summaries.py index 2b66e901..57c261e9 100644 --- a/kernelboard/api/leaderboard_summaries.py +++ b/kernelboard/api/leaderboard_summaries.py @@ -94,6 +94,7 @@ def index(): user_id, _ = get_id_and_username_from_session() whitelist = get_whitelist() if not user_id or user_id not in whitelist: + logger.info("[leaderboard_summaries] skip force_refresh since user is not admin") force_refresh = False # Choose strategy based on query params @@ -149,6 +150,7 @@ def _get_leaderboards_cached(total_start: float, force_refresh: bool = False): # 4. Try to get cached top_users for ended leaderboards cache_start = time.perf_counter() if force_refresh: + logger.info("[Cache] force_refresh=True, ignoring cache") cached_top_users = {} else: cached_top_users = _get_cached_top_users(redis_conn, ended_ids) @@ -254,7 +256,7 @@ def _get_leaderboards_original(total_start: float): total_time = (time.perf_counter() - total_start) * 1000 logger.info( - "[Perf] leaderboard_summaries (original) | " "db_conn=%.2fms | query=%.2fms | transform=%.2fms | total=%.2fms", + "[Perf] leaderboard_summaries (original) | db_conn=%.2fms | query=%.2fms | transform=%.2fms | total=%.2fms", db_conn_time, query_time, transform_time, From f646583d1b38b5a7800023a65874f07256ccbbb3 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Feb 2026 17:19:57 -0800 Subject: [PATCH 08/26] test1 --- kernelboard/api/leaderboard_summaries.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/kernelboard/api/leaderboard_summaries.py b/kernelboard/api/leaderboard_summaries.py index 57c261e9..1428a66f 100644 --- a/kernelboard/api/leaderboard_summaries.py +++ b/kernelboard/api/leaderboard_summaries.py @@ -158,16 +158,17 @@ def _get_leaderboards_cached(total_start: float, force_refresh: bool = False): # Find ended leaderboards not in cache uncached_ended_ids = [lb_id for lb_id in ended_ids if lb_id not in cached_top_users] + # 4. Compute top_users for: active + uncached ended leaderboards + ids_to_compute = active_ids + uncached_ended_ids + logger.info( - "[Cache] cached=%d | uncached=%d | active=%d", + "[Cache] cached=%d | uncached=%d | active=%d | ids_to_compute=%d", len(cached_top_users), len(uncached_ended_ids), len(active_ids), + len(ids_to_compute) ) - # 4. Compute top_users for: active + uncached ended leaderboards - ids_to_compute = active_ids + uncached_ended_ids - compute_start = time.perf_counter() if ids_to_compute: ids_tuple = tuple(ids_to_compute) From 2f9d45e47f8d900801ce69b8f702ceb3930cbdf4 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Feb 2026 17:31:06 -0800 Subject: [PATCH 09/26] test1 --- kernelboard/lib/redis_connection.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/kernelboard/lib/redis_connection.py b/kernelboard/lib/redis_connection.py index 381ba0a3..cc3f5c47 100644 --- a/kernelboard/lib/redis_connection.py +++ b/kernelboard/lib/redis_connection.py @@ -4,7 +4,6 @@ # Singleton Redis connection _redis_client: redis.Redis | None = None -_redis_initialized: bool = False def get_redis_connection( @@ -14,15 +13,13 @@ def get_redis_connection( Get a singleton Redis connection. Reuses the same connection across requests for better performance. """ - global _redis_client, _redis_initialized + global _redis_client - # Return cached connection if already initialized - if _redis_initialized: + if _redis_client is not None: return _redis_client url: str | None = os.getenv("REDIS_URL") if url is None: - _redis_initialized = True _redis_client = None return None @@ -31,5 +28,4 @@ def get_redis_connection( kwargs["ssl_cert_reqs"] = cert_reqs _redis_client = redis.from_url(url, **kwargs) - _redis_initialized = True return _redis_client From 60fca92749dcc5ba95db0d81f7504b6d8e5a406b Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 26 Feb 2026 17:34:41 -0800 Subject: [PATCH 10/26] test1 --- kernelboard/lib/redis_connection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kernelboard/lib/redis_connection.py b/kernelboard/lib/redis_connection.py index cc3f5c47..2443a92a 100644 --- a/kernelboard/lib/redis_connection.py +++ b/kernelboard/lib/redis_connection.py @@ -20,7 +20,6 @@ def get_redis_connection( url: str | None = os.getenv("REDIS_URL") if url is None: - _redis_client = None return None kwargs = {} From 6dbd8a4f7b6052f6ac46dcae0a1e22b8b6af18a3 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 3 Mar 2026 08:47:08 -0800 Subject: [PATCH 11/26] test3 --- .llms/skills/caching.md | 72 ++++++++++++++++++++++++ kernelboard/api/leaderboard_summaries.py | 5 +- 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 .llms/skills/caching.md diff --git a/.llms/skills/caching.md b/.llms/skills/caching.md new file mode 100644 index 00000000..f6f9c4c5 --- /dev/null +++ b/.llms/skills/caching.md @@ -0,0 +1,72 @@ +# Caching Best Practices + +## Redis Caching Footguns + +### 1. Stale Cache on Status Change +**Problem:** Cached data becomes stale when entity status changes. + +```python +# Example: Leaderboard deadline extended (ended → active) +# Old cached data still exists, will be used when it ends again +``` + +**Solution:** Delete cache when status changes: +```python +# Delete active leaderboards' cache (deadline may have been extended) +for lb_id in active_ids: + redis.delete(f"lb_top_users:{lb_id}") +``` + +### 2. Singleton Not Actually Single +**Problem:** Singleton returns early only when not None, but None is a valid "initialized" state. + +```python +# ❌ Wrong - checks env var every time when REDIS_URL not set +_client = None +def get_connection(): + if _client is not None: + return _client + url = os.getenv("REDIS_URL") + if url is None: + return None # _client still None, will re-check next time +``` + +**Solution:** Either accept this behavior (getenv is cheap) or use sentinel: +```python +_client = ... # Ellipsis as sentinel +def get_connection(): + if _client is not ...: + return _client +``` + +### 3. SQL Injection in Dynamic Queries +**Problem:** Building SQL with string formatting. + +```python +# ❌ Wrong +ids_str = ",".join(str(id) for id in ids) +query = f"WHERE id IN ({ids_str})" + +# ✅ Correct - use parameterized queries +cur.execute("WHERE id IN %s", (tuple(ids),)) +``` + +### 4. Cache Key Collisions +**Problem:** Generic cache keys without proper namespacing. + +```python +# ❌ Wrong +redis.set(f"user:{user_id}", data) + +# ✅ Correct - include context +redis.set(f"lb_top_users:{leaderboard_id}", data) +``` + +## Quick Reference + +| Issue | Solution | +|-------|----------| +| Stale cache | Delete cache on status change | +| Singleton check | Use sentinel or accept getenv cost | +| SQL injection | Parameterized queries | +| Key collision | Namespace cache keys | diff --git a/kernelboard/api/leaderboard_summaries.py b/kernelboard/api/leaderboard_summaries.py index 1428a66f..51227a0d 100644 --- a/kernelboard/api/leaderboard_summaries.py +++ b/kernelboard/api/leaderboard_summaries.py @@ -99,9 +99,10 @@ def index(): # Choose strategy based on query params if use_beta: - return _get_leaderboards_cached(total_start, force_refresh) - else: + # if use_beta is True, use the original query (will deprecate this one cached query is stable) return _get_leaderboards_original(total_start) + else: + return _get_leaderboards_cached(total_start, force_refresh) # ============================================================================= From dc539eb7ba4b787663e1f22c44f66e4a53b5293a Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 3 Mar 2026 09:06:50 -0800 Subject: [PATCH 12/26] test3 --- .llms/skills/caching.md | 49 +++++------------------------------------ 1 file changed, 6 insertions(+), 43 deletions(-) diff --git a/.llms/skills/caching.md b/.llms/skills/caching.md index f451e0be..ef49094d 100644 --- a/.llms/skills/caching.md +++ b/.llms/skills/caching.md @@ -1,46 +1,9 @@ # Caching Best Practices -## Redis Caching Footguns +## Redis Caching -### 1. Stale Cache on Status Change -**Problem:** Cached data becomes stale when entity status changes (e.g., leaderboard deadline extended). - -**Solution:** Admin uses `?force_refresh_cache` to manually refresh: -``` -GET /api/leaderboard-summaries?force_refresh_cache -``` - -### 2. Singleton Not Actually Single -**Problem:** Singleton returns early only when not None, but None is a valid "initialized" state. - -```python -# ⚠️ Checks env var every time when REDIS_URL not set (acceptable, getenv is cheap) -_client = None -def get_connection(): - if _client is not None: - return _client - url = os.getenv("REDIS_URL") - if url is None: - return None # _client still None, will re-check next time -``` - -**Alternative:** Use sentinel (`...`) if you want strict single-check. - -### 3. SQL Injection in Dynamic Queries -```python -# ❌ Wrong - string formatting -ids_str = ",".join(str(id) for id in ids) -query = f"SELECT * FROM table WHERE id IN ({ids_str})" -cur.execute(query) - -# ✅ Correct - parameterized queries -cur.execute("SELECT * FROM table WHERE id IN %s", (tuple(ids),)) -``` - -## Quick Reference - -| Issue | Solution | -|-------|----------| -| Stale cache | Admin: `?force_refresh_cache` | -| Singleton check | Accept getenv cost or use sentinel | -| SQL injection | Parameterized queries | +### Leaderboard Summaries Cache +- **Ended leaderboards**: Cached in Redis (`lb_top_users:{id}`) +- **Active leaderboards**: Computed in real-time +- **Deadline extended**: Cache auto-deleted when leaderboard becomes active again +- **User record deleted**: ⚠️ Cache stale, admin needs run`https://www.gpumode.com/home?use_beta&force_refresh` in website From 7f16b292ee447c8cd55a29d9b029eb1d5023ada4 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 3 Mar 2026 09:08:52 -0800 Subject: [PATCH 13/26] test3 --- frontend/src/pages/home/Home.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/frontend/src/pages/home/Home.tsx b/frontend/src/pages/home/Home.tsx index f5e1997b..fb624748 100644 --- a/frontend/src/pages/home/Home.tsx +++ b/frontend/src/pages/home/Home.tsx @@ -56,14 +56,6 @@ export default function Home() { call(useBeta, forceRefresh); }, [call, useBeta, forceRefresh]); - if (loading) { - return ; - } - - if (error) { - return ; - } - const leaderboards = data?.leaderboards || []; return ( From 78b963896365041c9c587d0ad1ca329f07303ffc Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 4 Mar 2026 13:24:01 -0800 Subject: [PATCH 14/26] test3 --- .gitignore | 1 + docker-compose.dev.yml | 37 + frontend/package-lock.json | 216 +++++ frontend/package.json | 3 + frontend/src/App.tsx | 2 + frontend/src/api/api.ts | 98 ++- .../src/pages/leaderboard/Leaderboard.tsx | 65 +- .../pages/leaderboard/LeaderboardEditor.tsx | 810 ++++++++++++++++++ .../components/LeaderboardSubmit.tsx | 50 +- 9 files changed, 1244 insertions(+), 38 deletions(-) create mode 100644 docker-compose.dev.yml create mode 100644 frontend/src/pages/leaderboard/LeaderboardEditor.tsx diff --git a/.gitignore b/.gitignore index adb75b74..2d18bcf8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ htmlcov/ node_modules/ .env +.env.test .vscode *.DS_Store kernelboard/static/app diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..f5e09e88 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,37 @@ +version: '3.8' + +services: + postgres: + image: postgres:16 + container_name: kernelboard-postgres + environment: + POSTGRES_USER: elainewy + POSTGRES_PASSWORD: dev + POSTGRES_DB: kernelboard + ports: + - "5433:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./tests/data.sql:/docker-entrypoint-initdb.d/data.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U elainewy -d kernelboard"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: kernelboard-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + redis_data: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4d3fa6ca..7b902864 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,11 +8,14 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@codemirror/lang-python": "^6.2.1", + "@codemirror/theme-one-dark": "^6.1.3", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.2.0", "@mui/material": "^7.2.0", "@types/react-syntax-highlighter": "^15.5.13", + "@uiw/react-codemirror": "^4.25.7", "dayjs": "^1.11.13", "echarts": "^6.0.0", "echarts-for-react": "^3.0.6", @@ -361,6 +364,103 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz", + "integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", + "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", + "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", + "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.39.16", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.16.tgz", + "integrity": "sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -1255,6 +1355,42 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", + "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==" + }, "node_modules/@mui/core-downloads-tracker": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.2.0.tgz", @@ -2343,6 +2479,57 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.7", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.7.tgz", + "integrity": "sha512-tPV/AGjF4yM22D5mnyH7EuYBkWO05wF5Y4x3lmQJo6LuHmhjh0RQsVDjqeIgNOkXT3UO9OdkL4dzxw465/JZVg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.7", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.7.tgz", + "integrity": "sha512-s/EbEe0dFANWEgfLbfdIrrOGv0R7M1XhkKG3ShroBeH6uP9pVNQy81YHOLRCSVcytTp9zAWRNfXR/+XxZTvV7w==", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.7", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -2785,6 +2972,20 @@ "node": ">=6" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2868,6 +3069,11 @@ "node": ">= 6" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6154,6 +6360,11 @@ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "dev": true }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==" + }, "node_modules/style-to-js": { "version": "1.1.17", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", @@ -6821,6 +7032,11 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 161f637d..30df2f41 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,11 +12,14 @@ "test": "vitest" }, "dependencies": { + "@codemirror/lang-python": "^6.2.1", + "@codemirror/theme-one-dark": "^6.1.3", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.2.0", "@mui/material": "^7.2.0", "@types/react-syntax-highlighter": "^15.5.13", + "@uiw/react-codemirror": "^4.25.7", "dayjs": "^1.11.13", "echarts": "^6.0.0", "echarts-for-react": "^3.0.6", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 446875c2..c7942bbb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import AppLayout from "./components/app-layout/AppLayout"; import { CssBaseline, ThemeProvider } from "@mui/material"; import { createAppTheme } from "./components/common/styles/theme"; import Leaderboard from "./pages/leaderboard/Leaderboard"; +import LeaderboardEditor from "./pages/leaderboard/LeaderboardEditor"; import Home from "./pages/home/Home"; import News from "./pages/news/News"; import WorkingGroups from "./pages/working-groups/WorkingGroups"; @@ -68,6 +69,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index e8238b56..db7d687e 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -229,7 +229,7 @@ export async function submitFile(form: FormData) { export async function fetchUserSubmissions( leaderboardId: number | string, - userId: number | string, + _userId: number | string, page: number = 1, pageSize: number = 10, ): Promise { @@ -387,3 +387,99 @@ export async function searchUsers( const r = await res.json(); return r.data; } + +export interface SubmitCodeResponse { + submission_id: number; + message?: string; +} + +export async function submitCode( + leaderboardId: string, + leaderboardName: string, + gpuType: string, + mode: string, + code: string, + fileName: string = "submission.py" +): Promise { + const blob = new Blob([code], { type: "text/plain" }); + const file = new File([blob], fileName, { type: "text/x-python" }); + + const form = new FormData(); + form.set("leaderboard_id", leaderboardId); + form.set("leaderboard", leaderboardName); + form.set("gpu_type", gpuType); + form.set("submission_mode", mode); + form.set("file", file, fileName); + + let resp: Response; + try { + resp = await fetch("/api/submission", { + method: "POST", + body: form, + }); + } catch (err) { + throw new Error("Network error: Unable to connect to server"); + } + + const text = await resp.text(); + if (!text) { + throw new Error("Server returned empty response. The submission service may be unavailable."); + } + + let data: Record; + try { + data = JSON.parse(text); + } catch { + throw new Error(`Server error: ${text.slice(0, 200)}`); + } + + if (!resp.ok) { + const msg = (data?.detail as string) || (data?.message as string) || "Submission failed"; + throw new Error(msg); + } + + return { + submission_id: (data.data as Record)?.submission_id as number || 0, + message: data.message as string | undefined, + }; +} + +export interface SubmissionStatusResponse { + submission_id: number; + status: string | null; + submission_done: boolean; + file_name?: string | null; + submitted_at?: string; + error?: string | null; + last_heartbeat?: string | null; + job_created_at?: string | null; + runs?: Array<{ + start_time: string; + end_time: string | null; + mode: string; + passed: boolean; + score: number | null; + meta: Record | null; + report: Record | null; + }>; +} + +export async function fetchSubmissionStatus( + leaderboardId: number | string, + submissionId: number +): Promise { + const res = await fetch( + `/api/submissions?leaderboard_id=${leaderboardId}&offset=0&limit=100` + ); + if (!res.ok) { + const json = await res.json(); + const message = json?.message || "Unknown error"; + throw new APIError(`Failed to fetch submission status: ${message}`, res.status); + } + const r = await res.json(); + const items = r.data?.items || []; + const submission = items.find( + (item: SubmissionStatusResponse) => item.submission_id === submissionId + ); + return submission || null; +} diff --git a/frontend/src/pages/leaderboard/Leaderboard.tsx b/frontend/src/pages/leaderboard/Leaderboard.tsx index 26baf563..b16182ea 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.tsx @@ -7,6 +7,7 @@ import { Tab, Tabs, Typography, + Button, } from "@mui/material"; import Grid from "@mui/material/Grid"; import { memo, useCallback, useEffect, useState } from "react"; @@ -17,19 +18,18 @@ import RankingsList from "./components/RankingLists"; import CodeBlock from "../../components/codeblock/CodeBlock"; import MarkdownRenderer from "../../components/markdown-renderer/MarkdownRenderer"; import { ErrorAlert } from "../../components/alert/ErrorAlert"; -import { useParams, useSearchParams } from "react-router-dom"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import Loading from "../../components/common/loading"; import { ConstrainedContainer } from "../../components/app-layout/ConstrainedContainer"; -import { SubmissionMode } from "../../lib/types/mode"; import { useAuthStore } from "../../lib/store/authStore"; import SubmissionHistorySection from "./components/submission-history/SubmissionHistorySection"; -import LeaderboardSubmit from "./components/LeaderboardSubmit"; import UserTrendChart from "./components/UserTrendChart"; import { SubmissionSidebarProvider, useSubmissionSidebarState, } from "./components/SubmissionSidebarContext"; import SubmissionCodeSidebar from "./components/SubmissionCodeSidebar"; +import CodeIcon from "@mui/icons-material/Code"; const DEFAULT_SIDEBAR_WIDTH = 600; @@ -71,6 +71,7 @@ function TabPanel(props: { // Inner component const LeaderboardContent = memo(function LeaderboardContent() { const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); const { data, loading, error, errorStatus, call } = fetcherApiCallback(fetchLeaderBoard); @@ -96,8 +97,7 @@ const LeaderboardContent = memo(function LeaderboardContent() { : "rankings"; })(); const [tab, setTab] = useState(initialTabFromUrl); - const [refreshFlag, setRefreshFlag] = useState(false); - const triggerRefresh = () => setRefreshFlag((f) => !f); + const [refreshFlag] = useState(false); useEffect(() => { const current = searchParams.get("tab"); @@ -169,6 +169,7 @@ const LeaderboardContent = memo(function LeaderboardContent() { if (loading) return ; if (error) return ; + if (!data) return null; const toDeadlineUTC = (raw: string) => { const verb = isExpired(raw) ? "Ended" : "Ends"; @@ -183,7 +184,28 @@ const LeaderboardContent = memo(function LeaderboardContent() { return ( -

{data.name}

+ {/* Header with title and Submit button */} + +

{data.name}

+ {isAuthed && !isExpired(data.deadline) && ( + + )} +
{/* Header info cards shown above tabs */} {info_items.map((info, idx) => ( @@ -297,19 +319,23 @@ const LeaderboardContent = memo(function LeaderboardContent() { justifyContent="space-between" mb={2} > - Submission - + Submission History + {!isExpired(data.deadline) && ( + + )} {/* Deadline Passed Message */} {isExpired(data.deadline) && ( @@ -334,7 +360,6 @@ const LeaderboardContent = memo(function LeaderboardContent() { )} -
); diff --git a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx new file mode 100644 index 00000000..5e839682 --- /dev/null +++ b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx @@ -0,0 +1,810 @@ +import { + Box, + Card, + CardContent, + Typography, + Button, + FormControl, + InputLabel, + Select, + MenuItem, + Stack, + Alert, + CircularProgress, + Chip, + IconButton, + Tooltip, + Dialog, + DialogTitle, + DialogContent, + ToggleButton, + ToggleButtonGroup, +} from "@mui/material"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import HistoryIcon from "@mui/icons-material/History"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import CloseIcon from "@mui/icons-material/Close"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import HourglassEmptyIcon from "@mui/icons-material/HourglassEmpty"; +import CodeIcon from "@mui/icons-material/Code"; +import UploadFileIcon from "@mui/icons-material/UploadFile"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + fetchLeaderBoard, + submitCode, + submitFile, + fetchSubmissionStatus, + type SubmissionStatusResponse, +} from "../../api/api"; +import { fetcherApiCallback } from "../../lib/hooks/useApi"; +import { ConstrainedContainer } from "../../components/app-layout/ConstrainedContainer"; +import Loading from "../../components/common/loading"; +import { ErrorAlert } from "../../components/alert/ErrorAlert"; +import MarkdownRenderer from "../../components/markdown-renderer/MarkdownRenderer"; +import { SubmissionMode } from "../../lib/types/mode"; +import { useAuthStore } from "../../lib/store/authStore"; +import SubmissionHistorySection from "./components/submission-history/SubmissionHistorySection"; +import CodeMirror from "@uiw/react-codemirror"; +import { python } from "@codemirror/lang-python"; +import { oneDark } from "@codemirror/theme-one-dark"; +import { useThemeStore } from "../../lib/store/themeStore"; + +const DEFAULT_CODE = "# Write your Python code here\n"; + +const styles = { + root: { + py: 3, + }, + header: { + display: "flex", + justifyContent: "space-between", + alignItems: "flex-start", + mb: 2, + }, + editorCard: { + mb: 2, + }, + editorWrapper: { + border: "1px solid", + borderColor: "divider", + borderRadius: 1, + overflow: "hidden", + }, + controlsRow: { + display: "flex", + alignItems: "center", + gap: 2, + flexWrap: "wrap", + }, + submitBtn: { + borderRadius: 2, + px: 3, + py: 1, + fontWeight: "bold", + textTransform: "none", + background: "linear-gradient(90deg, #10b981 0%, #059669 100%)", + "&:hover": { + background: "linear-gradient(90deg, #059669 0%, #047857 100%)", + }, + }, + historyBtn: { + borderRadius: 2, + textTransform: "none", + }, + statusCard: { + mt: 2, + }, + statusHeader: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + mb: 1, + }, + uploadArea: { + border: "2px dashed", + borderColor: "divider", + borderRadius: 2, + p: 4, + textAlign: "center", + cursor: "pointer", + transition: "all 0.2s", + "&:hover": { + borderColor: "primary.main", + bgcolor: "action.hover", + }, + }, + uploadAreaDragging: { + borderColor: "primary.main", + bgcolor: "action.hover", + transform: "scale(1.01)", + }, +} as const; + +type SubmitStatus = + | { kind: "idle" } + | { kind: "submitting" } + | { kind: "polling"; submissionId: number } + | { kind: "done"; submissionId: number; result: SubmissionStatusResponse } + | { kind: "error"; msg: string }; + +type SubmitMode = "editor" | "upload"; + +export default function LeaderboardEditor() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const resolvedMode = useThemeStore((s) => s.resolvedMode); + + const { data, loading, error, errorStatus, call } = + fetcherApiCallback(fetchLeaderBoard); + const me = useAuthStore((s) => s.me); + const isAuthed = !!(me && me.authenticated); + const userId = me?.user?.identity ?? null; + + // Editor state + const [code, setCode] = useState(DEFAULT_CODE); + const [editorStatus, setEditorStatus] = useState({ kind: "idle" }); + const editorPollingRef = useRef | null>(null); + + // Submit mode switch + const [submitMode, setSubmitMode] = useState("editor"); + + // Upload state + const [file, setFile] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [uploadError, setUploadError] = useState(null); + const [uploadStatus, setUploadStatus] = useState({ kind: "idle" }); + const fileInputRef = useRef(null); + const uploadPollingRef = useRef | null>(null); + + // Common state + const [gpuType, setGpuType] = useState(""); + const [mode, setMode] = useState(SubmissionMode.LEADERBOARD); + const [historyOpen, setHistoryOpen] = useState(false); + const [refreshFlag, setRefreshFlag] = useState(false); + + const modes = useMemo( + () => [SubmissionMode.LEADERBOARD, SubmissionMode.BENCHMARK, SubmissionMode.TEST], + [] + ); + + useEffect(() => { + if (id) call(id); + }, [id, call]); + + useEffect(() => { + if (data?.gpu_types?.length && !gpuType) { + setGpuType(data.gpu_types[0]); + } + }, [data?.gpu_types, gpuType]); + + // Editor polling + const stopEditorPolling = useCallback(() => { + if (editorPollingRef.current) { + clearInterval(editorPollingRef.current); + editorPollingRef.current = null; + } + }, []); + + const startEditorPolling = useCallback( + (submissionId: number) => { + if (!id) return; + stopEditorPolling(); + + const poll = async () => { + try { + const status = await fetchSubmissionStatus(id, submissionId); + if (!status) return; + if (status.submission_done) { + stopEditorPolling(); + setEditorStatus({ kind: "done", submissionId, result: status }); + setRefreshFlag((f) => !f); + } else { + setEditorStatus({ kind: "polling", submissionId }); + } + } catch (err) { + console.error("Editor polling error:", err); + } + }; + + poll(); + editorPollingRef.current = setInterval(poll, 3000); + }, + [stopEditorPolling, id] + ); + + // Upload polling + const stopUploadPolling = useCallback(() => { + if (uploadPollingRef.current) { + clearInterval(uploadPollingRef.current); + uploadPollingRef.current = null; + } + }, []); + + const startUploadPolling = useCallback( + (submissionId: number) => { + if (!id) return; + stopUploadPolling(); + + const poll = async () => { + try { + const status = await fetchSubmissionStatus(id, submissionId); + if (!status) return; + if (status.submission_done) { + stopUploadPolling(); + setUploadStatus({ kind: "done", submissionId, result: status }); + setRefreshFlag((f) => !f); + } else { + setUploadStatus({ kind: "polling", submissionId }); + } + } catch (err) { + console.error("Upload polling error:", err); + } + }; + + poll(); + uploadPollingRef.current = setInterval(poll, 3000); + }, + [stopUploadPolling, id] + ); + + useEffect(() => { + return () => { + stopEditorPolling(); + stopUploadPolling(); + }; + }, [stopEditorPolling, stopUploadPolling]); + + // Editor submit + const handleEditorSubmit = async () => { + if (!data || !id) return; + + if (!code.trim()) { + setEditorStatus({ kind: "error", msg: "Please write some code before submitting." }); + return; + } + + setEditorStatus({ kind: "submitting" }); + + try { + const result = await submitCode(id, data.name, gpuType, mode, code); + if (result?.submission_id) { + startEditorPolling(result.submission_id); + } else { + setEditorStatus({ kind: "error", msg: "Submission accepted but no ID returned." }); + } + } catch (err) { + setEditorStatus({ + kind: "error", + msg: err instanceof Error ? err.message : "Submission failed", + }); + } + }; + + // Upload submit + const handleUploadSubmit = async () => { + if (!data || !id || !file) return; + + setUploadStatus({ kind: "submitting" }); + setUploadError(null); + + try { + const form = new FormData(); + form.set("leaderboard_id", id); + form.set("leaderboard", data.name); + form.set("gpu_type", gpuType); + form.set("submission_mode", mode); + form.set("file", file, file.name); + const res = await submitFile(form); + const submissionId = (res.data as Record)?.submission_id as number || + (res as Record)?.submission_id as number || 0; + + if (submissionId) { + startUploadPolling(submissionId); + } else { + setUploadStatus({ kind: "error", msg: "Submission accepted but no ID returned." }); + } + } catch (err) { + setUploadStatus({ + kind: "error", + msg: err instanceof Error ? err.message : "Submission failed", + }); + } + }; + + const handleFileSelect = (e: React.ChangeEvent) => { + const f = e.target.files?.[0] ?? null; + if (!f) return; + validateAndSetFile(f); + }; + + const validateAndSetFile = (f: File) => { + const name = f.name.toLowerCase(); + if (!name.endsWith(".py")) { + setUploadError("Please select a .py file."); + setFile(null); + return; + } + if (f.size > 1 * 1024 * 1024) { + setUploadError("File too large (> 1 MB)"); + setFile(null); + return; + } + setFile(f); + setUploadError(null); + }; + + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + const files = e.dataTransfer.files; + if (files && files.length > 0) { + validateAndSetFile(files[0]); + } + }; + + const canEditorSubmit = useMemo(() => { + if (!gpuType || !mode) return false; + return code.trim().length > 0; + }, [code, gpuType, mode]); + + const canUploadSubmit = useMemo(() => { + if (!gpuType || !mode) return false; + return !!file; + }, [file, gpuType, mode]); + + const renderEditorStatusChip = () => { + switch (editorStatus.kind) { + case "submitting": + return ( + } + label="Submitting..." + color="info" + size="small" + /> + ); + case "polling": + return ( + } + label="Running..." + color="warning" + size="small" + /> + ); + case "done": + return ( + } + label="Completed" + color="success" + size="small" + /> + ); + case "error": + return ; + default: + return null; + } + }; + + const renderUploadStatusChip = () => { + switch (uploadStatus.kind) { + case "submitting": + return ( + } + label="Submitting..." + color="info" + size="small" + /> + ); + case "polling": + return ( + } + label="Running..." + color="warning" + size="small" + /> + ); + case "done": + return ( + } + label="Completed" + color="success" + size="small" + /> + ); + case "error": + return ; + default: + return null; + } + }; + + if (loading) return ; + if (error) return ; + if (!data) return null; + + if (!isAuthed) { + return ( + + + + Submit Code + + + Please login to submit your code. + + + + ); + } + + return ( + + + {/* Header */} + + + + {data.name} + + + Submit your Python code + + + + + + + + + {/* Description */} + + +
+ + Challenge 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")) + )} + +
  • + ))} +
+
+ )} +
+
+
+ + {/* Submission Section with Toggle */} + + + {/* Toggle between Editor and Upload */} + + v && setSubmitMode(v)} + size="small" + > + + + Code Editor + + + + Upload File + + + {submitMode === "editor" ? renderEditorStatusChip() : renderUploadStatusChip()} + + + {/* Editor Mode */} + {submitMode === "editor" && ( + <> + + setCode(value)} + /> + + + {/* Editor Controls */} + + + GPU Type + + + + + Mode + + + + + + {editorStatus.kind === "polling" && ( + + startEditorPolling(editorStatus.submissionId)}> + + + + )} + + + {/* Editor Status Messages */} + {editorStatus.kind === "error" && ( + + {editorStatus.msg} + + )} + + {editorStatus.kind === "done" && ( + + Submission completed! Check the history for results. + + )} + + )} + + {/* Upload Mode */} + {submitMode === "upload" && ( + <> + {file ? ( + + + + + {file.name} + + {(file.size / 1024).toFixed(1)} KB + + + + { + setFile(null); + setUploadError(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }} + sx={{ color: "text.secondary" }} + > + + + + ) : ( + fileInputRef.current?.click()} + onDragEnter={handleDragEnter} + onDragLeave={handleDragLeave} + onDragOver={handleDragOver} + onDrop={handleDrop} + > + + + + Drag & drop or click to select a .py file + + + )} + + {/* Upload Error */} + {uploadError && ( + + {uploadError} + + )} + + {/* Upload Controls */} + + + GPU Type + + + + + Mode + + + + + + {uploadStatus.kind === "polling" && ( + + startUploadPolling(uploadStatus.submissionId)}> + + + + )} + + + {/* Upload Status Messages */} + {uploadStatus.kind === "error" && ( + + {uploadStatus.msg} + + )} + + {uploadStatus.kind === "done" && ( + + Submission completed! Check the history for results. + + )} + + )} + + + + {/* History Dialog */} + setHistoryOpen(false)} + maxWidth="md" + fullWidth + > + + Submission History + setHistoryOpen(false)}> + + + + + + + +
+
+ ); +} diff --git a/frontend/src/pages/leaderboard/components/LeaderboardSubmit.tsx b/frontend/src/pages/leaderboard/components/LeaderboardSubmit.tsx index 4c1b6816..1c9db908 100644 --- a/frontend/src/pages/leaderboard/components/LeaderboardSubmit.tsx +++ b/frontend/src/pages/leaderboard/components/LeaderboardSubmit.tsx @@ -48,6 +48,8 @@ export default function LeaderboardSubmit({ modes, disabled = false, onSubmit, + open: externalOpen, + onClose: externalOnClose, }: { leaderboardId: string; leaderboardName: string; @@ -55,8 +57,17 @@ export default function LeaderboardSubmit({ modes: string[]; disabled?: boolean; onSubmit?: () => void; + open?: boolean; + onClose?: () => void; }) { - const [open, setOpen] = useState(false); + const [internalOpen, setInternalOpen] = useState(false); + const isControlled = externalOpen !== undefined; + const open = isControlled ? externalOpen : internalOpen; + const setOpen = isControlled + ? (val: boolean) => { + if (!val && externalOnClose) externalOnClose(); + } + : setInternalOpen; const [gpuType, setGpuType] = useState(gpuTypes?.[0] ?? ""); const [mode, setMode] = useState(modes?.[0] ?? ""); const [file, setFile] = useState(null); @@ -137,22 +148,27 @@ export default function LeaderboardSubmit({ } }, [status]); - const renderSubmitButton = () => ( - - ); + const renderSubmitButton = () => { + if (isControlled) { + return null; + } + return ( + + ); + }; return ( <> Date: Wed, 4 Mar 2026 15:14:19 -0800 Subject: [PATCH 15/26] test3 --- frontend/package-lock.json | 65 +++ frontend/package.json | 1 + .../pages/leaderboard/LeaderboardEditor.tsx | 205 ++++++- kernelboard/api/__init__.py | 1 + kernelboard/api/submission.py | 30 +- kernelboard/lib/mocks/__init__.py | 1 + kernelboard/lib/mocks/mock_submission.py | 503 ++++++++++++++++++ 7 files changed, 789 insertions(+), 17 deletions(-) create mode 100644 kernelboard/lib/mocks/__init__.py create mode 100644 kernelboard/lib/mocks/mock_submission.py diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7b902864..10d081f2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@codemirror/theme-one-dark": "^6.1.3", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^7.2.0", "@mui/material": "^7.2.0", "@types/react-syntax-highlighter": "^15.5.13", @@ -1391,6 +1392,27 @@ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==" }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mui/core-downloads-tracker": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.2.0.tgz", @@ -2218,6 +2240,13 @@ "@types/react": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true, + "peer": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -3215,6 +3244,15 @@ "csstype": "^3.0.2" } }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/echarts": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", @@ -4501,6 +4539,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -5405,6 +5455,16 @@ "node": "*" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6299,6 +6359,11 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 30df2f41..3ed2e91d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "@codemirror/theme-one-dark": "^6.1.3", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^7.2.0", "@mui/material": "^7.2.0", "@types/react-syntax-highlighter": "^15.5.13", diff --git a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx index 5e839682..3b2311a1 100644 --- a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx +++ b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx @@ -19,6 +19,8 @@ import { DialogContent, ToggleButton, ToggleButtonGroup, + Drawer, + Divider, } from "@mui/material"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import HistoryIcon from "@mui/icons-material/History"; @@ -28,6 +30,9 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import HourglassEmptyIcon from "@mui/icons-material/HourglassEmpty"; import CodeIcon from "@mui/icons-material/Code"; import UploadFileIcon from "@mui/icons-material/UploadFile"; +import ExpandLessIcon from "@mui/icons-material/ExpandLess"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import TerminalIcon from "@mui/icons-material/Terminal"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { @@ -45,6 +50,7 @@ import MarkdownRenderer from "../../components/markdown-renderer/MarkdownRendere import { SubmissionMode } from "../../lib/types/mode"; import { useAuthStore } from "../../lib/store/authStore"; import SubmissionHistorySection from "./components/submission-history/SubmissionHistorySection"; +import { SubmissionRunsTable } from "./components/submission-history/SubmissionRunsTable"; import CodeMirror from "@uiw/react-codemirror"; import { python } from "@codemirror/lang-python"; import { oneDark } from "@codemirror/theme-one-dark"; @@ -124,7 +130,7 @@ const styles = { type SubmitStatus = | { kind: "idle" } | { kind: "submitting" } - | { kind: "polling"; submissionId: number } + | { kind: "polling"; submissionId: number; result?: SubmissionStatusResponse } | { kind: "done"; submissionId: number; result: SubmissionStatusResponse } | { kind: "error"; msg: string }; @@ -163,6 +169,10 @@ export default function LeaderboardEditor() { const [historyOpen, setHistoryOpen] = useState(false); const [refreshFlag, setRefreshFlag] = useState(false); + // Job result drawer + const [drawerOpen, setDrawerOpen] = useState(false); + const [drawerHeight, setDrawerHeight] = useState(300); + const modes = useMemo( () => [SubmissionMode.LEADERBOARD, SubmissionMode.BENCHMARK, SubmissionMode.TEST], [] @@ -198,9 +208,9 @@ export default function LeaderboardEditor() { if (status.submission_done) { stopEditorPolling(); setEditorStatus({ kind: "done", submissionId, result: status }); - setRefreshFlag((f) => !f); } else { - setEditorStatus({ kind: "polling", submissionId }); + // Show intermediate status with runs while polling + setEditorStatus({ kind: "polling", submissionId, result: status }); } } catch (err) { console.error("Editor polling error:", err); @@ -208,7 +218,7 @@ export default function LeaderboardEditor() { }; poll(); - editorPollingRef.current = setInterval(poll, 3000); + editorPollingRef.current = setInterval(poll, 5000); }, [stopEditorPolling, id] ); @@ -233,9 +243,9 @@ export default function LeaderboardEditor() { if (status.submission_done) { stopUploadPolling(); setUploadStatus({ kind: "done", submissionId, result: status }); - setRefreshFlag((f) => !f); } else { - setUploadStatus({ kind: "polling", submissionId }); + // Show intermediate status with runs while polling + setUploadStatus({ kind: "polling", submissionId, result: status }); } } catch (err) { console.error("Upload polling error:", err); @@ -243,7 +253,7 @@ export default function LeaderboardEditor() { }; poll(); - uploadPollingRef.current = setInterval(poll, 3000); + uploadPollingRef.current = setInterval(poll, 5000); }, [stopUploadPolling, id] ); @@ -265,6 +275,7 @@ export default function LeaderboardEditor() { } setEditorStatus({ kind: "submitting" }); + setDrawerOpen(true); try { const result = await submitCode(id, data.name, gpuType, mode, code); @@ -287,6 +298,7 @@ export default function LeaderboardEditor() { setUploadStatus({ kind: "submitting" }); setUploadError(null); + setDrawerOpen(true); try { const form = new FormData(); @@ -319,6 +331,10 @@ export default function LeaderboardEditor() { }; const validateAndSetFile = (f: File) => { + // Clear previous errors and status + setUploadError(null); + setUploadStatus({ kind: "idle" }); + const name = f.name.toLowerCase(); if (!name.endsWith(".py")) { setUploadError("Please select a .py file."); @@ -331,7 +347,6 @@ export default function LeaderboardEditor() { return; } setFile(f); - setUploadError(null); }; const handleDragEnter = (e: React.DragEvent) => { @@ -525,7 +540,7 @@ export default function LeaderboardEditor() { {/* Toggle between Editor and Upload */} - + - {submitMode === "editor" ? renderEditorStatusChip() : renderUploadStatusChip()} {/* Editor Mode */} @@ -554,6 +568,15 @@ export default function LeaderboardEditor() { theme={resolvedMode === "dark" ? oneDark : undefined} extensions={[python()]} onChange={(value: string) => setCode(value)} + basicSetup={{ + lineNumbers: true, + highlightActiveLineGutter: true, + highlightActiveLine: true, + bracketMatching: true, + closeBrackets: true, + autocompletion: true, + indentOnInput: true, + }} /> @@ -603,12 +626,14 @@ export default function LeaderboardEditor() { ) } onClick={handleEditorSubmit} - disabled={!canEditorSubmit || editorStatus.kind === "submitting"} + disabled={!canEditorSubmit || editorStatus.kind === "submitting" || editorStatus.kind === "polling"} sx={styles.submitBtn} > {editorStatus.kind === "submitting" ? "Submitting..." : "Run"} + {renderEditorStatusChip()} + {editorStatus.kind === "polling" && ( startEditorPolling(editorStatus.submissionId)}> @@ -750,12 +775,14 @@ export default function LeaderboardEditor() { ) } onClick={handleUploadSubmit} - disabled={!canUploadSubmit || uploadStatus.kind === "submitting"} + disabled={!canUploadSubmit || uploadStatus.kind === "submitting" || uploadStatus.kind === "polling"} sx={styles.submitBtn} > {uploadStatus.kind === "submitting" ? "Submitting..." : "Run"} + {renderUploadStatusChip()} + {uploadStatus.kind === "polling" && ( startUploadPolling(uploadStatus.submissionId)}> @@ -804,6 +831,160 @@ export default function LeaderboardEditor() { /> + + {/* Job Result Drawer - Fixed Bottom Panel */} + setDrawerOpen(false)} + variant="persistent" + sx={{ + "& .MuiDrawer-paper": { + height: drawerHeight, + borderTopLeftRadius: 12, + borderTopRightRadius: 12, + boxShadow: "0 -4px 20px rgba(0,0,0,0.15)", + }, + }} + > + + + + + + Job Output + + {(editorStatus.kind === "polling" || uploadStatus.kind === "polling") && ( + } + label="Running..." + color="warning" + size="small" + /> + )} + {(editorStatus.kind === "done" || uploadStatus.kind === "done") && ( + } label="Completed" color="success" size="small" /> + )} + + + setDrawerHeight(drawerHeight === 300 ? 500 : 300)} + > + {drawerHeight === 300 ? : } + + setDrawerOpen(false)}> + + + + + + + {/* Show current job result */} + {editorStatus.kind === "done" && editorStatus.result && ( + <> + + ✓ Submission #{editorStatus.submissionId} - {editorStatus.result.status} + + {editorStatus.result.error && ( + + {editorStatus.result.error} + + )} + {editorStatus.result.runs && editorStatus.result.runs.length > 0 && ( + + )} + + )} + {uploadStatus.kind === "done" && uploadStatus.result && ( + <> + + ✓ Submission #{uploadStatus.submissionId} - {uploadStatus.result.status} + + {uploadStatus.result.error && ( + + {uploadStatus.result.error} + + )} + {uploadStatus.result.runs && uploadStatus.result.runs.length > 0 && ( + + )} + + )} + {(editorStatus.kind === "submitting" || uploadStatus.kind === "submitting") && ( + + + + Submitting... + + + )} + {(editorStatus.kind === "polling" || uploadStatus.kind === "polling") && ( + <> + + + + Running... + + + {/* Show intermediate runs while polling */} + {editorStatus.kind === "polling" && editorStatus.result?.runs && editorStatus.result.runs.length > 0 && ( + + )} + {uploadStatus.kind === "polling" && uploadStatus.result?.runs && uploadStatus.result.runs.length > 0 && ( + + )} + + )} + {editorStatus.kind === "idle" && uploadStatus.kind === "idle" && ( + + No job running. Submit code to see output here. + + )} + {editorStatus.kind === "error" && ( + {editorStatus.msg} + )} + {uploadStatus.kind === "error" && ( + {uploadStatus.msg} + )} + + + + + {/* Floating button to open drawer */} + {!drawerOpen && (editorStatus.kind !== "idle" || uploadStatus.kind !== "idle") && ( + + + + )} ); diff --git a/kernelboard/api/__init__.py b/kernelboard/api/__init__.py index 0acf139e..7b3fcee8 100644 --- a/kernelboard/api/__init__.py +++ b/kernelboard/api/__init__.py @@ -66,4 +66,5 @@ def get_about(): api.register_blueprint(auth_bp) api.register_blueprint(submission_bp) api.register_blueprint(events_bp) + return api diff --git a/kernelboard/api/submission.py b/kernelboard/api/submission.py index 3e2520b5..1e6057d4 100644 --- a/kernelboard/api/submission.py +++ b/kernelboard/api/submission.py @@ -32,22 +32,27 @@ "submission_mode", ] - WEB_AUTH_HEADER = "X-Web-Auth-Id" MAX_CONTENT_LENGTH = 1 * 1024 * 1024 # 1MB max file size -# This blocks the leaderboard to show all the ranking codes when the leaderboard is ended, since -# some of those competition codes can be affect the upcoming competition results +# This blocks the leaderboard to show all the ranking codes when the leaderboard is ended BLOCKED_CODE_LEADERBOARD_LIST: list[str] = ["598"] # leaderboard id to block show + +USE_MOCK_SUBMISSION: bool = os.environ.get( + "USE_MOCK_SUBMISSION", "" +).lower() == "true" +MOCK_FAILURE_MODE: str | None = os.environ.get("MOCK_FAILURE_MODE") or None +# ============================================================================ + + @submission_bp.route("/submission", methods=["POST"]) @login_required @limiter.limit( "60 per minute", - exempt_when=lambda: not current_user.is_authenticated, # ignore unauthenticated, since they won't hit the api + exempt_when=lambda: not current_user.is_authenticated, ) def submission(): - # make sure user is logged in logger.info("submission received") user_id, username = get_id_and_username_from_session() log_rate_limit() @@ -86,6 +91,21 @@ def submission(): gpu_type = request.form.get("gpu_type") submission_mode = request.form.get("submission_mode") leaderboard_name = request.form.get("leaderboard") + + # DEV: Use mock submission (writes directly to local DB) + if USE_MOCK_SUBMISSION: + logging.warning("[!MOCK DATA!]USE_MOCK_SUBMISSION is on! this should only be used in dev mode!") + from kernelboard.lib.mocks.mock_submission import create_mock_submission + files = {"file": (filename, f.stream, mime)} + return create_mock_submission( + user_id=str(user_id), + leaderboard_name=leaderboard_name, + file_name=filename, + files=files, + submission_mode=submission_mode, + failure_mode=MOCK_FAILURE_MODE, + ) + base = get_cluster_manager_endpoint() url = f"{base}/submission/{leaderboard_name}/{gpu_type}/{submission_mode}" files = { diff --git a/kernelboard/lib/mocks/__init__.py b/kernelboard/lib/mocks/__init__.py new file mode 100644 index 00000000..54d11136 --- /dev/null +++ b/kernelboard/lib/mocks/__init__.py @@ -0,0 +1 @@ +# Mock modules for local development diff --git a/kernelboard/lib/mocks/mock_submission.py b/kernelboard/lib/mocks/mock_submission.py new file mode 100644 index 00000000..b222e70a --- /dev/null +++ b/kernelboard/lib/mocks/mock_submission.py @@ -0,0 +1,503 @@ +""" +Mock submission for local development and testing. + +This simulates the real submission flow from discord-cluster-manager: +1. Create submission in DB (with code_files entry) +2. Create job status entry +3. After 10s: Create "test" run +4. After 20s: Create "leaderboard" run, mark done + +Usage: + 1. Set USE_MOCK_SUBMISSION = True in submission.py + 2. Submit code normally - it will use mock instead of real API +""" + +import json +import logging +import os +import random +import threading +import time +from datetime import datetime, timezone +from typing import Optional + +import psycopg2 + +from kernelboard.lib.status_code import http_error, http_success + +logger = logging.getLogger(__name__) + + +def _get_standalone_db_connection(): + """ + Get a standalone DB connection for background threads. + This doesn't use Flask's g object, so it works outside request context. + """ + database_url = os.environ.get("DATABASE_URL") + if not database_url: + raise RuntimeError("DATABASE_URL is not set") + return psycopg2.connect(database_url) + + +def _get_leaderboard_id(leaderboard_name: str) -> int: + """Get leaderboard ID from name.""" + conn = _get_standalone_db_connection() + cur = conn.cursor() + + try: + cur.execute( + """ + SELECT id FROM leaderboard.leaderboard WHERE name = %s + """, + (leaderboard_name,), + ) + result = cur.fetchone() + if result: + return result[0] + # Default to 1 if not found + return 1 + finally: + cur.close() + conn.close() + + +def _create_submission( + leaderboard_id: int, + user_id: str, + file_name: str, + code: str = "# mock submission code", +) -> int: + """ + Create a submission record following discord-cluster-manager pattern. + """ + conn = _get_standalone_db_connection() + cur = conn.cursor() + + try: + # 1. Check if code already exists (by hash) + cur.execute( + """ + SELECT id FROM leaderboard.code_files + WHERE hash = encode(sha256(%s::bytea), 'hex') + """, + (code.encode("utf-8"),), + ) + result = cur.fetchone() + + if result: + code_id = result[0] + else: + # Insert new code + cur.execute( + """ + INSERT INTO leaderboard.code_files (code) + VALUES (%s) + RETURNING id + """, + (code.encode("utf-8"),), + ) + code_id = cur.fetchone()[0] + + # 2. Create submission + cur.execute( + """ + INSERT INTO leaderboard.submission + (leaderboard_id, file_name, user_id, code_id, submission_time, done) + VALUES (%s, %s, %s, %s, %s, false) + RETURNING id + """, + (leaderboard_id, file_name, user_id, code_id, datetime.now(timezone.utc)), + ) + submission_id = cur.fetchone()[0] + + # 3. Create job status entry (pending) + cur.execute( + """ + INSERT INTO leaderboard.submission_job_status + (submission_id, status, created_at, last_heartbeat) + VALUES (%s, %s, %s, %s) + ON CONFLICT (submission_id) DO UPDATE + SET status = EXCLUDED.status, last_heartbeat = EXCLUDED.last_heartbeat + """, + (submission_id, "pending", datetime.now(timezone.utc), datetime.now(timezone.utc)), + ) + + conn.commit() + return submission_id + + finally: + cur.close() + conn.close() + + +def _upsert_job_status( + submission_id: int, + status: str, + error: Optional[str] = None, +): + """ + Update job status following discord-cluster-manager pattern. + """ + conn = _get_standalone_db_connection() + cur = conn.cursor() + + try: + cur.execute( + """ + INSERT INTO leaderboard.submission_job_status AS s + (submission_id, status, error, last_heartbeat) + VALUES (%s, %s, %s, %s) + ON CONFLICT (submission_id) DO UPDATE + SET + status = COALESCE(EXCLUDED.status, s.status), + error = COALESCE(EXCLUDED.error, s.error), + last_heartbeat = EXCLUDED.last_heartbeat + """, + (submission_id, status, error, datetime.now(timezone.utc)), + ) + conn.commit() + finally: + cur.close() + conn.close() + + +def _create_run( + submission_id: int, + mode: str, + runner: str, + passed: bool, + score: Optional[float] = None, + duration: float = 5.0, + stderr: Optional[str] = None, + stdout: Optional[str] = None, +): + """ + Create a run record following discord-cluster-manager pattern. + + meta contains: stdout, stderr, success, exit_code, command, duration + result contains: benchmark results (format that toReport can parse) + system_info contains: gpu info + """ + conn = _get_standalone_db_connection() + cur = conn.cursor() + + try: + start_time = datetime.now(timezone.utc) + end_time = start_time + + # meta follows discord-cluster-manager format + meta = { + "stdout": stdout or "", + "stderr": stderr or "", + "success": passed, + "exit_code": 0 if passed else 1, + "command": "mock_eval.py", + "duration": duration, + } + + # result format matches what toReport() expects + if mode == "test": + if passed: + result = { + "test.0.status": "pass", + "test.0.spec": "test_correctness", + "test.0.message": "All tests passed!", + "test.1.status": "pass", + "test.1.spec": "test_performance", + "test.1.message": "Performance within acceptable range.", + } + else: + result = { + "test.0.status": "fail", + "test.0.spec": "test_correctness", + "test.0.error": stderr or "Test failed", + } + elif mode == "benchmark": + if passed: + mean_ns = random.uniform(100000, 500000) * 1000 + err_ns = mean_ns * 0.05 + result = { + "benchmark-count": 1, + "benchmark.0.spec": "benchmark_kernel", + "benchmark.0.mean": mean_ns, + "benchmark.0.err": err_ns, + "benchmark.0.best": mean_ns * 0.95, + "benchmark.0.worst": mean_ns * 1.05, + } + else: + result = { + "benchmark-count": 1, + "benchmark.0.status": "fail", + "benchmark.0.spec": "benchmark_kernel", + "benchmark.0.error": stderr or "Benchmark failed", + } + elif mode == "leaderboard" and passed and score: + err_ns = score * 0.05 + result = { + "benchmark-count": 1, + "benchmark.0.spec": "leaderboard_kernel", + "benchmark.0.mean": score, + "benchmark.0.err": err_ns, + "benchmark.0.best": score * 0.95, + "benchmark.0.worst": score * 1.05, + } + else: + result = {} + + # system_info + system_info = { + "gpu": runner, + "driver_version": "mock-driver-535.104.05", + "cuda_version": "12.2", + } + + cur.execute( + """ + INSERT INTO leaderboard.runs + (submission_id, start_time, end_time, mode, secret, runner, + score, passed, compilation, meta, result, system_info) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING id + """, + ( + submission_id, + start_time, + end_time, + mode, + False, # secret + runner, + score, + passed, + None, # compilation + json.dumps(meta), + json.dumps(result), + json.dumps(system_info), + ), + ) + conn.commit() + finally: + cur.close() + conn.close() + + +def _mark_submission_done(submission_id: int): + """Mark submission as done.""" + conn = _get_standalone_db_connection() + cur = conn.cursor() + + try: + cur.execute( + """ + UPDATE leaderboard.submission + SET done = true + WHERE id = %s + """, + (submission_id,), + ) + conn.commit() + finally: + cur.close() + conn.close() + + +def _simulate_submission_flow( + submission_id: int, + submission_mode: str = "leaderboard", + runner: str = "NVIDIA A100", + simulate_test_failure: bool = False, + simulate_benchmark_failure: bool = False, +): + """ + Background thread that simulates submission flow based on submission_mode: + + - "test": Only runs test phase + - "leaderboard": Runs test → benchmark → leaderboard (with score) + """ + try: + # Phase 1: Running + logger.info( + "[MOCK] Submission %s: Starting (mode=%s)...", + submission_id, submission_mode + ) + _upsert_job_status(submission_id, "running") + time.sleep(10) + + # Phase 2: Test run (always runs first) + test_passed = not simulate_test_failure + test_stderr = None + if not test_passed: + test_stderr = ( + "Traceback (most recent call last):\n" + ' File "/root/eval.py", line 14, in \n' + " from utils import set_seed\n" + "ImportError: cannot import name 'set_seed' from 'utils'\n" + ) + + logger.info( + "[MOCK] Submission %s: Creating test run (passed=%s)", + submission_id, test_passed + ) + _create_run( + submission_id=submission_id, + mode="test", + runner=runner, + passed=test_passed, + score=None, + duration=random.uniform(1.0, 3.0), + stderr=test_stderr, + stdout="Running tests..." if test_passed else "", + ) + + if not test_passed: + _upsert_job_status(submission_id, "failed", "Test phase failed") + _mark_submission_done(submission_id) + logger.info("[MOCK] Submission %s: Finished (test failed)", submission_id) + return + + # For "test" mode, we're done after test passes + if submission_mode == "test": + _upsert_job_status(submission_id, "succeeded") + _mark_submission_done(submission_id) + logger.info("[MOCK] Submission %s: Finished (test mode)", submission_id) + return + + # Phase 3: Benchmark run (for leaderboard mode) + logger.info("[MOCK] Submission %s: Starting benchmark phase...", submission_id) + time.sleep(10) + + benchmark_passed = not simulate_benchmark_failure + benchmark_stderr = None + if not benchmark_passed: + benchmark_stderr = "RuntimeError: CUDA out of memory" + + logger.info( + "[MOCK] Submission %s: Creating benchmark run (passed=%s)", + submission_id, benchmark_passed + ) + _create_run( + submission_id=submission_id, + mode="benchmark", + runner=runner, + passed=benchmark_passed, + score=None, + duration=random.uniform(3.0, 8.0), + stderr=benchmark_stderr, + stdout="Benchmark completed." if benchmark_passed else "", + ) + + if not benchmark_passed: + _upsert_job_status(submission_id, "failed", "Benchmark phase failed") + _mark_submission_done(submission_id) + logger.info("[MOCK] Submission %s: Finished (benchmark failed)", submission_id) + return + + # Phase 4: Leaderboard run (with score) + logger.info("[MOCK] Submission %s: Creating leaderboard run...", submission_id) + time.sleep(5) + + # Generate score in nanoseconds (like real backend) + score_ns = random.uniform(100000, 500000) * 1000 + + _create_run( + submission_id=submission_id, + mode="leaderboard", + runner=runner, + passed=True, + score=score_ns, + duration=random.uniform(3.0, 8.0), + stderr=None, + stdout="Leaderboard run completed.", + ) + + # Mark as done + _upsert_job_status(submission_id, "succeeded") + _mark_submission_done(submission_id) + + logger.info("[MOCK] Submission %s: Finished (succeeded)", submission_id) + + except Exception as e: + logger.error("[MOCK] Submission %s error: %s", submission_id, e) + _upsert_job_status(submission_id, "failed", str(e)) + _mark_submission_done(submission_id) + + +def create_mock_submission( + user_id: str, + leaderboard_name: str, + file_name: str, + files: dict, + submission_mode: str = "leaderboard", + failure_mode: str = None, +): + """ + Called by submission.py when USE_MOCK_SUBMISSION = True. + Mimics the real cluster API: returns submission_id immediately, + then runs simulation in background. + + submission_mode: + - "test": Only runs test phase + - "leaderboard": Runs test → benchmark → leaderboard (with score) + + failure_mode: + - None: All phases pass + - "test": Test phase fails + - "benchmark": Test passes, benchmark fails + """ + try: + # Get leaderboard_id from name + lb_id = _get_leaderboard_id(leaderboard_name) if leaderboard_name else 1 + + # Extract file content from files dict + # files = {"file": (filename, fileobj, content_type)} + file_content = "# mock submission code" + if files and "file" in files: + file_tuple = files["file"] + if len(file_tuple) >= 2: + fileobj = file_tuple[1] + fileobj.seek(0) + file_content = fileobj.read() + if isinstance(file_content, bytes): + file_content = file_content.decode("utf-8") + + submission_id = _create_submission( + leaderboard_id=lb_id, + user_id=user_id, + file_name=file_name or "mock.py", + code=file_content, + ) + + logger.info( + "[MOCK] Created submission %s (mode=%s, failure=%s) for user %s", + submission_id, submission_mode, failure_mode, user_id + ) + + # Determine failure flags + simulate_test_failure = failure_mode == "test" + simulate_benchmark_failure = failure_mode == "benchmark" + + # Background thread simulates the job flow + thread = threading.Thread( + target=_simulate_submission_flow, + args=( + submission_id, + submission_mode, + "NVIDIA A100", + simulate_test_failure, + simulate_benchmark_failure, + ), + daemon=True, + ) + thread.start() + + # Return same format as real cluster API + return http_success( + message="submission success, please refresh submission history", + data={"submission_id": submission_id}, + ) + + except Exception as e: + logger.error("[MOCK] Failed to create submission: %s", e) + return http_error( + message=str(e), + status_code=500, + ) From 9cf5703d0096da070c581f269ee8fe50996b88d0 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 4 Mar 2026 15:16:23 -0800 Subject: [PATCH 16/26] test3 --- frontend/src/pages/leaderboard/LeaderboardEditor.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx index 3b2311a1..6d44ef9e 100644 --- a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx +++ b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx @@ -151,6 +151,7 @@ export default function LeaderboardEditor() { const [code, setCode] = useState(DEFAULT_CODE); const [editorStatus, setEditorStatus] = useState({ kind: "idle" }); const editorPollingRef = useRef | null>(null); + const editorTimeoutRef = useRef | null>(null); // Submit mode switch const [submitMode, setSubmitMode] = useState("editor"); @@ -162,6 +163,10 @@ export default function LeaderboardEditor() { const [uploadStatus, setUploadStatus] = useState({ kind: "idle" }); const fileInputRef = useRef(null); const uploadPollingRef = useRef | null>(null); + const uploadTimeoutRef = useRef | null>(null); + + // Polling timeout (20 minutes) + const POLLING_TIMEOUT_MS = 20 * 60 * 1000; // Common state const [gpuType, setGpuType] = useState(""); From 543f663c0ad291f9b0e6d6ceae56d143bf028a00 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 4 Mar 2026 15:21:00 -0800 Subject: [PATCH 17/26] test3 --- frontend/src/api/api.ts | 2 +- kernelboard/api/submission.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index db7d687e..689c2b9e 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -229,7 +229,7 @@ export async function submitFile(form: FormData) { export async function fetchUserSubmissions( leaderboardId: number | string, - _userId: number | string, + userId: number | string, page: number = 1, pageSize: number = 10, ): Promise { diff --git a/kernelboard/api/submission.py b/kernelboard/api/submission.py index 1e6057d4..6c7d7a0e 100644 --- a/kernelboard/api/submission.py +++ b/kernelboard/api/submission.py @@ -93,7 +93,7 @@ def submission(): leaderboard_name = request.form.get("leaderboard") # DEV: Use mock submission (writes directly to local DB) - if USE_MOCK_SUBMISSION: + if USE_MOCK_SUBMISSION == "true": logging.warning("[!MOCK DATA!]USE_MOCK_SUBMISSION is on! this should only be used in dev mode!") from kernelboard.lib.mocks.mock_submission import create_mock_submission files = {"file": (filename, f.stream, mime)} From e0c6329ee3ccb198b07bde0b51cb14390a3a8b29 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 5 Mar 2026 19:03:36 -0800 Subject: [PATCH 18/26] test3 --- frontend/package-lock.json | 65 - frontend/package.json | 1 - .../src/pages/leaderboard/Leaderboard.tsx | 106 +- .../pages/leaderboard/LeaderboardEditor.tsx | 1078 +++++------------ .../editor/ChallengeDescriptionPanel.tsx | 66 + .../components/editor/CodeEditorPanel.tsx | 42 + .../components/editor/EditorControls.tsx | 115 ++ .../components/editor/JobOutputPanel.tsx | 167 +++ .../components/editor/ResizableSplitPanel.tsx | 115 ++ .../leaderboard/components/editor/index.ts | 6 + .../leaderboard/components/editor/types.ts | 87 ++ kernelboard/api/submission.py | 6 +- 12 files changed, 974 insertions(+), 880 deletions(-) create mode 100644 frontend/src/pages/leaderboard/components/editor/ChallengeDescriptionPanel.tsx create mode 100644 frontend/src/pages/leaderboard/components/editor/CodeEditorPanel.tsx create mode 100644 frontend/src/pages/leaderboard/components/editor/EditorControls.tsx create mode 100644 frontend/src/pages/leaderboard/components/editor/JobOutputPanel.tsx create mode 100644 frontend/src/pages/leaderboard/components/editor/ResizableSplitPanel.tsx create mode 100644 frontend/src/pages/leaderboard/components/editor/index.ts create mode 100644 frontend/src/pages/leaderboard/components/editor/types.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 10d081f2..7b902864 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,7 +12,6 @@ "@codemirror/theme-one-dark": "^6.1.3", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^7.2.0", "@mui/material": "^7.2.0", "@types/react-syntax-highlighter": "^15.5.13", @@ -1392,27 +1391,6 @@ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==" }, - "node_modules/@monaco-editor/loader": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", - "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", - "dependencies": { - "state-local": "^1.0.6" - } - }, - "node_modules/@monaco-editor/react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", - "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", - "dependencies": { - "@monaco-editor/loader": "^1.5.0" - }, - "peerDependencies": { - "monaco-editor": ">= 0.25.0 < 1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/@mui/core-downloads-tracker": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.2.0.tgz", @@ -2240,13 +2218,6 @@ "@types/react": "*" } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "optional": true, - "peer": true - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -3244,15 +3215,6 @@ "csstype": "^3.0.2" } }, - "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", - "peer": true, - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, "node_modules/echarts": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", @@ -4539,18 +4501,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/marked": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", - "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", - "peer": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -5455,16 +5405,6 @@ "node": "*" } }, - "node_modules/monaco-editor": { - "version": "0.55.1", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", - "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", - "peer": true, - "dependencies": { - "dompurify": "3.2.7", - "marked": "14.0.0" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6359,11 +6299,6 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, - "node_modules/state-local": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", - "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" - }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3ed2e91d..30df2f41 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,6 @@ "@codemirror/theme-one-dark": "^6.1.3", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^7.2.0", "@mui/material": "^7.2.0", "@types/react-syntax-highlighter": "^15.5.13", diff --git a/frontend/src/pages/leaderboard/Leaderboard.tsx b/frontend/src/pages/leaderboard/Leaderboard.tsx index b16182ea..71547ecd 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.tsx @@ -30,6 +30,16 @@ import { } from "./components/SubmissionSidebarContext"; import SubmissionCodeSidebar from "./components/SubmissionCodeSidebar"; import CodeIcon from "@mui/icons-material/Code"; +import TerminalIcon from "@mui/icons-material/Terminal"; +import LeaderboardSubmit from "./components/LeaderboardSubmit"; +import { SubmissionMode } from "../../lib/types/mode"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from "@mui/material"; +import quickStartMarkdown from "../home/quick-start.md?raw"; const DEFAULT_SIDEBAR_WIDTH = 600; @@ -97,7 +107,8 @@ const LeaderboardContent = memo(function LeaderboardContent() { : "rankings"; })(); const [tab, setTab] = useState(initialTabFromUrl); - const [refreshFlag] = useState(false); + const [refreshFlag, setRefreshFlag] = useState(false); + const [isQuickStartOpen, setIsQuickStartOpen] = useState(false); useEffect(() => { const current = searchParams.get("tab"); @@ -188,24 +199,52 @@ const LeaderboardContent = memo(function LeaderboardContent() {

{data.name}

{isAuthed && !isExpired(data.deadline) && ( - + + + + )}
+ + {/* Quick Start Dialog */} + setIsQuickStartOpen(false)} + maxWidth="md" + fullWidth + > + Submit Your First Kernel + + + + + + + {/* Header info cards shown above tabs */} {info_items.map((info, idx) => ( @@ -281,13 +320,23 @@ const LeaderboardContent = memo(function LeaderboardContent() {
) : ( - + No Submission Yet Be the first to submit a solution for this challenge! + )} @@ -321,20 +370,13 @@ const LeaderboardContent = memo(function LeaderboardContent() { > Submission History {!isExpired(data.deadline) && ( - + setRefreshFlag((f) => !f)} + /> )} {/* Deadline Passed Message */} diff --git a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx index 6d44ef9e..b8594e23 100644 --- a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx +++ b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx @@ -4,142 +4,55 @@ import { CardContent, Typography, Button, - FormControl, - InputLabel, - Select, - MenuItem, - Stack, Alert, - CircularProgress, - Chip, IconButton, - Tooltip, Dialog, DialogTitle, DialogContent, - ToggleButton, - ToggleButtonGroup, - Drawer, - Divider, + DialogActions, + DialogContentText, + useMediaQuery, + useTheme, + Stack, + CircularProgress, + Chip, } from "@mui/material"; -import PlayArrowIcon from "@mui/icons-material/PlayArrow"; -import HistoryIcon from "@mui/icons-material/History"; -import RefreshIcon from "@mui/icons-material/Refresh"; import CloseIcon from "@mui/icons-material/Close"; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import HourglassEmptyIcon from "@mui/icons-material/HourglassEmpty"; -import CodeIcon from "@mui/icons-material/Code"; -import UploadFileIcon from "@mui/icons-material/UploadFile"; -import ExpandLessIcon from "@mui/icons-material/ExpandLess"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import TerminalIcon from "@mui/icons-material/Terminal"; +import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; +import HistoryIcon from "@mui/icons-material/History"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { fetchLeaderBoard, submitCode, - submitFile, fetchSubmissionStatus, - type SubmissionStatusResponse, } from "../../api/api"; import { fetcherApiCallback } from "../../lib/hooks/useApi"; -import { ConstrainedContainer } from "../../components/app-layout/ConstrainedContainer"; import Loading from "../../components/common/loading"; import { ErrorAlert } from "../../components/alert/ErrorAlert"; import MarkdownRenderer from "../../components/markdown-renderer/MarkdownRenderer"; import { SubmissionMode } from "../../lib/types/mode"; import { useAuthStore } from "../../lib/store/authStore"; import SubmissionHistorySection from "./components/submission-history/SubmissionHistorySection"; -import { SubmissionRunsTable } from "./components/submission-history/SubmissionRunsTable"; -import CodeMirror from "@uiw/react-codemirror"; -import { python } from "@codemirror/lang-python"; -import { oneDark } from "@codemirror/theme-one-dark"; import { useThemeStore } from "../../lib/store/themeStore"; - -const DEFAULT_CODE = "# Write your Python code here\n"; - -const styles = { - root: { - py: 3, - }, - header: { - display: "flex", - justifyContent: "space-between", - alignItems: "flex-start", - mb: 2, - }, - editorCard: { - mb: 2, - }, - editorWrapper: { - border: "1px solid", - borderColor: "divider", - borderRadius: 1, - overflow: "hidden", - }, - controlsRow: { - display: "flex", - alignItems: "center", - gap: 2, - flexWrap: "wrap", - }, - submitBtn: { - borderRadius: 2, - px: 3, - py: 1, - fontWeight: "bold", - textTransform: "none", - background: "linear-gradient(90deg, #10b981 0%, #059669 100%)", - "&:hover": { - background: "linear-gradient(90deg, #059669 0%, #047857 100%)", - }, - }, - historyBtn: { - borderRadius: 2, - textTransform: "none", - }, - statusCard: { - mt: 2, - }, - statusHeader: { - display: "flex", - alignItems: "center", - justifyContent: "space-between", - mb: 1, - }, - uploadArea: { - border: "2px dashed", - borderColor: "divider", - borderRadius: 2, - p: 4, - textAlign: "center", - cursor: "pointer", - transition: "all 0.2s", - "&:hover": { - borderColor: "primary.main", - bgcolor: "action.hover", - }, - }, - uploadAreaDragging: { - borderColor: "primary.main", - bgcolor: "action.hover", - transform: "scale(1.01)", - }, -} as const; - -type SubmitStatus = - | { kind: "idle" } - | { kind: "submitting" } - | { kind: "polling"; submissionId: number; result?: SubmissionStatusResponse } - | { kind: "done"; submissionId: number; result: SubmissionStatusResponse } - | { kind: "error"; msg: string }; - -type SubmitMode = "editor" | "upload"; +import { + CodeEditorPanel, + JobOutputPanel, + EditorControls, + ResizableSplitPanel, + DEFAULT_CODE, + editorStyles as styles, + type SubmitStatus, +} from "./components/editor"; export default function LeaderboardEditor() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const resolvedMode = useThemeStore((s) => s.resolvedMode); + const theme = useTheme(); + const isDesktop = useMediaQuery(theme.breakpoints.up("md")); const { data, loading, error, errorStatus, call } = fetcherApiCallback(fetchLeaderBoard); @@ -147,23 +60,18 @@ export default function LeaderboardEditor() { const isAuthed = !!(me && me.authenticated); const userId = me?.user?.identity ?? null; + // Resizable side panel state (for desktop) + const [sidePanelWidth, setSidePanelWidth] = useState(400); + const [isResizing, setIsResizing] = useState(false); + const containerRef = useRef(null); + // Editor state const [code, setCode] = useState(DEFAULT_CODE); + const [isEditorDirty, setIsEditorDirty] = useState(true); const [editorStatus, setEditorStatus] = useState({ kind: "idle" }); const editorPollingRef = useRef | null>(null); const editorTimeoutRef = useRef | null>(null); - - // Submit mode switch - const [submitMode, setSubmitMode] = useState("editor"); - - // Upload state - const [file, setFile] = useState(null); - const [isDragging, setIsDragging] = useState(false); - const [uploadError, setUploadError] = useState(null); - const [uploadStatus, setUploadStatus] = useState({ kind: "idle" }); const fileInputRef = useRef(null); - const uploadPollingRef = useRef | null>(null); - const uploadTimeoutRef = useRef | null>(null); // Polling timeout (20 minutes) const POLLING_TIMEOUT_MS = 20 * 60 * 1000; @@ -172,17 +80,57 @@ export default function LeaderboardEditor() { const [gpuType, setGpuType] = useState(""); const [mode, setMode] = useState(SubmissionMode.LEADERBOARD); const [historyOpen, setHistoryOpen] = useState(false); - const [refreshFlag, setRefreshFlag] = useState(false); - - // Job result drawer - const [drawerOpen, setDrawerOpen] = useState(false); - const [drawerHeight, setDrawerHeight] = useState(300); + const [refreshFlag] = useState(false); + const [confirmSubmitOpen, setConfirmSubmitOpen] = useState(false); const modes = useMemo( () => [SubmissionMode.LEADERBOARD, SubmissionMode.BENCHMARK, SubmissionMode.TEST], [] ); + // Handle panel resize + const handleMouseDown = useCallback(() => { + setIsResizing(true); + }, []); + + useEffect(() => { + let rafId: number | null = null; + + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing || !containerRef.current) return; + + if (rafId) cancelAnimationFrame(rafId); + + rafId = requestAnimationFrame(() => { + if (!containerRef.current) return; + const containerRect = containerRef.current.getBoundingClientRect(); + const newWidth = e.clientX - containerRect.left; + // Clamp between 250 and 600 + setSidePanelWidth(Math.max(250, Math.min(600, newWidth))); + }); + }; + + const handleMouseUp = () => { + if (rafId) cancelAnimationFrame(rafId); + setIsResizing(false); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + + if (isResizing) { + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + } + + return () => { + if (rafId) cancelAnimationFrame(rafId); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [isResizing]); + useEffect(() => { if (id) call(id); }, [id, call]); @@ -199,6 +147,10 @@ export default function LeaderboardEditor() { clearInterval(editorPollingRef.current); editorPollingRef.current = null; } + if (editorTimeoutRef.current) { + clearTimeout(editorTimeoutRef.current); + editorTimeoutRef.current = null; + } }, []); const startEditorPolling = useCallback( @@ -222,56 +174,29 @@ export default function LeaderboardEditor() { } }; - poll(); - editorPollingRef.current = setInterval(poll, 5000); - }, - [stopEditorPolling, id] - ); - - // Upload polling - const stopUploadPolling = useCallback(() => { - if (uploadPollingRef.current) { - clearInterval(uploadPollingRef.current); - uploadPollingRef.current = null; - } - }, []); - - const startUploadPolling = useCallback( - (submissionId: number) => { - if (!id) return; - stopUploadPolling(); - - const poll = async () => { - try { - const status = await fetchSubmissionStatus(id, submissionId); - if (!status) return; - if (status.submission_done) { - stopUploadPolling(); - setUploadStatus({ kind: "done", submissionId, result: status }); - } else { - // Show intermediate status with runs while polling - setUploadStatus({ kind: "polling", submissionId, result: status }); - } - } catch (err) { - console.error("Upload polling error:", err); - } - }; + // Set timeout (20 minutes) + editorTimeoutRef.current = setTimeout(() => { + stopEditorPolling(); + setEditorStatus({ + kind: "error", + msg: "Job timed out after 20 minutes. Please try again.", + }); + }, POLLING_TIMEOUT_MS); poll(); - uploadPollingRef.current = setInterval(poll, 5000); + editorPollingRef.current = setInterval(poll, 5000); }, - [stopUploadPolling, id] - ); + [stopEditorPolling, id, POLLING_TIMEOUT_MS] + ); useEffect(() => { return () => { stopEditorPolling(); - stopUploadPolling(); }; - }, [stopEditorPolling, stopUploadPolling]); + }, [stopEditorPolling]); - // Editor submit - const handleEditorSubmit = async () => { + // Editor submit - check if job is running first + const handleEditorSubmitClick = () => { if (!data || !id) return; if (!code.trim()) { @@ -279,12 +204,26 @@ export default function LeaderboardEditor() { return; } + // If a job is currently running, show confirmation dialog + if (editorStatus.kind === "polling") { + setConfirmSubmitOpen(true); + return; + } + + // Otherwise, submit directly + doEditorSubmit(); + }; + + const doEditorSubmit = async () => { + if (!data || !id) return; + + setConfirmSubmitOpen(false); setEditorStatus({ kind: "submitting" }); - setDrawerOpen(true); try { const result = await submitCode(id, data.name, gpuType, mode, code); if (result?.submission_id) { + setIsEditorDirty(false); startEditorPolling(result.submission_id); } else { setEditorStatus({ kind: "error", msg: "Submission accepted but no ID returned." }); @@ -295,174 +234,15 @@ export default function LeaderboardEditor() { msg: err instanceof Error ? err.message : "Submission failed", }); } - }; - - // Upload submit - const handleUploadSubmit = async () => { - if (!data || !id || !file) return; - - setUploadStatus({ kind: "submitting" }); - setUploadError(null); - setDrawerOpen(true); - - try { - const form = new FormData(); - form.set("leaderboard_id", id); - form.set("leaderboard", data.name); - form.set("gpu_type", gpuType); - form.set("submission_mode", mode); - form.set("file", file, file.name); - const res = await submitFile(form); - const submissionId = (res.data as Record)?.submission_id as number || - (res as Record)?.submission_id as number || 0; - - if (submissionId) { - startUploadPolling(submissionId); - } else { - setUploadStatus({ kind: "error", msg: "Submission accepted but no ID returned." }); - } - } catch (err) { - setUploadStatus({ - kind: "error", - msg: err instanceof Error ? err.message : "Submission failed", - }); - } - }; - - const handleFileSelect = (e: React.ChangeEvent) => { - const f = e.target.files?.[0] ?? null; - if (!f) return; - validateAndSetFile(f); - }; - - const validateAndSetFile = (f: File) => { - // Clear previous errors and status - setUploadError(null); - setUploadStatus({ kind: "idle" }); - - const name = f.name.toLowerCase(); - if (!name.endsWith(".py")) { - setUploadError("Please select a .py file."); - setFile(null); - return; - } - if (f.size > 1 * 1024 * 1024) { - setUploadError("File too large (> 1 MB)"); - setFile(null); - return; - } - setFile(f); - }; - - const handleDragEnter = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(true); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - - const files = e.dataTransfer.files; - if (files && files.length > 0) { - validateAndSetFile(files[0]); - } - }; + }; const canEditorSubmit = useMemo(() => { if (!gpuType || !mode) return false; - return code.trim().length > 0; - }, [code, gpuType, mode]); - - const canUploadSubmit = useMemo(() => { - if (!gpuType || !mode) return false; - return !!file; - }, [file, gpuType, mode]); - - const renderEditorStatusChip = () => { - switch (editorStatus.kind) { - case "submitting": - return ( - } - label="Submitting..." - color="info" - size="small" - /> - ); - case "polling": - return ( - } - label="Running..." - color="warning" - size="small" - /> - ); - case "done": - return ( - } - label="Completed" - color="success" - size="small" - /> - ); - case "error": - return ; - default: - return null; - } - }; - - const renderUploadStatusChip = () => { - switch (uploadStatus.kind) { - case "submitting": - return ( - } - label="Submitting..." - color="info" - size="small" - /> - ); - case "polling": - return ( - } - label="Running..." - color="warning" - size="small" - /> - ); - case "done": - return ( - } - label="Completed" - color="success" - size="small" - /> - ); - case "error": - return ; - default: - return null; - } - }; + if (!code.trim()) return false; + // Only enable if editor has been modified + if (!isEditorDirty) return false; + return true; + }, [code, gpuType, mode, isEditorDirty]); if (loading) return ; if (error) return ; @@ -470,7 +250,7 @@ export default function LeaderboardEditor() { if (!isAuthed) { return ( - + Submit Code @@ -479,13 +259,13 @@ export default function LeaderboardEditor() { Please login to submit your code. - + ); } return ( - - + + {/* Header */} @@ -496,323 +276,193 @@ export default function LeaderboardEditor() { Submit your Python code - + + {/* Show user's best rank and score */} + {(() => { + const priorityGpu = data.gpu_types?.[0] || ""; + const rankings = data.rankings?.[priorityGpu] || []; + const myBest = rankings.find( + (r) => r.user_name === me?.user?.display_name + ); + if (myBest) { + return ( + } + label={`Rank #${myBest.rank} • Score: ${myBest.score.toFixed(2)}`} + color="primary" + variant="outlined" + /> + ); + } + return null; + })()} - - {/* Description */} - - -
- - Challenge 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")) - )} - -
  • - ))} -
-
- )} -
-
-
- - {/* Submission Section with Toggle */} - - - {/* Toggle between Editor and Upload */} - - v && setSubmitMode(v)} - size="small" - > - - - Code Editor - - - - Upload File - - - - - {/* Editor Mode */} - {submitMode === "editor" && ( - <> - - setCode(value)} - basicSetup={{ - lineNumbers: true, - highlightActiveLineGutter: true, - highlightActiveLine: true, - bracketMatching: true, - closeBrackets: true, - autocompletion: true, - indentOnInput: true, - }} - /> - - - {/* Editor Controls */} - - - GPU Type - - - - - Mode - - - - - - {renderEditorStatusChip()} - - {editorStatus.kind === "polling" && ( - - startEditorPolling(editorStatus.submissionId)}> - - - - )} - - - {/* Editor Status Messages */} - {editorStatus.kind === "error" && ( - - {editorStatus.msg} - - )} - - {editorStatus.kind === "done" && ( - - Submission completed! Check the history for results. - - )} - - )} - - {/* Upload Mode */} - {submitMode === "upload" && ( - <> - {file ? ( - - - - - {file.name} - - {(file.size / 1024).toFixed(1)} KB - - - - { - setFile(null); - setUploadError(null); - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - }} - sx={{ color: "text.secondary" }} - > - - - - ) : ( - fileInputRef.current?.click()} - onDragEnter={handleDragEnter} - onDragLeave={handleDragLeave} - onDragOver={handleDragOver} - onDrop={handleDrop} - > - - - - Drag & drop or click to select a .py file + {/* Main Content - Side by side on desktop, stacked on mobile */} + + {/* Description Panel */} + + + + + Challenge 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")) + )} + +
  • + ))} +
)} +
+
+
- {/* Upload Error */} - {uploadError && ( - - {uploadError} - - )} - - {/* Upload Controls */} - - - GPU Type - - - - - Mode - - - - - - {renderUploadStatusChip()} - - {uploadStatus.kind === "polling" && ( - - startUploadPolling(uploadStatus.submissionId)}> - - - - )} - - - {/* Upload Status Messages */} - {uploadStatus.kind === "error" && ( - - {uploadStatus.msg} - - )} + const reader = new FileReader(); + reader.onload = (event) => { + const content = event.target?.result as string; + setCode(content); + setIsEditorDirty(true); + }; + reader.onerror = () => { + setEditorStatus({ kind: "error", msg: "Failed to read file" }); + }; + reader.readAsText(selectedFile); + }} + /> + + {/* Editor + Output split panel */} + { + setCode(value); + setIsEditorDirty(true); + }} + resolvedMode={resolvedMode} + /> + } + bottomPanel={ + + } + /> - {uploadStatus.kind === "done" && ( - - Submission completed! Check the history for results. - - )} - + {/* Controls */} + setHistoryOpen(true)} + onUploadClick={() => fileInputRef.current?.click()} + /> + + {/* Status Messages */} + {editorStatus.kind === "error" && ( + + {editorStatus.msg} + + )} + + {editorStatus.kind === "done" && ( + + Submission completed! + )}
+
+
{/* History Dialog */} - {/* Job Result Drawer - Fixed Bottom Panel */} - setDrawerOpen(false)} - variant="persistent" - sx={{ - "& .MuiDrawer-paper": { - height: drawerHeight, - borderTopLeftRadius: 12, - borderTopRightRadius: 12, - boxShadow: "0 -4px 20px rgba(0,0,0,0.15)", - }, - }} + {/* Confirm Submit Dialog */} + setConfirmSubmitOpen(false)} > - - - - - - Job Output - - {(editorStatus.kind === "polling" || uploadStatus.kind === "polling") && ( - } - label="Running..." - color="warning" - size="small" - /> - )} - {(editorStatus.kind === "done" || uploadStatus.kind === "done") && ( - } label="Completed" color="success" size="small" /> - )} - - - setDrawerHeight(drawerHeight === 300 ? 500 : 300)} - > - {drawerHeight === 300 ? : } - - setDrawerOpen(false)}> - - - - - - - {/* Show current job result */} - {editorStatus.kind === "done" && editorStatus.result && ( - <> - - ✓ Submission #{editorStatus.submissionId} - {editorStatus.result.status} - - {editorStatus.result.error && ( - - {editorStatus.result.error} - - )} - {editorStatus.result.runs && editorStatus.result.runs.length > 0 && ( - - )} - - )} - {uploadStatus.kind === "done" && uploadStatus.result && ( - <> - - ✓ Submission #{uploadStatus.submissionId} - {uploadStatus.result.status} - - {uploadStatus.result.error && ( - - {uploadStatus.result.error} - - )} - {uploadStatus.result.runs && uploadStatus.result.runs.length > 0 && ( - - )} - - )} - {(editorStatus.kind === "submitting" || uploadStatus.kind === "submitting") && ( - - - - Submitting... - - - )} - {(editorStatus.kind === "polling" || uploadStatus.kind === "polling") && ( - <> - - - - Running... - - - {/* Show intermediate runs while polling */} - {editorStatus.kind === "polling" && editorStatus.result?.runs && editorStatus.result.runs.length > 0 && ( - - )} - {uploadStatus.kind === "polling" && uploadStatus.result?.runs && uploadStatus.result.runs.length > 0 && ( - - )} - - )} - {editorStatus.kind === "idle" && uploadStatus.kind === "idle" && ( - - No job running. Submit code to see output here. - - )} - {editorStatus.kind === "error" && ( - {editorStatus.msg} - )} - {uploadStatus.kind === "error" && ( - {uploadStatus.msg} - )} - - - - - {/* Floating button to open drawer */} - {!drawerOpen && (editorStatus.kind !== "idle" || uploadStatus.kind !== "idle") && ( - - + - - )} + + +
-
+ ); } diff --git a/frontend/src/pages/leaderboard/components/editor/ChallengeDescriptionPanel.tsx b/frontend/src/pages/leaderboard/components/editor/ChallengeDescriptionPanel.tsx new file mode 100644 index 00000000..6341cdf6 --- /dev/null +++ b/frontend/src/pages/leaderboard/components/editor/ChallengeDescriptionPanel.tsx @@ -0,0 +1,66 @@ +import { Box, Card, CardContent, Typography, Stack, Chip } from "@mui/material"; +import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; +import MarkdownRenderer from "../../../../components/markdown-renderer/MarkdownRenderer"; + +interface BenchmarkShape { + name: string; + dims: unknown[]; +} + +interface ChallengeDescriptionPanelProps { + title: string; + description: string; + benchmarkShapes?: BenchmarkShape[]; + isDesktop: boolean; + height?: string; +} + +export function ChallengeDescriptionPanel({ + title, + description, + benchmarkShapes, + isDesktop, + height, +}: ChallengeDescriptionPanelProps) { + return ( + + + + + + {title} + + + + + Challenge Description + + + + + {benchmarkShapes && benchmarkShapes.length > 0 && ( + + + Benchmark Shapes + + + {benchmarkShapes.map((shape, idx) => ( + + ))} + + + )} + + + ); +} diff --git a/frontend/src/pages/leaderboard/components/editor/CodeEditorPanel.tsx b/frontend/src/pages/leaderboard/components/editor/CodeEditorPanel.tsx new file mode 100644 index 00000000..2ffaca05 --- /dev/null +++ b/frontend/src/pages/leaderboard/components/editor/CodeEditorPanel.tsx @@ -0,0 +1,42 @@ +import { Box } from "@mui/material"; +import CodeMirror from "@uiw/react-codemirror"; +import { python } from "@codemirror/lang-python"; +import { oneDark } from "@codemirror/theme-one-dark"; + +interface CodeEditorPanelProps { + code: string; + onChange: (value: string) => void; + resolvedMode: "light" | "dark"; + height?: string; + style?: React.CSSProperties; +} + +export function CodeEditorPanel({ + code, + onChange, + resolvedMode, + height = "100%", + style, +}: CodeEditorPanelProps) { + return ( + + + + ); +} diff --git a/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx b/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx new file mode 100644 index 00000000..3ee5a68c --- /dev/null +++ b/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx @@ -0,0 +1,115 @@ +import { + Box, + Stack, + Button, + FormControl, + InputLabel, + Select, + MenuItem, + Tooltip, + IconButton, + CircularProgress, +} from "@mui/material"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import HistoryIcon from "@mui/icons-material/History"; +import UploadFileIcon from "@mui/icons-material/UploadFile"; +import { editorStyles } from "./types"; + +interface EditorControlsProps { + gpuType: string; + setGpuType: (value: string) => void; + gpuTypes: string[]; + mode: string; + setMode: (value: string) => void; + modes: string[]; + canSubmit: boolean; + isSubmitting: boolean; + onSubmit: () => void; + onHistoryClick: () => void; + onUploadClick: () => void; +} + +export function EditorControls({ + gpuType, + setGpuType, + gpuTypes, + mode, + setMode, + modes, + canSubmit, + isSubmitting, + onSubmit, + onHistoryClick, + onUploadClick, +}: EditorControlsProps) { + return ( + + + GPU Type + + + + + Mode + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/pages/leaderboard/components/editor/JobOutputPanel.tsx b/frontend/src/pages/leaderboard/components/editor/JobOutputPanel.tsx new file mode 100644 index 00000000..31c97aea --- /dev/null +++ b/frontend/src/pages/leaderboard/components/editor/JobOutputPanel.tsx @@ -0,0 +1,167 @@ +import { Box, Stack, Typography, Chip, CircularProgress } from "@mui/material"; +import TerminalIcon from "@mui/icons-material/Terminal"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import { type SubmitStatus } from "./types"; + +interface JobOutputPanelProps { + editorStatus: SubmitStatus; + uploadStatus: SubmitStatus; +} + +export function JobOutputPanel({ editorStatus, uploadStatus }: JobOutputPanelProps) { + return ( + + + + + Job Output + + {(editorStatus.kind === "polling" || uploadStatus.kind === "polling") && ( + } + label="Running" + color="warning" + size="small" + sx={{ height: 20, fontSize: "0.7rem" }} + /> + )} + {(editorStatus.kind === "done" || uploadStatus.kind === "done") && ( + } + label="Done" + color="success" + size="small" + sx={{ height: 20, fontSize: "0.7rem" }} + /> + )} + + + {/* Log-style output */} + {editorStatus.kind === "idle" && uploadStatus.kind === "idle" && ( +
$ waiting for submission...
+ )} + + {(editorStatus.kind === "submitting" || uploadStatus.kind === "submitting") && ( +
Submitting code...
+ )} + + {editorStatus.kind === "polling" && ( + +
+ Submission #{editorStatus.submissionId} started +
+ {editorStatus.result?.runs?.map((run, i) => ( + +
+ [{run.mode.toUpperCase()}]{" "} + + {run.passed ? "✓ PASSED" : "✗ FAILED"} + + {run.score != null && ( + + {" "} + (score: {run.score.toFixed(2)}) + + )} +
+ {run.meta && Object.keys(run.meta).length > 0 && ( + + {Object.entries(run.meta).map(([key, value]) => ( +
+ {key}: {typeof value === "object" ? JSON.stringify(value) : String(value)} +
+ ))} +
+ )} + {run.report && Object.keys(run.report).length > 0 && ( + + {Object.entries(run.report).map(([key, value]) => ( +
+ {key}: {typeof value === "object" ? JSON.stringify(value) : String(value)} +
+ ))} +
+ )} +
+ ))} +
+ + Running... +
+
+ )} + + {editorStatus.kind === "done" && editorStatus.result && ( + +
+ Submission #{editorStatus.submissionId} completed +
+ {editorStatus.result.error && ( +
[ERROR] {editorStatus.result.error}
+ )} + {editorStatus.result.runs?.map((run, i) => ( + +
+ [{run.mode.toUpperCase()}]{" "} + + {run.passed ? "✓ PASSED" : "✗ FAILED"} + + {run.score != null && ( + + {" "} + (score: {run.score.toFixed(2)}) + + )} +
+ {run.meta && Object.keys(run.meta).length > 0 && ( + + {Object.entries(run.meta).map(([key, value]) => ( +
+ {key}: {typeof value === "object" ? JSON.stringify(value) : String(value)} +
+ ))} +
+ )} + {run.report && Object.keys(run.report).length > 0 && ( + + {Object.entries(run.report).map(([key, value]) => ( +
+ {key}: {typeof value === "object" ? JSON.stringify(value) : String(value)} +
+ ))} +
+ )} +
+ ))} +
$ Done
+
+ )} + + {editorStatus.kind === "error" && ( +
[ERROR] {editorStatus.msg}
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/leaderboard/components/editor/ResizableSplitPanel.tsx b/frontend/src/pages/leaderboard/components/editor/ResizableSplitPanel.tsx new file mode 100644 index 00000000..0296915e --- /dev/null +++ b/frontend/src/pages/leaderboard/components/editor/ResizableSplitPanel.tsx @@ -0,0 +1,115 @@ +import { Box } from "@mui/material"; +import { useRef, useState, useCallback, useEffect, type ReactNode } from "react"; + +interface ResizableSplitPanelProps { + topPanel: ReactNode; + bottomPanel: ReactNode; + initialRatio?: number; + minRatio?: number; + maxRatio?: number; + height?: string; + minHeight?: number; +} + +export function ResizableSplitPanel({ + topPanel, + bottomPanel, + initialRatio = 0.6, + minRatio = 0.2, + maxRatio = 0.8, + height = "calc(100vh - 280px)", + minHeight = 400, +}: ResizableSplitPanelProps) { + const [ratio, setRatio] = useState(initialRatio); + const [isResizing, setIsResizing] = useState(false); + const panelRef = useRef(null); + const startY = useRef(0); + const startRatio = useRef(0); + + const handleResizeStart = useCallback( + (e: React.MouseEvent) => { + startY.current = e.clientY; + startRatio.current = ratio; + setIsResizing(true); + }, + [ratio] + ); + + useEffect(() => { + let rafId: number | null = null; + + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing || !panelRef.current) return; + + if (rafId) cancelAnimationFrame(rafId); + + rafId = requestAnimationFrame(() => { + if (!panelRef.current) return; + const panelRect = panelRef.current.getBoundingClientRect(); + const deltaY = e.clientY - startY.current; + const deltaRatio = deltaY / panelRect.height; + const newRatio = startRatio.current + deltaRatio; + setRatio(Math.max(minRatio, Math.min(maxRatio, newRatio))); + }); + }; + + const handleMouseUp = () => { + if (rafId) cancelAnimationFrame(rafId); + setIsResizing(false); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + + if (isResizing) { + document.body.style.cursor = "row-resize"; + document.body.style.userSelect = "none"; + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + } + + return () => { + if (rafId) cancelAnimationFrame(rafId); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [isResizing, minRatio, maxRatio]); + + return ( + + {/* Top panel (Editor) */} + {topPanel} + + {/* Resize handle */} + + + + + {/* Bottom panel (Output) */} + + {bottomPanel} + + + ); +} diff --git a/frontend/src/pages/leaderboard/components/editor/index.ts b/frontend/src/pages/leaderboard/components/editor/index.ts new file mode 100644 index 00000000..d2df29c6 --- /dev/null +++ b/frontend/src/pages/leaderboard/components/editor/index.ts @@ -0,0 +1,6 @@ +export { CodeEditorPanel } from "./CodeEditorPanel"; +export { JobOutputPanel } from "./JobOutputPanel"; +export { EditorControls } from "./EditorControls"; +export { ChallengeDescriptionPanel } from "./ChallengeDescriptionPanel"; +export { ResizableSplitPanel } from "./ResizableSplitPanel"; +export { DEFAULT_CODE, editorStyles, type SubmitStatus } from "./types"; diff --git a/frontend/src/pages/leaderboard/components/editor/types.ts b/frontend/src/pages/leaderboard/components/editor/types.ts new file mode 100644 index 00000000..c39545d8 --- /dev/null +++ b/frontend/src/pages/leaderboard/components/editor/types.ts @@ -0,0 +1,87 @@ +import { type SubmissionStatusResponse } from "../../../../api/api"; + +export type SubmitStatus = + | { kind: "idle" } + | { kind: "submitting" } + | { kind: "polling"; submissionId: number; result?: SubmissionStatusResponse } + | { kind: "done"; submissionId: number; result: SubmissionStatusResponse } + | { kind: "error"; msg: string }; + +export const DEFAULT_CODE = `import torch +from task import input_t, output_t + + +def custom_kernel(data: input_t) -> output_t: + # Your implementation here + # Example: return torch.nn.functional.softmax(data, dim=-1) + pass +`; + +export const editorStyles = { + root: { + py: 3, + }, + header: { + display: "flex", + justifyContent: "space-between", + alignItems: "flex-start", + mb: 2, + }, + editorCard: { + mb: 2, + }, + editorWrapper: { + border: "1px solid", + borderColor: "divider", + borderRadius: 1, + overflow: "hidden", + }, + controlsRow: { + display: "flex", + alignItems: "center", + gap: 2, + flexWrap: "wrap", + }, + submitBtn: { + borderRadius: 2, + px: 3, + py: 1, + fontWeight: "bold", + textTransform: "none", + background: "linear-gradient(90deg, #10b981 0%, #059669 100%)", + "&:hover": { + background: "linear-gradient(90deg, #059669 0%, #047857 100%)", + }, + }, + historyBtn: { + borderRadius: 2, + textTransform: "none", + }, + statusCard: { + mt: 2, + }, + statusHeader: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + mb: 1, + }, + uploadArea: { + border: "2px dashed", + borderColor: "divider", + borderRadius: 2, + p: 4, + textAlign: "center", + cursor: "pointer", + transition: "all 0.2s", + "&:hover": { + borderColor: "primary.main", + bgcolor: "action.hover", + }, + }, + uploadAreaDragging: { + borderColor: "primary.main", + bgcolor: "action.hover", + transform: "scale(1.01)", + }, +} as const; diff --git a/kernelboard/api/submission.py b/kernelboard/api/submission.py index 6c7d7a0e..899b7176 100644 --- a/kernelboard/api/submission.py +++ b/kernelboard/api/submission.py @@ -92,8 +92,10 @@ def submission(): submission_mode = request.form.get("submission_mode") leaderboard_name = request.form.get("leaderboard") + logger.info(f"USE_MOCK_SUBMISSION: {USE_MOCK_SUBMISSION}, leaderboard_name: {leaderboard_name},submission_mode {submission_mode}, gpu_type {gpu_type}") + # DEV: Use mock submission (writes directly to local DB) - if USE_MOCK_SUBMISSION == "true": + if USE_MOCK_SUBMISSION: logging.warning("[!MOCK DATA!]USE_MOCK_SUBMISSION is on! this should only be used in dev mode!") from kernelboard.lib.mocks.mock_submission import create_mock_submission files = {"file": (filename, f.stream, mime)} @@ -105,6 +107,8 @@ def submission(): submission_mode=submission_mode, failure_mode=MOCK_FAILURE_MODE, ) + else: + logger.info(f"USE_MOCK_SUBMISSION {USE_MOCK_SUBMISSION}send submission request to cluster-management api") base = get_cluster_manager_endpoint() url = f"{base}/submission/{leaderboard_name}/{gpu_type}/{submission_mode}" From 3cb3164a5fc3ad5d8d7624466848830a3b2b7dc4 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 5 Mar 2026 19:05:24 -0800 Subject: [PATCH 19/26] test3 --- frontend/src/pages/leaderboard/LeaderboardEditor.tsx | 2 +- .../src/pages/leaderboard/components/editor/index.ts | 2 +- .../src/pages/leaderboard/components/editor/types.ts | 10 ---------- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx index b8594e23..6c2adc39 100644 --- a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx +++ b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx @@ -66,7 +66,7 @@ export default function LeaderboardEditor() { const containerRef = useRef(null); // Editor state - const [code, setCode] = useState(DEFAULT_CODE); + const [code, setCode] = useState(""); const [isEditorDirty, setIsEditorDirty] = useState(true); const [editorStatus, setEditorStatus] = useState({ kind: "idle" }); const editorPollingRef = useRef | null>(null); diff --git a/frontend/src/pages/leaderboard/components/editor/index.ts b/frontend/src/pages/leaderboard/components/editor/index.ts index d2df29c6..5e7621aa 100644 --- a/frontend/src/pages/leaderboard/components/editor/index.ts +++ b/frontend/src/pages/leaderboard/components/editor/index.ts @@ -3,4 +3,4 @@ export { JobOutputPanel } from "./JobOutputPanel"; export { EditorControls } from "./EditorControls"; export { ChallengeDescriptionPanel } from "./ChallengeDescriptionPanel"; export { ResizableSplitPanel } from "./ResizableSplitPanel"; -export { DEFAULT_CODE, editorStyles, type SubmitStatus } from "./types"; +export { editorStyles, type SubmitStatus } from "./types"; diff --git a/frontend/src/pages/leaderboard/components/editor/types.ts b/frontend/src/pages/leaderboard/components/editor/types.ts index c39545d8..12282a9e 100644 --- a/frontend/src/pages/leaderboard/components/editor/types.ts +++ b/frontend/src/pages/leaderboard/components/editor/types.ts @@ -7,16 +7,6 @@ export type SubmitStatus = | { kind: "done"; submissionId: number; result: SubmissionStatusResponse } | { kind: "error"; msg: string }; -export const DEFAULT_CODE = `import torch -from task import input_t, output_t - - -def custom_kernel(data: input_t) -> output_t: - # Your implementation here - # Example: return torch.nn.functional.softmax(data, dim=-1) - pass -`; - export const editorStyles = { root: { py: 3, From e9648bf78cb7d1bd6dc050b010521d3bd90d317a Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 5 Mar 2026 19:20:47 -0800 Subject: [PATCH 20/26] test3 --- .../pages/leaderboard/LeaderboardEditor.tsx | 70 +++++++++---------- .../components/editor/EditorControls.tsx | 17 +---- .../components/editor/ResizableSplitPanel.tsx | 38 +++++----- .../leaderboard/components/editor/types.ts | 3 +- 4 files changed, 57 insertions(+), 71 deletions(-) diff --git a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx index 6c2adc39..d91b0b72 100644 --- a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx +++ b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx @@ -14,12 +14,9 @@ import { useMediaQuery, useTheme, Stack, - CircularProgress, Chip, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; -import CheckCircleIcon from "@mui/icons-material/CheckCircle"; -import HourglassEmptyIcon from "@mui/icons-material/HourglassEmpty"; import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; import HistoryIcon from "@mui/icons-material/History"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -42,11 +39,12 @@ import { JobOutputPanel, EditorControls, ResizableSplitPanel, - DEFAULT_CODE, editorStyles as styles, type SubmitStatus, } from "./components/editor"; +const DEFAULT_CODE = `# Write your code here`; + export default function LeaderboardEditor() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); @@ -66,15 +64,15 @@ export default function LeaderboardEditor() { const containerRef = useRef(null); // Editor state - const [code, setCode] = useState(""); + const [code, setCode] = useState(DEFAULT_CODE); const [isEditorDirty, setIsEditorDirty] = useState(true); const [editorStatus, setEditorStatus] = useState({ kind: "idle" }); const editorPollingRef = useRef | null>(null); const editorTimeoutRef = useRef | null>(null); const fileInputRef = useRef(null); - // Polling timeout (20 minutes) - const POLLING_TIMEOUT_MS = 20 * 60 * 1000; + // Polling timeout (15 minutes) + const POLLING_TIMEOUT_MS = 15 * 60 * 1000; // Common state const [gpuType, setGpuType] = useState(""); @@ -179,7 +177,7 @@ export default function LeaderboardEditor() { stopEditorPolling(); setEditorStatus({ kind: "error", - msg: "Job timed out after 20 minutes. Please try again.", + msg: "Job timed out after 15 minutes. Please try again or refresh it manually.", }); }, POLLING_TIMEOUT_MS); @@ -412,7 +410,7 @@ export default function LeaderboardEditor() { }} /> - {/* Editor + Output split panel */} + {/* Editor + Controls + Output - all fit screen */} } + middleContent={ + <> + fileInputRef.current?.click()} + /> + {editorStatus.kind === "error" && ( + + {editorStatus.msg} + + )} + {editorStatus.kind === "done" && ( + + Submission completed! + + )} + + } bottomPanel={ } /> - - {/* Controls */} - setHistoryOpen(true)} - onUploadClick={() => fileInputRef.current?.click()} - /> - - {/* Status Messages */} - {editorStatus.kind === "error" && ( - - {editorStatus.msg} - - )} - - {editorStatus.kind === "done" && ( - - Submission completed! - - )} diff --git a/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx b/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx index 3ee5a68c..30d6a91c 100644 --- a/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx +++ b/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx @@ -1,5 +1,4 @@ import { - Box, Stack, Button, FormControl, @@ -11,7 +10,6 @@ import { CircularProgress, } from "@mui/material"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; -import HistoryIcon from "@mui/icons-material/History"; import UploadFileIcon from "@mui/icons-material/UploadFile"; import { editorStyles } from "./types"; @@ -25,7 +23,6 @@ interface EditorControlsProps { canSubmit: boolean; isSubmitting: boolean; onSubmit: () => void; - onHistoryClick: () => void; onUploadClick: () => void; } @@ -39,7 +36,6 @@ export function EditorControls({ canSubmit, isSubmitting, onSubmit, - onHistoryClick, onUploadClick, }: EditorControlsProps) { return ( @@ -94,22 +90,11 @@ export function EditorControls({ {isSubmitting ? "Submitting..." : "Submit"} - - - + - - ); } diff --git a/frontend/src/pages/leaderboard/components/editor/ResizableSplitPanel.tsx b/frontend/src/pages/leaderboard/components/editor/ResizableSplitPanel.tsx index 0296915e..961c1c20 100644 --- a/frontend/src/pages/leaderboard/components/editor/ResizableSplitPanel.tsx +++ b/frontend/src/pages/leaderboard/components/editor/ResizableSplitPanel.tsx @@ -4,6 +4,7 @@ import { useRef, useState, useCallback, useEffect, type ReactNode } from "react" interface ResizableSplitPanelProps { topPanel: ReactNode; bottomPanel: ReactNode; + middleContent?: ReactNode; initialRatio?: number; minRatio?: number; maxRatio?: number; @@ -14,6 +15,7 @@ interface ResizableSplitPanelProps { export function ResizableSplitPanel({ topPanel, bottomPanel, + middleContent, initialRatio = 0.6, minRatio = 0.2, maxRatio = 0.8, @@ -87,23 +89,25 @@ export function ResizableSplitPanel({ {/* Top panel (Editor) */} {topPanel} - {/* Resize handle */} - - + {/* Middle content (Controls) + Resize handle */} + + {middleContent} + + + {/* Bottom panel (Output) */} diff --git a/frontend/src/pages/leaderboard/components/editor/types.ts b/frontend/src/pages/leaderboard/components/editor/types.ts index 12282a9e..0f358be9 100644 --- a/frontend/src/pages/leaderboard/components/editor/types.ts +++ b/frontend/src/pages/leaderboard/components/editor/types.ts @@ -5,7 +5,8 @@ export type SubmitStatus = | { kind: "submitting" } | { kind: "polling"; submissionId: number; result?: SubmissionStatusResponse } | { kind: "done"; submissionId: number; result: SubmissionStatusResponse } - | { kind: "error"; msg: string }; + | { kind: "error"; msg: string } + | { kind: "warning"; msg: string }; export const editorStyles = { root: { From 851cdb4fd730a2738dd07fb187947176ae7e21b0 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 5 Mar 2026 19:25:36 -0800 Subject: [PATCH 21/26] test3 --- .../src/pages/leaderboard/Leaderboard.tsx | 1 + .../pages/leaderboard/LeaderboardEditor.tsx | 19 ------- .../components/editor/ResizableSplitPanel.tsx | 52 +++++++++++++++---- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/frontend/src/pages/leaderboard/Leaderboard.tsx b/frontend/src/pages/leaderboard/Leaderboard.tsx index 71547ecd..5a6ed438 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.tsx @@ -221,6 +221,7 @@ const LeaderboardContent = memo(function LeaderboardContent() { onClick={() => navigate(`/leaderboard/${id}/editor`)} sx={{ borderRadius: 2, + fontWeight: "bold", textTransform: "none", }} > diff --git a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx index d91b0b72..5e1ac4d6 100644 --- a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx +++ b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx @@ -275,25 +275,6 @@ export default function LeaderboardEditor() { - {/* Show user's best rank and score */} - {(() => { - const priorityGpu = data.gpu_types?.[0] || ""; - const rankings = data.rankings?.[priorityGpu] || []; - const myBest = rankings.find( - (r) => r.user_name === me?.user?.display_name - ); - if (myBest) { - return ( - } - label={`Rank #${myBest.rank} • Score: ${myBest.score.toFixed(2)}`} - color="primary" - variant="outlined" - /> - ); - } - return null; - })()} - - - )} + + + + {/* Quick Start Dialog */} diff --git a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx index 5e1ac4d6..f2065669 100644 --- a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx +++ b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx @@ -17,7 +17,6 @@ import { Chip, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; -import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; import HistoryIcon from "@mui/icons-material/History"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; diff --git a/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx b/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx index 30d6a91c..d82033fb 100644 --- a/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx +++ b/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx @@ -91,9 +91,15 @@ export function EditorControls({ - - - + ); From e09728075d65932f862d45b93c67bd7ea65bb15b Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 5 Mar 2026 19:37:55 -0800 Subject: [PATCH 23/26] test3 --- kernelboard/api/submission.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/kernelboard/api/submission.py b/kernelboard/api/submission.py index 899b7176..df2e338f 100644 --- a/kernelboard/api/submission.py +++ b/kernelboard/api/submission.py @@ -92,8 +92,6 @@ def submission(): submission_mode = request.form.get("submission_mode") leaderboard_name = request.form.get("leaderboard") - logger.info(f"USE_MOCK_SUBMISSION: {USE_MOCK_SUBMISSION}, leaderboard_name: {leaderboard_name},submission_mode {submission_mode}, gpu_type {gpu_type}") - # DEV: Use mock submission (writes directly to local DB) if USE_MOCK_SUBMISSION: logging.warning("[!MOCK DATA!]USE_MOCK_SUBMISSION is on! this should only be used in dev mode!") From 1ed218296a2c418c91e35d82c5ad52a5aab78bb0 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 5 Mar 2026 19:40:22 -0800 Subject: [PATCH 24/26] test3 --- frontend/src/api/api.ts | 2 +- frontend/src/pages/leaderboard/LeaderboardEditor.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 689c2b9e..eacc266c 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -417,7 +417,7 @@ export async function submitCode( method: "POST", body: form, }); - } catch (err) { + } catch (_err) { throw new Error("Network error: Unable to connect to server"); } diff --git a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx index f2065669..9cd1d6ec 100644 --- a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx +++ b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx @@ -14,7 +14,6 @@ import { useMediaQuery, useTheme, Stack, - Chip, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; import HistoryIcon from "@mui/icons-material/History"; From e4cd5a7b1813d6b710a89df3188762da8c8631c6 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 5 Mar 2026 19:43:25 -0800 Subject: [PATCH 25/26] test3 --- .../src/pages/leaderboard/Leaderboard.tsx | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/leaderboard/Leaderboard.tsx b/frontend/src/pages/leaderboard/Leaderboard.tsx index ecf151f1..deb23c94 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.tsx @@ -213,19 +213,21 @@ const LeaderboardContent = memo(function LeaderboardContent() { > Submit via CLI - + {searchParams.has("editor") && ( + + )} From 19c2cd07d55d08db2642ba41e867922f8792d61e Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 6 Mar 2026 07:36:21 -0800 Subject: [PATCH 26/26] test3 --- .../src/pages/leaderboard/components/editor/EditorControls.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx b/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx index d82033fb..f6170157 100644 --- a/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx +++ b/frontend/src/pages/leaderboard/components/editor/EditorControls.tsx @@ -6,7 +6,6 @@ import { Select, MenuItem, Tooltip, - IconButton, CircularProgress, } from "@mui/material"; import PlayArrowIcon from "@mui/icons-material/PlayArrow";