Compare commits
10 Commits
437bfe4c1f
...
23c6711ad6
Author | SHA1 | Date | |
---|---|---|---|
23c6711ad6 | |||
6b07b9e2ca | |||
8344cbe2bc | |||
c54643d8f9 | |||
1e849c8686 | |||
ba5fa18000 | |||
1a50a42355 | |||
126fa4ed44 | |||
aa1be3acb7 | |||
6d98f56385 |
@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
.vscode
|
||||
../../shared/**/*
|
Binary file not shown.
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 31 KiB |
@ -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();
|
||||
|
@ -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
24
web/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -101,4 +101,5 @@ model Contest {
|
||||
activeTeams ActiveTeam[]
|
||||
submissions Submission[]
|
||||
startTime DateTime?
|
||||
freezeTime DateTime?
|
||||
}
|
||||
|
@ -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,7 +81,9 @@ export async function createRepos(contestId: number) {
|
||||
return;
|
||||
}
|
||||
|
||||
contest.teams.forEach(async (team) => {
|
||||
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') {
|
||||
|
@ -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"
|
||||
}
|
||||
`;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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');
|
||||
|
Loading…
Reference in New Issue
Block a user