diff --git a/Cargo.lock b/Cargo.lock
index 29dedc5f5683f604006f374faa9b2c43eb35814b..40dcedbbe790b1c941a354848ada9b474eb2d8de 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
+version = 4
 
 [[package]]
 name = "actix-codec"
@@ -679,6 +679,7 @@ dependencies = [
  "iana-time-zone",
  "js-sys",
  "num-traits",
+ "serde",
  "wasm-bindgen",
  "windows-targets 0.52.6",
 ]
@@ -865,6 +866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
 dependencies = [
  "powerfmt",
+ "serde",
 ]
 
 [[package]]
@@ -1181,6 +1183,21 @@ version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64"
 
+[[package]]
+name = "git2"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
+dependencies = [
+ "bitflags 2.6.0",
+ "libc",
+ "libgit2-sys",
+ "log",
+ "openssl-probe",
+ "openssl-sys",
+ "url",
+]
+
 [[package]]
 name = "h2"
 version = "0.3.26"
@@ -1245,6 +1262,12 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
 
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
 [[package]]
 name = "hkdf"
 version = "0.12.4"
@@ -1407,6 +1430,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
 dependencies = [
  "autocfg",
  "hashbrown 0.12.3",
+ "serde",
 ]
 
 [[package]]
@@ -1417,6 +1441,7 @@ checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
 dependencies = [
  "equivalent",
  "hashbrown 0.14.5",
+ "serde",
 ]
 
 [[package]]
