Compare commits

...

10 Commits

15 changed files with 332 additions and 67 deletions

View File

@ -1,2 +1,3 @@
node_modules
.vscode
../../shared/**/*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -69,10 +69,12 @@ export const runCpp: IRunner<IRunnerParamsCpp> = async function (
child.stdout.setEncoding('utf8');
child.stdout.on('data', (data) => {
outputBuffer += data.toString();
params.outputCallback?.(data.toString());
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => {
outputBuffer += data.toString();
params.outputCallback?.(data.toString());
});
const runStartTime = performance.now();

View File

@ -22,7 +22,7 @@ services:
- ORIGIN=${ORIGIN}
- WEB_SANDBOX_SECRET=${WEB_SANDBOX_SECRET}
volumes:
- ./repo:/app/repo
- ./repo:/app/web/repo
depends_on:
- db
restart: unless-stopped

24
web/package-lock.json generated
View File

@ -28,13 +28,13 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@sveltejs/kit": "^2.5.3",
"@sveltejs/kit": "^2.5.4",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/bcrypt": "^5.0.2",
"@types/bootstrap": "^5.2.10",
"@types/diff": "^5.0.9",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.11.26",
"@types/node": "^20.11.27",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
@ -42,7 +42,7 @@
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"sass": "^1.71.1",
"sass": "^1.72.0",
"svelte": "^4.2.12",
"svelte-check": "^3.6.7",
"tslib": "^2.6.2",
@ -996,9 +996,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.3.tgz",
"integrity": "sha512-s6x7HBn/Fp+UNvyhJohjIA0FcJ+BWHGUDQ4PCg1D0EboUlvbimJQYchINu8G6sspLXYmlcsuNsp8bbcrRk85iw==",
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.4.tgz",
"integrity": "sha512-eDxK2d4EGzk99QsZNoPXe7jlzA5EGqfcCpUwZ912bhnalsZ2ZsG5wGRthkydupVjYyqdmzEanVKFhLxU2vkPSQ==",
"hasInstallScript": true,
"dependencies": {
"@types/cookie": "^0.6.0",
@ -1127,9 +1127,9 @@
}
},
"node_modules/@types/node": {
"version": "20.11.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz",
"integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==",
"version": "20.11.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz",
"integrity": "sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==",
"dependencies": {
"undici-types": "~5.26.4"
}
@ -3632,9 +3632,9 @@
}
},
"node_modules/sass": {
"version": "1.71.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
"integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
"version": "1.72.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.72.0.tgz",
"integrity": "sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA==",
"devOptional": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",

View File

@ -12,13 +12,13 @@
"format": "prettier --plugin prettier-plugin-svelte --write ."
},
"devDependencies": {
"@sveltejs/kit": "^2.5.3",
"@sveltejs/kit": "^2.5.4",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/bcrypt": "^5.0.2",
"@types/bootstrap": "^5.2.10",
"@types/diff": "^5.0.9",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.11.26",
"@types/node": "^20.11.27",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
@ -26,7 +26,7 @@
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"sass": "^1.71.1",
"sass": "^1.72.0",
"svelte": "^4.2.12",
"svelte-check": "^3.6.7",
"tslib": "^2.6.2",

View File

@ -101,4 +101,5 @@ model Contest {
activeTeams ActiveTeam[]
submissions Submission[]
startTime DateTime?
freezeTime DateTime?
}

View File

@ -10,6 +10,8 @@ import {
templateCppCMakeLists,
templateCppGitIgnore,
templateCppProblem,
templateCppVscodeLaunch,
templateCppVscodeTasks,
templateJavaProblem
} from './templates';
@ -48,10 +50,14 @@ async function addProblemsCSharp(opts: OptsAddProblems) {
async function addProblemsCPP(opts: OptsAddProblems) {
let cmakeLists = templateCppCMakeLists;
opts.contest.problems.forEach((problem) => {
cmakeLists += `add_executable(${problem.pascalName} ${problem.pascalName}/${problem.pascalName}.cpp)`;
cmakeLists += `add_executable(${problem.pascalName} ${problem.pascalName}/${problem.pascalName}.cpp)\n`;
});
opts.fs.writeFileSync(join(opts.dir, 'CMakeLists.txt'), cmakeLists);
opts.fs.mkdirSync(join(opts.dir, '.vscode'));
opts.fs.writeFileSync(join(opts.dir, '.vscode', 'launch.json'), templateCppVscodeLaunch);
opts.fs.writeFileSync(join(opts.dir, '.vscode', 'tasks.json'), templateCppVscodeTasks);
opts.contest.problems.forEach((problem) => {
opts.fs.mkdirSync(join(opts.dir, problem.pascalName));
const filledTemplate = templateCppProblem.replaceAll('%%pascalName%%', problem.pascalName);
@ -62,7 +68,7 @@ async function addProblemsCPP(opts: OptsAddProblems) {
});
}
export async function createRepos(contestId: number) {
export async function createRepos(contestId: number, teamIds: number[]) {
const vol = new memfs.Volume();
const fs = createFsFromVolume(vol);
@ -75,35 +81,37 @@ export async function createRepos(contestId: number) {
return;
}
contest.teams.forEach(async (team) => {
fs.mkdirSync(team.id.toString(), { recursive: true });
await git.init({ fs: fs, bare: false, defaultBranch: 'master', dir: team.id.toString() });
if (team.language === 'Java') {
addProblemsJava({ fs, dir: team.id.toString(), contest });
} else if (team.language === 'CSharp') {
addProblemsCSharp({ fs, dir: team.id.toString(), contest });
fs.writeFileSync(join(team.id.toString(), '.gitignore'), templateCSharpGitIgnore);
} else if (team.language === 'CPP') {
addProblemsCPP({ fs, dir: team.id.toString(), contest });
fs.writeFileSync(join(team.id.toString(), '.gitignore'), templateCppGitIgnore);
} else {
console.error('Language not supported');
return;
}
await git.add({ fs: fs, dir: team.id.toString(), filepath: '.' });
await git.commit({
fs: fs,
dir: team.id.toString(),
message: 'Initial',
author: { name: 'Admin' }
contest.teams
.filter((t) => teamIds.includes(t.id))
.forEach(async (team) => {
fs.mkdirSync(team.id.toString(), { recursive: true });
await git.init({ fs: fs, bare: false, defaultBranch: 'master', dir: team.id.toString() });
if (team.language === 'Java') {
addProblemsJava({ fs, dir: team.id.toString(), contest });
} else if (team.language === 'CSharp') {
addProblemsCSharp({ fs, dir: team.id.toString(), contest });
fs.writeFileSync(join(team.id.toString(), '.gitignore'), templateCSharpGitIgnore);
} else if (team.language === 'CPP') {
addProblemsCPP({ fs, dir: team.id.toString(), contest });
fs.writeFileSync(join(team.id.toString(), '.gitignore'), templateCppGitIgnore);
} else {
console.error('Language not supported');
return;
}
await git.add({ fs: fs, dir: team.id.toString(), filepath: '.' });
await git.commit({
fs: fs,
dir: team.id.toString(),
message: 'Initial',
author: { name: 'Admin' }
});
await git.push({
fs: fs,
http,
dir: team.id.toString(),
url: `http://127.0.0.1:${
process.env.GIT_PORT ?? 7006
}/${contest.id.toString()}/${team.id.toString()}`
});
});
await git.push({
fs: fs,
http,
dir: team.id.toString(),
url: `http://127.0.0.1:${
process.env.GIT_PORT ?? 7006
}/${contest.id.toString()}/${team.id.toString()}`
});
});
}

View File

@ -41,5 +41,67 @@ int main()
return 0;
}`;
export const templateCppGitIgnore = `/build
export const templateCppGitIgnore = `/**/build
`;
export const templateCppVscodeLaunch = `{
"configurations": [
{
"name": "C/C++: g++ build and debug active file",
"type": "cppdbg",
"request": "launch",
"program": "\${fileDirname}/build/\${fileBasenameNoExtension}.out",
"args": [],
"stopAtEntry": false,
"cwd": "\${fileDirname}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Set Disassembly Flavor to Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
],
"preLaunchTask": "C/C++: g++ build active file",
"miDebuggerPath": "/usr/bin/gdb"
}
],
"version": "2.0.0"
}
`;
export const templateCppVscodeTasks = `{
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: g++ build active file",
"command": "/usr/bin/g++",
"args": [
"-fdiagnostics-color=always",
"-g",
"\${file}",
"-o",
"\${fileDirname}/build/\${fileBasenameNoExtension}.out"
],
"options": {
"cwd": "\${fileDirname}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Task generated by Debugger."
}
],
"version": "2.0.0"
}
`;

View File

@ -3,3 +3,42 @@
</svelte:head>
<h1 style="text-align:center" class="mb-4"><i class="bi bi-speedometer2"></i> Dashboard</h1>
<div class="row">
<div class="col-md-6">
<a
class="card stats-card bg-body-tertiary stats-card-link stats-card-action"
draggable="false"
href="/public/scoreboard"
>
<h4><i class="bi bi-link-45deg"></i> Public Scoreboard</h4>
</a>
</div>
<div class="col-md-6 mt-3 mt-md-0"></div>
</div>
<style>
.stats-card {
text-align: left;
padding: 20px;
border-radius: 10px;
transform: scale(1);
transition: 0.1s;
text-decoration: none;
}
.stats-card-link {
color: inherit;
text-decoration: none;
vertical-align: top;
transform: scale(1);
transition: 0.1s;
}
.stats-card-link:hover {
transform: scale(1.02);
}
.stats-card-action:hover {
cursor: pointer;
}
</style>

View File

@ -96,7 +96,7 @@ export const actions = {
});
return { success: true };
},
repo: async ({ params }) => {
repo: async ({ params, request }) => {
if (!params.contestId) {
return { success: false };
}
@ -104,10 +104,46 @@ export const actions = {
if (isNaN(contestId)) {
return { success: false };
}
if (fs.existsSync(join('repo', contestId.toString()))) {
fs.removeSync(join('repo', contestId.toString()));
const form = await request.formData();
const formEntries = Array.from(form.entries());
const resetTeamIds = formEntries
.filter((e) => e[0].startsWith('teamId'))
.map((e) => {
return parseInt(e[1].toString());
});
resetTeamIds.forEach((teamId) => {
const repoPath = join('repo', contestId.toString(), `${teamId.toString()}.git`);
if (fs.existsSync(repoPath) === true) {
fs.removeSync(repoPath);
}
});
await createRepos(contestId, resetTeamIds);
return { success: true };
},
'freeze-time': async ({ params, request }) => {
if (!params.contestId) {
return { success: false, message: 'No contest Id specified' };
}
const contestId = parseInt(params.contestId);
if (isNaN(contestId)) {
return { success: false, message: 'Invalid contest Id' };
}
const form = await request.formData();
const formFreezeTime = form.get('freezeTime');
if (formFreezeTime === null) {
return { success: false, message: 'Invalid input' };
}
const freezeTime = new Date(formFreezeTime.toString());
const contest = await db.contest.findUnique({ where: { id: contestId } });
if (contest === null) {
return { success: false, message: 'Invalid contest' };
}
try {
await db.contest.update({ where: { id: contestId }, data: { freezeTime } });
} catch (e) {
console.error(`Database error: ${e}`);
return { success: false, message: `Database error: ${e}` };
}
await createRepos(contestId);
return { success: true };
}
} satisfies Actions;

View File

@ -3,11 +3,20 @@
import { page } from '$app/stores';
import ConfirmModal from '$lib/ConfirmModal.svelte';
import FormAlert from '$lib/FormAlert.svelte';
import type { PageData } from './$types';
import Modal from '$lib/Modal.svelte';
import type { Actions, PageData } from './$types';
export let data: PageData;
export let form: Actions;
$: if (form) {
freezeModal.hide();
repoModal.hide();
}
let confirmModal: ConfirmModal;
let freezeModal: Modal;
let repoModal: Modal;
function enhanceConfirm(form: HTMLFormElement, text: string) {
enhance(form, async ({ cancel }) => {
@ -19,6 +28,24 @@
};
});
}
let freezeTimeInputLocal: string | undefined;
let freezeTimeInput: string | null = null;
$: if (freezeTimeInputLocal !== undefined) {
freezeTimeInput = new Date(freezeTimeInputLocal).toISOString();
}
function repoSelectNone() {
document.querySelectorAll<HTMLInputElement>('.repoCheck').forEach((e) => {
e.checked = false;
});
}
function repoSelectAll() {
document.querySelectorAll<HTMLInputElement>('.repoCheck').forEach((e) => {
e.checked = true;
});
}
</script>
<svelte:head>
@ -27,6 +54,68 @@
<ConfirmModal bind:this={confirmModal} />
<Modal title="Freeze Time" bind:this={freezeModal}>
<form action="?/freeze-time" method="POST" use:enhance>
<div class="modal-body">
<label class="form-label" for="freezeTimeInput">Freeze At</label>
<input
bind:value={freezeTimeInputLocal}
id="freezeTimeInput"
class="form-control"
type="datetime-local"
/>
<input type="hidden" name="freezeTime" value={freezeTimeInput} />
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-outline-secondary"
on:click={() => {
freezeModal.hide();
}}>Cancel</button
>
<button type="submit" class="btn btn-success">Set</button>
</div>
</form>
</Modal>
<Modal title="Reset Repos" bind:this={repoModal}>
<form action="?/repo" method="POST" use:enhance>
<div class="modal-body">
<div class="d-flex flex-row gap-2 pb-2">
<button on:click={repoSelectNone} type="button" class="btn btn-sm btn-outline-secondary"
>Select None</button
>
<button on:click={repoSelectAll} type="button" class="btn btn-sm btn-outline-secondary"
>Select All</button
>
</div>
{#each data.teams as team}
<div class="form-check">
<input
name={`teamId${team.id}`}
class="form-check-input repoCheck"
type="checkbox"
value={team.id}
id={`repoCheck${team.id}`}
/>
<label class="form-check-label" for={`repoCheck${team.id}`}>{team.name}</label>
</div>
{/each}
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-outline-secondary"
on:click={() => {
repoModal.hide();
}}>Cancel</button
>
<button type="submit" class="btn btn-warning">Reset Selected</button>
</div>
</form>
</Modal>
<h1 style="text-align:center" class="mb-4"><i class="bi bi-flag"></i> Contest - {data.name}</h1>
<FormAlert />
@ -40,6 +129,20 @@
<a href="/admin/contests" class="btn btn-outline-primary">All Contests</a>
</div>
<div class="col-6 text-end">
<button
type="button"
class="btn btn-outline-info"
on:click={() => {
freezeModal.show();
}}>Set Freeze Time</button
>
<button
type="button"
class="btn btn-outline-warning"
on:click={() => {
repoModal.show();
}}>Reset Repos</button
>
{#if data.activeTeams === 0}
<form
method="POST"
@ -49,19 +152,11 @@
>
<button type="submit" class="btn btn-danger">Delete</button>
</form>
<form
method="POST"
action="?/repo"
class="d-inline"
use:enhanceConfirm={'Are you sure you want to recreate repos? This WILL DELETE ALL DATA on the repos currently.'}
>
<button type="submit" class="btn btn-warning">Recreate Repos</button>
</form>
<form
method="POST"
action="?/start"
class="d-inline"
use:enhanceConfirm={'Are you sure you want to start the contest?'}
use:enhanceConfirm={'Are you sure you want to start the contest? (THIS WILL DELETE ALL DATA IF THE CONTEST HAS ALREADY BEEN RUN)'}
>
<button type="submit" class="btn btn-success">Start</button>
</form>

View File

@ -45,7 +45,10 @@ export const actions = {
include: { teams: true, problems: true }
});
await createRepos(createdContest.id);
await createRepos(
createdContest.id,
teams.map((t) => t.id)
);
return { success: true };
}

View File

@ -153,7 +153,10 @@ export const actions = {
await db.activeTeam.create({ data: { teamId: team.id, contestId: contest.id } });
});
await createRepos(contest.id);
await createRepos(
contest.id,
fullContest.teams.map((t) => t.id)
);
}
}
} catch (err) {

View File

@ -8,9 +8,24 @@ export const load = (async ({ params }) => {
throw redirect(302, '/public/scoreboard');
}
const timestamp = new Date();
const contestQuery = await db.contest.findUnique({ where: { id: contestId } });
if (contestQuery === null) {
throw redirect(302, '/public/scoreboard');
}
const contest = await db.contest.findUnique({
where: { id: contestId },
include: { problems: true, teams: { include: { submissions: true } } }
include: {
problems: true,
teams: {
include: {
submissions:
contestQuery.freezeTime === null
? true
: { where: { createdAt: { lt: contestQuery.freezeTime } } }
}
}
}
});
if (contest === null) {
throw redirect(302, '/public/scoreboard');