Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ jobs:
- name: Install frontend dependencies
run: cd frontend && npm ci

- name: Lint Python
run: ruff check kernelboard/ tests/

- name: Lint frontend
run: cd frontend && npm run lint

- name: Run Python tests
run: pytest --tb=short
continue-on-error: true
Expand Down
36 changes: 36 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,42 @@ Look for the `hackathons` array around line 87. To add a hackathon:
Upcoming lectures are pulled live from Discord's scheduled events API (5-minute cache).
Requires `DISCORD_BOT_TOKEN` and `DISCORD_GUILD_ID` environment variables.

## Database Access

The production PostgreSQL database is hosted on Heroku under the `discord-cluster-manager` app:
- **Heroku Dashboard:** https://dashboard.heroku.com/apps/discord-cluster-manager
- **Connection:** Use `heroku pg:psql -a discord-cluster-manager` to connect interactively
- **Credentials:** Use `heroku pg:credentials:url -a discord-cluster-manager` to get the connection string
- **Schema:** All tables live under the `leaderboard` schema (e.g., `leaderboard.runs`, `leaderboard.submission`, `leaderboard.leaderboard`, `leaderboard.user_info`, `leaderboard.code_files`, `leaderboard.gpu_type`, `leaderboard.submission_job_status`)

### Key Tables
- `leaderboard.leaderboard` - Competition definitions (name, deadline, task JSONB)
- `leaderboard.submission` - User submissions linked to code files
- `leaderboard.runs` - Individual run results with scores (lower is better), GPU type, pass/fail
- `leaderboard.user_info` - User accounts (Discord/Google/GitHub OAuth)
- `leaderboard.gpu_type` - GPU types supported per leaderboard
- `leaderboard.code_files` - Submitted code with SHA256 hash
- `leaderboard.submission_job_status` - Job tracking (pending/running/succeeded/failed/timed_out)

### Ranking Logic
Rankings are computed via SQL window functions:
1. Best run per user per GPU type (lowest score wins, must be `passed=true`, `secret=false`, `score IS NOT NULL`)
2. Global rank via `RANK() OVER (PARTITION BY leaderboard_id, runner ORDER BY score ASC)`
3. GPU priority order: B200 > H100 > MI300 > A100 > L4 > T4

## Linting

CI enforces linting for both Python and the frontend. PRs will fail if either linter reports errors.

**Python (Ruff):**
- Check: `ruff check kernelboard/ tests/`
- Auto-fix: `ruff check --fix kernelboard/ tests/`
- Config: `pyproject.toml` under `[tool.ruff]`

**Frontend (ESLint):**
- Check: `cd frontend && npm run lint`
- Auto-fix: `cd frontend && npm run lint -- --fix`

## Project Structure

- `kernelboard/` - Flask backend
Expand Down
8 changes: 7 additions & 1 deletion frontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@ export default tseslint.config(
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
'error',
{ allowConstantExport: true },
],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
'eol-last': ['error', 'always']
},
},
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import App from "./App";

Expand Down
109 changes: 89 additions & 20 deletions frontend/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { User } from "../lib/types/user";