@@ -1501,6 +1526,7 @@ dependencies = [
  "diesel_migrations",
  "env_logger",
  "futures",
+ "git2",
  "handlebars",
  "itertools 0.11.0",
  "lazy_static",
@@ -1514,11 +1540,14 @@ dependencies = [
  "rust-argon2",
  "serde",
  "serde_json",
+ "serde_with",
  "thiserror",
  "tokio",
+ "toml",
  "tonic",
  "tonic-build",
  "uuid",
+ "walkdir",
  "which",
  "zip",
 ]
@@ -1541,6 +1570,20 @@ version = "0.2.159"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
 
+[[package]]
+name = "libgit2-sys"
+version = "0.17.0+1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
+dependencies = [
+ "cc",
+ "libc",
+ "libssh2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+]
+
 [[package]]
 name = "libsqlite3-sys"
 version = "0.26.0"
@@ -1552,6 +1595,32 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "libssh2-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.14"
@@ -1718,6 +1787,24 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
 
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "parking"
 version = "2.2.1"
@@ -2289,6 +2376,36 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_with"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.5.0",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "sha1"
 version = "0.10.6"
diff --git a/Cargo.toml b/Cargo.toml
index 87c6b922ef716a6705ede895204823d3243a1372..4f312a65c2ce7f5b5e39da0d0815d860304ac5b2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,6 +44,10 @@ prost = "0.12.1"
 async-channel = "2.0.0"
 dashmap = "5.5.3"
 itertools = "0.11.0"
+git2 = "0.19.0"
+walkdir = "2.5.0"
+toml = "0.8.19"
+serde_with = "3.11.0"
 
 [build-dependencies]
 tonic-build = "0.10.2"
diff --git a/Dockerfile b/Dockerfile
index eb4184c2e25ca9592e5c368f2e1fba218d6d5ad6..4330a395b8e679b223b20de9808f161045f7f6ee 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,15 @@ RUN apt-get update && \
         protobuf-compiler \
         gcc \
         g++ \
-        libpq-dev && \
+        libpq-dev \
+        python3 \
+        openjdk-17-jdk \
+        pdf2svg \
+        make \
+        pypy3 \
+        texlive-full \
+        librsvg2-bin \
+        pandoc \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/*
 RUN rustup component add rustfmt
diff --git a/alvokanto/Cargo.lock b/alvokanto/Cargo.lock
index 3ae40f80a84e5ee2089e10a9996e394cdca9b921..00a7da2b3e9085cedd8b74fe19bf30956550ee17 100644
--- a/alvokanto/Cargo.lock
+++ b/alvokanto/Cargo.lock
@@ -399,6 +399,7 @@ dependencies = [
  "tokio",
  "tonic",
  "tonic-build",
+ "walkdir",
  "which",
 ]
 
@@ -698,6 +699,7 @@ dependencies = [
  "iana-time-zone",
  "js-sys",
  "num-traits",
+ "serde",
  "wasm-bindgen",
  "windows-targets 0.52.6",
 ]
@@ -884,6 +886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
 dependencies = [
  "powerfmt",
+ "serde",
 ]
 
 [[package]]
@@ -1206,6 +1209,21 @@ version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64"
 
+[[package]]
+name = "git2"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
+dependencies = [
+ "bitflags 2.6.0",
+ "libc",
+ "libgit2-sys",
+ "log",
+ "openssl-probe",
+ "openssl-sys",
+ "url",
+]
+
 [[package]]
 name = "h2"
 version = "0.3.26"
@@ -1270,6 +1288,12 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
 
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
 [[package]]
 name = "hkdf"
 version = "0.12.4"
@@ -1432,6 +1456,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
 dependencies = [
  "autocfg",
  "hashbrown 0.12.3",
+ "serde",
 ]
 
 [[package]]
@@ -1442,6 +1467,7 @@ checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
 dependencies = [
  "equivalent",
  "hashbrown 0.14.5",
+ "serde",
 ]
 
 [[package]]
@@ -1526,6 +1552,7 @@ dependencies = [
  "diesel_migrations",
  "env_logger",
  "futures",
+ "git2",
  "handlebars",
  "itertools 0.11.0",
  "lazy_static",
@@ -1539,11 +1566,14 @@ dependencies = [
  "rust-argon2",
  "serde",
  "serde_json",
+ "serde_with",
  "thiserror",
  "tokio",
+ "toml",
  "tonic",
  "tonic-build",
  "uuid",
+ "walkdir",
  "which",
  "zip",
 ]
@@ -1566,6 +1596,20 @@ version = "0.2.159"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
 
+[[package]]
+name = "libgit2-sys"
+version = "0.17.0+1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
+dependencies = [
+ "cc",
+ "libc",
+ "libssh2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+]
+
 [[package]]
 name = "libsqlite3-sys"
 version = "0.26.0"
@@ -1577,6 +1621,32 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "libssh2-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.14"
@@ -1743,6 +1813,24 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
 
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "parking"
 version = "2.2.1"
@@ -2314,6 +2402,36 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_with"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.5.0",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "sha1"
 version = "0.10.6"
diff --git a/alvokanto/Cargo.toml b/alvokanto/Cargo.toml
index bdb29067a2025cc22dc134c76a6a555f9e4dd26d..c3aee9272ae9f3e60f498ed6eb5c188ca50dcf69 100644
--- a/alvokanto/Cargo.toml
+++ b/alvokanto/Cargo.toml
@@ -19,6 +19,7 @@ regex = "1.10.2"
 env_logger = "0.10.0"
 fs_extra = "1.3.0"
 chrono = "0.4.31"
+walkdir = "2"
 
 [build-dependencies]
 tonic-build = "0.10.2"
diff --git a/alvokanto/Dockerfile b/alvokanto/Dockerfile
index 045a119f78b277b2ab0df42481965fbdd381ffa2..e522444cb399217738c78e48dd8af8d495e67c92 100644
--- a/alvokanto/Dockerfile
+++ b/alvokanto/Dockerfile
@@ -40,6 +40,7 @@ RUN apt-get update && \
         fpc \
         openjdk-17-jdk \
         python3 \
+        pypy3 \
         libcap2 \
         libpq5 && \
     apt-get clean && \
diff --git a/alvokanto/dev.Dockerfile b/alvokanto/dev.Dockerfile
index 59f246c35f6952ca68336dc8b3f4c89cba0ee524..7926f06948c3c61939eb5660caff024b1e29f958 100644
--- a/alvokanto/dev.Dockerfile
+++ b/alvokanto/dev.Dockerfile
@@ -24,6 +24,7 @@ RUN apt-get update && \
         fpc \
         openjdk-17-jdk \
         python3 \
+        pypy3 \
         libcap2 \
         libpq-dev && \
     apt-get clean && \
diff --git a/alvokanto/src/isolate.rs b/alvokanto/src/isolate.rs
index 7c92fc5cde159252a573129ecb9491878d465d72..4ed0282a48359d7c2b1839eefa39a3d748985d55 100644
--- a/alvokanto/src/isolate.rs
+++ b/alvokanto/src/isolate.rs
@@ -186,6 +186,7 @@ pub fn run(
                 // java: /usr/bin/java and python: /usr/bin/python3
                 "--dir=usr/lib",
                 "--dir=usr/bin",
+                "--dir=etc/java-17-openjdk",
                 // java: libjli.so
                 "--dir=proc=proc:fs",
             ]
@@ -199,6 +200,7 @@ pub fn run(
                 "--dir=usr/bin",
                 "--dir=usr/include",
                 "--dir=proc=proc:fs",
+                "--dir=etc/java-17-openjdk",
             ]
         })
         .arg(format!("--processes={}", run_params.process_limit))
diff --git a/alvokanto/src/language.rs b/alvokanto/src/language.rs
index 79e6414e7f3b236b6dcf92905c1a0e23c9dcd5f6..b025b940cf4faaca8c205598ff23c20569f3441a 100644
--- a/alvokanto/src/language.rs
+++ b/alvokanto/src/language.rs
@@ -97,14 +97,10 @@ pub fn get_supported_languages() -> HashMap<String, LanguageParams> {
         build_gcc_params(2, "GNU G++20 {}", "/usr/bin/g++".into(), "c++", "c++20"),
     );
     languages.insert(
-        "cpp.17.g++".into(),
-        build_gcc_params(2, "GNU G++17 {}", "/usr/bin/g++".into(), "c++", "c++17"),
+        "c.11.gcc".into(),
+        build_gcc_params(5, "GNU GCC C11 {}", "/usr/bin/gcc".into(), "c", "c11"),
     );
-    languages.insert(
-        "c.18.gcc".into(),
-        build_gcc_params(5, "GNU GCC C18 {}", "/usr/bin/gcc".into(), "c", "c18"),
-    );
-    languages.insert(
+    /*languages.insert(
         "pascal.fpc".into(),
         LanguageParams {
             order: 6,
@@ -143,12 +139,12 @@ pub fn get_supported_languages() -> HashMap<String, LanguageParams> {
             },
             process_limit: 1,
         },
-    );
+    );*/
     languages.insert(
-        "java.8".into(),
+        "java.17".into(),
         LanguageParams {
             order: 7,
-            name: "Java 8".into(),
+            name: "Java 17".into(),
             suffix: ".java".into(),
             compile: Compile::Command(
                 &|source_text, source_name| {
@@ -163,7 +159,7 @@ pub fn get_supported_languages() -> HashMap<String, LanguageParams> {
                         .into()
                 },
                 CommandTuple {
-                    binary_path: "/usr/lib/jvm/java-1.8-openjdk/bin/javac".into(),
+                    binary_path: "/usr/lib/jvm/java-17-openjdk-amd64/bin/javac".into(),
                     args: vec![
                         "-cp".into(),
                         "\".;*\"".into(),
@@ -176,7 +172,7 @@ pub fn get_supported_languages() -> HashMap<String, LanguageParams> {
                 "{}.class".into(),
             ),
             run: CommandTuple {
-                binary_path: "/usr/bin/java".into(),
+                binary_path: "/usr/lib/jvm/java-17-openjdk-amd64/bin/java".into(),
                 args: vec![
                     "-Xmx512m".into(),
                     "-Xss64m".into(),
@@ -187,18 +183,18 @@ pub fn get_supported_languages() -> HashMap<String, LanguageParams> {
                     "{}".into(),
                 ],
             },
-            process_limit: 19,
+            process_limit: 60,
         },
     );
     languages.insert(
-        "python.3".into(),
+        "pypy.3".into(),
         LanguageParams {
             order: 8,
-            name: "Python 3".into(),
+            name: "PyPy Python 3.9.16".into(),
             suffix: ".py".into(),
             compile: Compile::NoCompile,
             run: CommandTuple {
-                binary_path: "/usr/bin/python3".into(),
+                binary_path: "/usr/bin/pypy3".into(),
                 args: vec!["{.}".into()],
             },
             process_limit: 1,
diff --git a/alvokanto/src/main.rs b/alvokanto/src/main.rs
index 0d81835417d97cd793db7509d63eee06d173ddfa..5b2f8db85a4c12233800b6fbb20c06e040613c65 100644
--- a/alvokanto/src/main.rs
+++ b/alvokanto/src/main.rs
@@ -1,10 +1,11 @@
 use chrono::Local;
-use jughisto::import_contest::format_width;
 use jughisto::job_protocol::job_queue_client::JobQueueClient;
 use jughisto::job_protocol::{job, job_result, GetJobRequest, JobResult, Language};
 use std::path::PathBuf;
 use which::which;
 
+use walkdir::WalkDir;
+
 use tonic::transport::channel::Channel;
 use tonic::Status;
 
@@ -14,12 +15,10 @@ mod language;
 use isolate::{new_isolate_box, CommandTuple, CompileParams, IsolateBox, RunStats, RunStatus};
 use language::Compile;
 use log::info;
-use std::convert::TryInto;
 use std::fs;
 use std::fs::read_to_string;
 use std::fs::File;
 use std::io::Write;
-use std::os::unix::fs::PermissionsExt;
 use tokio::time::{sleep, Duration};
 
 pub fn get_isolate_executable_path() -> PathBuf {
@@ -52,12 +51,13 @@ fn run_cached(
     let path_with_suffix = PathBuf::from("/data/").join(&request.source_path);
     let path_without_suffix = path_with_suffix.with_extension("");
 
+    isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed");
+
     if let Compile::Command(_, command, output) = &language.compile {
         let output_path = output
             .replace("{.}", path_with_suffix.to_str().unwrap())
             .replace("{}", path_without_suffix.to_str().unwrap());
         if !root_data.join(&output_path).exists() {
-            isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed");
             fs_extra::dir::copy(
                 path_with_suffix.parent().unwrap(),
                 &isolate_box.path,
@@ -174,7 +174,6 @@ fn run_cached(
             .collect(),
     };
 
-    isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed");
     let run_stats = isolate::execute(
         isolate_executable_path,
         isolate_box,
@@ -219,136 +218,6 @@ fn run_cached(
     }
 }
 
-fn compile_checker_if_necessary(
-    isolate_executable_path: &PathBuf,
-    isolate_box: &IsolateBox,
-    supported_languages: &HashMap<String, language::LanguageParams>,
-    uuid: &String,
-    request: &job::Judgement,
-) -> Option<JobResult> {
-    let root_data = PathBuf::from("/data/");
-
-    let checker_language = supported_languages.get(&request.checker_language);
-    if checker_language.is_none() {
-        return Some(JobResult {
-            uuid: uuid.to_string(),
-            code: job_result::Code::InvalidLanguage.into(),
-            which: None,
-        });
-    }
-    let checker_language = checker_language.unwrap();
-
-    let path_with_suffix = PathBuf::from("/data/").join(&request.checker_source_path);
-    let path_without_suffix = path_with_suffix.with_extension("");
-    if let Compile::Command(_, command, output) = &checker_language.compile {
-        let output_path = output
-            .replace("{.}", path_with_suffix.to_str().unwrap())
-            .replace("{}", path_without_suffix.to_str().unwrap());
-        if !root_data.join(&output_path).exists() {
-            isolate::reset(isolate_executable_path, isolate_box.id).expect("reset to work");
-            fs_extra::dir::copy(
-                path_with_suffix.parent().unwrap(),
-                &isolate_box.path,
-                &fs_extra::dir::CopyOptions {
-                    overwrite: false,
-                    skip_exist: false,
-                    buffer_size: 64000, //64kb
-                    copy_inside: true,
-                    content_only: true,
-                    depth: 0,
-                },
-            )
-            .unwrap();
-
-            let command = CommandTuple {
-                binary_path: command.binary_path.clone(),
-                args: command
-                    .args
-                    .iter()
-                    .map(|c| {
-                        c.replace(
-                            "{.}",
-                            path_with_suffix.file_name().unwrap().to_str().unwrap(),
-                        )
-                        .replace(
-                            "{}",
-                            path_without_suffix.file_name().unwrap().to_str().unwrap(),
-                        )
-                    })
-                    .collect(),
-            };
-
-            info!("Compiling: {:#?}", command);
-
-            let compile_stats = isolate::compile(
-                isolate_executable_path,
-                isolate_box,
-                CompileParams {
-                    uuid,
-                    // 1GiB
-                    memory_limit_kib: 1_024 * 1_024,
-                    // 25 seconds
-                    time_limit_ms: 25_000,
-                    command: &command,
-                },
-            )
-            .unwrap();
-
-            if match compile_stats {
-                RunStats {
-                    exit_code: Some(c), ..
-                } => c != 0,
-                RunStats {
-                    exit_code: None, ..
-                } => true,
-            } {
-                fs_extra::dir::create(&isolate_box.path, true).unwrap();
-                return Some(JobResult {
-                    uuid: uuid.to_string(),
-                    code: job_result::Code::Ok.into(),
-                    which: Some(job_result::Which::Judgement(job_result::Judgement {
-                        failed_test: 0,
-                        verdict: job_result::judgement::Verdict::CompilationError.into(),
-                        exit_code: compile_stats.exit_code.unwrap_or(42),
-                        exit_signal: compile_stats.exit_signal,
-                        memory_kib: compile_stats.memory_kib.unwrap(),
-                        time_ms: compile_stats.time_ms.unwrap(),
-                        time_wall_ms: compile_stats.time_wall_ms.unwrap(),
-                        error_output: read_to_string(compile_stats.stderr_path)
-                            .unwrap_or("".into()),
-                        judge_start_instant: Local::now()
-                            .naive_utc()
-                            .format("%Y-%m-%dT%H:%M:%S%.f")
-                            .to_string(),
-                        judge_end_instant: Local::now()
-                            .naive_utc()
-                            .format("%Y-%m-%dT%H:%M:%S%.f")
-                            .to_string(),
-                    })),
-                });
-            }
-
-            fs::copy(
-                isolate_box.path.join(
-                    output
-                        .replace(
-                            "{.}",
-                            path_with_suffix.file_name().unwrap().to_str().unwrap(),
-                        )
-                        .replace(
-                            "{}",
-                            path_without_suffix.file_name().unwrap().to_str().unwrap(),
-                        ),
-                ),
-                &output_path,
-            )
-            .unwrap();
-        }
-    }
-
-    None
-}
-
 fn judge(
     isolate_executable_path: &PathBuf,
     isolate_box: &IsolateBox,
@@ -369,23 +238,11 @@ fn judge(
     }
     let language = language.unwrap();
 
-    if let Some(res) = compile_checker_if_necessary(
-        isolate_executable_path,
-        isolate_box,
-        supported_languages,
-        &uuid,
-        &request,
-    ) {
-        return res;
-    }
-
     let judge_start_instant = Local::now().naive_utc();
 
-    let mut binary_bytes = request.source_text.as_bytes().to_vec();
+    isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed");
 
     if let Compile::Command(transform, command, _) = &language.compile {
-        isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed");
-
         {
             let mut file =
                 File::create(isolate_box.path.join(format!("x{}", language.suffix))).unwrap();
@@ -449,19 +306,10 @@ fn judge(
                 })),
             };
         }
-
-        binary_bytes = fs::read(
-            isolate_box.path.join(
-                language
-                    .run
-                    .binary_path
-                    .to_str()
-                    .unwrap()
-                    .replace("{.}", &format!("x{}", language.suffix))
-                    .replace("{}", "x"),
-            ),
-        )
-        .unwrap();
+    } else {
+        let mut file =
+            File::create(isolate_box.path.join(format!("x{}", language.suffix))).unwrap();
+        file.write_all(request.source_text.as_bytes()).unwrap();
     }
 
     let mut last_execute_stats: Option<RunStats> = None;
@@ -487,33 +335,33 @@ fn judge(
     };
 
     let mut error_output: Option<String> = None;
-    let mut failed_test: i32 = 0;
+    let mut failed_test: u32 = 0;
     let mut memory_kib = 0;
     let mut time_ms = 0;
     let mut time_wall_ms = 0;
 
-    for i in 1..request.test_count + 1 {
-        isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed");
-        let stdin_path = format_width(&request.test_pattern, i.try_into().unwrap());
-        let answer_path = format!("{}.a", stdin_path);
+    let mut testcases = WalkDir::new(format!("/data/{}/testcases/", request.package_id))
+        .into_iter()
+        .filter_map(|e| {
+            e.ok().filter(|e| {
+                e.file_name()
+                    .to_str()
+                    .map(|s| s.ends_with(".in"))
+                    .unwrap_or(false)
+            })
+        })
+        .collect::<Vec<_>>();
+    testcases.sort_by(|a, b| a.file_name().cmp(b.file_name()));
+    let testcase_count = testcases.len();
+    for (i, test) in testcases.into_iter().enumerate() {
+        let stdin_path = test.path().strip_prefix("/data/").unwrap();
+        let answer_path = stdin_path.with_extension("sol");
         info!(
             "Starting run {}/{} with test {:?}",
-            i, request.test_count, stdin_path
+            i + 1,
+            testcase_count,
+            stdin_path
         );
-        if let Compile::Command(_, _, _) = &language.compile {
-            let mut file = File::create(isolate_box.path.join(&command.binary_path)).unwrap();
-            let mut perms = file.metadata().unwrap().permissions();
-            perms.set_mode(0o755);
-            file.set_permissions(perms).unwrap();
-            file.write_all(&binary_bytes).unwrap();
-        } else {
-            let mut file =
-                File::create(isolate_box.path.join(format!("x{}", language.suffix))).unwrap();
-            let mut perms = file.metadata().unwrap().permissions();
-            perms.set_mode(0o555);
-            file.set_permissions(perms).unwrap();
-            file.write_all(&binary_bytes).unwrap();
-        }
         let execute_stats = isolate::execute(
             isolate_executable_path,
             isolate_box,
@@ -523,7 +371,7 @@ fn judge(
                 uuid: &uuid,
                 memory_limit_kib,
                 time_limit_ms,
-                stdin_path: Some(stdin_path.clone()),
+                stdin_path: Some(stdin_path.to_str().unwrap().into()),
             },
         )
         .expect("Crashed while running");
@@ -542,23 +390,22 @@ fn judge(
             } => true,
         } {
             error_output = Some(read_to_string(&execute_stats.stderr_path).unwrap_or("".into()));
-            failed_test = i;
+            failed_test = (i + 1) as u32;
             last_execute_stats = Some(execute_stats);
             break;
         }
 
         let output_bytes = fs::read(&execute_stats.stdout_path).unwrap();
-        isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed");
         {
             let mut file = File::create(isolate_box.path.join("stdin")).unwrap();
             file.write_all(&output_bytes).unwrap();
         }
         last_execute_stats = Some(execute_stats);
 
-        // TODO: Support non-compile based languages
         let command = CommandTuple {
             binary_path: PathBuf::from(format!("/data-{}/", &uuid))
-                .join(&request.checker_source_path)
+                .join(&request.package_id)
+                .join("evaluator")
                 .with_extension(""),
             args: vec![
                 PathBuf::from(format!("/data-{}/", &uuid))
@@ -601,7 +448,7 @@ fn judge(
             } => true,
         } {
             error_output = Some(read_to_string(checker_stats.stderr_path).unwrap_or("".into()));
-            failed_test = i;
+            failed_test = (i + 1) as u32;
             break;
         }
     }
diff --git a/dev.Dockerfile b/dev.Dockerfile
index c7f8ea3e5bd8aa91a39f771bdedb19ab30acf7af..baaa03b8a82cfb922ec8151ff62dc9aec775f12e 100644
--- a/dev.Dockerfile
+++ b/dev.Dockerfile
@@ -5,7 +5,17 @@ RUN apt-get update && \
         protobuf-compiler \
         gcc \
         g++ \
-        libpq-dev && \
+        libpq-dev \
+        libssl-dev \
+        pkg-config \
+        python3 \
+        openjdk-17-jdk \
+        pdf2svg \
+        make \
+        pypy3 \
+        texlive-full \
+        librsvg2-bin \
+        pandoc && \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/*
 RUN rustup component add rustfmt
@@ -14,4 +24,4 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
     cargo install systemfd && \
     cargo install cargo-watch
 WORKDIR /usr/src/jughisto
-CMD systemfd --no-pid -s http::0.0.0.0:8000 -- cargo watch -x 'run --color always' -i alvokanto/
+CMD systemfd --no-pid -s http::0.0.0.0:8000 -- cargo watch -w src -x 'run --color always' -i alvokanto/
diff --git a/docker-compose.yml b/docker-compose.yml
index e2a095519dabfc6a97853b33b439df08934d0f72..f8dd647b7c9aeba3a5dd40a2bec29e1dd72f8212 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,8 +11,5 @@ services:
     build:
       context: .
       dockerfile: alvokanto/Dockerfile
-    volumes:
-      - type: bind
-        source: /sys/fs/cgroup/isolate.slice
-        target: /sys/fs/cgroup/isolate.slice
     privileged: true
+    cgroup: host
diff --git a/migrations/2024-10-01-193839_create_repository/down.sql b/migrations/2024-10-01-193839_create_repository/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..c738d9360bfcda4cc9d9c26df7627db893e6781d
--- /dev/null
+++ b/migrations/2024-10-01-193839_create_repository/down.sql
@@ -0,0 +1,4 @@
+alter table problem
+drop column repository_id;
+
+drop table repository
diff --git a/migrations/2024-10-01-193839_create_repository/up.sql b/migrations/2024-10-01-193839_create_repository/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..17bb8248ecea6307cb2e87b619c58dbb3128263e
--- /dev/null
+++ b/migrations/2024-10-01-193839_create_repository/up.sql
@@ -0,0 +1,35 @@
+create table repository (
+  id serial primary key not null,
+  name text not null,
+  path text not null,
+  remote_url text not null,
+  creation_user_id integer references "user"(id) not null,
+  creation_instant timestamp not null
+);
+
+alter table problem
+drop column if exists checker_path;
+
+alter table problem
+drop column if exists checker_language;
+
+alter table problem
+drop column if exists validator_path;
+
+alter table problem
+drop column if exists validator_language;
+
+alter table problem
+drop column if exists main_solution_path;
+
+alter table problem
+drop column if exists main_solution_language;
+
+alter table problem
+drop column if exists test_count;
+
+alter table problem
+drop column if exists test_pattern;
+
+alter table problem
+add column repository_id int null references repository(id)
diff --git a/migrations/2024-12-09-010923_add_balloon_color_problem_name/down.sql b/migrations/2024-12-09-010923_add_balloon_color_problem_name/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..f60cd9315db02f406c08dd6a6bf36b536ce8e00c
--- /dev/null
+++ b/migrations/2024-12-09-010923_add_balloon_color_problem_name/down.sql
@@ -0,0 +1,5 @@
+alter table contest_problems
+drop column balloon_color;
+
+alter table contest_problems
+drop column problem_name_override;
diff --git a/migrations/2024-12-09-010923_add_balloon_color_problem_name/up.sql b/migrations/2024-12-09-010923_add_balloon_color_problem_name/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..9529196968dabc362c572cdbe93204466f8c85ee
--- /dev/null
+++ b/migrations/2024-12-09-010923_add_balloon_color_problem_name/up.sql
@@ -0,0 +1,5 @@
+alter table contest_problems
+add column balloon_color text null;
+
+alter table contest_problems
+add column problem_name_override text null;
diff --git a/migrations/2024-12-09-012754_add_user_full_name_single_contest/down.sql b/migrations/2024-12-09-012754_add_user_full_name_single_contest/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..1b80a25eb86a43f605220d504badecf0e09ae475
--- /dev/null
+++ b/migrations/2024-12-09-012754_add_user_full_name_single_contest/down.sql
@@ -0,0 +1,5 @@
+alter table "user"
+drop column full_name;
+
+alter table "user"
+drop column single_contest_id;
diff --git a/migrations/2024-12-09-012754_add_user_full_name_single_contest/up.sql b/migrations/2024-12-09-012754_add_user_full_name_single_contest/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..fdd1ff50d09c1898e86133c36df5dda0b294e5ca
--- /dev/null
+++ b/migrations/2024-12-09-012754_add_user_full_name_single_contest/up.sql
@@ -0,0 +1,5 @@
+alter table "user"
+add column full_name text null;
+
+alter table "user"
+add column single_contest_id int null references contest(id);
diff --git a/migrations/2024-12-09-013119_add_contest_attributes/down.sql b/migrations/2024-12-09-013119_add_contest_attributes/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..50a1af7448604bcd2389dd72b1878acc07de1303
--- /dev/null
+++ b/migrations/2024-12-09-013119_add_contest_attributes/down.sql
@@ -0,0 +1,5 @@
+alter table contest
+drop column frozen_scoreboard_instant;
+
+alter table contest
+drop column blind_scoreboard_instant;
diff --git a/migrations/2024-12-09-013119_add_contest_attributes/up.sql b/migrations/2024-12-09-013119_add_contest_attributes/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..facdb17779a8d1d243bc65e30817553cdd4786f5
--- /dev/null
+++ b/migrations/2024-12-09-013119_add_contest_attributes/up.sql
@@ -0,0 +1,5 @@
+alter table contest
+add column frozen_scoreboard_instant timestamp null;
+
+alter table contest
+add column blind_scoreboard_instant timestamp null;
diff --git a/migrations/2024-12-09-013806_create_printing_task/down.sql b/migrations/2024-12-09-013806_create_printing_task/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..db7d56ab7f3510619b6c76f863aabd9a32530291
--- /dev/null
+++ b/migrations/2024-12-09-013806_create_printing_task/down.sql
@@ -0,0 +1 @@
+drop table printing_task;
diff --git a/migrations/2024-12-09-013806_create_printing_task/up.sql b/migrations/2024-12-09-013806_create_printing_task/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..649a399f6c2b44888986eb57f7ab769fe3581b44
--- /dev/null
+++ b/migrations/2024-12-09-013806_create_printing_task/up.sql
@@ -0,0 +1,9 @@
+create table printing_task (
+  uuid text primary key not null,
+  contents text not null,
+  creation_instant timestamp not null,
+  status text not null,
+  done_instant timestamp null,
+  contest_id integer references contest(id) not null,
+  user_id integer references "user"(id) not null
+)
diff --git a/migrations/2024-12-09-014353_create_clarification/down.sql b/migrations/2024-12-09-014353_create_clarification/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..e33dbf836e70135c0b139b54d501a38c1bddb4cd
--- /dev/null
+++ b/migrations/2024-12-09-014353_create_clarification/down.sql
@@ -0,0 +1 @@
+drop table clarification
diff --git a/migrations/2024-12-09-014353_create_clarification/up.sql b/migrations/2024-12-09-014353_create_clarification/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..ebd52c7b2d43cac5062c28cfc43ecba67a089594
--- /dev/null
+++ b/migrations/2024-12-09-014353_create_clarification/up.sql
@@ -0,0 +1,12 @@
+create table clarification (
+  uuid text primary key not null,
+  question text not null,
+  question_instant timestamp not null,
+  answer text null,
+  answer_instant timestamp null,
+  answer_user_id integer references "user"(id) null,
+  answer_to_all boolean not null,
+  contest_problem_id integer references contest_problems(id) null,
+  contest_id integer references contest(id) not null,
+  user_id integer references "user"(id) not null
+)
diff --git a/migrations/2024-12-09-220325_create_backup/down.sql b/migrations/2024-12-09-220325_create_backup/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..34defdf0232284e7417a896095ccb7a85dae8630
--- /dev/null
+++ b/migrations/2024-12-09-220325_create_backup/down.sql
@@ -0,0 +1 @@
+drop table backup
diff --git a/migrations/2024-12-09-220325_create_backup/up.sql b/migrations/2024-12-09-220325_create_backup/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..b35a0decf9e72ed98819079a9bb33f771dcbccce
--- /dev/null
+++ b/migrations/2024-12-09-220325_create_backup/up.sql
@@ -0,0 +1,8 @@
+create table backup (
+  uuid text primary key not null,
+  filename text not null,
+  contents bytea not null,
+  creation_instant timestamp not null,
+  contest_id integer references contest(id) not null,
+  user_id integer references "user"(id) not null
+)
diff --git a/migrations/2024-12-14-075202_submission_balloon_delivered_instant/down.sql b/migrations/2024-12-14-075202_submission_balloon_delivered_instant/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..3558767a244ea5c700d64e16c598e750e4d023e7
--- /dev/null
+++ b/migrations/2024-12-14-075202_submission_balloon_delivered_instant/down.sql
@@ -0,0 +1,2 @@
+alter table submission
+drop column balloon_delivered_instant
diff --git a/migrations/2024-12-14-075202_submission_balloon_delivered_instant/up.sql b/migrations/2024-12-14-075202_submission_balloon_delivered_instant/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..3fb71c1241b7d18b56d260ea96b86fe77c231d00
--- /dev/null
+++ b/migrations/2024-12-14-075202_submission_balloon_delivered_instant/up.sql
@@ -0,0 +1,2 @@
+alter table submission
+add column balloon_delivered_instant timestamp null
diff --git a/proto/job_queue.proto b/proto/job_queue.proto
index c741b61c94b36cdb331b8f2c1da222420378d135..4701660348e45fd05709599c79406c63580b25bb 100644
--- a/proto/job_queue.proto
+++ b/proto/job_queue.proto
@@ -23,10 +23,7 @@ message Job {
     int32 time_limit_ms = 4;
     message Judgement {
         string source_text = 1;
-        int32 test_count = 2;
-        string test_pattern = 3;
-        string checker_language = 4;
-        string checker_source_path = 5;
+        string package_id = 2;
     };
     message RunCached {
         string source_path = 1;
@@ -58,7 +55,7 @@ message JobResult {
             RuntimeError = 5;
         };
         Verdict verdict = 1;
-        int32 failed_test = 2;
+        uint32 failed_test = 2;
         int32 time_ms = 3;
         int32 time_wall_ms = 4;
         int32 memory_kib = 5;
diff --git a/src/import_contest.rs b/src/import_contest.rs
index 799b33b014f83263679f9f4a9f452a762357e117..b10cd53e507c97eae12836980ee2c308abc9d5b3 100644
--- a/src/import_contest.rs
+++ b/src/import_contest.rs
@@ -46,6 +46,7 @@ mod xml {
         pub use super::super::ImportContestError;
     }
 
+    #[allow(dead_code)]
     pub mod contest {
         use super::prelude::*;
 
@@ -93,6 +94,7 @@ mod xml {
     pub mod problem {
         use super::prelude::*;
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Problem {
             #[serde(rename = "@url")]
@@ -115,6 +117,7 @@ mod xml {
             pub name: Vec<Name>,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Name {
             #[serde(rename = "@language")]
@@ -123,11 +126,13 @@ mod xml {
             pub value: String,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Statements {
             pub statement: Vec<Statement>,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Statement {
             #[serde(rename = "@charset")]
@@ -142,6 +147,7 @@ mod xml {
             pub r#type: String,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Judging {
             #[serde(rename = "@cpu-name")]
@@ -155,6 +161,7 @@ mod xml {
             pub testset: Vec<Testset>,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Testset {
             #[serde(rename = "@name")]
@@ -177,6 +184,7 @@ mod xml {
             pub test: Vec<Test>,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Test {
             #[serde(rename = "@method")]
@@ -189,28 +197,33 @@ mod xml {
             pub cmd: Option<String>,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Files {
             pub resources: Resources,
             pub executables: Executables,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Resources {
             pub file: Vec<File>,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct File {
             #[serde(rename = "@path")]
             pub path: String,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Executables {
             pub executable: Vec<Executable>,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Executable {
             pub source: Source,
@@ -225,6 +238,7 @@ mod xml {
             pub r#type: String,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Binary {
             #[serde(rename = "@path")]
@@ -233,6 +247,7 @@ mod xml {
             pub r#type: String,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Assets {
             pub checker: Checker,
@@ -240,6 +255,7 @@ mod xml {
             pub solutions: Solutions,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Checker {
             #[serde(rename = "@type")]
@@ -250,6 +266,7 @@ mod xml {
             pub testset: CheckerTestset,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct CheckerTestset {
             #[serde(rename = "test-count")]
@@ -261,17 +278,20 @@ mod xml {
             pub tests: VerdictTests,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Copy {
             #[serde(rename = "@path")]
             pub path: String,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Validators {
             pub validator: Vec<Validator>,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Validator {
             pub source: Source,
@@ -279,6 +299,7 @@ mod xml {
             pub testset: ValidatorTestset,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct ValidatorTestset {
             #[serde(rename = "test-count")]
@@ -288,11 +309,13 @@ mod xml {
             pub tests: VerdictTests,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct VerdictTests {
             pub test: Option<Vec<VerdictTest>>,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct VerdictTest {
             #[serde(rename = "@verdict")]
@@ -304,6 +327,7 @@ mod xml {
             pub solution: Vec<Solution>,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Solution {
             #[serde(rename = "@tag")]
@@ -312,11 +336,13 @@ mod xml {
             pub binary: Binary,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Properties {
             pub property: Option<Vec<Property>>,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Property {
             #[serde(rename = "@name")]
@@ -325,6 +351,7 @@ mod xml {
             pub value: String,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Stresses {
             #[serde(rename = "stress-count")]
@@ -337,11 +364,13 @@ mod xml {
         #[derive(Deserialize, Debug)]
         pub struct StressList {}
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Tags {
             pub tag: Option<Vec<Tag>>,
         }
 
+        #[allow(dead_code)]
         #[derive(Deserialize, Debug)]
         pub struct Tag {
             #[serde(rename = "@value")]
diff --git a/src/language.rs b/src/language.rs
index 0c7d4726141bd0cc4c36213dc6d914c9e18bba2b..7c44e4874d6bbb4d1eaae47f7a3b04acb474f913 100644
--- a/src/language.rs
+++ b/src/language.rs
@@ -61,10 +61,7 @@ pub async fn judge(
     job_result_sender: &broadcast::Sender<JobResult>,
     language: &String,
     source_text: String,
-    test_count: i32,
-    test_pattern: String,
-    checker_language: String,
-    checker_source_path: String,
+    package_id: String,
     memory_limit_kib: i32,
     time_limit_ms: i32,
 ) -> Result<job_result::Judgement, Box<dyn std::error::Error>> {
@@ -78,10 +75,7 @@ pub async fn judge(
             time_limit_ms,
             which: Some(job::Which::Judgement(job::Judgement {
                 source_text,
-                test_count,
-                test_pattern,
-                checker_language,
-                checker_source_path,
+                package_id,
             })),
         })
         .await?;
diff --git a/src/main.rs b/src/main.rs
index 4172ee18697cae07eb0ea86918340a6c579f1fbc..4bc897d7652ebf85ce842753c69c8ef661a5cc5e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -82,7 +82,7 @@ async fn update_database(
                     error_output: Some(judgement.error_output),
                     failed_test: match judgement.failed_test {
                         0 => None,
-                        test => Some(test),
+                        test => Some(test as i32),
                     },
                 },
             )
@@ -100,10 +100,7 @@ fn create_job_from_submission(submission: Submission, metadata: ProblemByContest
 
         which: Some(job::Which::Judgement(job::Judgement {
             source_text: submission.source_text,
-            test_count: metadata.test_count,
-            test_pattern: format!("./{}/{}", metadata.id, metadata.test_pattern),
-            checker_language: metadata.checker_language,
-            checker_source_path: format!("./{}/{}", metadata.id, metadata.checker_path),
+            package_id: metadata.id,
         })),
     }
 }
@@ -203,15 +200,45 @@ async fn main() -> Result<(), Box<dyn Error>> {
                     .service(pages::post_logout::post_logout)
                     .service(pages::get_main::get_main)
                     .service(pages::get_contests::get_contests)
+                    .service(pages::get_repositories::get_repositories)
                     .service(pages::get_contest_by_id::get_contest_by_id)
+                    .service(pages::patch_contest::patch_contest)
+                    .service(pages::patch_contest_problem::patch_contest_problem)
+                    .service(pages::get_single_contest_waiting::get_single_contest_waiting)
+                    .service(pages::get_single_contest_problems::get_single_contest_problems)
+                    .service(pages::get_single_contest_runs::get_single_contest_runs)
+                    .service(pages::get_single_contest_score::get_single_contest_score)
+                    .service(pages::get_single_contest_clarifications::get_single_contest_clarifications)
+                    .service(pages::get_printing_task_contents::get_printing_task_contents)
+                    .service(pages::get_single_contest_tasks::get_single_contest_tasks)
+                    .service(pages::create_printing_task::create_printing_task)
+                    .service(pages::post_printing_task_done::post_printing_task_done)
+                    .service(pages::get_single_contest_backup::get_single_contest_backup)
+                    .service(pages::create_backup::create_backup)
+                    .service(pages::get_backup_contents::get_backup_contents)
+                    .service(pages::create_clarification::create_clarification)
+                    .service(pages::patch_clarification::patch_clarification)
+                    .service(pages::create_contest_submission::create_contest_submission)
+                    .service(pages::post_submission_balloon_delivered::post_submission_balloon_delivered)
                     .service(pages::get_contest_scoreboard_by_id::get_contest_scoreboard_by_id)
+                    .service(pages::get_contest_clarifications_by_id::get_contest_clarifications_by_id)
+                    .service(pages::get_contest_tasks_by_id::get_contest_tasks_by_id)
+                    .service(pages::get_contest_webcast_by_id::get_contest_webcast_by_id)
                     .service(pages::get_contest_problem_by_id_label::get_contest_problem_by_id_label)
                     .service(pages::get_submissions_me::get_submissions_me)
                     .service(pages::get_submission::get_submission)
+                    .service(pages::get_submission_source::get_submission_source)
                     .service(pages::get_submissions::get_submissions)
+                    .service(pages::get_submissions_by_contest_id::get_submissions_by_contest_id)
                     .service(pages::get_submissions_me_by_contest_id::get_submissions_me_by_contest_id)
+                    .service(pages::get_submissions_by_contest_id_problem_label::get_submissions_by_contest_id_problem_label)
                     .service(pages::get_submissions_me_by_contest_id_problem_label::get_submissions_me_by_contest_id_problem_label)
                     .service(pages::rejudge_submission::rejudge_submission)
+                    .service(pages::import_repository::import_repository)
+                    .service(pages::sync_repository::sync_repository)
+                    .service(pages::sync_repository_contest::sync_repository_contest)
+                    .service(pages::get_repository_by_id::get_repository_by_id)
+                    .service(pages::get_repository_problem_by_path::get_repository_problem_by_path)
                     .service(pages::create_submission::create_submission)
                     .service(pages::create_contest::create_contest)
                     .service(pages::get_problems::get_problems)
diff --git a/src/models.rs b/src/models.rs
index 351f937a6275a364fb3e25195714ccd50d186912..c989a4e504c69c789b5a74ca8557100fa03314c9 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -1,4 +1,8 @@
+pub mod backup;
+pub mod clarification;
 pub mod contest;
+pub mod printing_task;
 pub mod problem;
+pub mod repository;
 pub mod submission;
 pub mod user;
diff --git a/src/models/backup.rs b/src/models/backup.rs
new file mode 100644
index 0000000000000000000000000000000000000000..69cd4e354d4bd0e82ef1fec31c557c5ff67c7a55
--- /dev/null
+++ b/src/models/backup.rs
@@ -0,0 +1,50 @@
+use chrono::prelude::*;
+use diesel::insert_into;
+use diesel::prelude::*;
+
+use crate::schema::backup;
+
+#[derive(Queryable)]
+pub struct Backup {
+    pub uuid: String,
+    pub filename: String,
+    pub contents: Vec<u8>,
+    pub creation_instant: NaiveDateTime,
+    pub contest_id: i32,
+    pub user_id: i32,
+}
+
+#[derive(Insertable)]
+#[diesel(table_name = backup)]
+pub struct NewBackup {
+    pub uuid: String,
+    pub filename: String,
+    pub contents: Vec<u8>,
+    pub creation_instant: NaiveDateTime,
+    pub contest_id: i32,
+    pub user_id: i32,
+}
+
+pub fn insert_backup(connection: &mut PgConnection, new_backup: NewBackup) -> QueryResult<()> {
+    insert_into(backup::table)
+        .values(new_backup)
+        .execute(connection)?;
+    Ok(())
+}
+
+pub fn get_backups_by_contest_user(
+    connection: &mut PgConnection,
+    user_id: i32,
+    contest_id: i32,
+) -> QueryResult<Vec<Backup>> {
+    backup::table
+        .filter(backup::user_id.eq(user_id))
+        .filter(backup::contest_id.eq(contest_id))
+        .load(connection)
+}
+
+pub fn get_backup_by_uuid(connection: &mut PgConnection, uuid: String) -> QueryResult<Backup> {
+    backup::table
+        .filter(backup::uuid.eq(uuid))
+        .first(connection)
+}
diff --git a/src/models/clarification.rs b/src/models/clarification.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f5e0f43405d3b6e6577c032a61c6203f21d330fb
--- /dev/null
+++ b/src/models/clarification.rs
@@ -0,0 +1,130 @@
+use chrono::prelude::*;
+use diesel::insert_into;
+use diesel::prelude::*;
+use diesel::update;
+
+use crate::schema::{clarification, contest_problems, user};
+use crate::user::{User, USER_COLUMNS};
+
+#[derive(Queryable)]
+pub struct Clarification {
+    pub uuid: String,
+    pub question: String,
+    pub question_instant: NaiveDateTime,
+    pub answer: Option<String>,
+    pub answer_instant: Option<NaiveDateTime>,
+    pub answer_user_id: Option<i32>,
+    pub answer_to_all: bool,
+    pub contest_problem_id: Option<i32>,
+    pub contest_id: i32,
+    pub user_id: i32,
+}
+
+pub const CLARIFICATION_COLUMNS: (
+    clarification::uuid,
+    clarification::question,
+    clarification::question_instant,
+    clarification::answer,
+    clarification::answer_instant,
+    clarification::answer_user_id,
+    clarification::answer_to_all,
+    clarification::contest_problem_id,
+    clarification::contest_id,
+    clarification::user_id,
+) = (
+    clarification::uuid,
+    clarification::question,
+    clarification::question_instant,
+    clarification::answer,
+    clarification::answer_instant,
+    clarification::answer_user_id,
+    clarification::answer_to_all,
+    clarification::contest_problem_id,
+    clarification::contest_id,
+    clarification::user_id,
+);
+
+#[derive(Insertable)]
+#[diesel(table_name = clarification)]
+pub struct NewClarification {
+    pub uuid: String,
+    pub question: String,
+    pub question_instant: NaiveDateTime,
+    pub answer_to_all: bool,
+    pub contest_problem_id: Option<i32>,
+    pub contest_id: i32,
+    pub user_id: i32,
+}
+
+pub fn insert_clarification(
+    connection: &mut PgConnection,
+    new_clarification: NewClarification,
+) -> QueryResult<Clarification> {
+    let uuid = new_clarification.uuid.clone();
+    insert_into(clarification::table)
+        .values(new_clarification)
+        .execute(connection)?;
+    clarification::table
+        .filter(clarification::uuid.eq(uuid))
+        .first(connection)
+}
+
+pub fn get_clarifications_by_contest_user(
+    connection: &mut PgConnection,
+    user_id: i32,
+    contest_id: i32,
+) -> QueryResult<Vec<(Clarification, User, Option<String>)>> {
+    clarification::table
+        .inner_join(user::table)
+        .left_join(contest_problems::table)
+        .filter(
+            clarification::answer_to_all
+                .eq(true)
+                .or(clarification::user_id.eq(user_id)),
+        )
+        .filter(clarification::contest_id.eq(contest_id))
+        .select((
+            CLARIFICATION_COLUMNS,
+            USER_COLUMNS,
+            contest_problems::label.nullable(),
+        ))
+        .order(clarification::question_instant.desc())
+        .load(connection)
+}
+
+pub fn get_clarifications_by_contest(
+    connection: &mut PgConnection,
+    contest_id: i32,
+) -> QueryResult<Vec<(Clarification, User, Option<String>)>> {
+    clarification::table
+        .inner_join(user::table)
+        .left_join(contest_problems::table)
+        .filter(clarification::contest_id.eq(contest_id))
+        .select((
+            CLARIFICATION_COLUMNS,
+            USER_COLUMNS,
+            contest_problems::label.nullable(),
+        ))
+        .order(clarification::question_instant.desc())
+        .load(connection)
+}
+
+pub fn update_clarification(
+    connection: &mut PgConnection,
+    uuid: String,
+    new_answer: String,
+    new_answer_to_all: bool,
+    new_answer_user_id: i32,
+    new_answer_instant: NaiveDateTime,
+) -> QueryResult<()> {
+    update(clarification::table)
+        .filter(clarification::uuid.eq(uuid))
+        .set((
+            clarification::answer.eq(new_answer),
+            clarification::answer_to_all.eq(new_answer_to_all),
+            clarification::answer_user_id.eq(new_answer_user_id),
+            clarification::answer_instant.eq(new_answer_instant),
+        ))
+        .execute(connection)?;
+    Ok(())
+}
diff --git a/src/models/contest.rs b/src/models/contest.rs
index 34187dcdd6802ed23234db93b4a22b5016e8f3b4..bd9554e1d443a49c03b82b5da53d2feeaadb6d03 100644
--- a/src/models/contest.rs
+++ b/src/models/contest.rs
@@ -1,6 +1,7 @@
 use chrono::prelude::*;
 use diesel::insert_into;
 use diesel::prelude::*;
+use diesel::update;
 
 use crate::schema::{contest, contest_problems};
 
@@ -14,6 +15,8 @@ pub struct Contest {
     pub creation_instant: NaiveDateTime,
     pub grade_ratio: Option<i32>,
     pub grade_after_ratio: Option<i32>,
+    pub frozen_scoreboard_instant: Option<NaiveDateTime>,
+    pub blind_scoreboard_instant: Option<NaiveDateTime>,
 }
 
 use diesel::sql_types;
@@ -28,6 +31,10 @@ pub struct ContestWithAcs {
     pub start_instant: Option<NaiveDateTime>,
     #[diesel(sql_type = sql_types::Nullable<sql_types::Timestamp>)]
     pub end_instant: Option<NaiveDateTime>,
+    #[diesel(sql_type = sql_types::Nullable<sql_types::Timestamp>)]
+    pub frozen_scoreboard_instant: Option<NaiveDateTime>,
+    #[diesel(sql_type = sql_types::Nullable<sql_types::Timestamp>)]
+    pub blind_scoreboard_instant: Option<NaiveDateTime>,
     #[diesel(sql_type = sql_types::Integer)]
     pub creation_user_id: i32,
     #[diesel(sql_type = sql_types::Timestamp)]
@@ -65,6 +72,8 @@ pub const CONTEST_COLUMNS: (
     contest::creation_instant,
     contest::grade_ratio,
     contest::grade_after_ratio,
+    contest::frozen_scoreboard_instant,
+    contest::blind_scoreboard_instant,
 ) = (
     contest::id,
     contest::name,
@@ -74,6 +83,8 @@ pub const CONTEST_COLUMNS: (
     contest::creation_instant,
     contest::grade_ratio,
     contest::grade_after_ratio,
+    contest::frozen_scoreboard_instant,
+    contest::blind_scoreboard_instant,
 );
 
 pub fn insert_contest(
@@ -109,6 +120,8 @@ pub fn get_contests_with_acs(
         contest.end_instant,
         contest.creation_user_id,
         contest.creation_instant,
+        contest.frozen_scoreboard_instant,
+        contest.blind_scoreboard_instant,
         contest.grade_ratio,
         contest.grade_after_ratio,
         coalesce(cast(count(first_ac_submission_instant) filter (where
@@ -140,6 +153,12 @@ pub fn get_contest_by_id(connection: &mut PgConnection, id: i32) -> QueryResult<
     contest::table.filter(contest::id.eq(id)).first(connection)
 }
 
+pub fn get_contest_by_name(connection: &mut PgConnection, name: &str) -> QueryResult<Contest> {
+    contest::table
+        .filter(contest::name.eq(name))
+        .first(connection)
+}
+
 pub fn get_contest_by_contest_problem_id(
     connection: &mut PgConnection,
     contest_problem_id: i32,
@@ -168,3 +187,74 @@ pub fn relate_problem(
         .execute(connection)?;
     Ok(())
 }
+
+#[derive(Queryable, Debug)]
+pub struct ContestProblem {
+    pub id: i32,
+    pub label: String,
+    pub contest_id: i32,
+    pub problem_id: String,
+}
+
+pub fn get_contest_problems_by_contest(
+    connection: &mut PgConnection,
+    contest_id: i32,
+) -> QueryResult<Vec<ContestProblem>> {
+    contest_problems::table
+        .filter(contest_problems::contest_id.eq(contest_id))
+        .select((
+            contest_problems::id,
+            contest_problems::label,
+            contest_problems::contest_id,
+            contest_problems::problem_id,
+        ))
+        .load(connection)
+}
+
+pub fn update_contest_problem(
+    connection: &mut PgConnection,
+    contest_problem_id: i32,
+    new_problem_id: String,
+    new_label: String,
+) -> QueryResult<()> {
+    update(contest_problems::table)
+        .filter(contest_problems::id.eq(contest_problem_id))
+        .set((
+            contest_problems::problem_id.eq(new_problem_id),
+            contest_problems::label.eq(new_label),
+        ))
+        .execute(connection)?;
+    Ok(())
+}
+
+pub fn update_contest_problem_balloon_color(
+    connection: &mut PgConnection,
+    contest_problem_id: i32,
+    new_balloon_color: Option<String>,
+) -> QueryResult<()> {
+    update(contest_problems::table)
+        .filter(contest_problems::id.eq(contest_problem_id))
+        .set((contest_problems::balloon_color.eq(new_balloon_color),))
+        .execute(connection)?;
+    Ok(())
+}
+
+pub fn update_contest(
+    connection: &mut PgConnection,
+    contest_id: i32,
+    start_instant: Option<NaiveDateTime>,
+    end_instant: Option<NaiveDateTime>,
+    frozen_scoreboard_instant: Option<NaiveDateTime>,
+    blind_scoreboard_instant: Option<NaiveDateTime>,
+) -> QueryResult<()> {
+    update(contest::table)
+        .filter(contest::id.eq(contest_id))
+        .set((
+            contest::start_instant.eq(start_instant),
+            contest::end_instant.eq(end_instant),
+            contest::frozen_scoreboard_instant.eq(frozen_scoreboard_instant),
+            contest::blind_scoreboard_instant.eq(blind_scoreboard_instant),
+        ))
+        .execute(connection)?;
+    Ok(())
+}
diff --git a/src/models/printing_task.rs b/src/models/printing_task.rs
new file mode 100644
index 0000000000000000000000000000000000000000..66efedc8f848b34660f996da7cc2f01a021c098a
--- /dev/null
+++ b/src/models/printing_task.rs
@@ -0,0 +1,108 @@
+use chrono::prelude::*;
+use diesel::insert_into;
+use diesel::prelude::*;
+use diesel::update;
+
+use crate::schema::{printing_task, user};
+use crate::user::{User, USER_COLUMNS};
+
+#[derive(Queryable)]
+pub struct PrintingTask {
+    pub uuid: String,
+    pub contents: String,
+    pub creation_instant: NaiveDateTime,
+    pub status: String,
+    pub done_instant: Option<NaiveDateTime>,
+    pub contest_id: i32,
+    pub user_id: i32,
+}
+
+pub const PRINTING_TASK_COLUMNS: (
+    printing_task::uuid,
+    printing_task::contents,
+    printing_task::creation_instant,
+    printing_task::status,
+    printing_task::done_instant,
+    printing_task::contest_id,
+    printing_task::user_id,
+) = (
+    printing_task::uuid,
+    printing_task::contents,
+    printing_task::creation_instant,
+    printing_task::status,
+    printing_task::done_instant,
+    printing_task::contest_id,
+    printing_task::user_id,
+);
+
+#[derive(Insertable)]
+#[diesel(table_name = printing_task)]
+pub struct NewPrintingTask {
+    pub uuid: String,
+    pub contents: String,
+    pub creation_instant: NaiveDateTime,
+    pub status: String,
+    pub done_instant: Option<NaiveDateTime>,
+    pub contest_id: i32,
+    pub user_id: i32,
+}
+
+pub fn insert_printing_task(
+    connection: &mut PgConnection,
+    new_printing_task: NewPrintingTask,
+) -> QueryResult<PrintingTask> {
+    let uuid = new_printing_task.uuid.clone();
+    insert_into(printing_task::table)
+        .values(new_printing_task)
+        .execute(connection)?;
+    printing_task::table
+        .filter(printing_task::uuid.eq(uuid))
+        .first(connection)
+}
+
+pub fn get_printing_tasks_by_contest_user(
+    connection: &mut PgConnection,
+    user_id: i32,
+    contest_id: i32,
+) -> QueryResult<Vec<PrintingTask>> {
+    printing_task::table
+        .filter(printing_task::user_id.eq(user_id))
+        .filter(printing_task::contest_id.eq(contest_id))
+        .load(connection)
+}
+
+pub fn get_printing_tasks_by_contest(
+    connection: &mut PgConnection,
+    contest_id: i32,
+) -> QueryResult<Vec<(PrintingTask, User)>> {
+    printing_task::table
+        .inner_join(user::table)
+        .filter(printing_task::contest_id.eq(contest_id))
+        .select((PRINTING_TASK_COLUMNS, USER_COLUMNS))
+        .load(connection)
+}
+
+pub fn get_printing_task_by_uuid(
+    connection: &mut PgConnection,
+    uuid: String,
+) -> QueryResult<(PrintingTask, User)> {
+    printing_task::table
+        .inner_join(user::table)
+        .filter(printing_task::uuid.eq(uuid))
+        .select((PRINTING_TASK_COLUMNS, USER_COLUMNS))
+        .first(connection)
+}
+
+pub fn update_printing_task(
+    connection: &mut PgConnection,
+    uuid: String,
+    new_done_instant: NaiveDateTime,
+) -> QueryResult<()> {
+    update(printing_task::table)
+        .filter(printing_task::uuid.eq(uuid))
+        .set((
+            printing_task::done_instant.eq(new_done_instant),
+        ))
+        .execute(connection)?;
+    Ok(())
+}
diff --git a/src/models/problem.rs b/src/models/problem.rs
index 0d9f63def7e80a6a6727135a21334dc926961b53..a6c05afc0815ae708b251b65023b8bcfcc0fa3d9 100644
--- a/src/models/problem.rs
+++ b/src/models/problem.rs
@@ -12,17 +12,10 @@ pub struct Problem {
     pub name: String,
     pub memory_limit_bytes: i32,
     pub time_limit_ms: i32,
-    pub checker_path: String,
-    pub checker_language: String,
-    pub validator_path: String,
-    pub validator_language: String,
-    pub main_solution_path: String,
-    pub main_solution_language: String,
-    pub test_count: i32,
-    pub test_pattern: String,
     pub status: String,
     pub creation_user_id: i32,
     pub creation_instant: NaiveDateTime,
+    pub repository_id: Option<i32>,
 }
 
 #[derive(Insertable)]
@@ -32,14 +25,6 @@ pub struct NewProblem {
     pub name: String,
     pub memory_limit_bytes: i32,
     pub time_limit_ms: i32,
-    pub checker_path: String,
-    pub checker_language: String,
-    pub validator_path: String,
-    pub validator_language: String,
-    pub main_solution_path: String,
-    pub main_solution_language: String,
-    pub test_count: i32,
-    pub test_pattern: String,
     pub status: String,
     pub creation_user_id: i32,
     pub creation_instant: NaiveDateTime,
@@ -76,10 +61,16 @@ use diesel::sql_types;
 pub struct ProblemByContestWithScore {
     #[diesel(sql_type = sql_types::Nullable<sql_types::Text>)]
     pub user_name: Option<String>,
+    #[diesel(sql_type = sql_types::Nullable<sql_types::Text>)]
+    pub user_full_name: Option<String>,
     #[diesel(sql_type = sql_types::Nullable<sql_types::Timestamp>)]
     pub first_ac_submission_instant: Option<NaiveDateTime>,
+    #[diesel(sql_type = sql_types::Nullable<sql_types::Timestamp>)]
+    pub first_ac_problem_submission_instant: Option<NaiveDateTime>,
     #[diesel(sql_type = sql_types::Integer)]
     pub failed_submissions: i32,
+    #[diesel(sql_type = sql_types::Nullable<sql_types::Text>)]
+    pub last_failed_submission_verdict: Option<String>,
     #[diesel(sql_type = sql_types::Integer)]
     pub id: i32,
     #[diesel(sql_type = sql_types::Text)]
@@ -92,218 +83,335 @@ pub struct ProblemByContestWithScore {
     pub time_limit_ms: i32,
     #[diesel(sql_type = sql_types::Integer)]
     pub user_accepted_count: i32,
+    #[diesel(sql_type = sql_types::Nullable<sql_types::Text>)]
+    pub balloon_color: Option<String>,
+}
+
+const WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT: &str = r#"
+first_ac as (
+    select
+        min(submission_instant) as first_ac_submission_instant,
+        contest_problem_id,
+        filtered_submission.user_id
+    from filtered_submission
+    where filtered_submission.verdict = 'AC'
+    group by filtered_submission.user_id, filtered_submission.contest_problem_id
+), first_ac_by_problem as (
+    select
+        min(submission_instant) as first_ac_problem_submission_instant,
+        contest_problem_id
+    from filtered_submission
+    where filtered_submission.verdict = 'AC'
+    group by filtered_submission.contest_problem_id
+), failed_submission_count as (
+    select
+        filtered_submission.user_id,
+        filtered_submission.contest_problem_id,
+        cast(count(*) as int) as count
+    from filtered_submission
+    left join first_ac on first_ac.contest_problem_id = filtered_submission.contest_problem_id
+        and filtered_submission.user_id = first_ac.user_id
+    where (
+        first_ac_submission_instant is null or
+        filtered_submission.submission_instant < first_ac.first_ac_submission_instant
+    )
+    group by filtered_submission.user_id, filtered_submission.contest_problem_id
+), last_failed_submission_instant as (
+    select
+        filtered_submission.user_id,
+        filtered_submission.contest_problem_id,
+        max(filtered_submission.submission_instant) as last_submission_instant
+    from filtered_submission
+    left join first_ac on first_ac.contest_problem_id = filtered_submission.contest_problem_id
+        and filtered_submission.user_id = first_ac.user_id
+    where (
+        first_ac_submission_instant is null or
+        filtered_submission.submission_instant < first_ac.first_ac_submission_instant
+    )
+    group by filtered_submission.user_id, filtered_submission.contest_problem_id
+), last_failed_submission as (
+    select
+        filtered_submission.user_id,
+        filtered_submission.contest_problem_id,
+        filtered_submission.verdict
+    from filtered_submission
+    inner join last_failed_submission_instant on last_failed_submission_instant.contest_problem_id = filtered_submission.contest_problem_id
+        and last_failed_submission_instant.user_id = filtered_submission.user_id
+        and last_failed_submission_instant.last_submission_instant = filtered_submission.submission_instant
+), user_ac_count as (
+    select
+        cast(count(distinct filtered_submission.user_id) as int) as user_accepted_count,
+        filtered_submission.contest_problem_id
+    from filtered_submission
+    where filtered_submission.verdict = 'AC'
+    group by filtered_submission.contest_problem_id
+)
+"#;
+
+const ALL_SUBMISSIONS: &str = r#"
+filtered_submission as (select * from submission)
+"#;
+
+const ALL_SUBMISSIONS_IGNORING_CE_VERDICTS: &str = r#"
+filtered_submission as (select * from submission where submission.verdict <> 'CE')
+"#;
+
+const BLIND_AND_FROZEN_SUBMISSIONS_IGNORING_CE_VERDICTS: &str = r#"
+filtered_submission as (
+    select
+        submission.uuid,
+        CASE WHEN submission.submission_instant < contest.blind_scoreboard_instant
+            THEN submission.verdict ELSE 'BL' END as verdict,
+        submission.submission_instant,
+        submission.contest_problem_id,
+        submission.user_id
+    from submission
+    inner join contest_problems on contest_problems.id = submission.contest_problem_id
+    inner join contest on contest.id = contest_problems.contest_id
+    where ((submission.user_id = $1) OR (submission.submission_instant < contest.frozen_scoreboard_instant))
+    AND submission.verdict <> 'CE'
+)
+"#;
+
+const FROZEN_SUBMISSIONS_IGNORING_CE_VERDICTS: &str = r#"
+filtered_submission as (
+    select
+        submission.uuid,
+        submission.verdict as verdict,
+        submission.submission_instant,
+        submission.contest_problem_id,
+        submission.user_id
+    from submission
+    inner join contest_problems on contest_problems.id = submission.contest_problem_id
+    inner join contest on contest.id = contest_problems.contest_id
+    where submission.submission_instant < contest.frozen_scoreboard_instant
+    and submission.verdict <> 'CE'
+)
+"#;
+
+const SELECT_PROBLEMS_USER_WITH_SCORE: &str = r#"
+    select
+        "user".name as user_name,
+        "user".full_name as user_full_name,
+        first_ac_submission_instant,
+        first_ac_problem_submission_instant,
+        coalesce(failed_submission_count.count, 0) as failed_submissions,
+        last_failed_submission.verdict as last_failed_submission_verdict,
+        contest_problems.id,
+        problem.name,
+        contest_problems.label,
+        problem.memory_limit_bytes,
+        problem.time_limit_ms,
+        coalesce(user_ac_count.user_accepted_count, 0) as user_accepted_count,
+        contest_problems.balloon_color
+    from contest_problems
+    inner join problem on problem.id = contest_problems.problem_id
+    inner join "user" on "user".id = $1
+    left join last_failed_submission
+        on last_failed_submission.contest_problem_id = contest_problems.id
+        and last_failed_submission.user_id = "user".id
+    left join failed_submission_count
+        on failed_submission_count.contest_problem_id = contest_problems.id
+        and failed_submission_count.user_id = "user".id
+    left join first_ac
+        on first_ac.contest_problem_id = contest_problems.id
+        and first_ac.user_id = "user".id
+    left join first_ac_by_problem
+        on first_ac_by_problem.contest_problem_id = contest_problems.id
+    left join user_ac_count
+        on user_ac_count.contest_problem_id = contest_problems.id
+"#;
+
+// all verdicts for a given user
+pub fn get_problems_user_with_score_without_blind_and_frozen(
+    connection: &mut PgConnection,
+    user_id: i32,
+) -> QueryResult<Vec<ProblemByContestWithScore>> {
+    diesel::sql_query(format!(
+        r#"
+        with {}, {}
+        {}
+        order by problem.creation_instant
+    "#,
+        ALL_SUBMISSIONS,
+        WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT,
+        SELECT_PROBLEMS_USER_WITH_SCORE
+    ))
+    .bind::<sql_types::Integer, _>(user_id)
+    .load(connection)
 }
 
-pub fn get_problems_user_with_score(
+pub fn get_problems_user_with_score_with_blind_and_frozen(
     connection: &mut PgConnection,
     user_id: i32,
 ) -> QueryResult<Vec<ProblemByContestWithScore>> {
-    diesel::sql_query(
+    diesel::sql_query(format!(
         r#"
-        with first_ac as (
-            select
-                min(submission_instant) as first_ac_submission_instant,
-                contest_problem_id,
-                submission.user_id
-            from submission
-            where submission.verdict = 'AC'
-            group by submission.user_id, submission.contest_problem_id
-        ), failed_submissions as (
-            select
-                submission.user_id,
-                submission.contest_problem_id,
-                cast(count(*) as int) as count
-            from submission
-            left join first_ac on first_ac.contest_problem_id = submission.contest_problem_id
-                and submission.user_id = first_ac.user_id
-            where (
-                first_ac_submission_instant is null or
-                submission.submission_instant < first_ac.first_ac_submission_instant
-            )
-            group by submission.user_id, submission.contest_problem_id
-        ), user_acs_count as (
-            select
-                cast(count(distinct submission.user_id) as int) as user_accepted_count,
-                submission.contest_problem_id
-            from submission
-            where submission.verdict = 'AC'
-            group by submission.contest_problem_id
-        )
-        select
-            "user".name as user_name,
-            first_ac_submission_instant,
-            coalesce(failed_submissions.count, 0) as failed_submissions,
-            contest_problems.id,
-            problem.name,
-            contest_problems.label,
-            problem.memory_limit_bytes,
-            problem.time_limit_ms,
-            coalesce(user_acs_count.user_accepted_count, 0) as user_accepted_count
-        from contest_problems
-        inner join problem on problem.id = contest_problems.problem_id
-        inner join "user" on "user".id = $1
-        left join failed_submissions
-            on failed_submissions.contest_problem_id = contest_problems.id
-            and failed_submissions.user_id = "user".id
-        left join first_ac
-            on first_ac.contest_problem_id = contest_problems.id
-            and first_ac.user_id = "user".id
-        left join user_acs_count
-            on user_acs_count.contest_problem_id = contest_problems.id
+        with {}, {}
+        {}
+        order by problem.creation_instant
+    "#,
+        BLIND_AND_FROZEN_SUBMISSIONS_IGNORING_CE_VERDICTS,
+        WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT,
+        SELECT_PROBLEMS_USER_WITH_SCORE
+    ))
+    .bind::<sql_types::Integer, _>(user_id)
+    .load(connection)
+}
+
+pub fn get_problems_user_by_contest_id_with_score_without_blind_and_frozen(
+    connection: &mut PgConnection,
+    user_id: i32,
+    contest_id: i32,
+) -> QueryResult<Vec<ProblemByContestWithScore>> {
+    diesel::sql_query(format!(
+        r#"
+        with {}, {}
+        {}
+        where contest_id = $2
         order by contest_problems.label
     "#,
-    )
+        ALL_SUBMISSIONS_IGNORING_CE_VERDICTS,
+        WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT,
+        SELECT_PROBLEMS_USER_WITH_SCORE
+    ))
     .bind::<sql_types::Integer, _>(user_id)
+    .bind::<sql_types::Integer, _>(contest_id)
     .load(connection)
 }
 
-pub fn get_problems_user_by_contest_id_with_score(
+pub fn get_problems_user_by_contest_id_with_score_with_blind_and_frozen(
     connection: &mut PgConnection,
     user_id: i32,
     contest_id: i32,
 ) -> QueryResult<Vec<ProblemByContestWithScore>> {
-    diesel::sql_query(
+    diesel::sql_query(format!(
         r#"
-        with first_ac as (
-            select
-                min(submission_instant) as first_ac_submission_instant,
-                contest_problem_id,
-                submission.user_id
-            from submission
-            where submission.verdict = 'AC'
-            group by submission.user_id, submission.contest_problem_id
-        ), failed_submissions as (
-            select
-                submission.user_id,
-                submission.contest_problem_id,
-                cast(count(*) as int) as count
-            from submission
-            left join first_ac on first_ac.contest_problem_id = submission.contest_problem_id
-                and submission.user_id = first_ac.user_id
-            where (
-                first_ac_submission_instant is null or
-                submission.submission_instant < first_ac.first_ac_submission_instant
-            )
-            group by submission.user_id, submission.contest_problem_id
-        ), user_acs_count as (
-            select
-                cast(count(distinct submission.user_id) as int) as user_accepted_count,
-                submission.contest_problem_id
-            from submission
-            where submission.verdict = 'AC'
-            group by submission.contest_problem_id
-        )
-        select
-            "user".name as user_name,
-            first_ac_submission_instant,
-            coalesce(failed_submissions.count, 0) as failed_submissions,
-            contest_problems.id,
-            problem.name,
-            contest_problems.label,
-            problem.memory_limit_bytes,
-            problem.time_limit_ms,
-            coalesce(user_acs_count.user_accepted_count, 0) as user_accepted_count
-        from contest_problems
-        inner join problem on problem.id = contest_problems.problem_id
-        inner join "user" on "user".id = $1
-        left join failed_submissions
-            on failed_submissions.contest_problem_id = contest_problems.id
-            and failed_submissions.user_id = "user".id
-        left join first_ac
-            on first_ac.contest_problem_id = contest_problems.id
-            and first_ac.user_id = "user".id
-        left join user_acs_count
-            on user_acs_count.contest_problem_id = contest_problems.id
+        with {}, {}
+        {}
         where contest_id = $2
         order by contest_problems.label
     "#,
-    )
+        BLIND_AND_FROZEN_SUBMISSIONS_IGNORING_CE_VERDICTS,
+        WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT,
+        SELECT_PROBLEMS_USER_WITH_SCORE
+    ))
     .bind::<sql_types::Integer, _>(user_id)
     .bind::<sql_types::Integer, _>(contest_id)
     .load(connection)
 }
 
-pub fn get_problems_by_contest_id_with_score(
+const SELECT_PROBLEMS_BY_CONTEST_ID_WITH_SCORE: &str = r#"
+    (select
+        null as user_name,
+        null as user_full_name,
+        null as first_ac_submission_instant,
+        null as first_ac_problem_submission_instant,
+        0 as failed_submissions,
+        null as last_failed_submission_verdict,
+        contest_problems.id,
+        problem.name,
+        contest_problems.label,
+        problem.memory_limit_bytes,
+        problem.time_limit_ms,
+        0 as user_accepted_count,
+        contest_problems.balloon_color
+    from contest_problems
+    inner join problem on problem.id = contest_problems.problem_id
+    where contest_problems.contest_id = $2
+    order by contest_problems.label)
+    union all
+    (select
+        "user".name as user_name,
+        "user".full_name as user_full_name,
+        first_ac_submission_instant,
+        first_ac_problem_submission_instant,
+        coalesce(failed_submission_count.count, 0) as failed_submissions,
+        last_failed_submission.verdict as last_failed_submission_verdict,
+        contest_problems.id,
+        problem.name,
+        contest_problems.label,
+        problem.memory_limit_bytes,
+        problem.time_limit_ms,
+        coalesce(user_ac_count.user_accepted_count, 0) as user_accepted_count,
+        contest_problems.balloon_color
+    from contest_problems
+    inner join problem on problem.id = contest_problems.problem_id
+    inner join (
+        select user_id, contest_id
+        from filtered_submission
+        inner join contest_problems on contest_problems.id = filtered_submission.contest_problem_id
+        group by user_id, contest_id
+    ) as all_submitters on all_submitters.contest_id = $2
+    inner join "user" on "user".id = all_submitters.user_id
+    left join failed_submission_count
+        on failed_submission_count.contest_problem_id = contest_problems.id
+        and failed_submission_count.user_id = "user".id
+    left join last_failed_submission
+        on last_failed_submission.contest_problem_id = contest_problems.id
+        and last_failed_submission.user_id = "user".id
+    left join first_ac
+        on first_ac.contest_problem_id = contest_problems.id
+        and first_ac.user_id = "user".id
+    left join first_ac_by_problem
+        on first_ac_by_problem.contest_problem_id = contest_problems.id
+    left join user_ac_count
+        on user_ac_count.contest_problem_id = contest_problems.id
+    where contest_problems.contest_id = $2
+    order by "user".name, contest_problems.label)
+"#;
+
+pub fn get_problems_by_contest_id_with_score_without_blind_and_frozen(
     connection: &mut PgConnection,
     contest_id: i32,
 ) -> QueryResult<Vec<ProblemByContestWithScore>> {
-    diesel::sql_query(
+    diesel::sql_query(format!(
         r#"
-        with first_ac as (
-            select
-                min(submission_instant) as first_ac_submission_instant,
-                contest_problem_id,
-                submission.user_id
-            from submission
-            where submission.verdict = 'AC'
-            group by submission.user_id, submission.contest_problem_id
-        ), failed_submissions as (
-            select
-                submission.user_id,
-                submission.contest_problem_id,
-                cast(count(*) as int) as count
-            from submission
-            left join first_ac on first_ac.contest_problem_id = submission.contest_problem_id
-                and submission.user_id = first_ac.user_id
-            where (
-                first_ac_submission_instant is null or
-                submission.submission_instant < first_ac.first_ac_submission_instant
-            )
-            group by submission.user_id, submission.contest_problem_id
-        ), all_submitters as (
-            select user_id, contest_id
-            from submission
-            inner join contest_problems on contest_problems.id = submission.contest_problem_id
-            group by user_id, contest_id
-        ), user_acs_count as (
-            select
-                cast(count(distinct submission.user_id) as int) as user_accepted_count,
-                submission.contest_problem_id
-            from submission
-            where submission.verdict = 'AC'
-            group by submission.contest_problem_id
-        )
-        (select
-            null as user_name,
-            null as first_ac_submission_instant,
-            0 as failed_submissions,
-            contest_problems.id,
-            problem.name,
-            contest_problems.label,
-            problem.memory_limit_bytes,
-            problem.time_limit_ms,
-            0 as user_accepted_count
-        from contest_problems
-        inner join problem on problem.id = contest_problems.problem_id
-        where contest_problems.contest_id = $1
-        order by contest_problems.label)
-        union all
-        (select
-            "user".name as user_name,
-            first_ac_submission_instant,
-            coalesce(failed_submissions.count, 0) as failed_submissions,
-            contest_problems.id,
-            problem.name,
-            contest_problems.label,
-            problem.memory_limit_bytes,
-            problem.time_limit_ms,
-            coalesce(user_acs_count.user_accepted_count, 0) as user_accepted_count
-        from contest_problems
-        inner join problem on problem.id = contest_problems.problem_id
-        inner join all_submitters on all_submitters.contest_id = $1
-        inner join "user" on "user".id = all_submitters.user_id
-        left join failed_submissions
-            on failed_submissions.contest_problem_id = contest_problems.id
-            and failed_submissions.user_id = "user".id
-        left join first_ac
-            on first_ac.contest_problem_id = contest_problems.id
-            and first_ac.user_id = "user".id
-        left join user_acs_count
-            on user_acs_count.contest_problem_id = contest_problems.id
-        where contest_problems.contest_id = $1
-        order by "user".name, contest_problems.label)
+        with {}, {}
+        {}
     "#,
-    )
+        ALL_SUBMISSIONS_IGNORING_CE_VERDICTS,
+        WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT,
+        SELECT_PROBLEMS_BY_CONTEST_ID_WITH_SCORE
+    ))
+    .bind::<sql_types::Integer, _>(0)
+    .bind::<sql_types::Integer, _>(contest_id)
+    .load(connection)
+}
+
+pub fn get_problems_by_contest_id_with_score_with_blind_and_frozen(
+    connection: &mut PgConnection,
+    user_id: i32,
+    contest_id: i32,
+) -> QueryResult<Vec<ProblemByContestWithScore>> {
+    diesel::sql_query(format!(
+        r#"
+        with {}, {}
+        {}
+    "#,
+        BLIND_AND_FROZEN_SUBMISSIONS_IGNORING_CE_VERDICTS,
+        WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT,
+        SELECT_PROBLEMS_BY_CONTEST_ID_WITH_SCORE
+    ))
+    .bind::<sql_types::Integer, _>(user_id)
+    .bind::<sql_types::Integer, _>(contest_id)
+    .load(connection)
+}
+
+pub fn get_problems_by_contest_id_with_score_with_frozen(
+    connection: &mut PgConnection,
+    contest_id: i32,
+) -> QueryResult<Vec<ProblemByContestWithScore>> {
+    diesel::sql_query(format!(
+        r#"
+        with {}, {}
+        {}
+    "#,
+        FROZEN_SUBMISSIONS_IGNORING_CE_VERDICTS,
+        WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT,
+        SELECT_PROBLEMS_BY_CONTEST_ID_WITH_SCORE
+    ))
+    .bind::<sql_types::Integer, _>(0)
     .bind::<sql_types::Integer, _>(contest_id)
     .load(connection)
 }
@@ -330,17 +438,10 @@ pub fn get_problem_by_contest_id_label(
 #[derive(Queryable)]
 pub struct ProblemByContestMetadata {
     pub contest_id: i32,
+    pub label: String,
     pub id: String,
     pub memory_limit_bytes: i32,
     pub time_limit_ms: i32,
-    pub checker_path: String,
-    pub checker_language: String,
-    pub validator_path: String,
-    pub validator_language: String,
-    pub main_solution_path: String,
-    pub main_solution_language: String,
-    pub test_count: i32,
-    pub test_pattern: String,
     pub status: String,
 }
 
@@ -353,17 +454,10 @@ pub fn get_problem_by_contest_id_metadata(
         .filter(contest_problems::id.eq(contest_problem_id))
         .select((
             contest_problems::contest_id,
+            contest_problems::label,
             problem::id,
             problem::memory_limit_bytes,
             problem::time_limit_ms,
-            problem::checker_path,
-            problem::checker_language,
-            problem::validator_path,
-            problem::validator_language,
-            problem::main_solution_path,
-            problem::main_solution_language,
-            problem::test_count,
-            problem::test_pattern,
             problem::status,
         ))
         .first(connection)
diff --git a/src/models/repository.rs b/src/models/repository.rs
new file mode 100644
index 0000000000000000000000000000000000000000..24d28e74e3dc1f907b4ce46d2a0c1a73fdb786f2
--- /dev/null
+++ b/src/models/repository.rs
@@ -0,0 +1,49 @@
+use chrono::prelude::*;
+use diesel::insert_into;
+use diesel::prelude::*;
+
+use crate::schema::repository;
+
+#[derive(Queryable)]
+pub struct Repository {
+    pub id: i32,
+    pub name: String,
+    pub path: String,
+    pub remote_url: String,
+    pub creation_user_id: i32,
+    pub creation_instant: NaiveDateTime,
+}
+
+#[derive(Insertable)]
+#[diesel(table_name = repository)]
+pub struct NewRepository {
+    pub name: String,
+    pub path: String,
+    pub remote_url: String,
+    pub creation_user_id: i32,
+    pub creation_instant: NaiveDateTime,
+}
+
+pub fn insert_repository(
+    connection: &mut PgConnection,
+    new_repository: NewRepository,
+) -> QueryResult<Repository> {
+    insert_into(repository::table)
+        .values(new_repository)
+        .execute(connection)?;
+    repository::table
+        .order(repository::id.desc())
+        .first(connection)
+}
+
+pub fn get_repositories(connection: &mut PgConnection) -> QueryResult<Vec<Repository>> {
+    repository::table
+        .order(repository::creation_instant.desc())
+        .load(connection)
+}
+
+pub fn get_repository_by_id(connection: &mut PgConnection, id: i32) -> QueryResult<Repository> {
+    repository::table
+        .filter(repository::id.eq(id))
+        .first(connection)
+}
diff --git a/src/models/submission.rs b/src/models/submission.rs
index 5ecad3644972fb7d10d7d510aa56bbec253f3c41..aca7c4bf47ee4ee93798194046bb0946a78eb24e 100644
--- a/src/models/submission.rs
+++ b/src/models/submission.rs
@@ -22,6 +22,7 @@ pub struct Submission {
     pub contest_problem_id: i32,
     pub user_id: i32,
     pub failed_test: Option<i32>,
+    pub balloon_delivered_instant: Option<NaiveDateTime>,
 }
 
 #[allow(clippy::type_complexity)]
@@ -40,6 +41,7 @@ const SUBMISSION_COLUMNS: (
     submission::contest_problem_id,
     submission::user_id,
     submission::failed_test,
+    submission::balloon_delivered_instant,
 ) = (
     submission::uuid,
     submission::verdict,
@@ -55,6 +57,7 @@ const SUBMISSION_COLUMNS: (
     submission::contest_problem_id,
     submission::user_id,
     submission::failed_test,
+    submission::balloon_delivered_instant,
 );
 
 #[derive(Queryable)]
@@ -63,6 +66,8 @@ pub struct ContestProblem {
     pub label: String,
     pub contest_id: i32,
     pub problem_id: String,
+    pub balloon_color: Option<String>,
+    pub problem_name_override: Option<String>,
 }
 
 const CONTEST_PROBLEMS_COLUMNS: (
@@ -70,11 +75,15 @@ const CONTEST_PROBLEMS_COLUMNS: (
     contest_problems::label,
     contest_problems::contest_id,
     contest_problems::problem_id,
+    contest_problems::balloon_color,
+    contest_problems::problem_name_override,
 ) = (
     contest_problems::id,
     contest_problems::label,
     contest_problems::contest_id,
     contest_problems::problem_id,
+    contest_problems::balloon_color,
+    contest_problems::problem_name_override,
 );
 
 #[derive(Insertable)]
@@ -137,22 +146,15 @@ pub fn get_waiting_judge_submissions(
 ) -> QueryResult<Vec<(Submission, ProblemByContestMetadata)>> {
     submission::table
         .inner_join(contest_problems::table.inner_join(problem::table))
-        .order_by(submission::submission_instant.asc())
+        .order_by(submission::submission_instant.desc())
         .select((
             SUBMISSION_COLUMNS,
             (
                 contest_problems::contest_id,
+                contest_problems::label,
                 problem::id,
                 problem::memory_limit_bytes,
                 problem::time_limit_ms,
-                problem::checker_path,
-                problem::checker_language,
-                problem::validator_path,
-                problem::validator_language,
-                problem::main_solution_path,
-                problem::main_solution_language,
-                problem::test_count,
-                problem::test_pattern,
                 problem::status,
             ),
         ))
@@ -202,6 +204,19 @@ pub fn get_submissions_user(
         .load::<(Submission, ContestProblem, User)>(connection)
 }
 
+pub fn get_submissions_by_contest(
+    connection: &mut PgConnection,
+    contest_id: i32,
+) -> QueryResult<Vec<(Submission, ContestProblem, User)>> {
+    submission::table
+        .inner_join(contest_problems::table)
+        .inner_join(user::table)
+        .filter(contest_problems::contest_id.eq(contest_id))
+        .order_by(submission::submission_instant.desc())
+        .select((SUBMISSION_COLUMNS, CONTEST_PROBLEMS_COLUMNS, USER_COLUMNS))
+        .load::<(Submission, ContestProblem, User)>(connection)
+}
+
 pub fn get_submissions_user_by_contest(
     connection: &mut PgConnection,
     user_id: i32,
@@ -217,6 +232,21 @@ pub fn get_submissions_user_by_contest(
         .load::<(Submission, ContestProblem, User)>(connection)
 }
 
+pub fn get_submissions_by_contest_problem(
+    connection: &mut PgConnection,
+    contest_id: i32,
+    problem_label: &str,
+) -> QueryResult<Vec<(Submission, ContestProblem, User)>> {
+    submission::table
+        .inner_join(contest_problems::table)
+        .inner_join(user::table)
+        .filter(contest_problems::contest_id.eq(contest_id))
+        .filter(contest_problems::label.eq(problem_label))
+        .order_by(submission::submission_instant.desc())
+        .select((SUBMISSION_COLUMNS, CONTEST_PROBLEMS_COLUMNS, USER_COLUMNS))
+        .load::<(Submission, ContestProblem, User)>(connection)
+}
+
 pub fn get_submissions_user_by_contest_problem(
     connection: &mut PgConnection,
     user_id: i32,
@@ -233,3 +263,17 @@ pub fn get_submissions_user_by_contest_problem(
         .select((SUBMISSION_COLUMNS, CONTEST_PROBLEMS_COLUMNS, USER_COLUMNS))
         .load::<(Submission, ContestProblem, User)>(connection)
 }
+
+pub fn deliver_balloon(
+    connection: &mut PgConnection,
+    uuid: String,
+    new_balloon_delivered_instant: Option<NaiveDateTime>,
+) -> QueryResult<()> {
+    diesel::update(submission::table)
+        .filter(submission::uuid.eq(uuid))
+        .set((
+            submission::balloon_delivered_instant.eq(new_balloon_delivered_instant),
+        ))
+        .execute(connection)?;
+    Ok(())
+}
diff --git a/src/models/user.rs b/src/models/user.rs
index 510da99613799eb0003bfba8aea11c71e3d4a19e..734fafa80e270b175667ef09b032b33411ff23c7 100644
--- a/src/models/user.rs
+++ b/src/models/user.rs
@@ -19,15 +19,30 @@ struct UserWithHashedPassword {
 pub struct User {
     pub id: i32,
     pub name: String,
+    pub full_name: Option<String>,
+    pub single_contest_id: Option<i32>,
     pub is_admin: bool,
 }
-pub const USER_COLUMNS: (user::id, user::name, user::is_admin) =
-    (user::id, user::name, user::is_admin);
+pub const USER_COLUMNS: (
+    user::id,
+    user::name,
+    user::full_name,
+    user::single_contest_id,
+    user::is_admin,
+) = (
+    user::id,
+    user::name,
+    user::full_name,
+    user::single_contest_id,
+    user::is_admin,
+);
 
 #[derive(Insertable)]
 #[diesel(table_name = user)]
 struct DatabaseNewUser<'a> {
     pub name: &'a str,
+    pub full_name: Option<&'a str>,
+    pub single_contest_id: Option<i32>,
     pub hashed_password: &'a str,
     pub is_admin: bool,
     pub creation_instant: NaiveDateTime,
@@ -36,6 +51,8 @@ struct DatabaseNewUser<'a> {
 
 pub struct NewUser<'a> {
     pub name: &'a str,
+    pub full_name: Option<&'a str>,
+    pub single_contest_id: Option<i32>,
     pub password: &'a str,
     pub is_admin: bool,
     pub creation_instant: NaiveDateTime,
@@ -133,6 +150,8 @@ pub fn insert_new_user(
     insert_into(user::table)
         .values(DatabaseNewUser {
             name: new_user.name,
+            full_name: new_user.full_name,
+            single_contest_id: new_user.single_contest_id,
             hashed_password: &hashed_password,
             is_admin: new_user.is_admin,
             creation_instant: new_user.creation_instant,
@@ -142,3 +161,14 @@ pub fn insert_new_user(
 
     Ok(get_user_by_name(connection, new_user.name)?)
 }
+
+pub fn get_users_by_single_contest_id(
+    connection: &mut PgConnection,
+    single_contest_id: i32,
+) -> QueryResult<Vec<User>> {
+    user::table
+        .filter(user::single_contest_id.eq(single_contest_id))
+        .select(USER_COLUMNS)
+        .order(user::name)
+        .load(connection)
+}
diff --git a/src/pages/change_password.rs b/src/pages/change_password.rs
index 8daa2c6eea90f4a787d097c052b04d4ed199b44d..ca8e9da1c74f65dd434606c66ee49a2951fe0070 100644
--- a/src/pages/change_password.rs
+++ b/src/pages/change_password.rs
@@ -1,5 +1,6 @@
 use crate::models::user;
 use crate::models::user::PasswordMatched;
+use crate::pages::assert_not_single_contest;
 use crate::pages::prelude::*;
 
 #[derive(Serialize, Deserialize)]
@@ -16,7 +17,10 @@ async fn change_password(
     pool: Data<DbPool>,
     request: HttpRequest,
 ) -> PageResult {
-    let identity = require_identity(&identity)?;
+    let logged_user = require_identity(&identity)?;
+    if let Some(redirect) = assert_not_single_contest(&logged_user) {
+        return redirect;
+    }
     if form.new_password != form.new_password_repeat {
         return Err(PageError::Validation("Senhas são diferentes".into()));
     }
@@ -25,7 +29,7 @@ async fn change_password(
 
     match user::change_password(
         &mut connection,
-        identity.id,
+        logged_user.id,
         &form.old_password,
         &form.new_password,
     )? {
diff --git a/src/pages/create_backup.rs b/src/pages/create_backup.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c399d9a46f20b93ef6810fadf22521431a45a3b9
--- /dev/null
+++ b/src/pages/create_backup.rs
@@ -0,0 +1,90 @@
+use chrono::Local;
+use std::io::{Cursor, Write};
+use uuid::Uuid;
+
+use actix_multipart::Multipart;
+use futures::{StreamExt, TryStreamExt};
+
+use crate::models::{backup, contest};
+use crate::pages::assert_contest_not_started;
+use crate::pages::prelude::*;
+
+#[post("/contests/{id}/backups/")]
+async fn create_backup(
+    identity: Identity,
+    mut payload: Multipart,
+    pool: Data<DbPool>,
+    path: Path<(i32,)>,
+    request: HttpRequest,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    #[derive(Debug)]
+    struct Form {
+        contents: Option<Vec<u8>>,
+        filename: Option<String>,
+    }
+
+    let mut form = Form {
+        contents: None,
+        filename: None,
+    };
+
+    while let Ok(Some(mut field)) = payload.try_next().await {
+        let mut cursor = Cursor::new(vec![]);
+        while let Some(chunk) = field.next().await {
+            let data = chunk.unwrap();
+            cursor
+                .write(&data)
+                .map_err(|_| PageError::Validation("Corpo inválido".into()))?;
+        }
+
+        cursor.set_position(0);
+
+        let content_disposition = field.content_disposition();
+        match content_disposition.get_name() {
+            Some("contents") => {
+                form.filename = Some(
+                    content_disposition
+                        .get_filename()
+                        .ok_or(PageError::Validation(format!(
+                            "Campo contents sem nome de arquivo"
+                        )))?
+                        .into(),
+                );
+                form.contents = Some(cursor.into_inner());
+            }
+            _ => {}
+        }
+    }
+
+    if let None = form.contents {
+        return Err(PageError::Validation("Arquivo não submetido".into()));
+    }
+
+    let contents = form.contents.unwrap();
+    let filename = form.filename.unwrap();
+
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    assert_contest_not_started(&logged_user, &contest)?;
+
+    let uuid = Uuid::new_v4();
+    backup::insert_backup(
+        &mut connection,
+        backup::NewBackup {
+            uuid: uuid.to_string(),
+            filename,
+            contents,
+            creation_instant: Local::now().naive_utc(),
+            contest_id: contest.id,
+            user_id: logged_user.id,
+        },
+    )?;
+
+    Ok(redirect_to_referer(
+        "Backup criado com sucesso".into(),
+        &request,
+    ))
+}
diff --git a/src/pages/create_clarification.rs b/src/pages/create_clarification.rs
new file mode 100644
index 0000000000000000000000000000000000000000..cf8a8d7caa262f79a5f65e2d2beec55b53977290
--- /dev/null
+++ b/src/pages/create_clarification.rs
@@ -0,0 +1,52 @@
+use chrono::Local;
+use serde_with::serde_as;
+use serde_with::NoneAsEmptyString;
+use uuid::Uuid;
+
+use crate::models::clarification::NewClarification;
+use crate::models::{clarification, contest};
+use crate::pages::assert_contest_not_started;
+use crate::pages::prelude::*;
+
+#[serde_as]
+#[derive(Serialize, Deserialize)]
+struct CreateClarificationForm {
+    question: String,
+    #[serde_as(as = "NoneAsEmptyString")]
+    contest_problem_id: Option<i32>,
+}
+
+#[post("/contests/{id}/clarifications/")]
+async fn create_clarification(
+    identity: Identity,
+    pool: Data<DbPool>,
+    form: Form<CreateClarificationForm>,
+    path: Path<(i32,)>,
+    request: HttpRequest,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    assert_contest_not_started(&logged_user, &contest)?;
+
+    let uuid = Uuid::new_v4();
+    clarification::insert_clarification(
+        &mut connection,
+        NewClarification {
+            uuid: uuid.to_string(),
+            question: form.question.clone(),
+            question_instant: Local::now().naive_utc(),
+            answer_to_all: false,
+            contest_problem_id: form.contest_problem_id,
+            contest_id: contest.id,
+            user_id: logged_user.id,
+        },
+    )?;
+
+    Ok(redirect_to_referer(
+        "Clarificação criada com sucesso".into(),
+        &request,
+    ))
+}
diff --git a/src/pages/create_contest.rs b/src/pages/create_contest.rs
index a71d36c394b88023b65ce0e4eed1f73913c4f54c..4392e914bd03ee01bdc20031dc7ba8d2c1e75204 100644
--- a/src/pages/create_contest.rs
+++ b/src/pages/create_contest.rs
@@ -227,39 +227,35 @@ async fn create_contest(
                 name: metadata.names.name[0].value.clone(),
                 memory_limit_bytes: metadata.judging.testset[0].memory_limit.parse().unwrap(),
                 time_limit_ms: metadata.judging.testset[0].time_limit.parse().unwrap(),
-                checker_path: metadata.assets.checker.source.path.clone(),
-                checker_language: map_codeforces_language(&metadata.assets.checker.r#type)?,
-                validator_path: metadata.assets.validators.validator[0].source.path.clone(),
-                validator_language: map_codeforces_language(
-                    &metadata.assets.validators.validator[0].source.r#type,
-                )?,
-                main_solution_path: main_solution.path.clone(),
-                main_solution_language: map_codeforces_language(&main_solution.r#type)?,
-                test_pattern: metadata.judging.testset[0].input_path_pattern.clone(),
-                test_count: metadata.judging.testset[0].test_count.parse().unwrap(),
                 status: "compiled".into(),
                 creation_instant: Local::now().naive_utc(),
                 creation_user_id: logged_user.id,
             },
         )?;
 
+        let main_solution_path = &main_solution.path;
+        let main_solution_language = map_codeforces_language(&main_solution.r#type)?;
+        let test_pattern = &metadata.judging.testset[0].input_path_pattern;
+        let test_count: u32 = metadata.judging.testset[0].test_count.parse().unwrap();
+
         for (i, test) in metadata.judging.testset[0].tests.test.iter().enumerate() {
             let i = i + 1;
             let test_path = format!(
                 "./{}/{}",
                 problem_id,
-                import_contest::format_width(&problem.test_pattern, i)
+                import_contest::format_width(&test_pattern, i)
             );
 
             info!(
-                "Iterating through test {} to {:#?}, which is {}",
+                "Iterating through test {}/{} to {:#?}, which is {}",
                 i,
                 test_path,
+                test_count,
                 test.method.as_ref().unwrap()
             );
             if test.method.as_ref().unwrap() == "manual" {
-                let test_name = PathBuf::from(&name)
-                    .join(import_contest::format_width(&problem.test_pattern, i));
+                let test_name =
+                    PathBuf::from(&name).join(import_contest::format_width(&test_pattern, i));
                 info!("Extracting {:#?} from zip", test_name);
                 std::io::copy(
                     &mut zip.by_name(test_name.to_str().unwrap())?,
@@ -293,8 +289,8 @@ async fn create_contest(
             let run_stats = language::run_cached(
                 &job_sender,
                 &job_result_sender,
-                &problem.main_solution_language,
-                format!("./{}/{}", problem.id, problem.main_solution_path),
+                &main_solution_language,
+                format!("./{}/{}", problem.id, main_solution_path),
                 vec![],
                 Some(test_path.clone()),
                 Some(format!("{}.a", test_path)),
@@ -313,15 +309,12 @@ async fn create_contest(
         language::judge(
             &job_sender,
             &job_result_sender,
-            &problem.main_solution_language,
+            &main_solution_language,
             fs::read_to_string(PathBuf::from(format!(
                 "/data/{}/{}",
-                problem.id, problem.main_solution_path
+                problem.id, main_solution_path
             )))?,
-            problem.test_count,
-            format!("./{}/{}", problem.id, problem.test_pattern),
-            problem.checker_language,
-            format!("./{}/{}", problem.id, problem.checker_path),
+            problem.id,
             problem.memory_limit_bytes / 1_024,
             problem.time_limit_ms,
         )
diff --git a/src/pages/create_contest_submission.rs b/src/pages/create_contest_submission.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fe4cbf296e9bba782fed70923110c0980054ba34
--- /dev/null
+++ b/src/pages/create_contest_submission.rs
@@ -0,0 +1,128 @@
+use chrono::Local;
+use std::io::{Cursor, Read, Write};
+use uuid::Uuid;
+
+use actix_multipart::Multipart;
+use futures::{StreamExt, TryStreamExt};
+
+use crate::models::{contest, problem, submission};
+use crate::pages::assert_contest_not_over;
+use crate::pages::assert_contest_not_started;
+use crate::pages::prelude::*;
+use crate::queue::job_protocol::{job, Job, Language};
+
+#[post("/contests/{id}/submissions/")]
+async fn create_contest_submission(
+    identity: Identity,
+    mut payload: Multipart,
+    pool: Data<DbPool>,
+    job_sender: Data<Sender<Job>>,
+    languages: Data<Arc<DashMap<String, Language>>>,
+    session: Session,
+    request: HttpRequest,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let mut connection = pool.get()?;
+
+    #[derive(Debug)]
+    struct Form {
+        contest_problem_id: Option<i32>,
+        language: Option<String>,
+        source_text: Option<String>,
+    }
+
+    let mut form = Form {
+        contest_problem_id: None,
+        language: None,
+        source_text: None,
+    };
+
+    while let Ok(Some(mut field)) = payload.try_next().await {
+        let mut cursor = Cursor::new(vec![]);
+        while let Some(chunk) = field.next().await {
+            let data = chunk.unwrap();
+            cursor
+                .write(&data)
+                .map_err(|_| PageError::Validation("Corpo inválido".into()))?;
+        }
+
+        cursor.set_position(0);
+
+        fn parse_field(field: &str, cursor: &mut Cursor<Vec<u8>>) -> Result<String, PageError> {
+            let mut value = String::new();
+            cursor
+                .read_to_string(&mut value)
+                .map_err(|_| PageError::Validation(format!("Campo {} inválido", field)))?;
+            Ok(value)
+        }
+
+        match field.content_disposition().get_name() {
+            Some("language") => form.language = Some(parse_field("language", &mut cursor)?),
+            Some("contest_problem_id") => {
+                form.contest_problem_id =
+                    parse_field("contest_problem_id", &mut cursor)?.parse().ok()
+            }
+            Some("source_text") => {
+                form.source_text = Some(parse_field("source_text", &mut cursor)?)
+            }
+            _ => {}
+        }
+    }
+
+    if let None = form.source_text {
+        return Err(PageError::Validation("Código-fonte não submetido".into()));
+    }
+
+    let language = form.language.unwrap_or("".to_string());
+
+    languages
+        .get(&language)
+        .ok_or_else(|| PageError::Validation("Linguagem inexistente".into()))?;
+
+    let contest = contest::get_contest_by_contest_problem_id(
+        &mut connection,
+        form.contest_problem_id.unwrap(),
+    )?;
+    assert_contest_not_started(&logged_user, &contest)?;
+    assert_contest_not_over(&logged_user, &contest)?;
+
+    let source_text = form.source_text.unwrap();
+
+    let uuid = Uuid::new_v4();
+    submission::insert_submission(
+        &mut connection,
+        submission::NewSubmission {
+            uuid: uuid.to_string(),
+            source_text: source_text.clone(),
+            language: language.clone(),
+            submission_instant: Local::now().naive_utc(),
+            contest_problem_id: form.contest_problem_id.unwrap(),
+            user_id: logged_user.id,
+        },
+    )?;
+
+    let metadata = problem::get_problem_by_contest_id_metadata(
+        &mut connection,
+        form.contest_problem_id.unwrap(),
+    )?;
+
+    job_sender
+        .send(Job {
+            uuid: uuid.to_string(),
+            language: language.clone(),
+            time_limit_ms: metadata.time_limit_ms,
+            memory_limit_kib: metadata.memory_limit_bytes / 1_024,
+
+            which: Some(job::Which::Judgement(job::Judgement {
+                source_text: source_text.clone(),
+                package_id: metadata.id,
+            })),
+        })
+        .await?;
+
+    session.insert("language", language.clone())?;
+    Ok(redirect_to_referer(
+        format!("Submetido {} com sucesso!", uuid),
+        &request,
+    ))
+}
diff --git a/src/pages/create_printing_task.rs b/src/pages/create_printing_task.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a993dd042c0f5ce2c3748382311b7424c7c008c9
--- /dev/null
+++ b/src/pages/create_printing_task.rs
@@ -0,0 +1,83 @@
+use chrono::Local;
+use std::io::{Cursor, Read, Write};
+use uuid::Uuid;
+
+use actix_multipart::Multipart;
+use futures::{StreamExt, TryStreamExt};
+
+use crate::models::{contest, printing_task};
+use crate::pages::assert_contest_not_started;
+use crate::pages::prelude::*;
+
+#[post("/contests/{id}/printing-tasks/")]
+async fn create_printing_task(
+    identity: Identity,
+    mut payload: Multipart,
+    pool: Data<DbPool>,
+    path: Path<(i32,)>,
+    request: HttpRequest,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    #[derive(Debug)]
+    struct Form {
+        contents: Option<String>,
+    }
+
+    let mut form = Form { contents: None };
+
+    while let Ok(Some(mut field)) = payload.try_next().await {
+        let mut cursor = Cursor::new(vec![]);
+        while let Some(chunk) = field.next().await {
+            let data = chunk.unwrap();
+            cursor
+                .write(&data)
+                .map_err(|_| PageError::Validation("Corpo inválido".into()))?;
+        }
+
+        cursor.set_position(0);
+
+        fn parse_field(field: &str, cursor: &mut Cursor<Vec<u8>>) -> Result<String, PageError> {
+            let mut value = String::new();
+            cursor
+                .read_to_string(&mut value)
+                .map_err(|_| PageError::Validation(format!("Campo {} inválido", field)))?;
+            Ok(value)
+        }
+
+        match field.content_disposition().get_name() {
+            Some("contents") => form.contents = Some(parse_field("contents", &mut cursor)?),
+            _ => {}
+        }
+    }
+
+    if let None = form.contents {
+        return Err(PageError::Validation("Arquivo não submetido".into()));
+    }
+
+    let contents = form.contents.unwrap();
+
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    assert_contest_not_started(&logged_user, &contest)?;
+
+    let uuid = Uuid::new_v4();
+    printing_task::insert_printing_task(
+        &mut connection,
+        printing_task::NewPrintingTask {
+            uuid: uuid.to_string(),
+            contents: contents.clone(),
+            creation_instant: Local::now().naive_utc(),
+            status: "pending".into(),
+            done_instant: None,
+            contest_id: contest.id,
+            user_id: logged_user.id,
+        },
+    )?;
+
+    Ok(redirect_to_referer(
+        "Tarefa de impressão criada com sucesso".into(),
+        &request,
+    ))
+}
diff --git a/src/pages/create_submission.rs b/src/pages/create_submission.rs
index 73ccfecdb4f5da4f1adec8df5d40aac9c8a50b0f..02a67f28654d14d304a54f036aa35833cec91d6a 100644
--- a/src/pages/create_submission.rs
+++ b/src/pages/create_submission.rs
@@ -65,10 +65,7 @@ async fn create_submission(
 
             which: Some(job::Which::Judgement(job::Judgement {
                 source_text: (&form.source_text).into(),
-                test_count: metadata.test_count,
-                test_pattern: format!("./{}/{}", metadata.id, metadata.test_pattern),
-                checker_language: metadata.checker_language,
-                checker_source_path: format!("./{}/{}", metadata.id, metadata.checker_path),
+                package_id: metadata.id,
             })),
         })
         .await?;
diff --git a/src/pages/create_user.rs b/src/pages/create_user.rs
index 16def27691251c93db86ac8b999037bb78ab96ea..7f14facbe7603b715a4c654b59b2c03aa004168e 100644
--- a/src/pages/create_user.rs
+++ b/src/pages/create_user.rs
@@ -6,6 +6,8 @@ use crate::pages::prelude::*;
 #[derive(Serialize, Deserialize)]
 struct CreateUserForm {
     name: String,
+    full_name: Option<String>,
+    single_contest_id: Option<i32>,
     password: String,
     is_admin: Option<bool>,
 }
@@ -31,6 +33,8 @@ async fn create_user(
         user::NewUser {
             name: &form.name,
             password: &form.password,
+            full_name: form.full_name.as_deref(),
+            single_contest_id: form.single_contest_id,
             is_admin: form.is_admin.unwrap_or(false),
             creation_instant: Local::now().naive_utc(),
             creation_user_id: Some(identity.id),
diff --git a/src/pages/get_backup_contents.rs b/src/pages/get_backup_contents.rs
new file mode 100644
index 0000000000000000000000000000000000000000..265286963b6001871639d279a661292ad8e5a8bc
--- /dev/null
+++ b/src/pages/get_backup_contents.rs
@@ -0,0 +1,26 @@
+use crate::models::backup;
+use crate::pages::prelude::*;
+use actix_web::{http::header::ContentType, HttpResponse};
+
+#[get("/backups/{uuid}/contents")]
+async fn get_backup_contents(
+    identity: Identity,
+    pool: Data<DbPool>,
+    path: Path<(String,)>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (backup_uuid,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    let backup = backup::get_backup_by_uuid(&mut connection, backup_uuid)?;
+
+    if backup.user_id != logged_user.id && !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Não é possível acessar um backup de outro usuário".into(),
+        ));
+    }
+
+    Ok(HttpResponse::Ok()
+        .content_type(ContentType::octet_stream())
+        .body(backup.contents))
+}
diff --git a/src/pages/get_contest_by_id.rs b/src/pages/get_contest_by_id.rs
index f6abc8923e69be8d0a056bf8e7e117dda41e0f74..a5e496ec25e455d6359054fd188aebda74baaece 100644
--- a/src/pages/get_contest_by_id.rs
+++ b/src/pages/get_contest_by_id.rs
@@ -1,8 +1,9 @@
 use crate::models::{contest, problem, submission};
 use crate::pages::prelude::*;
 use crate::pages::{
-    assert_contest_not_started, get_formatted_contest, get_formatted_problem_by_contest_with_score,
-    get_formatted_submissions, FormattedContest, FormattedProblemByContestWithScore,
+    assert_contest_not_started, assert_not_single_contest, get_formatted_contest,
+    get_formatted_problem_by_contest_with_score, get_formatted_submissions,
+    get_formatted_submissions_with_blind, FormattedContest, FormattedProblemByContestWithScore,
     FormattedSubmission,
 };
 
@@ -29,14 +30,28 @@ pub async fn get_contest_by_id(
     let mut connection = pool.get()?;
     let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
     assert_contest_not_started(&logged_user, &contest)?;
+    if let Some(redirect) = assert_not_single_contest(&logged_user) {
+        return redirect;
+    }
 
-    let problems = problem::get_problems_user_by_contest_id_with_score(
-        &mut connection,
-        logged_user.id,
-        contest_id,
-    )?;
-    let submissions =
-        submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?;
+    let problems = if !logged_user.is_admin {
+        problem::get_problems_user_by_contest_id_with_score_with_blind_and_frozen(
+            &mut connection,
+            logged_user.id,
+            contest_id,
+        )?
+    } else {
+        problem::get_problems_user_by_contest_id_with_score_without_blind_and_frozen(
+            &mut connection,
+            logged_user.id,
+            contest_id,
+        )?
+    };
+    let submissions = if !logged_user.is_admin {
+        submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?
+    } else {
+        submission::get_submissions_by_contest(&mut connection, contest_id)?
+    };
 
     render(
         &hb,
@@ -48,7 +63,11 @@ pub async fn get_contest_by_id(
                 .iter()
                 .map(|p| get_formatted_problem_by_contest_with_score(p, &contest))
                 .collect(),
-            submissions: get_formatted_submissions(&tz, &submissions),
+            submissions: if !logged_user.is_admin {
+                get_formatted_submissions_with_blind(&tz, &contest, &submissions)
+            } else {
+                get_formatted_submissions(&tz, &submissions)
+            },
         },
     )
 }
diff --git a/src/pages/get_contest_clarifications_by_id.rs b/src/pages/get_contest_clarifications_by_id.rs
new file mode 100644
index 0000000000000000000000000000000000000000..527f4a187588a428e708c3a782bddd818a7a754f
--- /dev/null
+++ b/src/pages/get_contest_clarifications_by_id.rs
@@ -0,0 +1,90 @@
+use crate::models::{contest, clarification};
+use crate::pages::prelude::*;
+use crate::pages::{
+    assert_contest_not_started, assert_not_single_contest, get_formatted_contest,
+    FormattedContest,
+};
+
+#[derive(Serialize)]
+struct FormattedClarification {
+    pub uuid: String,
+    pub question: String,
+    pub question_instant: String,
+    pub answer: Option<String>,
+    pub answer_instant: Option<String>,
+    pub answer_user_id: Option<i32>,
+    pub answer_to_all: bool,
+    pub contest_problem_id: Option<i32>,
+    pub problem_label: Option<String>,
+    pub contest_id: i32,
+    pub user_id: i32,
+    pub user_name: String,
+    pub user_full_name: Option<String>,
+}
+
+#[get("/contests/{id}/clarifications")]
+pub async fn get_contest_clarifications_by_id(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32,)>,
+    tz: Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        contest: FormattedContest,
+        clarifications: Vec<FormattedClarification>,
+    }
+
+    let mut connection = pool.get()?;
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    assert_contest_not_started(&logged_user, &contest)?;
+    if let Some(redirect) = assert_not_single_contest(&logged_user) {
+        return redirect;
+    }
+
+    let clarifications = if !logged_user.is_admin {
+        clarification::get_clarifications_by_contest_user(
+            &mut connection,
+            logged_user.id,
+            contest_id,
+        )?
+    } else {
+        clarification::get_clarifications_by_contest(
+            &mut connection,
+            contest_id,
+        )?
+    };
+
+    render(
+        &hb,
+        "clarifications",
+        &Context {
+            base,
+            contest: get_formatted_contest(&tz, &contest),
+            clarifications: clarifications
+                .into_iter()
+                .map(|(c, u, problem_label)| FormattedClarification {
+                    uuid: c.uuid,
+                    question: c.question,
+                    question_instant: format_utc_date_time(&tz, c.question_instant),
+                    answer: c.answer,
+                    answer_instant: c.answer_instant.map(|i| format_utc_date_time(&tz, i)),
+                    answer_user_id: c.answer_user_id,
+                    answer_to_all: c.answer_to_all,
+                    contest_problem_id: c.contest_problem_id,
+                    problem_label,
+                    contest_id: c.contest_id,
+                    user_id: c.user_id,
+                    user_name: u.name,
+                    user_full_name: u.full_name,
+                })
+                .collect(),
+        },
+    )
+}
diff --git a/src/pages/get_contest_problem_by_id_label.rs b/src/pages/get_contest_problem_by_id_label.rs
index fc60d1690affddf19ce931f55be839a5582b0a76..78acf0ead0a75d5954a3cd89cc0b5b06b5ebac64 100644
--- a/src/pages/get_contest_problem_by_id_label.rs
+++ b/src/pages/get_contest_problem_by_id_label.rs
@@ -3,7 +3,8 @@ use crate::models::{contest, problem, submission};
 use crate::pages::get_editor::{get_formatted_languages, FormattedLanguage};
 use crate::pages::prelude::*;
 use crate::pages::{
-    assert_contest_not_started, get_formatted_contest, get_formatted_submissions, FormattedContest,
+    assert_contest_not_started, assert_not_single_contest, get_formatted_contest,
+    get_formatted_submissions, get_formatted_submissions_with_blind, FormattedContest,
     FormattedSubmission,
 };
 use crate::Language;
@@ -20,6 +21,9 @@ async fn get_contest_problem_by_id_label(
     tz: Data<Tz>,
 ) -> PageResult {
     let logged_user = require_identity(&identity)?;
+    if let Some(redirect) = assert_not_single_contest(&logged_user) {
+        return redirect;
+    }
     let (contest_id, problem_label) = path.into_inner();
 
     #[derive(Serialize)]
@@ -39,12 +43,16 @@ async fn get_contest_problem_by_id_label(
     let problems = problem::get_problems_by_contest_id(&mut connection, contest_id)?;
     let problem =
         problem::get_problem_by_contest_id_label(&mut connection, contest_id, &problem_label)?;
-    let submissions = submission::get_submissions_user_by_contest_problem(
-        &mut connection,
-        logged_user.id,
-        contest_id,
-        &problem_label,
-    )?;
+    let submissions = if logged_user.is_admin {
+        submission::get_submissions_by_contest_problem(&mut connection, contest_id, &problem_label)?
+    } else {
+        submission::get_submissions_user_by_contest_problem(
+            &mut connection,
+            logged_user.id,
+            contest_id,
+            &problem_label,
+        )?
+    };
 
     render(
         &hb,
@@ -56,7 +64,11 @@ async fn get_contest_problem_by_id_label(
             problems,
             problem,
             language: session.get("language")?,
-            submissions: get_formatted_submissions(&tz, &submissions),
+            submissions: if !logged_user.is_admin {
+                get_formatted_submissions_with_blind(&tz, &contest, &submissions)
+            } else {
+                get_formatted_submissions(&tz, &submissions)
+            },
         },
     )
 }
diff --git a/src/pages/get_contest_scoreboard_by_id.rs b/src/pages/get_contest_scoreboard_by_id.rs
index 1e427aa94e3d52f66502be56381b020df6036e2c..0262b6dddd476002174cae23f41048651e0fdf87 100644
--- a/src/pages/get_contest_scoreboard_by_id.rs
+++ b/src/pages/get_contest_scoreboard_by_id.rs
@@ -3,8 +3,9 @@ use itertools::Itertools;
 use crate::models::{contest, problem, submission};
 use crate::pages::prelude::*;
 use crate::pages::{
-    assert_contest_not_started, get_formatted_contest, get_formatted_problem_by_contest_with_score,
-    get_formatted_submissions, FormattedContest, FormattedProblemByContestWithScore,
+    assert_contest_not_started, assert_not_single_contest, get_formatted_contest,
+    get_formatted_problem_by_contest_with_score, get_formatted_submissions,
+    get_formatted_submissions_with_blind, FormattedContest, FormattedProblemByContestWithScore,
     FormattedSubmission,
 };
 
@@ -18,11 +19,15 @@ async fn get_contest_scoreboard_by_id(
     tz: Data<Tz>,
 ) -> PageResult {
     let logged_user = require_identity(&identity)?;
+    if let Some(redirect) = assert_not_single_contest(&logged_user) {
+        return redirect;
+    }
     let (contest_id,) = path.into_inner();
 
     #[derive(Serialize)]
     struct Score {
         pub user_name: Option<String>,
+        pub user_full_name: Option<String>,
         pub problems: Vec<FormattedProblemByContestWithScore>,
         pub solved_count: i64,
         pub penalty: i64,
@@ -39,24 +44,29 @@ async fn get_contest_scoreboard_by_id(
     let mut connection = pool.get()?;
     let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
     assert_contest_not_started(&logged_user, &contest)?;
-    let scores = problem::get_problems_by_contest_id_with_score(&mut connection, contest_id)?;
+    let scores = if logged_user.is_admin {
+        problem::get_problems_by_contest_id_with_score_without_blind_and_frozen(&mut connection, contest_id)?
+    } else {
+        problem::get_problems_by_contest_id_with_score_with_frozen(&mut connection, contest_id)?
+    };
     let submissions =
         submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?;
-    let mut scores: Vec<_> = scores.into_iter().group_by(|e| e.user_name.as_ref().cloned()).into_iter().map(|(user_name, problems)| {
-    let problems: Vec<_> = problems.map(|p| get_formatted_problem_by_contest_with_score(&p, &contest)).collect();
+    let mut scores: Vec<_> = scores.into_iter().group_by(|e| (e.user_name.as_ref().cloned(), e.user_full_name.as_ref().cloned())).into_iter().map(|((user_name, user_full_name), problems)| {
+        let problems: Vec<_> = problems.map(|p| get_formatted_problem_by_contest_with_score(&p, &contest)).collect();
 
-    Score {
-        user_name,
-        solved_count: i64::try_from(problems.iter().filter(|p| !p.first_ac_submission_time.is_empty()).count()).unwrap(),
-        penalty: problems.iter().filter(|p| !p.first_ac_submission_time.is_empty())
-            .map(|p| match p.first_ac_submission_minutes {
-                Some(x) if x >= 0 => x,
-                _ => 0,
-            } + i64::from(20*p.failed_submissions))
-            .sum(),
-            problems
-    }
-}).collect();
+        Score {
+            user_name,
+            user_full_name,
+            solved_count: i64::try_from(problems.iter().filter(|p| !p.first_ac_submission_time.is_empty()).count()).unwrap(),
+            penalty: problems.iter().filter(|p| !p.first_ac_submission_time.is_empty())
+                .map(|p| match p.first_ac_submission_minutes {
+                    Some(x) if x >= 0 => x,
+                    _ => 0,
+                } + i64::from(20*p.failed_submissions))
+                .sum(),
+                problems
+        }
+    }).collect();
     scores.sort_by(|a, b| {
         (
             a.user_name.is_some(),
@@ -79,7 +89,11 @@ async fn get_contest_scoreboard_by_id(
             base,
             contest: get_formatted_contest(&tz, &contest),
             scores,
-            submissions: get_formatted_submissions(&tz, &submissions),
+            submissions: if !logged_user.is_admin {
+                get_formatted_submissions_with_blind(&tz, &contest, &submissions)
+            } else {
+                get_formatted_submissions(&tz, &submissions)
+            },
         },
     )
 }
diff --git a/src/pages/get_contest_tasks_by_id.rs b/src/pages/get_contest_tasks_by_id.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c8be86ea7434674ccbc4c2daf76c082638f6e64b
--- /dev/null
+++ b/src/pages/get_contest_tasks_by_id.rs
@@ -0,0 +1,152 @@
+use chrono::NaiveDateTime;
+use std::collections::HashSet;
+
+use crate::models::{contest, clarification, submission, printing_task};
+use crate::pages::prelude::*;
+use crate::pages::{get_formatted_contest, FormattedContest,
+    get_formatted_submissions, FormattedSubmission,
+};
+
+#[get("/contests/{id}/tasks")]
+pub async fn get_contest_tasks_by_id(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32,)>,
+    tz: Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let (contest_id,) = path.into_inner();
+
+    struct Task {
+        uuid: String,
+        creation_instant: NaiveDateTime,
+        t: String,
+        user_name: String,
+        user_full_name: Option<String>,
+        description: String,
+        balloon_color: Option<String>,
+        first_solve: bool,
+        done: bool
+    }
+
+    #[derive(Serialize)]
+    struct FormattedTask {
+        uuid: String,
+        creation_instant: String,
+        t: String,
+        user_name: String,
+        user_full_name: Option<String>,
+        description: String,
+        balloon_color: Option<String>,
+        first_solve: bool,
+        done: bool
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        contest: FormattedContest,
+        tasks: Vec<FormattedTask>,
+        submissions: Vec<FormattedSubmission>,
+    }
+
+    let mut connection = pool.get()?;
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+
+    let mut tasks: Vec<Task> = vec![];
+
+    let clarifications = clarification::get_clarifications_by_contest(&mut connection, contest_id)?;
+    for (clarification, user, problem_label) in clarifications {
+        tasks.push(Task {
+            uuid: clarification.uuid,
+            creation_instant: clarification.question_instant,
+            t: "clarification".into(),
+            user_name: user.name,
+            user_full_name: user.full_name,
+            balloon_color: None,
+            description: match problem_label {
+                Some(problem_label) => format!("Problema {}", problem_label),
+                None => "Geral".into()
+            },
+            first_solve: false,
+            done: clarification.answer_instant.is_some()
+        });
+    }
+
+    let mut submissions = submission::get_submissions_by_contest(&mut connection, contest_id)?;
+    submissions.reverse();
+    let mut solved_contest_problems: HashSet<i32> = Default::default();
+    let mut solved_contest_problems_by_user: HashSet<(i32, i32)> = Default::default();
+    for (submission, contest_problem, user) in &submissions {
+        if submission.verdict.as_ref().map(|e| e != "AC").unwrap_or(true) { continue }
+        if solved_contest_problems_by_user.contains(&(contest_problem.id, user.id)) {
+            continue
+        }
+        // if contest.frozen_scoreboard_instant.map(|e| submission.submission_instant >= e).unwrap_or(false) {
+        //     continue
+        // }
+        tasks.push(Task {
+            uuid: submission.uuid.clone(),
+            creation_instant: submission.submission_instant,
+            t: "balloon".into(),
+            user_name: user.name.clone(),
+            user_full_name: user.full_name.clone(),
+            balloon_color: contest_problem.balloon_color.clone(),
+            description: format!("Problema {}", contest_problem.label),
+            first_solve: !solved_contest_problems.contains(&contest_problem.id),
+            done: submission.balloon_delivered_instant.is_some(),
+        });
+        solved_contest_problems.insert(contest_problem.id);
+        solved_contest_problems_by_user.insert((contest_problem.id, user.id));
+    }
+
+    let printing_tasks = printing_task::get_printing_tasks_by_contest(
+        &mut connection,
+        contest_id
+    )?;
+    for (printing_task, user) in printing_tasks {
+        tasks.push(Task {
+            uuid: printing_task.uuid.clone(),
+            creation_instant: printing_task.creation_instant,
+            t: "printing".into(),
+            user_name: user.name.clone(),
+            user_full_name: user.full_name.clone(),
+            balloon_color: None,
+            description: format!("{} bytes", printing_task.contents.len()),
+            first_solve: false,
+            done: printing_task.done_instant.is_some(),
+        });
+    }
+
+    tasks.sort_by(|a, b| {
+        (a.done, b.creation_instant).cmp(&(b.done, a.creation_instant))
+    });
+
+    render(
+        &hb,
+        "tasks",
+        &Context {
+            base,
+            contest: get_formatted_contest(&tz, &contest),
+            tasks: tasks.into_iter().map(|e| FormattedTask {
+                uuid: e.uuid,
+                creation_instant: format_utc_date_time(&tz, e.creation_instant),
+                t: e.t,
+                user_name: e.user_name,
+                user_full_name: e.user_full_name,
+                balloon_color: e.balloon_color,
+                description: e.description,
+                first_solve: e.first_solve,
+                done: e.done
+            }).collect(),
+            submissions: get_formatted_submissions(&tz, &submissions),
+        },
+    )
+}
diff --git a/src/pages/get_contest_webcast_by_id.rs b/src/pages/get_contest_webcast_by_id.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0592c1ddfba884881da15c4a27916ccf30a58f1a
--- /dev/null
+++ b/src/pages/get_contest_webcast_by_id.rs
@@ -0,0 +1,98 @@
+use std::collections::HashSet;
+use zip::ZipWriter;
+use zip::write::FileOptions;
+use std::io::Write;
+use std::cmp::min;
+use chrono::Local;
+use regex::Regex;
+
+use crate::models::{submission, contest, problem, user};
+use crate::pages::prelude::*;
+
+#[derive(Deserialize)]
+struct SecretParams {
+    secret: String,
+}
+
+#[get("/contests/{id}/webcast.zip")]
+async fn get_contest_webcast_by_id(
+    pool: Data<DbPool>,
+    path: Path<(i32,)>,
+    query: Query<SecretParams>
+) -> PageResult {
+    let (contest_id,) = path.into_inner();
+
+    let mut connection = pool.get()?;
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    let problems = problem::get_problems_by_contest_id(&mut connection, contest_id)?;
+    let users_contest = user::get_users_by_single_contest_id(&mut connection, contest_id)?;
+    let users_contest_users: HashSet<String> =
+        HashSet::from_iter(users_contest.iter().map(|e| e.name.clone()));
+    let submissions = submission::get_submissions_by_contest(&mut connection, contest_id)?;
+
+    let mut buf = [0; 65536];
+    {
+        let mut zip = ZipWriter::new(std::io::Cursor::new(&mut buf[..]));
+
+        let options = FileOptions::default().compression_method(zip::CompressionMethod::Stored);
+
+        zip.start_file("contest", options)?;
+        zip.write(contest.name.as_bytes())?;
+        zip.write(b"\n")?;
+        let start_instant = contest.start_instant.unwrap();
+        let end_instant = contest.end_instant.unwrap();
+        let maximum_time = (end_instant - start_instant).num_minutes();
+        let current_time = min(maximum_time, (Local::now().naive_utc() - start_instant).num_minutes());
+        let score_freeze_time = match contest.frozen_scoreboard_instant {
+            None => maximum_time,
+            Some(frozen_scoreboard_instant) => (frozen_scoreboard_instant - start_instant).num_minutes()
+        };
+        let penalty = 20;
+        let contest_params = format!("{}\x1c{}\x1c{}\x1c{}\n", maximum_time, current_time, score_freeze_time, penalty);
+        zip.write(contest_params.as_bytes())?;
+        let number_teams = users_contest.len();
+        let number_problems = problems.len();
+        let team_params = format!("{}\x1c{}\n", number_teams, number_problems);
+        zip.write(team_params.as_bytes())?;
+        let re = Regex::new(r"^\[([^\]]+)\]").unwrap();
+        for user in users_contest {
+            let institution = user.full_name.as_ref().and_then(|e| re.captures(e))
+                .and_then(|e| e.get(1)).map(|e| e.as_str()).unwrap_or("".into());
+            let user_full_name = user.full_name.clone().unwrap_or("".into());
+            zip.write(format!("{}\x1c{}\x1c{}\n", user.name, institution, user_full_name).as_bytes())?;
+        }
+        zip.write(b"1\x1c1\n")?;
+        zip.write(format!("{}\x1cY\n", number_problems).as_bytes())?;
+
+        let mut i = 1;
+        zip.start_file("runs", options)?;
+        for (submission, contest_problem, user) in submissions {
+            if !users_contest_users.contains(&user.name) { continue }
+            zip.write(format!(
+                "{}\x1c{}\x1c{}\x1c{}\x1c{}\n",
+                (submission.submission_instant.and_utc().timestamp())*10 + i,
+                (submission.submission_instant - start_instant).num_minutes(),
+                user.name,
+                contest_problem.label,
+                match submission.verdict.as_ref().map(|e| e.as_str()) {
+                    Some("WJ") | None => "?",
+                    Some("AC") => "Y",
+                    Some("CE") => "X",
+                    _ => "N",
+                }
+            ).as_bytes())?;
+            i += 1;
+            if i > 10 { i -= 10; }
+        }
+
+        zip.start_file("time", options)?;
+        zip.write(format!("{}", min((end_instant - start_instant).num_seconds(), (Local::now().naive_utc() - start_instant).num_seconds())).as_bytes())?;
+
+        zip.start_file("version", options)?;
+        zip.write(b"1.0")?;
+
+        zip.finish()?;
+    }
+
+    Ok(HttpResponse::Ok().body(Vec::from(buf)))
+}
diff --git a/src/pages/get_contests.rs b/src/pages/get_contests.rs
index 1caf6c677a6b0ffc82387e2bb4cbd55d792192e5..1ce114f3e884b8949062aa1aa0cb440be95842f5 100644
--- a/src/pages/get_contests.rs
+++ b/src/pages/get_contests.rs
@@ -1,3 +1,4 @@
+use crate::pages::assert_not_single_contest;
 use crate::pages::prelude::*;
 use crate::pages::{get_formatted_contests, FormattedContest};
 
@@ -10,6 +11,9 @@ async fn get_contests(
     tz: Data<Tz>,
 ) -> PageResult {
     let logged_user = require_identity(&identity)?;
+    if let Some(redirect) = assert_not_single_contest(&logged_user) {
+        return redirect;
+    }
 
     #[derive(Serialize)]
     struct Context {
@@ -23,7 +27,7 @@ async fn get_contests(
         "contests",
         &Context {
             base,
-            contests: get_formatted_contests(&mut connection, Some(logged_user.id), &tz)?,
+            contests: get_formatted_contests(&mut connection, None, &tz)?,
         },
     )
 }
diff --git a/src/pages/get_main.rs b/src/pages/get_main.rs
index e0c6bb8eaa22a852068cd658f56bb2e6b9aebb3e..acec1aae7a4136e5ee5f525d9549b301f2c014b2 100644
--- a/src/pages/get_main.rs
+++ b/src/pages/get_main.rs
@@ -1,7 +1,8 @@
 use crate::models::submission;
 use crate::pages::prelude::*;
 use crate::pages::{
-    get_formatted_contests, get_formatted_submissions, FormattedContest, FormattedSubmission,
+    assert_not_single_contest, get_formatted_contests, get_formatted_submissions, FormattedContest,
+    FormattedSubmission,
 };
 
 #[get("/")]
@@ -13,6 +14,9 @@ async fn get_main(
     tz: Data<Tz>,
 ) -> PageResult {
     let logged_user = require_identity(&identity)?;
+    if let Some(redirect) = assert_not_single_contest(&logged_user) {
+        return redirect;
+    }
 
     #[derive(Serialize)]
     struct Context {
@@ -22,7 +26,11 @@ async fn get_main(
     }
 
     let mut connection = pool.get()?;
-    let submissions = submission::get_submissions(&mut connection)?;
+    let submissions = if !logged_user.is_admin {
+        submission::get_submissions_user(&mut connection, logged_user.id)?
+    } else {
+        submission::get_submissions(&mut connection)?
+    };
 
     render(
         &hb,
@@ -30,7 +38,11 @@ async fn get_main(
         &Context {
             base,
             contests: get_formatted_contests(&mut connection, Some(logged_user.id), &tz)?,
-            submissions: get_formatted_submissions(&tz, &submissions),
+            submissions: if logged_user.is_admin {
+                get_formatted_submissions(&tz, &submissions)
+            } else {
+                vec![]
+            },
         },
     )
 }
diff --git a/src/pages/get_me.rs b/src/pages/get_me.rs
index f1b8705963066ac28a3a3e9782240a14c9920e23..d5a997c7d20348c6a0b20b550ce3f859d7a388ae 100644
--- a/src/pages/get_me.rs
+++ b/src/pages/get_me.rs
@@ -1,8 +1,12 @@
+use crate::pages::assert_not_single_contest;
 use crate::pages::prelude::*;
 
 #[get("/me")]
 async fn get_me(base: BaseContext, identity: Identity, hb: Data<Handlebars<'_>>) -> PageResult {
-    require_identity(&identity)?;
+    let logged_user = require_identity(&identity)?;
+    if let Some(redirect) = assert_not_single_contest(&logged_user) {
+        return redirect;
+    }
     #[derive(Serialize)]
     struct Context {
         base: BaseContext,
diff --git a/src/pages/get_printing_task_contents.rs b/src/pages/get_printing_task_contents.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7abb59905563d2270d9fe5a47be1d75f9480204e
--- /dev/null
+++ b/src/pages/get_printing_task_contents.rs
@@ -0,0 +1,59 @@
+use crate::models::printing_task;
+use crate::pages::prelude::*;
+use actix_web::{http::header::ContentType, HttpResponse};
+
+#[get("/printing-tasks/{uuid}/contents")]
+async fn get_printing_task_contents(
+    identity: Identity,
+    pool: Data<DbPool>,
+    path: Path<(String,)>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (printing_task_uuid,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    let (printing_task, user) =
+        printing_task::get_printing_task_by_uuid(&mut connection, printing_task_uuid)?;
+
+    if user.id != logged_user.id && !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Não é possível acessar uma tarefa de impressão de outro usuário".into(),
+        ));
+    }
+
+    let user_full_name = user.full_name.unwrap_or("".into());
+    Ok(HttpResponse::Ok()
+        .content_type(ContentType::plaintext())
+        .body(format!(
+            r#"///// {} -- {} -- {}
+///// {} -- {} -- {}
+///// {} -- {} -- {}
+
+
+{}
+
+///// {} -- {} -- {}
+///// {} -- {} -- {}
+///// {} -- {} -- {}
+"#,
+            user.name,
+            user_full_name,
+            user.name,
+            user.name,
+            user_full_name,
+            user.name,
+            user.name,
+            user_full_name,
+            user.name,
+            printing_task.contents,
+            user.name,
+            user_full_name,
+            user.name,
+            user.name,
+            user_full_name,
+            user.name,
+            user.name,
+            user_full_name,
+            user.name
+        )))
+}
diff --git a/src/pages/get_problem_by_id_assets.rs b/src/pages/get_problem_by_id_assets.rs
index 60380ccb305a6e2b8e502871accb506a93e84ff5..2d8924bbe0352ee412d346af95916b8eb55142ea 100644
--- a/src/pages/get_problem_by_id_assets.rs
+++ b/src/pages/get_problem_by_id_assets.rs
@@ -1,6 +1,8 @@
 use std::path::PathBuf;
 
 use actix_files::NamedFile;
+use actix_web::http::header::ContentDisposition;
+use actix_web::http::header::DispositionParam;
 
 use crate::models::{contest, problem};
 use crate::pages::assert_contest_not_started;
@@ -18,9 +20,20 @@ async fn get_problem_by_id_assets(
     let problem = problem::get_problem_by_contest_id_metadata(&mut connection, problem_id)?;
     let contest = contest::get_contest_by_id(&mut connection, problem.contest_id)?;
     assert_contest_not_started(&logged_user, &contest)?;
-    let file_path = PathBuf::from("/data/")
-        .join(problem.id)
-        .join("statements/.html/portuguese/")
-        .join(asset_filename);
-    Ok(NamedFile::open(file_path)?)
+    let file_path_prefix = PathBuf::from("/data/").join(problem.id).join("statement");
+    let file_path = file_path_prefix.join(asset_filename).canonicalize()?;
+    if !file_path.starts_with(file_path_prefix) {
+        return Err(PageError::Forbidden(
+            "Caminho para o problema deve estar dentro do repositório".into(),
+        ));
+    }
+    let extension = file_path.extension().unwrap().to_str().unwrap();
+    let mut named_file = NamedFile::open(file_path.clone())?;
+    let content_disposition = named_file.content_disposition().clone();
+    named_file = named_file.set_content_disposition(ContentDisposition {
+        disposition: content_disposition.disposition,
+        parameters: vec![DispositionParam::Filename(problem.label + "." + extension)],
+    });
+    println!("{}", named_file.content_disposition());
+    Ok(named_file)
 }
diff --git a/src/pages/get_problems.rs b/src/pages/get_problems.rs
index e07654ed9beb15008425bd98b49f3b2ff26e4187..3be948b57a98f9e06df45f72b21c8e63196bbf11 100644
--- a/src/pages/get_problems.rs
+++ b/src/pages/get_problems.rs
@@ -1,6 +1,6 @@
 use crate::models::problem;
 use crate::pages::prelude::*;
-use crate::pages::FormattedProblemByContestWithScore;
+use crate::pages::{assert_not_single_contest, FormattedProblemByContestWithScore};
 
 #[get("/problems/")]
 pub async fn get_problems(
@@ -10,6 +10,9 @@ pub async fn get_problems(
     hb: Data<Handlebars<'_>>,
 ) -> PageResult {
     let logged_user = require_identity(&identity)?;
+    if let Some(redirect) = assert_not_single_contest(&logged_user) {
+        return redirect;
+    }
 
     #[derive(Serialize)]
     struct Context {
@@ -18,7 +21,17 @@ pub async fn get_problems(
     }
 
     let mut connection = pool.get()?;
-    let problems = problem::get_problems_user_with_score(&mut connection, logged_user.id)?;
+    let problems = if logged_user.is_admin {
+        problem::get_problems_user_with_score_without_blind_and_frozen(
+            &mut connection,
+            logged_user.id,
+        )?
+    } else {
+        problem::get_problems_user_with_score_with_blind_and_frozen(
+            &mut connection,
+            logged_user.id,
+        )?
+    };
 
     render(
         &hb,
@@ -30,8 +43,12 @@ pub async fn get_problems(
                 .map(|p| FormattedProblemByContestWithScore {
                     first_ac_submission_time: "".into(),
                     first_ac_submission_minutes: None,
-                    failed_submissions: p.failed_submissions,
+                    failed_submissions: 0,
+                    first_solve: false,
+                    submission_count: 0,
+                    last_failed_submission_verdict: p.last_failed_submission_verdict.clone(),
                     id: p.id,
+                    balloon_color: None,
                     name: p.name.clone(),
                     label: p.label.clone(),
                     memory_limit_mib: p.memory_limit_bytes / 1_024 / 1_024,
diff --git a/src/pages/get_repositories.rs b/src/pages/get_repositories.rs
new file mode 100644
index 0000000000000000000000000000000000000000..187cc39596b18c7f645cff481948ebfc76203e8c
--- /dev/null
+++ b/src/pages/get_repositories.rs
@@ -0,0 +1,50 @@
+use crate::models::repository;
+use crate::pages::prelude::*;
+
+#[get("/repositories/")]
+async fn get_repositories(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    tz: Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+
+    #[derive(Serialize)]
+    struct FormattedRepository {
+        pub id: i32,
+        pub name: String,
+        pub remote_url: String,
+        pub creation_instant: String,
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        repositories: Vec<FormattedRepository>,
+    }
+
+    let mut connection = pool.get()?;
+    render(
+        &hb,
+        "repositories",
+        &Context {
+            base,
+            repositories: repository::get_repositories(&mut connection)?
+                .iter()
+                .map(|r| FormattedRepository {
+                    id: r.id,
+                    name: r.name.clone(),
+                    remote_url: r.remote_url.clone(),
+                    creation_instant: format_utc_date_time(&tz, r.creation_instant),
+                })
+                .collect(),
+        },
+    )
+}
diff --git a/src/pages/get_repository_by_id.rs b/src/pages/get_repository_by_id.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fc05c2ee8c921f60dcef6cca27f9b1577c356b74
--- /dev/null
+++ b/src/pages/get_repository_by_id.rs
@@ -0,0 +1,188 @@
+use crate::models::repository;
+use crate::pages::prelude::*;
+use std::fs;
+use toml;
+use walkdir::WalkDir;
+
+#[derive(Deserialize)]
+struct BlemConfig {
+    authors: Vec<String>,
+    time_limit_ms: i32,
+    memory_limit_kb: Option<i32>,
+    br: LanguageConfig,
+}
+
+#[derive(Deserialize)]
+struct LanguageConfig {
+    name: String,
+}
+
+#[derive(Deserialize)]
+struct ContestProblemConfig {
+    label: String,
+    path: String,
+    name: String,
+}
+
+#[derive(Deserialize)]
+struct ContestConfig {
+    languages: Vec<String>,
+    br: LanguageConfig,
+    problems: Vec<ContestProblemConfig>,
+}
+
+#[get("/repositories/{id}")]
+pub async fn get_repository_by_id(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32,)>,
+    tz: Data<Tz>,
+) -> PageResult {
+    use std::path::Path;
+
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let (repository_id,) = path.into_inner();
+
+    #[derive(Serialize)]
+    struct FormattedRepository {
+        pub id: i32,
+        pub name: String,
+        pub remote_url: String,
+        pub creation_instant: String,
+    }
+
+    #[derive(Serialize)]
+    struct RepositoryProblem {
+        pub name: String,
+        pub authors: Vec<String>,
+        pub time_limit_ms: i32,
+        pub memory_limit_kb: i32,
+        pub path: String,
+    }
+
+    #[derive(Serialize)]
+    struct RepositoryContestProblem {
+        pub label: String,
+        pub name: String,
+        pub path: String,
+        pub time_limit_ms: Option<i32>,
+        pub authors: Vec<String>,
+        pub last_commit: String,
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        repository: FormattedRepository,
+        problems: Vec<RepositoryProblem>,
+        contest_name: Option<String>,
+        contest_problems: Vec<RepositoryContestProblem>,
+    }
+
+    let mut connection = pool.get()?;
+    let repository = repository::get_repository_by_id(&mut connection, repository_id)?;
+    let git_repository =
+        git2::Repository::open(Path::new(&format!("/data/{}/", repository.path))).unwrap();
+    let head_commit = git_repository.head().unwrap();
+    let head_commit_short_id = head_commit
+        .peel_to_commit()
+        .unwrap()
+        .as_object()
+        .short_id()
+        .unwrap();
+    let head_commit_shorthand = head_commit_short_id.as_str().unwrap();
+
+    let mut contest_name = None;
+    let mut contest_problems = Vec::new();
+    let maybe_contest_toml_contents =
+        fs::read_to_string(format!("/data/{}/contest.toml", repository.path));
+    if let Ok(contest_toml_contents) = maybe_contest_toml_contents {
+        let maybe_contest_config = toml::from_str::<ContestConfig>(&contest_toml_contents);
+        if let Ok(contest_config) = maybe_contest_config {
+            contest_name = Some(contest_config.br.name);
+            contest_problems = contest_config
+                .problems
+                .into_iter()
+                .map(|c| RepositoryContestProblem {
+                    label: c.label,
+                    name: c.name,
+                    path: c.path,
+                    time_limit_ms: None,
+                    last_commit: head_commit_shorthand.into(),
+                    authors: vec![],
+                })
+                .collect();
+        }
+    }
+
+    let mut problems = Vec::new();
+    for entry in WalkDir::new(format!("/data/{}", repository.path))
+        .into_iter()
+        .filter_entry(|e| e.file_name().to_str().map(|s| s != "blem").unwrap_or(false))
+        .filter_map(|e| e.ok().filter(|e| e.file_name() == "blem.toml"))
+    {
+        let maybe_blem_config = toml::from_str::<BlemConfig>(&fs::read_to_string(entry.path())?);
+        let path = entry
+            .path()
+            .strip_prefix(format!("/data/{}", repository.path))
+            .unwrap()
+            .parent()
+            .unwrap()
+            .to_str()
+            .unwrap()
+            .into();
+        if let Ok(blem_config) = maybe_blem_config {
+            let mut found = false;
+            for contest_problem in &mut contest_problems {
+                if contest_problem.path == path {
+                    contest_problem.name =
+                        format!("{0} ({1})", contest_problem.name, blem_config.br.name);
+                    contest_problem.time_limit_ms = Some(blem_config.time_limit_ms);
+                    contest_problem.authors = blem_config.authors.clone();
+                    found = true;
+                }
+            }
+            if !found {
+                problems.push(RepositoryProblem {
+                    name: blem_config.br.name,
+                    time_limit_ms: blem_config.time_limit_ms,
+                    memory_limit_kb: blem_config.memory_limit_kb.unwrap_or(204800),
+                    authors: blem_config.authors,
+                    path,
+                });
+            }
+        } else {
+            problems.push(RepositoryProblem {
+                name: "???".to_string(),
+                time_limit_ms: 0,
+                memory_limit_kb: 0,
+                authors: vec![],
+                path,
+            });
+        }
+    }
+
+    render(
+        &hb,
+        "repository",
+        &Context {
+            base,
+            problems,
+            contest_name,
+            contest_problems,
+            repository: FormattedRepository {
+                id: repository.id,
+                name: repository.name.clone(),
+                remote_url: repository.remote_url.clone(),
+                creation_instant: format_utc_date_time(&tz, repository.creation_instant),
+            },
+        },
+    )
+}
diff --git a/src/pages/get_repository_problem_by_path.rs b/src/pages/get_repository_problem_by_path.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fa914459004fbd1d51dcf5d47491ba7f90323444
--- /dev/null
+++ b/src/pages/get_repository_problem_by_path.rs
@@ -0,0 +1,99 @@
+use crate::models::repository;
+use crate::pages::prelude::*;
+use std::fs;
+use toml;
+
+#[derive(Deserialize)]
+struct BlemConfig {
+    authors: Vec<String>,
+    time_limit_ms: i32,
+    br: LanguageConfig,
+}
+
+#[derive(Deserialize)]
+struct LanguageConfig {
+    name: String,
+}
+
+#[get("/repositories/{id}/problems-by-path/{path:.*}")]
+pub async fn get_repository_problem_by_path(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32, String)>,
+    tz: Data<Tz>,
+) -> PageResult {
+    use std::path::Path;
+
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let (repository_id, problem_path) = path.into_inner();
+
+    #[derive(Serialize)]
+    struct FormattedRepository {
+        pub id: i32,
+        pub name: String,
+        pub remote_url: String,
+        pub creation_instant: String,
+    }
+
+    #[derive(Serialize)]
+    struct RepositoryProblem {
+        pub name: String,
+        pub authors: Vec<String>,
+        pub time_limit_ms: i32,
+        pub path: String,
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        repository: FormattedRepository,
+        problems: Vec<RepositoryProblem>,
+    }
+
+    let mut connection = pool.get()?;
+    let repository = repository::get_repository_by_id(&mut connection, repository_id)?;
+
+    let path = Path::new(&format!("/data/{}", repository.path))
+        .join(problem_path)
+        .canonicalize()
+        .unwrap();
+    if !path.starts_with(format!("/data/{}", repository.path)) {
+        return Err(PageError::Forbidden(
+            "Caminho para o problema deve estar dentro do repositório".into(),
+        ));
+    }
+
+    let maybe_blem_config =
+        toml::from_str::<BlemConfig>(&fs::read_to_string(path.join("blem.toml"))?);
+    let mut problems = Vec::new();
+    if let Ok(blem_config) = maybe_blem_config {
+        problems.push(RepositoryProblem {
+            name: blem_config.br.name,
+            time_limit_ms: blem_config.time_limit_ms,
+            authors: blem_config.authors,
+            path: path.to_str().unwrap().into(),
+        });
+    }
+
+    render(
+        &hb,
+        "repository",
+        &Context {
+            base,
+            problems,
+            repository: FormattedRepository {
+                id: repository.id,
+                name: repository.name.clone(),
+                remote_url: repository.remote_url.clone(),
+                creation_instant: format_utc_date_time(&tz, repository.creation_instant),
+            },
+        },
+    )
+}
diff --git a/src/pages/get_single_contest_backup.rs b/src/pages/get_single_contest_backup.rs
new file mode 100644
index 0000000000000000000000000000000000000000..25ffbde0a0e1b73b0565b51dfb1f98b852837e3c
--- /dev/null
+++ b/src/pages/get_single_contest_backup.rs
@@ -0,0 +1,65 @@
+use crate::models::{backup, contest, submission};
+use crate::pages::prelude::*;
+use crate::pages::{
+    assert_single_contest_not_started, get_formatted_contest,
+    get_formatted_submissions_with_blind_ascending, FormattedContest, FormattedSubmission,
+};
+
+#[get("/single-contest/{id}/backup")]
+async fn get_single_contest_backup(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32,)>,
+    tz: Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+
+    #[derive(Serialize)]
+    struct FormattedBackup {
+        pub uuid: String,
+        pub creation_instant: String,
+        pub filename: String,
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        contest: FormattedContest,
+        backups: Vec<FormattedBackup>,
+        submissions: Vec<FormattedSubmission>,
+    }
+
+    let mut connection = pool.get()?;
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    if let Some(redirect) = assert_single_contest_not_started(&logged_user, &contest) {
+        return redirect;
+    }
+    let submissions =
+        submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?;
+    let backups = backup::get_backups_by_contest_user(&mut connection, logged_user.id, contest_id)?;
+
+    render(
+        &hb,
+        "single_backup",
+        &Context {
+            base,
+            contest: get_formatted_contest(&tz, &contest),
+            backups: backups
+                .into_iter()
+                .map(|b| FormattedBackup {
+                    uuid: b.uuid,
+                    creation_instant: format_utc_date_time(&tz, b.creation_instant),
+                    filename: b.filename,
+                })
+                .collect(),
+            submissions: get_formatted_submissions_with_blind_ascending(
+                &tz,
+                &contest,
+                &submissions,
+            ),
+        },
+    )
+}
diff --git a/src/pages/get_single_contest_clarifications.rs b/src/pages/get_single_contest_clarifications.rs
new file mode 100644
index 0000000000000000000000000000000000000000..090f03ac8231563833fbf51ff8358944597d0ae1
--- /dev/null
+++ b/src/pages/get_single_contest_clarifications.rs
@@ -0,0 +1,102 @@
+use crate::models::{clarification, contest, problem, submission};
+use crate::pages::prelude::*;
+use crate::pages::{
+    assert_single_contest_not_started, get_formatted_contest,
+    get_formatted_submissions_with_blind_ascending, FormattedContest, FormattedSubmission,
+};
+
+#[derive(Serialize)]
+struct FormattedClarification {
+    pub uuid: String,
+    pub question: String,
+    pub question_instant: String,
+    pub answer: Option<String>,
+    pub answer_instant: Option<String>,
+    pub answer_user_id: Option<i32>,
+    pub answer_to_all: bool,
+    pub contest_problem_id: Option<i32>,
+    pub problem_label: Option<String>,
+    pub contest_id: i32,
+    pub user_id: i32,
+    pub user_name: String,
+}
+
+#[get("/single-contest/{id}/clarifications")]
+async fn get_single_contest_clarifications(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32,)>,
+    tz: Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+
+    #[derive(Serialize)]
+    struct FormattedProblem {
+        pub id: i32,
+        pub label: String,
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        contest: FormattedContest,
+        problems: Vec<FormattedProblem>,
+        clarifications: Vec<FormattedClarification>,
+        submissions: Vec<FormattedSubmission>,
+    }
+
+    let mut connection = pool.get()?;
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    if let Some(redirect) = assert_single_contest_not_started(&logged_user, &contest) {
+        return redirect;
+    }
+    let submissions =
+        submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?;
+    let clarifications = clarification::get_clarifications_by_contest_user(
+        &mut connection,
+        logged_user.id,
+        contest_id,
+    )?;
+    let problems = problem::get_problems_by_contest_id(&mut connection, contest_id)?;
+
+    render(
+        &hb,
+        "single_clarifications",
+        &Context {
+            base,
+            problems: problems
+                .iter()
+                .map(|p| FormattedProblem {
+                    id: p.id,
+                    label: p.label.clone(),
+                })
+                .collect(),
+            contest: get_formatted_contest(&tz, &contest),
+            clarifications: clarifications
+                .into_iter()
+                .map(|(c, u, problem_label)| FormattedClarification {
+                    uuid: c.uuid,
+                    question: c.question,
+                    question_instant: format_utc_date_time(&tz, c.question_instant),
+                    answer: c.answer,
+                    answer_instant: c.answer_instant.map(|i| format_utc_date_time(&tz, i)),
+                    answer_user_id: c.answer_user_id,
+                    answer_to_all: c.answer_to_all,
+                    contest_problem_id: c.contest_problem_id,
+                    problem_label,
+                    contest_id: c.contest_id,
+                    user_id: c.user_id,
+                    user_name: u.name,
+                })
+                .collect(),
+            submissions: get_formatted_submissions_with_blind_ascending(
+                &tz,
+                &contest,
+                &submissions,
+            ),
+        },
+    )
+}
diff --git a/src/pages/get_single_contest_problems.rs b/src/pages/get_single_contest_problems.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ec6161e06a844179f9f5ec89ac3896c8a4c8bae2
--- /dev/null
+++ b/src/pages/get_single_contest_problems.rs
@@ -0,0 +1,60 @@
+use crate::models::{contest, problem, submission};
+use crate::pages::prelude::*;
+use crate::pages::{
+    assert_single_contest_not_started, get_formatted_contest,
+    get_formatted_problem_by_contest_with_score, get_formatted_submissions_with_blind_ascending,
+    FormattedContest, FormattedProblemByContestWithScore, FormattedSubmission,
+};
+
+#[get("/single-contest/{id}/problems")]
+pub async fn get_single_contest_problems(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32,)>,
+    tz: Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        contest: FormattedContest,
+        problems: Vec<FormattedProblemByContestWithScore>,
+        submissions: Vec<FormattedSubmission>,
+    }
+
+    let mut connection = pool.get()?;
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    if let Some(redirect) = assert_single_contest_not_started(&logged_user, &contest) {
+        return redirect;
+    }
+
+    let problems = problem::get_problems_user_by_contest_id_with_score_with_blind_and_frozen(
+        &mut connection,
+        logged_user.id,
+        contest_id,
+    )?;
+    let submissions =
+        submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?;
+
+    render(
+        &hb,
+        "single_problems",
+        &Context {
+            base,
+            contest: get_formatted_contest(&tz, &contest),
+            problems: problems
+                .iter()
+                .map(|p| get_formatted_problem_by_contest_with_score(p, &contest))
+                .collect(),
+            submissions: get_formatted_submissions_with_blind_ascending(
+                &tz,
+                &contest,
+                &submissions,
+            ),
+        },
+    )
+}
diff --git a/src/pages/get_single_contest_runs.rs b/src/pages/get_single_contest_runs.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2f679d5c30092ab05b59665383499176cc124454
--- /dev/null
+++ b/src/pages/get_single_contest_runs.rs
@@ -0,0 +1,82 @@
+use crate::models::contest;
+use crate::models::problem;
+use crate::models::submission;
+use crate::pages::get_editor::{get_formatted_languages, FormattedLanguage};
+use crate::pages::prelude::*;
+use crate::pages::{
+    assert_single_contest_not_started, get_formatted_contest,
+    get_formatted_submissions_with_blind_ascending, FormattedContest, FormattedSubmission,
+};
+use crate::Language;
+
+#[derive(Deserialize)]
+struct ContestProblemHint {
+    contest_problem_id: Option<i32>,
+}
+
+#[get("/single-contest/{id}/runs")]
+async fn get_single_contest_runs(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    languages: Data<Arc<DashMap<String, Language>>>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32,)>,
+    contest_problem_hint: Query<ContestProblemHint>,
+    session: Session,
+    tz: Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+    let mut connection = pool.get()?;
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    if let Some(redirect) = assert_single_contest_not_started(&logged_user, &contest) {
+        return redirect;
+    }
+
+    let submissions =
+        submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?;
+
+    let problems = problem::get_problems_by_contest_id(&mut connection, contest_id)?;
+
+    #[derive(Serialize)]
+    struct FormattedProblem {
+        pub id: i32,
+        pub label: String,
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        contest: FormattedContest,
+        problems: Vec<FormattedProblem>,
+        submissions: Vec<FormattedSubmission>,
+        language: Option<String>,
+        languages: Vec<FormattedLanguage>,
+        contest_problem_id_hint: Option<i32>,
+    }
+
+    render(
+        &hb,
+        "single_runs",
+        &Context {
+            base,
+            contest: get_formatted_contest(&tz, &contest),
+            problems: problems
+                .iter()
+                .map(|p| FormattedProblem {
+                    id: p.id,
+                    label: p.label.clone(),
+                })
+                .collect(),
+            submissions: get_formatted_submissions_with_blind_ascending(
+                &tz,
+                &contest,
+                &submissions,
+            ),
+            language: session.get("language")?,
+            languages: get_formatted_languages(&languages, true),
+            contest_problem_id_hint: contest_problem_hint.contest_problem_id,
+        },
+    )
+}
diff --git a/src/pages/get_single_contest_score.rs b/src/pages/get_single_contest_score.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7b48d30e03de1bd246a78a6a9920f44e9969e077
--- /dev/null
+++ b/src/pages/get_single_contest_score.rs
@@ -0,0 +1,143 @@
+use itertools::Itertools;
+use std::collections::HashSet;
+use chrono::Local;
+
+use crate::models::{contest, problem, submission, user};
+use crate::pages::prelude::*;
+use crate::pages::{
+    assert_single_contest_not_started, get_formatted_contest,
+    get_formatted_problem_by_contest_with_score, get_formatted_submissions_with_blind_ascending,
+    FormattedContest, FormattedProblemByContestWithScore, FormattedSubmission,
+};
+
+#[get("/single-contest/{id}/score")]
+async fn get_single_contest_score(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32,)>,
+    tz: Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+
+    #[derive(Serialize)]
+    struct Score {
+        pub user_name: Option<String>,
+        pub user_full_name: Option<String>,
+        pub problems: Vec<FormattedProblemByContestWithScore>,
+        pub solved_count: i64,
+        pub penalty: i64,
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        contest: FormattedContest,
+        scores: Vec<Score>,
+        submissions: Vec<FormattedSubmission>,
+        frozen: bool,
+    }
+
+    let mut connection = pool.get()?;
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    if let Some(redirect) = assert_single_contest_not_started(&logged_user, &contest) {
+        return redirect;
+    }
+    let scores =
+        problem::get_problems_by_contest_id_with_score_with_frozen(&mut connection, contest_id)?;
+    let submissions =
+        submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?;
+    let users_contest = user::get_users_by_single_contest_id(&mut connection, contest_id)?;
+    let users_contest_users: HashSet<String> =
+        HashSet::from_iter(users_contest.iter().map(|e| e.name.clone()));
+    let mut scores: Vec<_> = scores.into_iter().group_by(|e|
+        (e.user_name.as_ref().cloned(), e.user_full_name.clone())
+    ).into_iter().filter_map(|((user_name, user_full_name), problems)| {
+        let problems: Vec<_> = problems.map(|p| get_formatted_problem_by_contest_with_score(&p, &contest)).collect();
+        if match user_name {
+            None => false,
+            Some(ref user_name) => !users_contest_users.contains(user_name)
+        } {
+            return None
+        }
+        Some(Score {
+            user_name,
+            user_full_name,
+            solved_count: i64::try_from(problems.iter().filter(|p| !p.first_ac_submission_time.is_empty()).count()).unwrap(),
+            penalty: problems.iter().filter(|p| !p.first_ac_submission_time.is_empty())
+                .map(|p| match p.first_ac_submission_minutes {
+                    Some(x) if x >= 0 => x,
+                    _ => 0,
+                } + i64::from(20*p.failed_submissions))
+                .sum(),
+            problems
+        })
+    }).collect();
+    scores.sort_by(|a, b| {
+        (
+            a.user_name.is_some(),
+            -a.solved_count,
+            a.penalty,
+            &a.user_name,
+        )
+            .cmp(&(
+                b.user_name.is_some(),
+                -b.solved_count,
+                b.penalty,
+                &b.user_name,
+            ))
+    });
+
+    let scored_users: HashSet<String> =
+        HashSet::from_iter(scores.iter().filter_map(|e| e.user_name.clone()));
+    for user in users_contest {
+        if !scored_users.contains(&user.name) {
+            scores.push(Score {
+                user_name: Some(user.name),
+                user_full_name: user.full_name,
+                solved_count: 0,
+                penalty: 0,
+                problems: scores[0]
+                    .problems
+                    .iter()
+                    .map(|e| FormattedProblemByContestWithScore {
+                        first_ac_submission_time: e.first_ac_submission_time.clone(),
+                        first_ac_submission_minutes: e.first_ac_submission_minutes,
+                        first_solve: false,
+                        user_accepted_count: e.user_accepted_count,
+                        failed_submissions: e.failed_submissions,
+                        submission_count: e.submission_count,
+                        last_failed_submission_verdict: None,
+                        balloon_color: e.balloon_color.clone(),
+                        id: e.id,
+                        name: e.name.clone(),
+                        label: e.label.clone(),
+                        memory_limit_mib: e.memory_limit_mib,
+                        time_limit: e.time_limit.clone(),
+                    })
+                    .collect(),
+            });
+        }
+    }
+
+    render(
+        &hb,
+        "single_score",
+        &Context {
+            base,
+            contest: get_formatted_contest(&tz, &contest),
+            scores,
+            submissions: get_formatted_submissions_with_blind_ascending(
+                &tz,
+                &contest,
+                &submissions,
+            ),
+            frozen: match contest.frozen_scoreboard_instant {
+                Some(frozen_scoreboard_instant) => Local::now().naive_utc() >= frozen_scoreboard_instant,
+                None => false
+            }
+        },
+    )
+}
diff --git a/src/pages/get_single_contest_tasks.rs b/src/pages/get_single_contest_tasks.rs
new file mode 100644
index 0000000000000000000000000000000000000000..72b0b757d45529a5453558a60742ffa81b18a88b
--- /dev/null
+++ b/src/pages/get_single_contest_tasks.rs
@@ -0,0 +1,71 @@
+use crate::models::{contest, printing_task, submission};
+use crate::pages::prelude::*;
+use crate::pages::{
+    assert_single_contest_not_started, get_formatted_contest,
+    get_formatted_submissions_with_blind_ascending, FormattedContest, FormattedSubmission,
+};
+
+#[get("/single-contest/{id}/tasks")]
+async fn get_single_contest_tasks(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32,)>,
+    tz: Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+
+    #[derive(Serialize)]
+    struct FormattedPrintingTask {
+        pub uuid: String,
+        pub creation_instant: String,
+        pub status: String,
+        pub done_instant: Option<String>,
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        contest: FormattedContest,
+        printing_tasks: Vec<FormattedPrintingTask>,
+        submissions: Vec<FormattedSubmission>,
+    }
+
+    let mut connection = pool.get()?;
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+    if let Some(redirect) = assert_single_contest_not_started(&logged_user, &contest) {
+        return redirect;
+    }
+    let submissions =
+        submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?;
+    let printing_tasks = printing_task::get_printing_tasks_by_contest_user(
+        &mut connection,
+        logged_user.id,
+        contest_id,
+    )?;
+
+    render(
+        &hb,
+        "single_tasks",
+        &Context {
+            base,
+            contest: get_formatted_contest(&tz, &contest),
+            printing_tasks: printing_tasks
+                .into_iter()
+                .map(|p| FormattedPrintingTask {
+                    uuid: p.uuid,
+                    status: p.status,
+                    creation_instant: format_utc_date_time(&tz, p.creation_instant),
+                    done_instant: p.done_instant.map(|i| format_utc_date_time(&tz, i)),
+                })
+                .collect(),
+            submissions: get_formatted_submissions_with_blind_ascending(
+                &tz,
+                &contest,
+                &submissions,
+            ),
+        },
+    )
+}
diff --git a/src/pages/get_single_contest_waiting.rs b/src/pages/get_single_contest_waiting.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a975d6eeb95177c91d8d47d5111817d4d712640f
--- /dev/null
+++ b/src/pages/get_single_contest_waiting.rs
@@ -0,0 +1,68 @@
+use crate::models::contest;
+use crate::pages::get_formatted_contest;
+use crate::pages::prelude::*;
+use actix_web::http::header;
+use actix_web::http::header::HeaderValue;
+use chrono::Local;
+use std::env;
+
+#[get("/single-contest/{id}/waiting")]
+pub async fn get_single_contest_waiting(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32,)>,
+    tz: Data<Tz>,
+) -> PageResult {
+    require_identity(&identity)?;
+    let (contest_id,) = path.into_inner();
+    let mut connection = pool.get()?;
+    let contest = contest::get_contest_by_id(&mut connection, contest_id)?;
+
+    if contest
+        .start_instant
+        .map_or(true, |s| s < Local::now().naive_utc())
+    {
+        return Ok(HttpResponse::SeeOther()
+            .append_header((
+                header::LOCATION,
+                HeaderValue::from_str(&format!(
+                    "{}/single-contest/{}/problems",
+                    env::var("BASE_URL").expect("BASE_URL environment variable is not set"),
+                    contest_id
+                ))
+                .unwrap(),
+            ))
+            .finish());
+    }
+
+    let start_instant = contest.start_instant.unwrap();
+    let f = get_formatted_contest(&tz, &contest);
+
+    #[derive(Serialize)]
+    struct FormattedContest {
+        start_instant: String,
+        id: i32,
+        contest_status: String,
+    }
+
+    #[derive(Serialize)]
+    struct Context {
+        base: BaseContext,
+        contest: FormattedContest,
+    }
+
+    render(
+        &hb,
+        "single_waiting",
+        &Context {
+            base,
+            contest: FormattedContest {
+                id: contest_id,
+                start_instant: start_instant.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
+                contest_status: f.contest_status,
+            },
+        },
+    )
+}
diff --git a/src/pages/get_submission_source.rs b/src/pages/get_submission_source.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9998d3a14df01740bb1637ce398d81dee6ce1cb2
--- /dev/null
+++ b/src/pages/get_submission_source.rs
@@ -0,0 +1,27 @@
+use crate::models::submission;
+use crate::pages::prelude::*;
+use actix_web::{http::header::ContentType, HttpResponse};
+
+#[get("/submissions/{uuid}/source")]
+async fn get_submission_source(
+    identity: Identity,
+    pool: Data<DbPool>,
+    path: Path<(String,)>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let (submission_uuid,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    let (submission, user, _, _) =
+        submission::get_submission_by_uuid(&mut connection, submission_uuid)?;
+
+    if user.id != logged_user.id && !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Não é possível acessar uma submissão de outro usuário".into(),
+        ));
+    }
+
+    Ok(HttpResponse::Ok()
+        .content_type(ContentType::plaintext())
+        .body(submission.source_text))
+}
diff --git a/src/pages/get_submissions.rs b/src/pages/get_submissions.rs
index 2fbaa90e248c0d03119f33a38a61bc25a04ea7d0..698cc5420caa34a1a57ae6863a2e247c1912d971 100644
--- a/src/pages/get_submissions.rs
+++ b/src/pages/get_submissions.rs
@@ -2,7 +2,7 @@ use submission::{ContestProblem, Submission};
 
 use crate::models::submission;
 use crate::pages::prelude::*;
-use crate::pages::{get_formatted_submissions, FormattedSubmission};
+use crate::pages::{assert_not_single_contest, get_formatted_submissions, FormattedSubmission};
 use crate::user::User;
 
 pub fn render_submissions(
@@ -16,7 +16,6 @@ pub fn render_submissions(
         base: BaseContext,
         submissions: Vec<FormattedSubmission>,
     }
-
     render(
         hb,
         "submissions",
@@ -36,15 +35,19 @@ async fn get_submissions(
     tz: Data<Tz>,
 ) -> PageResult {
     let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    if let Some(redirect) = assert_not_single_contest(&logged_user) {
+        return redirect;
+    }
     let mut connection = pool.get()?;
     render_submissions(
         base,
         &hb,
         &tz,
-        if logged_user.is_admin {
-            submission::get_submissions(&mut connection)?
-        } else {
-            submission::get_submissions_user(&mut connection, logged_user.id)?
-        },
+        submission::get_submissions(&mut connection)?,
     )
 }
diff --git a/src/pages/get_submissions_by_contest_id.rs b/src/pages/get_submissions_by_contest_id.rs
new file mode 100644
index 0000000000000000000000000000000000000000..07d7162880f33ed27ceafdc90086efeaaa6213ba
--- /dev/null
+++ b/src/pages/get_submissions_by_contest_id.rs
@@ -0,0 +1,29 @@
+use crate::models::submission;
+use crate::pages::get_submissions::render_submissions;
+use crate::pages::prelude::*;
+
+#[get("/submissions/contests/{id}")]
+async fn get_submissions_by_contest_id(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32,)>,
+    tz: Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let (contest_id,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    render_submissions(
+        base,
+        &hb,
+        &tz,
+        submission::get_submissions_by_contest(&mut connection, contest_id)?,
+    )
+}
diff --git a/src/pages/get_submissions_by_contest_id_problem_label.rs b/src/pages/get_submissions_by_contest_id_problem_label.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f5c07279a534b87dc1697e8b45483035446e8803
--- /dev/null
+++ b/src/pages/get_submissions_by_contest_id_problem_label.rs
@@ -0,0 +1,32 @@
+use crate::models::submission;
+use crate::pages::get_submissions::render_submissions;
+use crate::pages::prelude::*;
+
+#[get("/submissions/contests/{id}/{label}")]
+async fn get_submissions_by_contest_id_problem_label(
+    base: BaseContext,
+    identity: Identity,
+    pool: Data<DbPool>,
+    hb: Data<Handlebars<'_>>,
+    path: Path<(i32, String)>,
+    tz: Data<Tz>,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    let mut connection = pool.get()?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let (contest_id, problem_label) = path.into_inner();
+    render_submissions(
+        base,
+        &hb,
+        &tz,
+        submission::get_submissions_by_contest_problem(
+            &mut connection,
+            contest_id,
+            &problem_label,
+        )?,
+    )
+}
diff --git a/src/pages/get_submissions_me.rs b/src/pages/get_submissions_me.rs
index eec35a1d809da6beb42dadd6dba98f660283fe00..d28086c3c32131229c34c0b7f3ac01b6d7547543 100644
--- a/src/pages/get_submissions_me.rs
+++ b/src/pages/get_submissions_me.rs
@@ -1,4 +1,5 @@
 use crate::models::submission;
+use crate::pages::assert_not_single_contest;
 use crate::pages::get_submissions::render_submissions;
 use crate::pages::prelude::*;
 
@@ -11,6 +12,9 @@ async fn get_submissions_me(
     tz: Data<Tz>,
 ) -> PageResult {
     let logged_user = require_identity(&identity)?;
+    if let Some(redirect) = assert_not_single_contest(&logged_user) {
+        return redirect;
+    }
     let mut connection = pool.get()?;
     render_submissions(
         base,
diff --git a/src/pages/get_submissions_me_by_contest_id.rs b/src/pages/get_submissions_me_by_contest_id.rs
index ca136b2eae630ab5eea7b687e9b4ee09e9688e73..561a76ed815488ea64e759ad323b691ff7ee25a0 100644
--- a/src/pages/get_submissions_me_by_contest_id.rs
+++ b/src/pages/get_submissions_me_by_contest_id.rs
@@ -1,4 +1,5 @@
 use crate::models::submission;
+use crate::pages::assert_not_single_contest;
 use crate::pages::get_submissions::render_submissions;
 use crate::pages::prelude::*;
 
@@ -12,6 +13,9 @@ async fn get_submissions_me_by_contest_id(
     tz: Data<Tz>,
 ) -> PageResult {
     let logged_user = require_identity(&identity)?;
+    if let Some(redirect) = assert_not_single_contest(&logged_user) {
+        return redirect;
+    }
     let (contest_id,) = path.into_inner();
     let mut connection = pool.get()?;
 
diff --git a/src/pages/impersonate_user.rs b/src/pages/impersonate_user.rs
index 887e32f634d1e845abdbb2299622c3099d3f92c3..f0056145d2993ac1fe343ee0c2ca2a40e7e1b432 100644
--- a/src/pages/impersonate_user.rs
+++ b/src/pages/impersonate_user.rs
@@ -30,6 +30,8 @@ async fn impersonate_user(
         serde_json::to_string(&LoggedUser {
             id: user.id,
             name: (&user.name).into(),
+            full_name: user.full_name,
+            single_contest_id: user.single_contest_id,
             is_admin: user.is_admin,
         })
         .map_err(|_| PageError::Custom("Usuário no banco de dados inconsistente".into()))?,
diff --git a/src/pages/import_repository.rs b/src/pages/import_repository.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6293f22e003a97157b059b7eb7ea2978ccca426a
--- /dev/null
+++ b/src/pages/import_repository.rs
@@ -0,0 +1,73 @@
+use crate::models::repository;
+use crate::pages::prelude::*;
+use chrono::Local;
+use git2::{CertificateCheckStatus, Cred, RemoteCallbacks};
+use itertools::Itertools;
+use std::path::Path;
+
+#[derive(Serialize, Deserialize)]
+struct RepositoryForm {
+    name: String,
+    remote_url: String,
+}
+
+#[post("/repositories/")]
+async fn import_repository(
+    identity: Identity,
+    pool: Data<DbPool>,
+    form: Form<RepositoryForm>,
+    request: HttpRequest,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let mut connection = pool.get()?;
+
+    let (_, url) = form.remote_url.split("@").collect_tuple().unwrap();
+    let path = url.replace(':', "+").replace('/', ".");
+
+    let mut callbacks = RemoteCallbacks::new();
+    callbacks.credentials(|_url, username_from_url, _allowed_types| {
+        Cred::ssh_key(
+            username_from_url.unwrap(),
+            None,
+            Path::new("/data/id_ed25519_git"),
+            None,
+        )
+    });
+    callbacks.certificate_check(|_cert, _hostname| {
+        return Ok(CertificateCheckStatus::CertificateOk);
+    });
+
+    let mut fo = git2::FetchOptions::new();
+    fo.remote_callbacks(callbacks);
+
+    let mut builder = git2::build::RepoBuilder::new();
+    builder.fetch_options(fo);
+
+    if let Err(e) = builder.clone(&form.remote_url, Path::new(&format!("/data/{}/", path))) {
+        return Err(PageError::Validation(format!(
+            "Repositório {} importado com erro {}",
+            form.remote_url, e
+        )));
+    }
+
+    repository::insert_repository(
+        &mut connection,
+        repository::NewRepository {
+            name: form.name.clone(),
+            path,
+            remote_url: form.remote_url.clone(),
+            creation_user_id: logged_user.id,
+            creation_instant: Local::now().naive_utc(),
+        },
+    )?;
+
+    Ok(redirect_to_referer(
+        format!("Repositório {} importado com sucesso!", form.remote_url),
+        &request,
+    ))
+}
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index b3f00885eb3ce7fd7842066ab1c13e7b7263a624..ef844ddbef01e715891374d014e17f82d76c18d7 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -5,7 +5,8 @@ use actix_web::http::header::HeaderValue;
 use actix_web::http::{header, StatusCode};
 use actix_web::middleware::ErrorHandlerResponse;
 use actix_web_flash_messages::FlashMessage;
-use chrono::{Local, NaiveDateTime, TimeZone};
+use chrono::Local;
+use chrono::TimeDelta;
 use chrono_tz::Tz;
 use contest::{Contest, ContestWithAcs};
 use diesel::pg::PgConnection;
@@ -15,32 +16,62 @@ use submission::{ContestProblem, Submission};
 use crate::models::{contest, problem, submission};
 use crate::user::User;
 
+pub mod get_contest_webcast_by_id;
 pub mod change_password;
+pub mod create_backup;
+pub mod create_clarification;
 pub mod create_contest;
+pub mod create_contest_submission;
+pub mod create_printing_task;
 pub mod create_submission;
 pub mod create_user;
 pub mod get_about;
+pub mod get_backup_contents;
 pub mod get_contest_by_id;
 pub mod get_contest_problem_by_id_label;
 pub mod get_contest_scoreboard_by_id;
+pub mod get_contest_clarifications_by_id;
+pub mod get_contest_tasks_by_id;
+pub mod patch_clarification;
 pub mod get_contests;
 pub mod get_editor;
 pub mod get_login;
 pub mod get_main;
 pub mod get_me;
+pub mod get_printing_task_contents;
+pub mod post_printing_task_done;
+pub mod post_submission_balloon_delivered;
 pub mod get_problem_by_id_assets;
 pub mod get_problems;
+pub mod get_repositories;
+pub mod get_repository_by_id;
+pub mod get_repository_problem_by_path;
+pub mod get_single_contest_backup;
+pub mod get_single_contest_clarifications;
+pub mod get_single_contest_problems;
+pub mod get_single_contest_runs;
+pub mod get_single_contest_score;
+pub mod get_single_contest_tasks;
+pub mod get_single_contest_waiting;
 pub mod get_submission;
+pub mod get_submission_source;
 pub mod get_submissions;
+pub mod get_submissions_by_contest_id;
+pub mod get_submissions_by_contest_id_problem_label;
 pub mod get_submissions_me;
 pub mod get_submissions_me_by_contest_id;
 pub mod get_submissions_me_by_contest_id_problem_label;
 pub mod impersonate_user;
+pub mod import_repository;
+pub mod patch_contest;
+pub mod patch_contest_problem;
 pub mod post_login;
 pub mod post_logout;
 pub mod prelude;
 pub mod rejudge_submission;
 pub mod submission_updates;
+pub mod sync_repository;
+pub mod sync_repository_contest;
 
 use prelude::*;
 
@@ -51,7 +82,7 @@ pub fn render_401<B>(
     res.response_mut().headers_mut().insert(
         header::LOCATION,
         HeaderValue::from_str(&format!(
-            "{}login",
+            "{}/login",
             &env::var("BASE_URL").expect("BASE_URL environment variable is not set")
         ))
         .unwrap(),
@@ -60,8 +91,23 @@ pub fn render_401<B>(
     Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
 }
 
-pub fn render_400<B>(res: dev::ServiceResponse<B>) -> actix_web::Result<ErrorHandlerResponse<B>> {
+pub fn render_400<B>(
+    mut res: dev::ServiceResponse<B>,
+) -> actix_web::Result<ErrorHandlerResponse<B>> {
     FlashMessage::error("Entrada inválida").send();
+    let referer = res
+        .request()
+        .headers()
+        .get("Referer")
+        .and_then(|h| h.to_str().ok())
+        .map_or_else(
+            || env::var("BASE_URL").expect("BASE_URL environment variable is not set") + "/",
+            std::convert::Into::into,
+        );
+    res.response_mut()
+        .headers_mut()
+        .insert(header::LOCATION, HeaderValue::from_str(&referer).unwrap());
+    *res.response_mut().status_mut() = StatusCode::SEE_OTHER;
     Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
 }
 
@@ -87,8 +133,12 @@ fn format_duration(duration: chrono::Duration) -> String {
 struct FormattedProblemByContestWithScore {
     pub first_ac_submission_time: String,
     pub first_ac_submission_minutes: Option<i64>,
+    pub first_solve: bool,
     pub user_accepted_count: i32,
+    pub balloon_color: Option<String>,
     pub failed_submissions: i32,
+    pub submission_count: i32,
+    pub last_failed_submission_verdict: Option<String>,
     pub id: i32,
     pub name: String,
     pub label: String,
@@ -111,13 +161,24 @@ fn get_formatted_problem_by_contest_with_score(
         first_ac_submission_minutes: p
             .first_ac_submission_instant
             .and_then(|t| contest.start_instant.map(|cs| (t - cs).num_minutes())),
+        first_solve: p.first_ac_problem_submission_instant == p.first_ac_submission_instant,
         failed_submissions: p.failed_submissions,
+        submission_count: p.failed_submissions
+            + match p.first_ac_submission_instant {
+                None => 0,
+                _ => 1,
+            },
+        last_failed_submission_verdict: p
+            .last_failed_submission_verdict
+            .as_ref()
+            .map(|s| s.clone()),
         id: p.id,
         name: p.name.clone(),
         label: p.label.clone(),
         memory_limit_mib: p.memory_limit_bytes / 1_024 / 1_024,
         time_limit: format!("{}", f64::from(p.time_limit_ms) / 1000.0).replacen('.', ",", 1),
         user_accepted_count: p.user_accepted_count,
+        balloon_color: p.balloon_color.clone(),
     }
 }
 
@@ -127,16 +188,68 @@ fn assert_contest_not_started(
 ) -> Result<(), PageError> {
     if contest
         .start_instant
-        .map_or(false, |s| s > Local::now().naive_utc())
+        .map_or(false, |s| Local::now().naive_utc() < s)
         && !logged_user.is_admin
     {
-        return Err(PageError::Forbidden(
+        return Err(PageError::Validation(
             "Essa competição ainda não começou".into(),
         ));
     }
     Ok(())
 }
 
+fn assert_contest_not_over(logged_user: &LoggedUser, contest: &Contest) -> Result<(), PageError> {
+    if contest
+        .end_instant
+        .map_or(false, |s| s < Local::now().naive_utc())
+        && !logged_user.is_admin
+    {
+        return Err(PageError::Validation("Essa competição acabou".into()));
+    }
+    Ok(())
+}
+
+fn assert_single_contest_not_started(
+    logged_user: &LoggedUser,
+    contest: &Contest,
+) -> Option<PageResult> {
+    if contest
+        .start_instant
+        .map_or(false, |s| s > Local::now().naive_utc())
+        && !logged_user.is_admin
+    {
+        return Some(Ok(HttpResponse::SeeOther()
+            .append_header((
+                header::LOCATION,
+                HeaderValue::from_str(&format!(
+                    "{}/single-contest/{}/waiting",
+                    env::var("BASE_URL").expect("BASE_URL environment variable is not set"),
+                    contest.id
+                ))
+                .unwrap(),
+            ))
+            .finish()));
+    }
+    None
+}
+
+fn assert_not_single_contest(logged_user: &LoggedUser) -> Option<PageResult> {
+    if let Some(single_contest_id) = logged_user.single_contest_id {
+        return Some(Ok(HttpResponse::SeeOther()
+            .append_header((
+                header::LOCATION,
+                HeaderValue::from_str(&format!(
+                    "{}/single-contest/{}/problems",
+                    env::var("BASE_URL").expect("BASE_URL environment variable is not set"),
+                    single_contest_id
+                ))
+                .unwrap(),
+            ))
+            .finish()));
+    }
+    None
+}
+
 #[derive(Serialize)]
 struct FormattedSubmission {
     uuid: String,
@@ -144,16 +257,12 @@ struct FormattedSubmission {
     problem_label: String,
     submission_instant: String,
     error_output: Option<String>,
+    language: String,
     user_name: String,
     time_ms: Option<i32>,
     memory: String,
     failed_test: Option<i32>,
-}
-
-fn format_utc_date_time(tz: &Tz, input: NaiveDateTime) -> String {
-    tz.from_utc_datetime(&input)
-        .format("%d/%m/%Y %H:%M:%S")
-        .to_string()
+    balloon_color: Option<String>,
 }
 
 fn get_formatted_submissions(
@@ -180,17 +289,72 @@ fn get_formatted_submissions(
                 Some(k) if k < 1_024 => format!("{}KiB", k),
                 Some(k) => format!("{}MiB", k / 1_024),
             },
+            language: submission.language.clone(),
+            failed_test: submission.failed_test,
+            balloon_color: contest_problem.balloon_color.clone(),
+        })
+        .collect()
+}
+
+fn get_formatted_submissions_with_blind(
+    tz: &Tz,
+    contest: &Contest,
+    vec: &[(Submission, ContestProblem, User)],
+) -> Vec<FormattedSubmission> {
+    if let None = contest.blind_scoreboard_instant {
+        return get_formatted_submissions(tz, vec);
+    }
+    let blind_scoreboard_instant = contest.blind_scoreboard_instant.unwrap();
+    vec.iter()
+        .map(|(submission, contest_problem, user)| FormattedSubmission {
+            uuid: (&submission.uuid).into(),
+            verdict: if submission.submission_instant >= blind_scoreboard_instant {
+                "BL".into()
+            } else {
+                submission
+                    .verdict
+                    .as_ref()
+                    .map_or_else(|| "WJ".into(), String::from)
+            },
+            problem_label: contest_problem.label.clone(),
+            submission_instant: format_utc_date_time(tz, submission.submission_instant),
+            error_output: submission
+                .error_output
+                .as_ref()
+                .map(std::convert::Into::into),
+            user_name: user.name.clone(),
+            time_ms: submission.time_ms,
+            memory: match submission.memory_kib {
+                None | Some(0) => "".into(),
+                Some(k) if k < 1_024 => format!("{}KiB", k),
+                Some(k) => format!("{}MiB", k / 1_024),
+            },
+            language: submission.language.clone(),
             failed_test: submission.failed_test,
+            balloon_color: contest_problem.balloon_color.clone(),
         })
         .collect()
 }
 
+fn get_formatted_submissions_with_blind_ascending(
+    tz: &Tz,
+    contest: &Contest,
+    vec: &[(Submission, ContestProblem, User)],
+) -> Vec<FormattedSubmission> {
+    let mut formatted = get_formatted_submissions_with_blind(tz, contest, vec);
+    formatted.reverse();
+    formatted
+}
+
 #[derive(Serialize)]
 struct FormattedContest {
     pub id: i32,
     pub name: String,
     pub start_instant: Option<String>,
     pub end_instant: Option<String>,
+    pub frozen_scoreboard_instant: Option<String>,
+    pub blind_scoreboard_instant: Option<String>,
+    pub contest_status: String,
     pub creation_instant: String,
     pub grade_ratio: Option<i32>,
     pub grade_after_ratio: Option<i32>,
@@ -207,6 +371,63 @@ fn get_formatted_contest(tz: &Tz, contest: &Contest) -> FormattedContest {
         name: contest.name.clone(),
         start_instant: contest.start_instant.map(|i| format_utc_date_time(tz, i)),
         end_instant: contest.end_instant.map(|i| format_utc_date_time(tz, i)),
+        frozen_scoreboard_instant: contest
+            .frozen_scoreboard_instant
+            .map(|i| format_utc_date_time(tz, i)),
+        blind_scoreboard_instant: contest
+            .blind_scoreboard_instant
+            .map(|i| format_utc_date_time(tz, i)),
+        contest_status: match contest.start_instant {
+            None => "contest always running".into(),
+            Some(start_instant)
+                if Local::now().naive_utc() + TimeDelta::hours(1) < start_instant =>
+            {
+                format!(
+                    "{} hour(s) to start",
+                    (start_instant - Local::now().naive_utc()).num_hours()
+                )
+            }
+            Some(start_instant)
+                if Local::now().naive_utc() + TimeDelta::minutes(1) < start_instant =>
+            {
+                format!(
+                    "{} min(s) to start",
+                    (start_instant - Local::now().naive_utc()).num_minutes()
+                )
+            }
+            Some(start_instant) if Local::now().naive_utc() < start_instant => {
+                format!(
+                    "{} second(s) to start",
+                    (start_instant - Local::now().naive_utc()).num_seconds()
+                )
+            }
+            Some(_) => match contest.end_instant {
+                None => "infinite minutes left".into(),
+                Some(end_instant)
+                    if Local::now().naive_utc() + TimeDelta::hours(1) < end_instant =>
+                {
+                    format!(
+                        "{} hour(s) left",
+                        (end_instant - Local::now().naive_utc()).num_hours()
+                    )
+                }
+                Some(end_instant)
+                    if Local::now().naive_utc() + TimeDelta::minutes(1) < end_instant =>
+                {
+                    format!(
+                        "{} min(s) left",
+                        (end_instant - Local::now().naive_utc()).num_minutes()
+                    )
+                }
+                Some(end_instant) if Local::now().naive_utc() < end_instant => {
+                    format!(
+                        "{} second(s) left",
+                        (end_instant - Local::now().naive_utc()).num_seconds()
+                    )
+                }
+                _ => "contest not running".into(),
+            },
+        },
         creation_instant: format_utc_date_time(tz, contest.creation_instant),
         grade_ratio: contest.grade_ratio,
         grade_after_ratio: contest.grade_after_ratio,
@@ -224,6 +445,13 @@ fn get_formatted_contest_acs(tz: &Tz, contest: &ContestWithAcs) -> FormattedCont
         name: contest.name.clone(),
         start_instant: contest.start_instant.map(|i| format_utc_date_time(tz, i)),
         end_instant: contest.end_instant.map(|i| format_utc_date_time(tz, i)),
+        frozen_scoreboard_instant: contest
+            .frozen_scoreboard_instant
+            .map(|i| format_utc_date_time(tz, i)),
+        blind_scoreboard_instant: contest
+            .blind_scoreboard_instant
+            .map(|i| format_utc_date_time(tz, i)),
+        contest_status: "".into(),
         creation_instant: format_utc_date_time(tz, contest.creation_instant),
         grade_ratio: contest.grade_ratio,
         grade_after_ratio: contest.grade_after_ratio,
diff --git a/src/pages/patch_clarification.rs b/src/pages/patch_clarification.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c5ecadc69cad536e2325468e050ff718fa04b095
--- /dev/null
+++ b/src/pages/patch_clarification.rs
@@ -0,0 +1,42 @@
+use chrono::Local;
+
+use crate::models::clarification;
+use crate::pages::prelude::*;
+
+#[derive(Deserialize)]
+struct PatchClarificationForm {
+    answer: String,
+    answer_to_all: Option<String>,
+}
+
+#[patch("/clarifications/{uuid}")]
+pub async fn patch_clarification(
+    identity: Identity,
+    pool: Data<DbPool>,
+    form: Form<PatchClarificationForm>,
+    path: Path<(String,)>,
+    request: HttpRequest,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let (clarification_uuid,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    clarification::update_clarification(
+        &mut connection,
+        clarification_uuid,
+        form.answer.clone(),
+        form.answer_to_all.as_ref().map(|e| e == "on").unwrap_or(false),
+        logged_user.id,
+        Local::now().naive_utc(),
+    )?;
+
+    Ok(redirect_to_referer(
+        "Clarificação respondida com sucesso".into(),
+        &request,
+    ))
+}
diff --git a/src/pages/patch_contest.rs b/src/pages/patch_contest.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3d192141ff216a19fa7566a4cc18ff576864370b
--- /dev/null
+++ b/src/pages/patch_contest.rs
@@ -0,0 +1,61 @@
+use chrono::NaiveDateTime;
+
+use crate::models::contest;
+use crate::pages::prelude::*;
+
+#[derive(Deserialize)]
+struct PatchContestForm {
+    start_instant: Option<String>,
+    end_instant: Option<String>,
+    frozen_scoreboard_instant: Option<String>,
+    blind_scoreboard_instant: Option<String>,
+}
+
+#[patch("/contests/{id}")]
+pub async fn patch_contest(
+    identity: Identity,
+    pool: Data<DbPool>,
+    form: Form<PatchContestForm>,
+    path: Path<(i32,)>,
+    tz: Data<Tz>,
+    request: HttpRequest,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let (contest_id,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    contest::update_contest(
+        &mut connection,
+        contest_id,
+        form.start_instant
+            .as_ref()
+            .and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").ok())
+            .and_then(|d| d.and_local_timezone(**tz).single())
+            .map(|d| d.naive_utc()),
+        form.end_instant
+            .as_ref()
+            .and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").ok())
+            .and_then(|d| d.and_local_timezone(**tz).single())
+            .map(|d| d.naive_utc()),
+        form.frozen_scoreboard_instant
+            .as_ref()
+            .and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").ok())
+            .and_then(|d| d.and_local_timezone(**tz).single())
+            .map(|d| d.naive_utc()),
+        form.blind_scoreboard_instant
+            .as_ref()
+            .and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").ok())
+            .and_then(|d| d.and_local_timezone(**tz).single())
+            .map(|d| d.naive_utc()),
+    )?;
+
+    Ok(redirect_to_referer(
+        "Competição atualizada com sucesso".into(),
+        &request,
+    ))
+}
diff --git a/src/pages/patch_contest_problem.rs b/src/pages/patch_contest_problem.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d20c38f2a9498dba90cd9b2263e945d8186cb326
--- /dev/null
+++ b/src/pages/patch_contest_problem.rs
@@ -0,0 +1,41 @@
+use serde_with::serde_as;
+use serde_with::NoneAsEmptyString;
+
+use crate::models::contest;
+use crate::pages::prelude::*;
+
+#[serde_as]
+#[derive(Deserialize)]
+struct PatchContestForm {
+    #[serde_as(as = "NoneAsEmptyString")]
+    balloon_color: Option<String>,
+}
+
+#[patch("/contests/{id}/problems/{contest_problem_id}")]
+pub async fn patch_contest_problem(
+    identity: Identity,
+    pool: Data<DbPool>,
+    form: Form<PatchContestForm>,
+    path: Path<(i32, i32)>,
+    request: HttpRequest,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let (_, contest_problem_id) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    contest::update_contest_problem_balloon_color(
+        &mut connection,
+        contest_problem_id,
+        form.balloon_color.clone(),
+    )?;
+
+    Ok(redirect_to_referer(
+        "Problema atualizado com sucesso".into(),
+        &request,
+    ))
+}
diff --git a/src/pages/post_login.rs b/src/pages/post_login.rs
index 90ae035fc48ee102a7c6016e09f8a08f3304b8cd..9c071189979d17f75f0a14662772b8570a6b57c1 100644
--- a/src/pages/post_login.rs
+++ b/src/pages/post_login.rs
@@ -35,6 +35,8 @@ async fn post_login(pool: Data<DbPool>, form: Form<LoginForm>, request: HttpRequ
                 serde_json::to_string(&LoggedUser {
                     id: logged_user.id,
                     name: (&logged_user.name).into(),
+                    full_name: logged_user.full_name,
+                    single_contest_id: logged_user.single_contest_id,
                     is_admin: logged_user.is_admin,
                 })
                 .map_err(|_| PageError::Custom("Usuário no banco de dados inconsistente".into()))?,
diff --git a/src/pages/post_printing_task_done.rs b/src/pages/post_printing_task_done.rs
new file mode 100644
index 0000000000000000000000000000000000000000..542b7fc96e3cc03679d3d5847351e23b662386fe
--- /dev/null
+++ b/src/pages/post_printing_task_done.rs
@@ -0,0 +1,32 @@
+use chrono::Local;
+
+use crate::models::printing_task;
+use crate::pages::prelude::*;
+
+#[post("/printing-tasks/{uuid}/done")]
+pub async fn post_printing_task_done(
+    identity: Identity,
+    pool: Data<DbPool>,
+    path: Path<(String,)>,
+    request: HttpRequest,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let (printing_task_uuid,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    printing_task::update_printing_task(
+        &mut connection,
+        printing_task_uuid,
+        Local::now().naive_utc(),
+    )?;
+
+    Ok(redirect_to_referer(
+        "Tarefa fechada com sucesso".into(),
+        &request,
+    ))
+}
diff --git a/src/pages/post_submission_balloon_delivered.rs b/src/pages/post_submission_balloon_delivered.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3ab4ae43a655b451c92fc24f9c0f55dc58057f57
--- /dev/null
+++ b/src/pages/post_submission_balloon_delivered.rs
@@ -0,0 +1,32 @@
+use chrono::Local;
+
+use crate::models::submission;
+use crate::pages::prelude::*;
+
+#[post("/submissions/{uuid}/balloon-delivered")]
+pub async fn post_submission_balloon_delivered(
+    identity: Identity,
+    pool: Data<DbPool>,
+    path: Path<(String,)>,
+    request: HttpRequest,
+) -> PageResult {
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let (submission_uuid,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    submission::deliver_balloon(
+        &mut connection,
+        submission_uuid,
+        Some(Local::now().naive_utc()),
+    )?;
+
+    Ok(redirect_to_referer(
+        "Tarefa fechada com sucesso".into(),
+        &request,
+    ))
+}
diff --git a/src/pages/prelude.rs b/src/pages/prelude.rs
index 30b588340de8fff9e7e24519caabac2b5a924ce7..5d540f0830d83b6d4d63defa461b62c2045a5e9f 100644
--- a/src/pages/prelude.rs
+++ b/src/pages/prelude.rs
@@ -2,8 +2,8 @@ pub use std::sync::{Arc, Mutex};
 
 pub use actix_identity::Identity;
 pub use actix_session::Session;
-pub use actix_web::web::{Data, Form, Path};
-pub use actix_web::{get, post, FromRequest, HttpRequest, HttpResponse};
+pub use actix_web::web::{Data, Form, Path, Query};
+pub use actix_web::{get, patch, post, FromRequest, HttpRequest, HttpResponse};
 pub use actix_web_flash_messages::FlashMessage;
 pub use async_channel::Sender;
 pub use chrono_tz::Tz;
@@ -16,6 +16,7 @@ pub use crate::queue::job_protocol::Job;
 
 pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
 
+use chrono::{NaiveDateTime, TimeZone};
 use std::env;
 use std::future::Future;
 use std::pin::Pin;
@@ -33,6 +34,8 @@ use thiserror::Error;
 pub struct LoggedUser {
     pub id: i32,
     pub name: String,
+    pub full_name: Option<String>,
+    pub single_contest_id: Option<i32>,
     pub is_admin: bool,
 }
 
@@ -172,3 +175,9 @@ pub fn redirect_to_root() -> HttpResponse {
         ))
         .finish()
 }
+
+pub fn format_utc_date_time(tz: &Tz, input: NaiveDateTime) -> String {
+    tz.from_utc_datetime(&input)
+        .format("%Y-%m-%d %H:%M:%S")
+        .to_string()
+}
diff --git a/src/pages/sync_repository.rs b/src/pages/sync_repository.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1c0cd4d939242a6cff0a89b60425463514c6b94d
--- /dev/null
+++ b/src/pages/sync_repository.rs
@@ -0,0 +1,61 @@
+use crate::models::repository;
+use crate::pages::prelude::*;
+use git2::{CertificateCheckStatus, Cred, RemoteCallbacks};
+
+#[post("/repositories/{id}/sync")]
+async fn sync_repository(
+    identity: Identity,
+    pool: Data<DbPool>,
+    path: Path<(i32,)>,
+    request: HttpRequest,
+) -> PageResult {
+    use std::path::Path;
+
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let (repository_id,) = path.into_inner();
+    let mut connection = pool.get()?;
+    let repository = repository::get_repository_by_id(&mut connection, repository_id)?;
+    let git_repository =
+        git2::Repository::open(Path::new(&format!("/data/{}/", repository.path))).unwrap();
+
+    let mut fo = git2::FetchOptions::new();
+    let mut callbacks = RemoteCallbacks::new();
+    callbacks.credentials(|_url, username_from_url, _allowed_types| {
+        Cred::ssh_key(
+            username_from_url.unwrap(),
+            None,
+            Path::new("/data/id_ed25519_git"),
+            None,
+        )
+    });
+    callbacks.certificate_check(|_cert, _hostname| {
+        return Ok(CertificateCheckStatus::CertificateOk);
+    });
+    fo.remote_callbacks(callbacks);
+
+    git_repository
+        .find_remote("origin")
+        .unwrap()
+        .fetch(&["main"], Some(&mut fo), None)
+        .unwrap();
+    let oid = git_repository
+        .refname_to_id("refs/remotes/origin/main")
+        .unwrap();
+    git_repository
+        .reset(
+            &git_repository.find_object(oid, None).unwrap(),
+            git2::ResetType::Hard,
+            None,
+        )
+        .unwrap();
+
+    Ok(redirect_to_referer(
+        format!("Repositório {} atualizado com sucesso!", repository.name),
+        &request,
+    ))
+}
diff --git a/src/pages/sync_repository_contest.rs b/src/pages/sync_repository_contest.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0f9b39b4ee86c100876d107cb2e267410a64bc72
--- /dev/null
+++ b/src/pages/sync_repository_contest.rs
@@ -0,0 +1,153 @@
+use crate::models::contest;
+use crate::models::contest::NewContest;
+use crate::models::contest::NewContestProblems;
+use crate::models::problem;
+use crate::models::problem::NewProblem;
+use crate::models::repository;
+use crate::pages::prelude::*;
+use chrono::Local;
+use diesel::NotFound;
+use std::fs;
+use std::process::Command;
+
+#[derive(Deserialize)]
+struct BlemConfig {
+    authors: Vec<String>,
+    time_limit_ms: i32,
+    memory_limit_kb: Option<i32>,
+    br: LanguageConfig,
+}
+
+#[derive(Deserialize)]
+struct LanguageConfig {
+    name: String,
+}
+
+#[derive(Deserialize)]
+struct ContestProblemConfig {
+    label: String,
+    path: String,
+    name: String,
+}
+
+#[derive(Deserialize)]
+struct ContestConfig {
+    br: LanguageConfig,
+    problems: Vec<ContestProblemConfig>,
+}
+
+#[post("/repositories/{id}/contest/sync")]
+async fn sync_repository_contest(
+    identity: Identity,
+    pool: Data<DbPool>,
+    path: Path<(i32,)>,
+    request: HttpRequest,
+) -> PageResult {
+    use std::path::Path;
+
+    let logged_user = require_identity(&identity)?;
+    if !logged_user.is_admin {
+        return Err(PageError::Forbidden(
+            "Apenas administradores podem fazer isso".into(),
+        ));
+    }
+    let (repository_id,) = path.into_inner();
+    let mut connection = pool.get()?;
+
+    let repository = repository::get_repository_by_id(&mut connection, repository_id)?;
+    let git_repository =
+        git2::Repository::open(Path::new(&format!("/data/{}/", repository.path))).unwrap();
+    let head_commit = git_repository.head().unwrap();
+    let head_commit_short_id = head_commit
+        .peel_to_commit()
+        .unwrap()
+        .as_object()
+        .short_id()
+        .unwrap();
+    let head_commit_shorthand = head_commit_short_id.as_str().unwrap();
+
+    let contest_toml_contents =
+        fs::read_to_string(format!("/data/{}/contest.toml", repository.path)).unwrap();
+    let contest_config = toml::from_str::<ContestConfig>(&contest_toml_contents).unwrap();
+    let contest = match contest::get_contest_by_name(&mut connection, &(String::from(&contest_config.br.name) + " (revisado)")) {
+        Ok(contest) => contest,
+        Err(NotFound) => contest::insert_contest(
+            &mut connection,
+            NewContest {
+                name: contest_config.br.name,
+                start_instant: None,
+                end_instant: None,
+                creation_user_id: logged_user.id,
+                creation_instant: Local::now().naive_utc(),
+                grade_ratio: None,
+                grade_after_ratio: None,
+            },
+        )?,
+        Err(e) => return Err(PageError::Database(e)),
+    };
+    let contest_problems = contest::get_contest_problems_by_contest(&mut connection, contest.id)?;
+    for problem in contest_config.problems {
+        let problem_id_prefix = format!("jughisto+{}+{}+", repository.path, problem.path);
+        let problem_id = format!("{}{}", problem_id_prefix, head_commit_shorthand);
+        let package_path = format!("/data/{}", problem_id);
+        let repository_problem_path = format!("/data/{}/{}", repository.path, problem.path);
+        let output = Command::new(format!("/data/{}/blem/blem", repository.path))
+            .args([
+                "make-full-jughisto-package",
+                &package_path,
+                &repository_problem_path,
+            ])
+            .output()
+            .expect("failed to execute process");
+        print!("{}", String::from_utf8_lossy(&output.stdout));
+        print!("{}", String::from_utf8_lossy(&output.stderr));
+        if output.status.success() {
+            println!("Upserting problem");
+            let blem_config = toml::from_str::<BlemConfig>(&fs::read_to_string(format!(
+                "{}/blem.toml",
+                package_path
+            ))?)
+            .unwrap();
+            problem::upsert_problem(
+                &mut connection,
+                NewProblem {
+                    id: problem_id.clone(),
+                    name: problem.name,
+                    memory_limit_bytes: blem_config.memory_limit_kb.unwrap_or(204800) * 1024,
+                    time_limit_ms: blem_config.time_limit_ms,
+                    status: "compiled".into(),
+                    creation_instant: Local::now().naive_utc(),
+                    creation_user_id: logged_user.id,
+                },
+            )?;
+        }
+        if let Some(contest_problem) = contest_problems
+            .iter()
+            .find(|x| x.problem_id.starts_with(&problem_id_prefix))
+        {
+            contest::update_contest_problem(
+                &mut connection,
+                contest_problem.id,
+                problem_id.clone(),
+                problem.label,
+            )?;
+        } else {
+            contest::relate_problem(
+                &mut connection,
+                NewContestProblems {
+                    label: problem.label,
+                    contest_id: contest.id,
+                    problem_id: problem_id.clone(),
+                },
+            )?;
+        }
+    }
+
+    Ok(redirect_to_referer(
+        format!(
+            "Competição do repositório {} atualizada com sucesso!",
+            repository.name
+        ),
+        &request,
+    ))
+}
diff --git a/src/schema.rs b/src/schema.rs
index 11fd8cfba25787620f2447593b32934e8217ded7..f631183b317abbdbab6c9cf8fbf596a25bd57e4d 100644
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -1,3 +1,31 @@
+// @generated automatically by Diesel CLI.
+
+diesel::table! {
+    backup (uuid) {
+        uuid -> Text,
+        filename -> Text,
+        contents -> Bytea,
+        creation_instant -> Timestamp,
+        contest_id -> Int4,
+        user_id -> Int4,
+    }
+}
+
+diesel::table! {
+    clarification (uuid) {
+        uuid -> Text,
+        question -> Text,
+        question_instant -> Timestamp,
+        answer -> Nullable<Text>,
+        answer_instant -> Nullable<Timestamp>,
+        answer_user_id -> Nullable<Int4>,
+        answer_to_all -> Bool,
+        contest_problem_id -> Nullable<Int4>,
+        contest_id -> Int4,
+        user_id -> Int4,
+    }
+}
+
 diesel::table! {
     contest (id) {
         id -> Int4,
@@ -8,6 +36,8 @@ diesel::table! {
         creation_instant -> Timestamp,
         grade_ratio -> Nullable<Int4>,
         grade_after_ratio -> Nullable<Int4>,
+        frozen_scoreboard_instant -> Nullable<Timestamp>,
+        blind_scoreboard_instant -> Nullable<Timestamp>,
     }
 }
 
@@ -17,6 +47,20 @@ diesel::table! {
         label -> Text,
         contest_id -> Int4,
         problem_id -> Text,
+        balloon_color -> Nullable<Text>,
+        problem_name_override -> Nullable<Text>,
+    }
+}
+
+diesel::table! {
+    printing_task (uuid) {
+        uuid -> Text,
+        contents -> Text,
+        creation_instant -> Timestamp,
+        status -> Text,
+        done_instant -> Nullable<Timestamp>,
+        contest_id -> Int4,
+        user_id -> Int4,
     }
 }
 
@@ -26,17 +70,21 @@ diesel::table! {
         name -> Text,
         memory_limit_bytes -> Int4,
         time_limit_ms -> Int4,
-        checker_path -> Text,
-        checker_language -> Text,
-        validator_path -> Text,
-        validator_language -> Text,
-        main_solution_path -> Text,
-        main_solution_language -> Text,
-        test_count -> Int4,
-        test_pattern -> Text,
         status -> Text,
         creation_user_id -> Int4,
         creation_instant -> Timestamp,
+        repository_id -> Nullable<Int4>,
+    }
+}
+
+diesel::table! {
+    repository (id) {
+        id -> Int4,
+        name -> Text,
+        path -> Text,
+        remote_url -> Text,
+        creation_user_id -> Int4,
+        creation_instant -> Timestamp,
     }
 }
 
@@ -56,6 +104,7 @@ diesel::table! {
         contest_problem_id -> Int4,
         user_id -> Int4,
         failed_test -> Nullable<Int4>,
+        balloon_delivered_instant -> Nullable<Timestamp>,
     }
 }
 
@@ -67,14 +116,34 @@ diesel::table! {
         is_admin -> Bool,
         creation_user_id -> Nullable<Int4>,
         creation_instant -> Timestamp,
+        full_name -> Nullable<Text>,
+        single_contest_id -> Nullable<Int4>,
     }
 }
 
-diesel::joinable!(contest -> user (creation_user_id));
+diesel::joinable!(backup -> contest (contest_id));
+diesel::joinable!(backup -> user (user_id));
+diesel::joinable!(clarification -> user (user_id));
+diesel::joinable!(clarification -> contest (contest_id));
+diesel::joinable!(clarification -> contest_problems (contest_problem_id));
 diesel::joinable!(contest_problems -> contest (contest_id));
 diesel::joinable!(contest_problems -> problem (problem_id));
+diesel::joinable!(printing_task -> contest (contest_id));
+diesel::joinable!(printing_task -> user (user_id));
+diesel::joinable!(problem -> repository (repository_id));
 diesel::joinable!(problem -> user (creation_user_id));
+diesel::joinable!(repository -> user (creation_user_id));
 diesel::joinable!(submission -> contest_problems (contest_problem_id));
 diesel::joinable!(submission -> user (user_id));
 
-diesel::allow_tables_to_appear_in_same_query!(contest, contest_problems, problem, submission, user,);
+diesel::allow_tables_to_appear_in_same_query!(
+    backup,
+    clarification,
+    contest,
+    contest_problems,
+    printing_task,
+    problem,
+    repository,
+    submission,
+    user,
+);
diff --git a/src/setup.rs b/src/setup.rs
index e1fba9967a8ae82f4df01b57360b98ee5a96d4d1..a8f62e3225be13a133925b49585de852330b12e8 100644
--- a/src/setup.rs
+++ b/src/setup.rs
@@ -20,6 +20,8 @@ pub fn setup_admin(connection: &mut PgConnection) {
                 connection,
                 NewUser {
                     name: admin_user_name,
+                    full_name: None,
+                    single_contest_id: None,
                     password: admin_user_password,
                     is_admin: true,
                     creation_instant: Local::now().naive_local(),
diff --git a/static/balloon.svg b/static/balloon.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fe055ba0cef9cd720ad6e3ecb67d9f74cd976446
--- /dev/null
+++ b/static/balloon.svg
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg:svg
+   version="1.1"
+   id="balloon"
+   width="29.999998"
+   height="39.999996"
+   viewBox="0 0 29.999998 39.999999"
+   sodipodi:docname="balloon.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <svg:defs
+     id="defs874" />
+  <sodipodi:namedview
+     id="namedview872"
+     pagecolor="#505050"
+     bordercolor="#ffffff"
+     borderopacity="1"
+     inkscape:pageshadow="0"
+     inkscape:pageopacity="0"
+     inkscape:pagecheckerboard="1"
+     showgrid="false"
+     inkscape:zoom="4.9186512"
+     inkscape:cx="24.60024"
+     inkscape:cy="44.626055"
+     inkscape:window-width="1920"
+     inkscape:window-height="1012"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1"
+     inkscape:showpageshadow="2"
+     inkscape:deskcolor="#505050" />
+  <svg:g
+     inkscape:groupmode="layer"
+     id="layer1"
+     inkscape:label="Image 1"
+     style="display:inline"
+     transform="translate(30.979957,5.207267)">
+    <svg:path
+       style="fill:currentColor;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m -29.919637,15.847037 c -1.275765,-4.960284 -1.059588,-16.76613609 8.395563,-19.6674503 9.455142,-2.9013145 15.7609761,1.762701 18.7308504,6.7618205 2.96988324,4.999119 0.5345054,13.7336418 -3.739728,18.0854108 -4.2742364,4.351773 -9.3484174,6.044046 -11.8021434,6.437009 0,0 -0.51726,1.041029 -1.594333,0.96905 0.320106,2.622532 0.0534,1.538237 0.557868,3.195666 -1.101138,0.15878 -0.943229,0.0661 -2.121607,0.03649 -0.457696,1.844678 -1.543352,2.822692 -2.920588,2.597697 -1.377238,-0.225014 -1.759219,-1.071281 -2.091859,-1.966945 -0.332638,-0.895664 -0.263681,-2.254951 -1.406782,-2.46445 -1.143106,-0.209476 -1.458071,1.501213 -2.025272,1.312815 -0.332546,-0.13543 -0.689285,-0.467192 -0.228005,-1.039427 0.461243,-0.572236 1.457956,-1.467951 2.332039,-1.341026 0.874083,0.126931 1.355462,0.625786 1.798958,1.501522 0.443485,0.875738 0.29054,2.194507 1.295268,2.612614 1.004756,0.418116 2.055227,0.383094 2.117569,-1.360056 l -1.059957,-0.429295 c 0,0 1.654275,-2.189494 1.959115,-3.034982 -0.727031,-0.55079 -0.759458,-1.319123 -0.759458,-1.319123 0,0 -6.161685,-5.927087 -7.437461,-10.887378 z"
+       id="path993"
+       sodipodi:nodetypes="zzzzcccczzzczzzzccccz" />
+  </svg:g>
+  <script>{
+            document.currentScript.dataset.injected = true;
+            const o = JSON.parse(decodeURIComponent(escape(atob('eyJ1c2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoWDExOyBMaW51eCB4ODZfNjQ7IHJ2OjEyOC4wKSBHZWNrby8yMDEwMDEwMSBGaXJlZm94LzEyOC4wIiwiYXBwVmVyc2lvbiI6IjUuMCAoWDExKSIsInBsYXRmb3JtIjoiTGludXgiLCJ2ZW5kb3IiOiIiLCJwcm9kdWN0IjoiR2Vja28iLCJ1c2VyQWdlbnREYXRhIjoiW2RlbGV0ZV0iLCJvc2NwdSI6IkxpbnV4IHg4Nl82NCIsInByb2R1Y3RTdWIiOiIyMDEwMDEwMSIsImJ1aWxkSUQiOiIyMDE4MTAwMTAwMDAwMCJ9'))));
+
+            if (o.userAgentDataBuilder) {
+              const v = new class NavigatorUAData {
+                #p;
+
+                constructor({p, ua}) {
+                  this.#p = p;
+
+                  const version = p.browser.major;
+                  const name = p.browser.name === 'Chrome' ? 'Google Chrome' : p.browser.name;
+
+                  this.brands = [{
+                    brand: name,
+                    version
+                  }, {
+                    brand: 'Chromium',
+                    version
+                  }, {
+                    brand: 'Not=A?Brand',
+                    version: '24'
+                  }];
+
+                  this.mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
+
+                  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA-Platform
+                  this.platform = 'Unknown';
+                  if (p.os &amp;&amp; p.os.name) {
+                    const name = p.os.name.toLowerCase();
+                    if (name.includes('mac')) {
+                      this.platform = 'macOS';
+                    }
+                    else if (name.includes('debian')) {
+                      this.platform = 'Linux';
+                    }
+                    else {
+                      this.platform = p.os.name;
+                    }
+                  }
+                }
+                toJSON() {
+                  return {
+                    brands: this.brands,
+                    mobile: this.mobile,
+                    platform: this.platform
+                  };
+                }
+                getHighEntropyValues(hints) {
+                  if (!hints || Array.isArray(hints) === false) {
+                    return Promise.reject(Error(&quot;Failed to execute 'getHighEntropyValues' on 'NavigatorUAData'&quot;));
+                  }
+
+                  const r = this.toJSON();
+
+                  if (hints.includes('architecture')) {
+                    r.architecture = this.#p?.cpu?.architecture || 'x86';
+                  }
+                  if (hints.includes('bitness')) {
+                    r.bitness = '64';
+                  }
+                  if (hints.includes('model')) {
+                    r.model = '';
+                  }
+                  if (hints.includes('platformVersion')) {
+                    r.platformVersion = this.#p?.os?.version || '10.0.0';
+                  }
+                  if (hints.includes('uaFullVersion')) {
+                    r.uaFullVersion = this.brands[0].version;
+                  }
+                  if (hints.includes('fullVersionList')) {
+                    r.fullVersionList = this.brands;
+                  }
+                  return Promise.resolve(r);
+                }
+              }(o.userAgentDataBuilder);
+
+              navigator.__defineGetter__('userAgentData', () =&gt; {
+                return v;
+              });
+            }
+            delete o.userAgentDataBuilder;
+
+            for (const key of Object.keys(o)) {
+              if (o[key] === '[delete]') {
+                delete Object.getPrototypeOf(window.navigator)[key];
+              }
+              else {
+                navigator.__defineGetter__(key, () =&gt; {
+                  if (o[key] === 'empty') {
+                    return '';
+                  }
+                  return o[key];
+                });
+              }
+            }
+          }</script>
+</svg:svg>
diff --git a/static/star.svg b/static/star.svg
new file mode 100644
index 0000000000000000000000000000000000000000..395564deaee2c948b98e4dd749641e0e4ffa0afe
--- /dev/null
+++ b/static/star.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="29.5pt"
+   viewBox="0 -10 30.80912 29.5"
+   width="30.749735pt"
+   version="1.1"
+   id="svg1"
+   sodipodi:docname="star.svg"
+   inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <path
+     d="M 30.355353,1.2332253 C 30.158586,0.62457761 29.618738,0.19228551 28.980047,0.13472361 L 20.304403,-0.65303317 16.87382,-8.6826481 c -0.252953,-0.5884668 -0.829036,-0.9693883 -1.469103,-0.9693883 -0.640067,0 -1.21615,0.3809215 -1.469103,0.9707643 L 10.50503,-0.65303317 1.8280105,0.13472361 C 1.1904664,0.19366161 0.65199408,0.62457761 0.45408022,1.2332253 0.25616636,1.8418735 0.43894425,2.5094599 0.92123026,2.9302848 L 7.4789896,8.6814823 5.545259,17.199574 c -0.1414981,0.626307 0.1015942,1.273713 0.6212614,1.649359 0.2793272,0.201812 0.6061255,0.304553 0.9356767,0.304553 0.2841425,0 0.565992,-0.0766 0.8189454,-0.227956 l 7.4835745,-4.472669 7.480822,4.472669 c 0.547416,0.329322 1.237477,0.299279 1.755998,-0.0766 0.519896,-0.376793 0.762759,-1.024427 0.621262,-1.649359 L 23.329068,8.6814823 29.886827,2.9314314 c 0.482286,-0.4219715 0.66644,-1.0884113 0.468526,-1.6982061 z m 0,0"
+     fill="#ffc107"
+     id="balloon"
+     style="fill:currentColor;stroke:#000000;stroke-width:0.751448;stroke-dasharray:none" />
+</svg>
diff --git a/static/styles.css b/static/styles.css
index 8154b94ad26b41f5451f1be078854b9d445878ea..5c8f14e70fd4207c92957a321e20c34578971e68 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -4,6 +4,85 @@
   box-sizing: border-box;
 }
 
+header.boca {
+  display: block;
+  max-height: unset;
+  min-height: unset;
+  background-color: unset;
+  padding: unset;
+  margin: 8px;
+  line-height: 1.15;
+}
+
+header.boca .table {
+  max-height: 33px;
+  min-height: 33px;
+  border: 1px outset #000;
+  padding: 2px;
+  background-color: white;
+  display: flex;
+  font-size: 20px;
+  gap: 2px;
+}
+
+header.boca .block {
+  border: 1px inset #000;
+  margin: 0;
+  background-color: #35a7ff;
+  max-height: 28px;
+  padding: 0 2px;
+  display: flex;
+}
+
+header.boca h1 {
+  align-items: end;
+}
+
+header.boca h1 a {
+  font-size: 26px;
+}
+
+header.boca h1 sub {
+  font-size: 9px;
+}
+
+header.boca nav {
+  padding: 2px;
+  margin-left: 0;
+  background-color: unset;
+  justify-content: space-around;
+}
+
+header.boca nav a {
+  padding: 4px;
+  color: black;
+  font-family: "PT Sans";
+  text-transform: unset;
+}
+
+header.boca nav button {
+  padding: 4px;
+  margin: 0;
+  background: none;
+  border: none;
+  font-family: "PT Sans";
+  font-weight: 800;
+  font-size: 18px;
+  cursor: pointer;
+}
+
+header.boca nav button:hover, header.boca nav a:hover {
+  border-bottom: 1px solid #555555;
+  border-right: 1px solid #555555;
+  border-top: 1px solid white;
+  border-left: 1px solid white;
+  margin: -1px;
+}
+
+header .block span {
+  margin-right: 4px;
+}
+
 #breadcrumb {
   display: flex;
   align-items: center;
@@ -87,11 +166,13 @@ main {
 .contest, .problem {
   background: #fff;
   box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 2px 1px -1px rgba(0, 0, 0, 0.12);
-  display: block;
-  text-decoration: none;
-  color: black;
   display: flex;
   height: 56px;
+  text-decoration: none;
+}
+
+.problem-info {
+  text-decoration: none;
 }
 
 .contest-info {
@@ -228,6 +309,8 @@ html, body {
   padding: 8px;
   font-size: 14px;
   box-sizing: border-box;
+  height: 36px;
+  animation: vanish 5s forwards;
 }
 
 .flash-Info {
@@ -378,6 +461,23 @@ table {
   border-collapse: collapse;
 }
 
+table.boca {
+  border-collapse: unset;
+}
+
+table.boca .balloon {
+  width: 20px;
+  margin: -4px 0;
+}
+
+table.boca a {
+  display: unset;
+}
+
+table.boca td {
+  padding: 8px;
+}
+
 th {
   padding: 8px;
   text-align: left;
@@ -397,14 +497,15 @@ td {
   min-height: 32px;
   border-top: 1px solid #ddd;
   border-bottom: 1px solid #ddd;
-  padding: 0;
+  padding: 8px;
 }
 
 table a {
   display: block;
-  max-height: 32px;
-  min-height: 32px;
+  max-height: 40px;
+  min-height: 40px;
   padding: 8px;
+  margin: -8px;
   text-decoration: none;
 }
 
@@ -520,3 +621,67 @@ textarea #source-text {
 #logo {
   margin: 16px;
 }
+
+.balloon {
+  margin: 2px;
+}
+
+.boca-info {
+  margin: 8px;
+  margin-top: 0;
+}
+
+.buttons { display: flex; }
+.buttons button { flex: 1; }
+
+form.boca {
+  align-self: center;
+}
+
+label {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+@keyframes vanish {
+    90%  {opacity:1; height: 36px; padding: 8px; }
+    100%  {opacity:0; height: 0; padding: 0; }
+}
+
+#timer {
+  flex: 1;
+  font-size: 64px;
+  font-weight: 500;
+  text-align: center;
+}
+
+.boca-score {
+  font-size: 12px;
+  width: 75px;
+}
+
+.form-balloon-color {
+  height: 44px;
+  display: flex;
+  flex-flow: row;
+  align-self: center;
+}
+
+.center {
+  text-align: center;
+  margin-bottom: 16px;
+}
+
+.table-balloon {
+  width: 20px;
+  margin: -4px 2px;
+}
+
+.pending {
+  background: #ffe140;
+}
+
+.done {
+  background: #97ff40;
+}
diff --git a/templates/base.hbs b/templates/base.hbs
index 033de2665198a63e00e46399a5f1f584d8cf896b..68263d2cd7e0c70f26f62a8435b024520ca6b711 100644
--- a/templates/base.hbs
+++ b/templates/base.hbs
@@ -5,7 +5,7 @@
   <meta charset="utf-8"/>
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <link rel="stylesheet" href="static/styles.css"/>
-  <link rel="stylesheet" data-name="vs/editor/editor.main" href="https://unpkg.com/monaco-editor@0.21.2/min/vs/editor/editor.main.css"/>
+  <!-- <link rel="stylesheet" data-name="vs/editor/editor.main" href="https://unpkg.com/monaco-editor@0.21.2/min/vs/editor/editor.main.css"/> -->
   <script src="static/htmx.min.js"></script>
   <link rel="stylesheet" href="static/flatpickr.min.css">
   <script src="static/flatpickr.min.js"></script>
@@ -15,16 +15,12 @@
   <link rel="manifest" href="static/site.webmanifest">
 </head>
 <body>
-  {{ #unless no_header }}
+  {{ #> header }}
   <header>
     <h1><a href=".">juĝisto</a></h1>
     <nav>
-      <a href=".">Início</a>
       <a href="contests/">Competições</a>
-      <a href="problems/">Problemas</a>
-      <a href="submissions/">Submissões</a>
-      <a href="setting/">Criações</a>
-      <a href="editor">Editor</a>
+      <a href="repositories/">Repositórios</a>
       <a href="about">Sobre</a>
     </nav>
     <div class="span"></div>
@@ -39,7 +35,7 @@
       {{ /if }}
     </div>
   </header>
-  {{ /unless }}
+  {{ /header }}
   {{ #if base.flash_messages }}
   {{ #each base.flash_messages }}
   {{ #each this }}
diff --git a/templates/clarifications.hbs b/templates/clarifications.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..0854c6ccd8234a767ad2cb5c1e1b6afc00a2fc07
--- /dev/null
+++ b/templates/clarifications.hbs
@@ -0,0 +1,83 @@
+{{ #> base title="Clarificações" }}
+  <div id="contest">
+    <div id="breadcrumb">
+      <a href=".">Início</a>
+      /
+      <a href="contests/">Competições</a>
+      /
+      <a href="contests/{{contest.id}}">{{contest.name}}</a>
+      /
+      <a href="contests/{{contest.id}}/clarifications">Clarificações</a>
+    </div>
+
+    <table>
+      <thead>
+        <tr>
+          <th>Usuário</th>
+          <th>Momento da Pergunta</th>
+          <th>Problema</th>
+          <th>Estado</th>
+          <th>Pergunta</th>
+          <th>Momento da Resposta</th>
+          <th>Resposta</th>
+        </tr>
+      </thead>
+      <tbody>
+        {{ #each clarifications }}
+        <tr>
+          <td>
+            {{ this.user_name }} -
+            {{ this.user_full_name }}
+          </td>
+          <td>
+            {{ this.question_instant }}
+          </td>
+          <td>
+            {{ #if this.problem_label }}
+            {{ this.problem_label }}
+            {{ else }}
+            Geral
+            {{ /if }}
+          </td>
+          <td>
+            {{ #if this.answer_instant }}
+              {{ #if this.answer_to_all }}
+              Respondido para todos
+              {{ else }}
+              Respondido
+              {{ /if }}
+            {{ else }}
+              Pendente
+            {{ /if }}
+          </td>
+          <td>
+            <textarea cols="60" rows="8" readonly>{{ this.question }}</textarea>
+          </td>
+          <td>
+            {{ this.answer_instant }}
+          </td>
+          <td>
+            <textarea form="form-answer-{{this.uuid}}" name="answer" cols="60" rows="8">{{ this.answer }}</textarea>
+            <label>
+              Para todos:
+              <input form="form-answer-{{this.uuid}}" {{ #if this.answer_to_all }}checked{{ /if }} type="checkbox" name="answer_to_all">
+            </label>
+            <button form="form-answer-{{this.uuid}}" type="submit">Responder</button>
+          </td>
+        </tr>
+        {{ /each }}
+      </tbody>
+    </table>
+
+    {{ #each clarifications }}
+    <form id="form-answer-{{ this.uuid }}" hx-target="body" hx-patch="clarifications/{{ this.uuid }}"></form>
+    {{ /each }}
+  </div>
+
+  <div id="submissions" hx-sse="connect:submission_updates/">
+    Submissões
+    <div id="submissions-list" hx-get="submissions/me/contests/{{ contest.id }}" hx-trigger="load, sse:update_submission">
+    {{> submissions }}
+    </div>
+  </div>
+{{ /base }}
diff --git a/templates/contest.hbs b/templates/contest.hbs
index b26f20efc405d0997c8a9302c4d18d415fde170d..93fc46b5de2d9359ec99798e2855065902f2a67e 100644
--- a/templates/contest.hbs
+++ b/templates/contest.hbs
@@ -7,6 +7,12 @@
       /
       <a href="contests/{{contest.id}}">{{contest.name}}</a>
       <div class="span"></div>
+      <a href="contests/{{contest.id}}/tasks">
+        Ver Tarefas ►
+      </a>
+      <a href="contests/{{contest.id}}/clarifications">
+        Ver Clarificações ►
+      </a>
       <a href="contests/{{contest.id}}/scoreboard">
         Ver Placar ►
       </a>
@@ -14,22 +20,31 @@
 
     <div id="problems">
       {{ #each problems }}
-      <a href="contests/{{ ../contest.id }}/{{ this.label }}" class="problem">
-        <div class="problem-info">
+      <div class="problem">
+        <a href="contests/{{ ../contest.id }}/{{ this.label }}" class="problem-info">
           <div class="name">
             {{ this.label }} · {{ this.name }}
           </div>
           <div class="extra">
             {{this.time_limit}}s · {{ this.memory_limit_mib }}MiB
           </div>
-        </div>
-        <div class="span"></div>
+        </a>
+        <a href="contests/{{ ../contest.id }}/{{ this.label }}" class="span"></a>
+        {{ #if ../base.logged_user.is_admin }}
+        <form class="form-balloon-color" hx-target="body" hx-patch="contests/{{ ../contest.id }}/problems/{{ this.id }}">
+          {{ #if this.balloon_color }}
+          <svg class="balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg>
+          {{ /if }}
+          <input type="text" name="balloon_color" value="{{ this.balloon_color }}">
+          <button type="submit">Atualizar</button>
+        </form>
+        {{ /if }}
+        <div class="score">
         {{#if this.user_accepted_count}}
-          <div class="score">
             <i class="gg-user"></i>
             <div class="time">x{{this.user_accepted_count}}</div>
-          </div>
         {{/if}}
+        </div>
         {{#if this.first_ac_submission_time}}
           <div class="score accepted">
             <div>
@@ -50,14 +65,71 @@
             <div class="score"></div>
           {{/if}}
         {{/if}}
-      </a>
+      </div>
       {{ /each }}
     </div>
   </div>
 
+  {{#if base.logged_user.is_admin}}
+    <form id="create-user" method="post" action="users/">
+      <strong>Criar usuário da competição</strong>
+
+      <input type="hidden" name="single_contest_id" value="{{ contest.id }}">
+      <label for="name">
+        Nome
+      </label>
+      <input type="text" name="name">
+      <label for="full_name">
+        Nome Completo
+      </label>
+      <input type="text" name="full_name">
+      <label for="password">
+        Senha
+      </label>
+      <input type="password" name="password">
+      <input type="hidden" value="false" name="is_admin">
+      <button type="submit">
+        Criar usuário
+      </button>
+    </form>
+
+    <form id="update-competition" hx-target="body" hx-patch="contests/{{ contest.id }}">
+      <strong>Atualizar competição</strong>
+      <label for="start_instant">
+        Momento do Início
+      </label>
+      <input id="start_instant" type="text" name="start_instant" value="{{ contest.start_instant }}"/>
+
+      <label for="start_instant">
+        Momento do Final
+      </label>
+      <input id="start_instant" type="text" name="end_instant" value="{{ contest.end_instant }}"/>
+
+      <label for="frozen_scoreboard_instant">
+        Momento do Frozen
+      </label>
+      <input id="frozen_scoreboard_instant" type="text" name="frozen_scoreboard_instant" value="{{ contest.frozen_scoreboard_instant }}"/>
+
+      <label for="blind_scoreboard_instant">
+        Momento do Blind
+      </label>
+      <input id="blind_scoreboard_instant" type="text" name="blind_scoreboard_instant" value="{{ contest.blind_scoreboard_instant }}"/>
+
+      <button type="submit">
+        Atualizar competição
+      </button>
+    </form>
+    <script>
+      flatpickr("#start_instant", { enableTime: true, enableSeconds: true, time_24hr: true });
+      flatpickr("#end_instant", { enableTime: true, enableSeconds: true, time_24hr: true });
+      flatpickr("#frozen_scoreboard_instant", { enableTime: true, enableSeconds: true, time_24hr: true });
+      flatpickr("#blind_scoreboard_instant", { enableTime: true, enableSeconds: true, time_24hr: true });
+    </script>
+  {{/if}}
+
   <div id="submissions" hx-sse="connect:submission_updates/">
     Submissões
-    <div id="submissions-list" hx-get="submissions/me/contests/{{ contest.id }}" hx-trigger="load, sse:update_submission">
+    <div id="submissions-list" hx-get="submissions/{{#if (not base.logged_user.is_admin)}}me/{{/if}}contests/{{ contest.id }}" hx-trigger="load, sse:update_submission">
     {{> submissions }}
     </div>
   </div>
diff --git a/templates/contest_problem.hbs b/templates/contest_problem.hbs
index c8f855d329368304896c37d02cb4eb0381806fe2..cec40a9479385fa8977d4d5795d998f3bd69ea14 100644
--- a/templates/contest_problem.hbs
+++ b/templates/contest_problem.hbs
@@ -11,7 +11,7 @@
     </div>
 
     <div id="statement-submission">
-      <iframe src="problems/{{problem.id}}/assets/problem.html" id="statement"></iframe>
+      <iframe src="problems/{{problem.id}}/assets/br.html" id="statement"></iframe>
       <form id="submission" method="post" action="submissions/">
         <input type="hidden" name="contest_problem_id" value="{{ problem.id }}">
 
@@ -26,7 +26,7 @@
 
   <div id="submissions" hx-sse="connect:submission_updates/">
     Submissões
-    <div id="submissions-list" hx-get="submissions/me/contests/{{ contest.id }}/{{ problem.label }}" hx-trigger="load, sse:update_submission">
+    <div id="submissions-list" hx-get="submissions/{{#if (not base.logged_user.is_admin)}}me/{{/if}}contests/{{ contest.id }}/{{ problem.label }}" hx-trigger="load, sse:update_submission">
     {{> submissions }}
     </div>
   </div>
diff --git a/templates/login.hbs b/templates/login.hbs
index 9664e1a8cf278ba175a070e14acfbeba5b7492c9..ef17e637a2a36c4bac565cf8882cad1fa1fc25f7 100644
--- a/templates/login.hbs
+++ b/templates/login.hbs
@@ -1,4 +1,6 @@
-{{ #> base title="Entrar" no_header=true }}
+{{ #*inline "header" }}
+{{ /inline }}
+{{ #> base title="Entrar" }}
   <div id="login-outer">
     <img src="static/logo/logo.svg" id="logo">
     <form type="submit" method="post" action="login" id="login-form">
diff --git a/templates/main.hbs b/templates/main.hbs
index e19e1f908e7a7a1002d398067fa9351d194b5e56..53adccd5fe9aa8bb3bff7a8924d1261ef21b6961 100644
--- a/templates/main.hbs
+++ b/templates/main.hbs
@@ -33,12 +33,10 @@
     {{ /each }}
   </div>
 
-  {{#if base.logged_user.is_admin}}
-    <div id="submissions" hx-sse="connect:submission_updates/">
-      Submissões
-      <div id="submissions-list" hx-get="submissions/" hx-trigger="load, sse:update_submission">
-      {{> submissions }}
-      </div>
+  <div id="submissions" hx-sse="connect:submission_updates/">
+    Submissões
+    <div id="submissions-list" hx-get="submissions/{{#if (not base.logged_user.is_admin)}}me/{{/if}}" hx-trigger="load, sse:update_submission">
+    {{> submissions }}
     </div>
-  {{/if}}
+  </div>
 {{ /base }}
diff --git a/templates/repositories.hbs b/templates/repositories.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..79e4d24b645ecd9f81e2697635b1eb33c1da1d21
--- /dev/null
+++ b/templates/repositories.hbs
@@ -0,0 +1,46 @@
+{{#> base title="Competições"}}
+  <div id="contests">
+    <div id="breadcrumb">
+      <a href=".">Início</a>
+      /
+      <a href="repositories/">Repositórios</a>
+    </div>
+
+    <table>
+      <thead>
+        <tr>
+          <th>Nome</th>
+          <th>URL</th>
+          <th class="right">Momento da Criação</th>
+        </tr>
+      </thead>
+      <tbody>
+        {{ #each repositories }}
+        <tr>
+          <td><a href="repositories/{{ this.id }}">{{ this.name }}</a></td>
+          <td><a href="repositories/{{ this.id }}">{{ this.remote_url }}</a></td>
+          <td class="right"><a href="repositories/{{ this.id }}">{{ this.creation_instant }}</a></td>
+        </tr>
+        {{ /each }}
+      </tbody>
+    </table>
+  </div>
+
+  {{#if base.logged_user.is_admin }}
+    <form id="import-repository-form" hx-target="body" hx-swap="innerHTML" hx-post="repositories/">
+      <label for="name">
+        Nome
+      </label>
+      <input name="name" type="text" />
+
+      <label for="remote_url">
+        URL do Repositório
+      </label>
+      <input name="remote_url" type="text" />
+
+      <button type="submit">
+        Importar Repositório
+      </button>
+    </form>
+  {{/if}}
+{{/base}}
diff --git a/templates/repository.hbs b/templates/repository.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..30f18c50560f13c1ab4175a8d70d463c56937107
--- /dev/null
+++ b/templates/repository.hbs
@@ -0,0 +1,58 @@
+{{#> base title="Competições"}}
+  <div id="contests">
+    <div id="breadcrumb">
+      <a href=".">Início</a>
+      /
+      <a href="repositories/">Repositórios</a>
+      /
+      <a href="repositories/{{ repository.id }}">{{repository.name}}</a>
+    </div>
+
+    <button hx-post="repositories/{{ repository.id }}/sync" hx-trigger="click" hx-target="body" hx-swap="innerHTML">
+      Atualizar repositório
+    </button>
+
+    <h1>{{ contest_name }}</h1>
+
+    <button hx-post="repositories/{{ repository.id }}/contest/sync" hx-trigger="click" hx-target="body" hx-swap="innerHTML">
+      Sincronizar competição
+    </button>
+
+    <table>
+      <thead>
+        <tr>
+          <th>Índice</th>
+          <th>Nome</th>
+          <th>Autores</th>
+          <th>Tempo Limite (ms)</th>
+          <th>Último Commit</th>
+        </tr>
+      </thead>
+      <tbody>
+        {{ #each contest_problems }}
+        <tr>
+          <td>{{ this.label }}</td>
+          <td>
+            <a href="repositories/{{ ../repository.id }}/problems-by-path/{{ this.path }}">
+              {{ this.name }}
+            </a>
+          </td>
+          <td>{{ this.authors }}</td>
+          <td>{{ this.time_limit_ms }}</td>
+          <td>{{ this.last_commit }}</td>
+        </tr>
+        {{ /each }}
+      </tbody>
+    </table>
+
+    <ul>
+      {{ #each problems }}
+      <li>
+        <a href="repositories/{{ ../repository.id }}/problems-by-path/{{ this.path }}">
+          {{ this.name }}
+        </a>
+      </li>
+      {{ /each }}
+    </ul>
+  </div>
+{{/base}}
diff --git a/templates/scoreboard.hbs b/templates/scoreboard.hbs
index f3cb6c9cbda1ce32229c1e1e3618de5398f22225..ef490f2ebfefc604e2dd21c399a04cf7810c9766 100644
--- a/templates/scoreboard.hbs
+++ b/templates/scoreboard.hbs
@@ -1,4 +1,4 @@
-{{ #> base title="Competição" }}
+{{ #> base title="Placar" }}
   <div id="contest">
     <div id="breadcrumb">
       <a href=".">Início</a>
@@ -18,7 +18,7 @@
           {{@index}}
         </div>
         <div class="user">
-          {{this.user_name}}
+          {{this.user_name}} - {{ this.user_full_name }}
         </div>
         <div class="score">
           {{this.solved_count}}
@@ -74,7 +74,7 @@
 
   <div id="submissions" hx-sse="connect:submission_updates/">
     Submissões
-    <div id="submissions-list" hx-get="submissions/me/contests/{{ contest.id }}" hx-trigger="load, sse:update_submission">
+    <div id="submissions-list" hx-get="submissions/{{#if (not base.logged_user.is_admin)}}me/{{/if}}contests/{{ contest.id }}" hx-trigger="load, sse:update_submission">
     {{> submissions }}
     </div>
   </div>
diff --git a/templates/single_backup.hbs b/templates/single_backup.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..bf505bf81b488448b5f0ecb8d54836a55cf30d60
--- /dev/null
+++ b/templates/single_backup.hbs
@@ -0,0 +1,43 @@
+{{ #> single_base title="Tasks" }}
+  <div id="contest">
+    <table class="boca" border="1">
+      <thead>
+        <tr>
+          <th>Bkp #</th>
+          <th>Time</th>
+          <th>File</th>
+          <th>Actions</th>
+        </tr>
+      </thead>
+      <tbody>
+        {{ #each backups }}
+        <tr>
+          <td>
+            {{ this.uuid }}
+          </td>
+          <td>
+            {{ this.creation_instant }}
+          </td>
+          <td>
+            <a href="backups/{{ this.uuid }}/contents" download="{{ this.filename }}">{{ this.filename }}</a>
+          </td>
+        </tr>
+        {{ /each }}
+      </tbody>
+    </table>
+
+    <form class="boca" id="single-contest-create-backup-form" hx-target="body" hx-swap="innerHTML" hx-encoding="multipart/form-data" hx-post="contests/{{ contest.id }}/backups/">
+      <strong>To submit a new backup file, just fill in the following field:</strong>
+
+      <label>
+        File name:
+        <input name="contents" type="file"/>
+      </label>
+
+      <div class="buttons">
+        <button type="submit">Send</button>
+        <button type="reset">Clear</button>
+      </div>
+    </form>
+  </div>
+{{ /single_base }}
diff --git a/templates/single_base.hbs b/templates/single_base.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..0069481d9fbf69dd406faf4dae47b380cb3e860c
--- /dev/null
+++ b/templates/single_base.hbs
@@ -0,0 +1,37 @@
+{{ #*inline "header" }}
+<header class="boca">
+  <div class="table">
+    <h1 class="block">
+      <a href=".">juĝisto</a>
+      <sub>BOCA edition</sub>
+    </h1>
+    <div class="block span">
+      <span>{{ base.logged_user.name }}{{ #if base.logged_user.is_admin }} (admin){{ /if }} - {{ base.logged_user.full_name }}</span>
+      {{ #each submissions }}
+      {{ #if (eq this.verdict "AC") }}
+        {{ #if this.balloon_color }}
+        <svg class="balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg>
+        {{ /if }}
+      {{ /if }}
+      {{ /each }}
+    </div>
+    <div class="block">
+      {{ contest.contest_status }}
+    </div>
+  </div>
+  <nav>
+    <a href="single-contest/{{ contest.id }}/problems">Problems</a>
+    <a href="single-contest/{{ contest.id }}/runs">Runs</a>
+    <a href="single-contest/{{ contest.id }}/score">Score</a>
+    <a href="single-contest/{{ contest.id }}/clarifications">Clarifications</a>
+    <a href="single-contest/{{ contest.id }}/tasks">Tasks</a>
+    <a href="single-contest/{{ contest.id }}/backup">Backup</a>
+    <form id="logout-form" method="post" action="logout">
+      <button type="submit">Logout</button>
+    </form>
+  </nav>
+</header>
+{{ /inline }}
+{{ #> base this }}
+  {{> @partial-block }}
+{{ /base }}
diff --git a/templates/single_clarifications.hbs b/templates/single_clarifications.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..162042871ff40af2371ce34f44033520ce27c0d0
--- /dev/null
+++ b/templates/single_clarifications.hbs
@@ -0,0 +1,75 @@
+{{ #> single_base title="Clarifications" }}
+  <div id="contest">
+    <table class="boca" border="1">
+      <thead>
+        <tr>
+          <th>Time</th>
+          <th>Problem</th>
+          <th>Status</th>
+          <th>Question</th>
+          <th>Answer</th>
+        </tr>
+      </thead>
+      <tbody>
+        {{ #each clarifications }}
+        <tr>
+          <td>
+            {{ this.question_instant }}
+          </td>
+          <td>
+            {{ #if this.problem_label }}
+            {{ this.problem_label }}
+            {{ else }}
+            General
+            {{ /if }}
+          </td>
+          <td>
+            {{ #if this.answer_instant }}
+              {{ #if this.answer_to_all }}
+              answeredall
+              {{ else }}
+              answered
+              {{ /if }}
+            {{ else }}
+              pending
+            {{ /if }}
+          </td>
+          <td>
+            <textarea cols="60" rows="8" readonly>{{ this.question }}</textarea>
+          </td>
+          <td>
+            <textarea cols="60" rows="8" readonly>{{ this.answer }}</textarea>
+          </td>
+        </tr>
+        {{ /each }}
+      </tbody>
+    </table>
+
+    <form class="boca" id="single-contest-create-clarification-form" hx-target="body" hx-swap="innerHTML" hx-post="contests/{{ contest.id }}/clarifications/">
+      <strong>To submit a clarification, just fill in the following fields:</strong>
+
+      <label>
+        Problem:
+        <select id="contest_problem_id" name="contest_problem_id">
+        <option value="">General</option>
+        {{ #each problems }}
+        <option
+        value="{{ this.id }}">
+        {{ this.label }}
+        </option>
+        {{ /each }}
+        </select>
+      </label>
+
+      <label>
+        Clarification:
+        <textarea cols="60" rows="8" name="question"></textarea>
+      </label>
+
+      <div class="buttons">
+        <button type="submit">Send</button>
+        <button type="reset">Clear</button>
+      </div>
+    </form>
+  </div>
+{{ /single_base }}
diff --git a/templates/single_problems.hbs b/templates/single_problems.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..578bc14a51f7477f411bd44872651dec2ad44f28
--- /dev/null
+++ b/templates/single_problems.hbs
@@ -0,0 +1,78 @@
+{{ #> single_base title="Problems" }}
+  <div id="contest">
+    <span class="boca-info">
+      <strong>Information</strong>: <a href="static/prova.pdf">Complete Problem Set</a> <a href="static/info.pdf">Information Sheet</a>
+    </span>
+    <table class="boca" border="1">
+      <thead>
+        <tr>
+          <th>Index</th>
+          <th>Name</th>
+          <th>Time Limit</th>
+          <th>Memory Limit</th>
+          <th>Statement</th>
+          <th>Last Answer</th>
+          <th class="right">Total Solves</th>
+        </tr>
+      </thead>
+      <tbody>
+        {{ #each problems }}
+        <tr>
+          <td>
+            {{ #if this.balloon_color }}
+            <svg class="balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg>
+            {{ /if }}
+            {{ this.label }}
+          </td>
+          <td>
+            {{ this.name }}
+          </td>
+          <td>
+            {{this.time_limit}}s
+          </td>
+          <td>
+            {{ this.memory_limit_mib }}MiB
+          </td>
+          <td>
+            <a href="problems/{{ this.id }}/assets/br.html">View Statement</a> /
+            <a href="problems/{{ this.id }}/assets/br.pdf" download="{{ this.label }}.pdf">Download PDF</a>
+          </td>
+          <td>
+            {{#if this.first_ac_submission_time}}
+              YES
+              {{ #if this.balloon_color }}
+              <svg class="balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg>
+              {{ /if }}
+            {{else}}
+              {{#if this.failed_submissions}}
+                {{ #if (eq this.last_failed_submission_verdict "WJ") }}
+                Not answered yet
+                {{ /if }}
+                {{ #if (eq this.last_failed_submission_verdict "WA") }}
+                NO - WRONG ANSWER
+                {{ /if }}
+                {{ #if (eq this.last_failed_submission_verdict "RE") }}
+                NO - RUNTIME ERROR
+                {{ /if }}
+                {{ #if (eq this.last_failed_submission_verdict "ML") }}
+                NO - MEMORY LIMIT EXCEEDED
+                {{ /if }}
+                {{ #if (eq this.last_failed_submission_verdict "TL") }}
+                NO - TIME LIMIT EXCEEDED
+                {{ /if }}
+                {{ #if (eq this.last_failed_submission_verdict "CE") }}
+                NO - COMPILATION ERROR
+                {{ /if }}
+              {{/if}}
+              <a href="single-contest/{{ ../contest.id }}/runs?contest_problem_id={{ this.id }}">Submit</a>
+            {{/if}}
+          </td>
+          <td class="right">
+            {{this.user_accepted_count}}
+          </td>
+        </tr>
+        {{ /each }}
+      </tbody>
+    </table>
+  </div>
+{{ /single_base }}
diff --git a/templates/single_runs.hbs b/templates/single_runs.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..dc2d61822e00f1d21a9e1575d40a400b3d396c9e
--- /dev/null
+++ b/templates/single_runs.hbs
@@ -0,0 +1,110 @@
+{{ #> single_base title="Runs" }}
+  <div id="contest">
+    <table class="boca" border="1">
+      <thead>
+        <tr>
+          <th>Run #</th>
+          <th>Time</th>
+          <th>Problem</th>
+          <th>Language</th>
+          <th>Answer</th>
+          <th>File</th>
+        </tr>
+      </thead>
+      <tbody>
+        {{ #each submissions }}
+        <tr>
+          <td>
+            {{ this.uuid }}
+          </td>
+          <td>
+            {{ this.submission_instant }}
+          </td>
+          <td>
+            {{ this.problem_label }}
+          </td>
+          <td>
+            {{ this.language }}
+          </td>
+          <td>
+            {{ #if (eq this.verdict "WJ") }}
+            Not answered yet
+            {{ /if }}
+            {{ #if (eq this.verdict "AC") }}
+            YES
+            {{ #if this.balloon_color }}
+            <svg class="balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg>
+            {{ /if }}
+            {{ /if }}
+            {{ #if (eq this.verdict "WA") }}
+            NO - WRONG ANSWER
+            {{ /if }}
+            {{ #if (eq this.verdict "RE") }}
+            NO - RUNTIME ERROR
+            {{ /if }}
+            {{ #if (eq this.verdict "ML") }}
+            NO - MEMORY LIMIT EXCEEDED
+            {{ /if }}
+            {{ #if (eq this.verdict "TL") }}
+            NO - TIME LIMIT EXCEEDED
+            {{ /if }}
+            {{ #if (eq this.verdict "CE") }}
+            NO - COMPILATION ERROR
+            {{ /if }}
+            {{ #if (eq this.verdict "BL") }}
+            HIDDEN VERDICT - BLIND
+            {{ /if }}
+          </td>
+          <td>
+            <a href="submissions/{{ this.uuid }}/source">Download</a>
+          </td>
+        </tr>
+        {{ /each }}
+      </tbody>
+    </table>
+  </div>
+  <form id="single-contest-create-submission-form" hx-target="body" hx-swap="innerHTML" hx-encoding="multipart/form-data" hx-post="contests/{{ contest.id }}/submissions/">
+    <strong>To submit a program, just fill in the following fields:</strong>
+
+    <label>
+      Problem:
+      <select id="contest_problem_id" name="contest_problem_id">
+      {{ #each problems }}
+      <option
+      {{ #if (eq this.id ../contest_problem_id_hint) }}
+      selected
+      {{ /if }}
+      value="{{ this.id }}">
+      {{ this.label }}
+      </option>
+      {{ /each }}
+      </select>
+    </label>
+
+    <label>
+      Language:
+      <select id="language" name="language">
+      {{ #each languages }}
+      <option
+      {{ #if (eq ../language this.value) }}
+      selected
+      {{ /if }}
+      value="{{ this.value }}"
+      >
+      {{ this.name }}
+      </option>
+      {{ /each }}
+      </select>
+    </label>
+
+    <label>
+      Source code:
+      <input name="source_text" type="file"/>
+    </label>
+
+    <div class="buttons">
+      <button type="submit">Send</button>
+      <button type="reset">Clear</button>
+    </div>
+  </form>
+{{ /single_base }}
diff --git a/templates/single_score.hbs b/templates/single_score.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..2b9282ead9ccc287d86528d99a4d370ded18bba1
--- /dev/null
+++ b/templates/single_score.hbs
@@ -0,0 +1,59 @@
+{{ #> single_base title="Score" }}
+  <div id="contest">
+    {{ #if this.frozen }}
+    <div class="center">SCOREBOARD FROZEN</div>
+    {{ /if }}
+    <table class="boca" border="1">
+      {{#each scores}}
+      {{#if this.user_name}}
+      <tr>
+        <td>
+          {{@index}}
+        </td>
+        <td>
+          {{this.user_name}}
+        </td>
+        <td>
+          {{this.user_full_name}}
+        </td>
+        {{#each this.problems}}
+          <td class="boca-score">
+          {{#if this.first_ac_submission_time}}
+            {{ #if this.balloon_color }}
+            {{ #if this.first_solve }}
+            <svg class="table-balloon" viewBox="0 -10 30 30" color="{{ this.balloon_color }}"><use href="/static/star.svg#balloon"/></svg>
+            {{ else }}
+            <svg class="balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg>
+            {{ /if }}
+            {{ /if }}
+            {{this.submission_count}}/{{#if (ne this.first_ac_submission_time "*")}}{{this.first_ac_submission_minutes}}{{/if}}
+          {{else}}
+            {{#if this.failed_submissions}}
+              {{ this.failed_submissions }}/-
+            {{/if}}
+          {{/if}}
+          </td>
+        {{/each}}
+        <td class="right">
+          {{this.solved_count}} ({{ this.penalty }})
+        </td>
+      </tr>
+      {{else}}
+      <thead>
+        <tr>
+          <th>#</th>
+          <th>User</th>
+          <th>Name</th>
+          {{#each this.problems}}
+            <th>{{this.label}}</th>
+          {{/each}}
+          <th class="right">Total</th>
+        </tr>
+      </thead>
+      <tbody>
+      {{/if}}
+      {{/each}}
+      </tbody>
+    </table>
+  </div>
+{{ /single_base }}
diff --git a/templates/single_tasks.hbs b/templates/single_tasks.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..ca2d133b940daa1be6aab1f77b004fd90742a63a
--- /dev/null
+++ b/templates/single_tasks.hbs
@@ -0,0 +1,46 @@
+{{ #> single_base title="Tasks" }}
+  <div id="contest">
+    <table class="boca" border="1">
+      <thead>
+        <tr>
+          <th>Task #</th>
+          <th>Time</th>
+          <th>File</th>
+          <th>Status</th>
+        </tr>
+      </thead>
+      <tbody>
+        {{ #each printing_tasks }}
+        <tr>
+          <td>
+            {{ this.uuid }}
+          </td>
+          <td>
+            {{ this.creation_instant }}
+          </td>
+          <td>
+            <a href="printing-tasks/{{ this.uuid }}/contents">Download</a>
+          </td>
+          <td>
+            {{ this.status }}
+          </td>
+        </tr>
+        {{ /each }}
+      </tbody>
+    </table>
+
+    <form class="boca" id="single-contest-create-printing-task-form" hx-target="body" hx-swap="innerHTML" hx-encoding="multipart/form-data" hx-post="contests/{{ contest.id }}/printing-tasks/">
+      <strong>To submit a file for printing, just fill in the following field:</strong>
+
+      <label>
+        File name:
+        <input name="contents" type="file"/>
+      </label>
+
+      <div class="buttons">
+        <button type="submit">Send</button>
+        <button type="reset">Clear</button>
+      </div>
+    </form>
+  </div>
+{{ /single_base }}
diff --git a/templates/single_waiting.hbs b/templates/single_waiting.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..57c6a0ed138a205a9e05234639433b0ca4e6ac8a
--- /dev/null
+++ b/templates/single_waiting.hbs
@@ -0,0 +1,21 @@
+{{ #> single_base title="Waiting" }}
+<div id="timer"></div>
+<script>
+const timer = document.querySelector('#timer');
+const target = new Date('{{ contest.start_instant }}');
+const MS_PER_SECOND = 1000;
+const MS_PER_MINUTE = MS_PER_SECOND*60;
+const MS_PER_HOUR = MS_PER_MINUTE*60;
+const MS_PER_DAY = MS_PER_HOUR*24;
+function updateTimer() {
+  const diff = Math.max(target - new Date(), 0);
+  if (diff == 0) { location.reload(); }
+  timer.innerHTML = `-${
+    Math.floor(diff/MS_PER_HOUR%24).toString().padStart(2, '0')}:${
+    Math.floor(diff/MS_PER_MINUTE%60).toString().padStart(2, '0')}:${
+    Math.floor(diff/MS_PER_SECOND%60).toString().padStart(2, '0')}`;
+}
+updateTimer();
+setInterval(updateTimer, 1000);
+</script>
+{{ /single_base }}
diff --git a/templates/tasks.hbs b/templates/tasks.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..74173c5ac4435456238ebc278de97d7bfc7e84d1
--- /dev/null
+++ b/templates/tasks.hbs
@@ -0,0 +1,94 @@
+{{ #> base title="Tarefas" }}
+  <div id="contest">
+    <div id="breadcrumb">
+      <a href=".">Início</a>
+      /
+      <a href="contests/">Competições</a>
+      /
+      <a href="contests/{{contest.id}}">{{contest.name}}</a>
+      /
+      <a href="contests/{{contest.id}}/tasks">Tarefas</a>
+    </div>
+
+    <table>
+      <thead>
+        <tr>
+          <th>Momento da Tarefa</th>
+          <th>Usuário</th>
+          <th>Descrição</th>
+          <th>Estado</th>
+          <th>Ações</th>
+        </tr>
+      </thead>
+      <tbody>
+        {{ #each tasks }}
+        <tr class="{{ #if this.done }}done{{ else }}pending{{ /if }}">
+          <td>
+            {{ this.creation_instant }}
+          </td>
+          <td>
+            {{ this.user_name }} -
+            {{ this.user_full_name }}
+          </td>
+          <td>
+            {{ #if (eq this.t "clarification") }}
+            Clarificação:
+            {{ /if }}
+            {{ #if (eq this.t "balloon") }}
+              Balão:
+              {{ #if this.first_solve }}
+              <svg class="table-balloon" viewBox="0 -10 30 30" color="{{ this.balloon_color }}"><use href="/static/star.svg#balloon"/></svg>
+              {{ else }}
+              <svg class="table-balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg>
+              {{ /if }}
+            {{ /if }}
+            {{ #if (eq this.t "printing") }}
+              Impressão:
+            {{ /if }}
+            {{ this.description }}
+          </td>
+          <td>
+            {{ #if this.done }}
+            Pronto
+            {{ else }}
+            Pendente
+            {{ /if }}
+          </td>
+          <td>
+            {{ #if (eq this.t "clarification") }}
+            <a href="contests/{{ ../contest.id }}/clarifications">Ir para Clarificações</a>
+            {{ /if }}
+            {{ #if (eq this.t "balloon") }}
+            <button type="submit" form="form-submission-balloon-delivered-{{ this.uuid }}">Fechar</button>
+            {{ /if }}
+            {{ #if (eq this.t "printing") }}
+            <a href="printing-tasks/{{ this.uuid }}/contents" style="display: inline">Ver Conteúdo</a>
+            {{ #unless this.done }}
+            <button type="submit" form="form-printing-task-done-{{ this.uuid }}">Fechar</button>
+            {{ /unless }}
+            {{ /if }}
+          </td>
+        </tr>
+        {{ /each }}
+      </tbody>
+    </table>
+
+    {{ #each tasks }}
+    {{ #unless this.done }}
+    {{ #if (eq this.t "printing") }}
+    <form id="form-printing-task-done-{{ this.uuid }}" hx-target="body" hx-post="printing-tasks/{{ this.uuid }}/done"></form>
+    {{ /if }}
+    {{ #if (eq this.t "balloon") }}
+    <form id="form-submission-balloon-delivered-{{ this.uuid }}" hx-target="body" hx-post="submissions/{{ this.uuid }}/balloon-delivered"></form>
+    {{ /if }}
+    {{ /unless }}
+    {{ /each }}
+  </div>
+
+  <div id="submissions" hx-sse="connect:submission_updates/">
+    Submissões
+    <div id="submissions-list" hx-get="submissions/contests/{{ contest.id }}" hx-trigger="load, sse:update_submission">
+    {{> submissions }}
+    </div>
+  </div>
+{{ /base }}