export class APIError extends Error {
status: number;
constructor(message: string, status: number) {
Expand All @@ -16,7 +18,75 @@ export async function fetchAboutInfo(): Promise<string> {
return r.data.message;
}

export async function fetchLeaderBoard(id: string): Promise<any> {
export interface LeaderboardDetail {
deadline: string;
description: string;
name: string;
reference: string;
gpu_types: string[];
rankings: Record<
string,
Array<{
file_name: string;
prev_score: number;
rank: number;
score: number;
user_name: string;
submission_id: number;
}>
>;
}

export interface CodesResponse {
results: Array<{
submission_id: number;
code: string;
}>;
}

export interface NewsPost {
id: string;
title: string;
content: string;
[key: string]: unknown;
}

export interface LeaderboardSummary {
id: number;
name: string;
deadline: string;
gpu_types: string[];
priority_gpu_type: string;
top_users: Array<{ rank: number; score: number; user_name: string }> | null;
}

export interface LeaderboardSummariesResponse {
leaderboards: LeaderboardSummary[];
now: string;
}

export interface UserSubmissionsResponse {
items: Array<{
submission_id: number;
file_name?: string | null;
submitted_at: string;
status?: string | null;
submission_done: boolean;
runs?: Array<{
start_time: string;
end_time: string | null;
mode: string;
passed: boolean;
score: number | null;
meta: Record<string, unknown> | null;
report: Record<string, unknown> | null;
}>;
}>;
total: number;
limit: number;
}

export async function fetchLeaderBoard(id: string): Promise<LeaderboardDetail> {
const start = performance.now();
const res = await fetch(`/api/leaderboard/${id}`);
const fetchTime = performance.now() - start;
Expand All @@ -41,7 +111,7 @@ export async function fetchLeaderBoard(id: string): Promise<any> {
export async function fetchCodes(
leaderboardId: number | string,
submissionIds: (number | string)[],
): Promise<any> {
): Promise<CodesResponse> {
const res = await fetch("/api/codes", {
method: "POST",
headers: {
Expand All @@ -62,7 +132,7 @@ export async function fetchCodes(
return r.data;
}

export async function fetchAllNews(): Promise<any> {
export async function fetchAllNews(): Promise<NewsPost[]> {
const res = await fetch("/api/news");
if (!res.ok) {
const json = await res.json();
Expand All @@ -73,7 +143,7 @@ export async function fetchAllNews(): Promise<any> {
return r.data;
}

export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise<any> {
export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise<LeaderboardSummariesResponse> {
const start = performance.now();
const url = useV1
? "/api/leaderboard-summaries?v1_query"
Expand Down Expand Up @@ -102,7 +172,7 @@ export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise
return r.data;
}

export async function getMe(): Promise<any> {
export async function getMe(): Promise<User> {
const res = await fetch("/api/me");
if (!res.ok) {
const json = await res.json();
Expand All @@ -113,15 +183,14 @@ export async function getMe(): Promise<any> {
return r.data;
}

export async function logout(): Promise<any> {
export async function logout(): Promise<void> {
const res = await fetch("/api/logout");
if (!res.ok) {
const json = await res.json();
const message = json?.message || "Unknown error";
throw new APIError(`Failed to fetch news contents: ${message}`, res.status);
}
const r = await res.json();
return r.data;
await res.json();
}

export async function submitFile(form: FormData) {
Expand All @@ -131,15 +200,15 @@ export async function submitFile(form: FormData) {
});

const text = await resp.text();
let data: any;
let data: Record<string, unknown>;
try {
data = JSON.parse(text);
} catch {
data = { raw: text };
}

if (!resp.ok) {
const msg = data?.detail || data?.message || "Submission failed";
const msg = (data?.detail as string) || (data?.message as string) || "Submission failed";
throw new Error(msg);
}

Expand All @@ -151,7 +220,7 @@ export async function fetchUserSubmissions(
userId: number | string,
page: number = 1,
pageSize: number = 10,
): Promise<any> {
): Promise<UserSubmissionsResponse> {
const offset = (page - 1) * pageSize;
const res = await fetch(
`/api/submissions?leaderboard_id=${leaderboardId}&offset=${offset}&limit=${pageSize}`,
Expand Down Expand Up @@ -190,7 +259,7 @@ export async function fetchEvents(): Promise<DiscordEvent[]> {
return r.data;
}

export interface AiTrendDataPoint {
export interface CustomTrendDataPoint {
score: string;
submission_id: number;
submission_time: string;
Expand All @@ -200,23 +269,23 @@ export interface AiTrendDataPoint {
model?: string;
}

export interface AiTrendTimeSeries {
export interface CustomTrendTimeSeries {
[gpuType: string]: {
[model: string]: AiTrendDataPoint[];
[model: string]: CustomTrendDataPoint[];
};
}

export interface AiTrendResponse {
export interface CustomTrendResponse {
leaderboard_id: number;
time_series: AiTrendTimeSeries;
time_series: CustomTrendTimeSeries;
}

export async function fetchAiTrend(leaderboardId: string): Promise<AiTrendResponse> {
const res = await fetch(`/api/leaderboard/${leaderboardId}/ai_trend`);
export async function fetchCustomTrend(leaderboardId: string): Promise<CustomTrendResponse> {
const res = await fetch(`/api/leaderboard/${leaderboardId}/custom_trend`);
if (!res.ok) {
const json = await res.json();
const message = json?.message || "Unknown error";
throw new APIError(`Failed to fetch AI trend: ${message}`, res.status);
throw new APIError(`Failed to fetch custom trend: ${message}`, res.status);
}
const r = await res.json();
return r.data;
Expand All @@ -225,7 +294,7 @@ export async function fetchAiTrend(leaderboardId: string): Promise<AiTrendRespon
export interface UserTrendResponse {
leaderboard_id: number;
user_ids: string[];
time_series: AiTrendTimeSeries;
time_series: CustomTrendTimeSeries;
}

export async function fetchUserTrend(
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/app-layout/NavUserProfile.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useState } from "react";
import {
Alert,
Avatar,
Button,
Divider,
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/components/common/EllipsisWithTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
Tooltip,
Typography,
type SxProps,
type Theme,
type TypographyVariant,
} from "@mui/material";
import React, { useEffect, useRef, useState } from "react";
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/common/styles/shared_style.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import styled from "@emotion/styled";
import { Container } from "@mui/material";

// eslint-disable-next-line react-refresh/only-export-components
export const flexRowCenter = {
display: "flex",
alignItems: "center",
gap: 0.5,
};

// eslint-disable-next-line react-refresh/only-export-components
export const flexRowCenterMediumGap = {
display: "flex",
alignItems: "center",
gap: 5,
};

// eslint-disable-next-line react-refresh/only-export-components
export const mediumText = {
fontSize: "1.25rem",
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeRaw, rehypeKatex]}
components={{
a: ({ node, ...props }) => (
a: ({ node: _node, ...props }) => (
<a
style={{
color: theme.palette.primary.main,
Expand All @@ -63,10 +63,10 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
{...props}
/>
),
figure: ({ node, ...props }) => (
figure: ({ node: _node, ...props }) => (
<figure style={{ textAlign: align, margin: "1.5rem 0" }} {...props} />
),
figcaption: ({ node, ...props }) => (
figcaption: ({ node: _node, ...props }) => (
<figcaption
style={{
fontStyle: "italic",
Expand All @@ -76,7 +76,7 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
{...props}
/>
),
img: ({ node, ...props }) => {
img: ({ node: _node, ...props }) => {
return (
<div style={{ textAlign: align, margin: "0" }}>
<img style={styleProps} {...props} alt={props.alt} />
Expand Down
Loading
Loading