From 0184576a6c71f6b2516fbbfd9b7f27033ae326da Mon Sep 17 00:00:00 2001 From: Steven Ewald Date: Fri, 4 Oct 2024 11:30:45 -0500 Subject: [PATCH] Checkpoint 5 --- docker/dev/docker-compose.yml | 2 +- exchange/CMakeLists.txt | 1 + exchange/docker/dev/grafana_data/grafana.db | Bin 1363968 -> 1363968 bytes exchange/docker/linter/LinterDockerfile | 34 ++++++------ exchange/docker/sandbox/Dockerfile | 5 +- .../src/common/compilation/compile_cpp.cpp | 45 ++++++++++++++++ .../src/common/compilation/compile_cpp.hpp | 13 +++++ .../types/algorithm/local_algorithm.cpp | 35 +------------ .../types/algorithm/local_algorithm.hpp | 1 - .../types/algorithm/remote_algorithm.hpp | 25 ++++++++- exchange/src/exchange/sandbox_server/crow.cpp | 2 +- .../src/linter/runtime/cpp/cpp_runtime.cpp | 49 +++++++++--------- .../src/linter/runtime/cpp/cpp_runtime.hpp | 35 ++++++++----- .../linter/runtime/python/python_runtime.cpp | 16 +++--- .../linter/runtime/python/python_runtime.hpp | 8 +-- exchange/src/linter/runtime/runtime.hpp | 12 ++--- exchange/src/linter/spawner/main.cpp | 32 +++++++++--- exchange/src/linter/spawning/spawning.cpp | 5 +- .../api/protected/db/user/createAlgo/route.ts | 10 +++- .../createUser/generateApplicationEmail.ts | 5 +- web/app/dash/submissions/[id]/page.tsx | 2 +- web/lib/s3.ts | 2 +- webserver/src/main.rs | 2 +- 23 files changed, 214 insertions(+), 127 deletions(-) create mode 100644 exchange/src/common/compilation/compile_cpp.cpp create mode 100644 exchange/src/common/compilation/compile_cpp.hpp diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index 35413c17..d0e7cea8 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -28,7 +28,7 @@ services: restart: unless-stopped build: context: ../.. - dockerfile: linter/LinterDockerfile + dockerfile: exchange/docker/linter/LinterDockerfile args: firebase_emulator: "false" diff --git a/exchange/CMakeLists.txt b/exchange/CMakeLists.txt index 4b123828..1f56a311 100644 --- a/exchange/CMakeLists.txt +++ b/exchange/CMakeLists.txt @@ -313,6 +313,7 @@ add_library(COMMON_lib OBJECT src/common/types/decimal.cpp src/common/types/algorithm/local_algorithm.cpp src/common/logging/logging.cpp + src/common/compilation/compile_cpp.cpp ) target_include_directories( diff --git a/exchange/docker/dev/grafana_data/grafana.db b/exchange/docker/dev/grafana_data/grafana.db index d89a7d5437c48ebeec93b742542f37ac61601f27..01fcdcb0f3983230eed944b571397c2a8ac5236e 100644 GIT binary patch delta 644 zcmbu6OKTHR6vyv8>da)Exk;OfwU)G1C@3WJxbwKhLclK|bRh^OcOG{lZD}C3f~!WM zQ@V?EF=QdcMLvK=oP`U;t>8vC#f_Wnl=%W`yxoaA@f?1f#s8ec|8RQ4&E9aczsTiJ zuiobJ7i3i9wO^;w@(L^C8_S=y*>Xj=#y_ClhqT+;?L<4^=(Jpq>Utl@O1HB+R~2q@ z2TX4T2mAM1`<)%Sr^78!;r^I896zt$HDwd88^*eYpOBPrDdAHhq(n@~OiC`iCX&=s z1b7CwVIRI4SDN==wp~aoGvC`_W0Lg?siw$nfE@t9YWAj(u1IaLG!HH>WoL!7I$v#r zg=Zxu3UCDAS2%+2;R##w2#SjW=CikjbcqK?*=Q;Sr3ug``WJPe4p_&Dnh#X@4u1%g z#~RCpHAQ40hqf1*2C{+(M;5gr*RZi`BNX_i9s54^u|*?_+z7>{?|O#q(EtV5GH4hg zhngt1qad_=-+{m|Mgkvl5%6{TI2{w}0_B?7iX7+JQ zzn%?sxxo(K_|OKo749GawDvVjiTrQI8kIrwZ;;DgbK;*+w4wH0S|fcNYt zhb?-F7X19;I(@u1g&kT0OFF%A3WL`MBQ*t zg_Yd3jL4pdcsF8QHha@(8vt4B$X&P5b3gVov7e3oT?|nrX79@BKci;$q zm{nT$VY0JOR`cI^`ru+RS}653g{QJ7Nz#ov%S!~v{gCy-f&O^=VUG^Bqdk2<`(2%< zZ7n%oC|563`JdU{qA29xRDwU?6n=s)#p$PT<#LV)!_(?4nWhRYo=D{r^;>CDnux-S z(tJ_^_p;9pqOv9x4$5LK-2_0EW#CedS%^EB+t@Q)W_Xkufn(apq1ZA_93aLlY7#fV z79oxo8ki&7wyB8%lTqY#I6)?1p=WeDm@tevcO4sd?7$%mxj|qNkK2?}UNh~`@`&Yl zlm`T33n9-*K5wemlh-O}%Hmab=3i9sQ)``7Yo6A@N7(uwyVK+j( +#include + +#include +#include + +namespace nutc::common { + +// TODO: shouldnt return filepath as string +std::string compile_cpp(const std::filesystem::path& filepath); +} // namespace nutc::common diff --git a/exchange/src/common/types/algorithm/local_algorithm.cpp b/exchange/src/common/types/algorithm/local_algorithm.cpp index cffd6296..22eb5d52 100644 --- a/exchange/src/common/types/algorithm/local_algorithm.cpp +++ b/exchange/src/common/types/algorithm/local_algorithm.cpp @@ -1,6 +1,7 @@ #include "local_algorithm.hpp" #include "base_algorithm.hpp" +#include "common/compilation/compile_cpp.hpp" #include "common/file_operations/file_operations.hpp" #include @@ -10,38 +11,6 @@ #include namespace nutc::common { -namespace { -std::string -get_cpp_template_path() -{ - static const char* template_path_env = std::getenv("NUTC_CPP_TEMPLATE_PATH"); - if (template_path_env == nullptr) - throw std::runtime_error("Template.cpp path not set, unable to compile cpp"); - return template_path_env; -} -} // namespace - -std::string -LocalAlgorithm::compile_cpp(const std::filesystem::path& filepath) -{ - std::string binary_output = (boost::filesystem::temp_directory_path() - / boost::filesystem::unique_path("%%%%-%%%%-%%%%.tmp")) - .string(); - - std::string command = fmt::format( - "g++ -std=c++20 -fPIC -shared -o {} -include {} {}", binary_output, - filepath.string(), get_cpp_template_path() - ); - - int result = system(command.c_str()); - - if (result != 0) { - throw std::runtime_error( - fmt::format("Compilation of {} failed", filepath.string()) - ); - } - return binary_output; -} LocalAlgorithm::LocalAlgorithm(AlgoLanguage language, std::filesystem::path filepath) : BaseAlgorithm{language}, filepath_{std::move(filepath)} @@ -57,7 +26,7 @@ std::string LocalAlgorithm::get_algo_string() const { if (get_language() == AlgoLanguage::cpp) { - return common::read_file_content(compile_cpp(filepath_)); + return common::read_file_content(common::compile_cpp(filepath_)); } if (get_language() == AlgoLanguage::python) { return common::read_file_content(filepath_); diff --git a/exchange/src/common/types/algorithm/local_algorithm.hpp b/exchange/src/common/types/algorithm/local_algorithm.hpp index d1770916..d828d30e 100644 --- a/exchange/src/common/types/algorithm/local_algorithm.hpp +++ b/exchange/src/common/types/algorithm/local_algorithm.hpp @@ -17,6 +17,5 @@ class LocalAlgorithm : public BaseAlgorithm { std::string get_id() const; private: - static std::string compile_cpp(const std::filesystem::path& filepath); }; } // namespace nutc::common diff --git a/exchange/src/common/types/algorithm/remote_algorithm.hpp b/exchange/src/common/types/algorithm/remote_algorithm.hpp index 1cfeee8e..bc37d0bf 100644 --- a/exchange/src/common/types/algorithm/remote_algorithm.hpp +++ b/exchange/src/common/types/algorithm/remote_algorithm.hpp @@ -1,6 +1,12 @@ #pragma once #include "base_algorithm.hpp" +#include "common/compilation/compile_cpp.hpp" +#include "common/file_operations/file_operations.hpp" + +#include + +#include namespace nutc::common { // TODO @@ -17,7 +23,24 @@ class RemoteAlgorithm : public BaseAlgorithm { std::string get_algo_string() const { - return algo_data_; + if (get_language() == AlgoLanguage::cpp) { + // TODO: clean up + std::string binary_output = + (boost::filesystem::temp_directory_path() + / boost::filesystem::unique_path("%%%%-%%%%-%%%%.tmp")) + .string(); + + std::ofstream algo_file(binary_output); + algo_file << algo_data_ << std::flush; + algo_file.close(); + + return common::read_file_content(common::compile_cpp(binary_output)); + } + if (get_language() == AlgoLanguage::python) { + return algo_data_; + } + + throw std::runtime_error("Unknown algo language"); } std::string diff --git a/exchange/src/exchange/sandbox_server/crow.cpp b/exchange/src/exchange/sandbox_server/crow.cpp index 5ab337f2..349945e5 100644 --- a/exchange/src/exchange/sandbox_server/crow.cpp +++ b/exchange/src/exchange/sandbox_server/crow.cpp @@ -1,9 +1,9 @@ #include "crow.hpp" +#include "common/logging/logging.hpp" #include "common/messages_exchange_to_wrapper.hpp" #include "common/types/algorithm/base_algorithm.hpp" #include "exchange/config/dynamic/config.hpp" -#include "common/logging/logging.hpp" #include "exchange/traders/trader_types/algo_trader.hpp" #include diff --git a/exchange/src/linter/runtime/cpp/cpp_runtime.cpp b/exchange/src/linter/runtime/cpp/cpp_runtime.cpp index 4981db38..fdca5cdb 100644 --- a/exchange/src/linter/runtime/cpp/cpp_runtime.cpp +++ b/exchange/src/linter/runtime/cpp/cpp_runtime.cpp @@ -1,5 +1,8 @@ #include "cpp_runtime.hpp" +#include "common/compilation/compile_cpp.hpp" +#include "common/file_operations/file_operations.hpp" + #include #include #include @@ -12,6 +15,7 @@ #include #include +#include #include namespace { @@ -51,11 +55,16 @@ namespace nutc::lint { CppRuntime::CppRuntime( std::string algo, LimitOrderFunction limit_order, MarketOrderFunction market_order, CancelOrderFunction cancel_order -) : Runtime(std::move(algo), limit_order, market_order, cancel_order) +) : + Runtime( + std::move(algo), std::move(limit_order), std::move(market_order), + std::move(cancel_order) + ) {} CppRuntime::~CppRuntime() { + // TODO: shoudl not do dlclose(dl_handle_); close(fd_); } @@ -63,15 +72,17 @@ CppRuntime::~CppRuntime() std::optional CppRuntime::init() { - auto [fd, path] = get_temp_file(); - - fd_ = fd; + boost::filesystem::path temp_dir = boost::filesystem::temp_directory_path(); + boost::filesystem::path temp_file = + temp_dir / boost::filesystem::unique_path("tempfile-%%%%-%%%%"); - std::ofstream algo_file(path); + std::ofstream algo_file(temp_file.string()); algo_file << algo_ << std::flush; algo_file.close(); - dl_handle_ = dlopen(path.c_str(), RTLD_NOW); + std::string compiled_binary_path = common::compile_cpp(temp_file.string()); + + dl_handle_ = dlopen(compiled_binary_path.c_str(), RTLD_NOW); if (dl_handle_ == nullptr) { std::string err = dlerror(); close(fd_); @@ -87,8 +98,8 @@ CppRuntime::init() on_account_update_func_ = reinterpret_cast(dlsym(dl_handle_, "on_account_update")); - if (!init_func || !on_trade_update_func_ || !on_orderbook_update_func_ - || !on_account_update_func_) { + if (init_func == nullptr || on_trade_update_func_ == nullptr + || on_orderbook_update_func_ == nullptr || on_account_update_func_ == nullptr) { dlclose(dl_handle_); close(fd_); return fmt::format("[linter] failed to dynamically load functions"); @@ -101,36 +112,26 @@ CppRuntime::init() void CppRuntime::fire_on_trade_update( - common::Ticker ticker, common::Side side, double price, double quantity + common::Ticker ticker, common::Side side, float price, float quantity ) const { - on_trade_update_func_( - strategy_object_, ticker, side, static_cast(quantity), - static_cast(price) - ); + on_trade_update_func_(strategy_object_, ticker, side, quantity, price); } void CppRuntime::fire_on_orderbook_update( - common::Ticker ticker, common::Side side, double price, double quantity + common::Ticker ticker, common::Side side, float price, float quantity ) const { - on_orderbook_update_func_( - strategy_object_, ticker, side, static_cast(quantity), - static_cast(price) - ); + on_orderbook_update_func_(strategy_object_, ticker, side, quantity, price); } void CppRuntime::fire_on_account_update( - common::Ticker ticker, common::Side side, double price, double quantity, - double capital + common::Ticker ticker, common::Side side, float price, float quantity, float capital ) const { - on_account_update_func_( - strategy_object_, ticker, side, static_cast(quantity), - static_cast(price), static_cast(capital) - ); + on_account_update_func_(strategy_object_, ticker, side, quantity, price, capital); } } // namespace nutc::lint diff --git a/exchange/src/linter/runtime/cpp/cpp_runtime.hpp b/exchange/src/linter/runtime/cpp/cpp_runtime.hpp index 94f65ffd..b410a8ce 100644 --- a/exchange/src/linter/runtime/cpp/cpp_runtime.hpp +++ b/exchange/src/linter/runtime/cpp/cpp_runtime.hpp @@ -7,8 +7,10 @@ namespace nutc::lint { class CppRuntime : public Runtime { public: CppRuntime( - std::string algo, LimitOrderFunction limit_order, - MarketOrderFunction market_order, CancelOrderFunction cancel_order + std::string algo, + LimitOrderFunction limit_order, + MarketOrderFunction market_order, + CancelOrderFunction cancel_order ); ~CppRuntime() override; @@ -16,27 +18,32 @@ class CppRuntime : public Runtime { std::optional init() override; void fire_on_trade_update( - common::Ticker ticker, common::Side side, double price, double quantity - ) const override; + common::Ticker ticker, common::Side side, float price, float quantity + ) const override; void fire_on_orderbook_update( - common::Ticker ticker, common::Side side, double price, double quantity - ) const override; + common::Ticker ticker, common::Side side, float price, float quantity + ) const override; void fire_on_account_update( - common::Ticker ticker, common::Side side, double price, double quantity, - double capital - ) const override; - + common::Ticker ticker, + common::Side side, + float price, + float quantity, + float capital + ) const override; private: + using Strategy = void; - using InitFunc = Strategy* (*)(MarketOrderFunction, LimitOrderFunction, + using InitFunc = Strategy* (*)(MarketOrderFunction, + LimitOrderFunction, CancelOrderFunction); using OnTradeUpdateFunc = - void (*)(Strategy*, common::Ticker, common::Side, double, double); + void (*)(Strategy*, common::Ticker, common::Side, float, float); using OnOrderBookUpdateFunc = OnTradeUpdateFunc; - using OnAccountUpdateFunc = - void (*)(Strategy*, common::Ticker, common::Side, double, double, double); + using OnAccountUpdateFunc = void (*)( + Strategy*, common::Ticker, common::Side, float, float, float + ); OnTradeUpdateFunc on_trade_update_func_; OnOrderBookUpdateFunc on_orderbook_update_func_; diff --git a/exchange/src/linter/runtime/python/python_runtime.cpp b/exchange/src/linter/runtime/python/python_runtime.cpp index 4e678610..df86f36e 100644 --- a/exchange/src/linter/runtime/python/python_runtime.cpp +++ b/exchange/src/linter/runtime/python/python_runtime.cpp @@ -31,33 +31,33 @@ PyRuntime::init() void PyRuntime::fire_on_trade_update( - common::Ticker ticker, common::Side side, double price, double quantity + common::Ticker ticker, common::Side side, float price, float quantity ) const { py::globals()["strategy"].attr("on_trade_update")( - ticker, side, static_cast(quantity), static_cast(price) + ticker, side, static_cast(quantity), static_cast(price) ); } void PyRuntime::fire_on_orderbook_update( - common::Ticker ticker, common::Side side, double price, double quantity + common::Ticker ticker, common::Side side, float price, float quantity ) const { py::globals()["strategy"].attr("on_orderbook_update")( - ticker, side, static_cast(quantity), static_cast(price) + ticker, side, static_cast(quantity), static_cast(price) ); } void PyRuntime::fire_on_account_update( - common::Ticker ticker, common::Side side, double price, double quantity, - double capital + common::Ticker ticker, common::Side side, float price, float quantity, + float capital ) const { py::globals()["strategy"].attr("on_account_update")( - ticker, side, static_cast(quantity), static_cast(price), - static_cast(capital) + ticker, side, static_cast(quantity), static_cast(price), + static_cast(capital) ); } diff --git a/exchange/src/linter/runtime/python/python_runtime.hpp b/exchange/src/linter/runtime/python/python_runtime.hpp index ff8221fd..4abad44a 100644 --- a/exchange/src/linter/runtime/python/python_runtime.hpp +++ b/exchange/src/linter/runtime/python/python_runtime.hpp @@ -25,16 +25,16 @@ class PyRuntime : public Runtime { std::optional init() override; void fire_on_trade_update( - common::Ticker ticker, common::Side side, double price, double quantity + common::Ticker ticker, common::Side side, float price, float quantity ) const override; void fire_on_orderbook_update( - common::Ticker ticker, common::Side side, double price, double quantity + common::Ticker ticker, common::Side side, float price, float quantity ) const override; void fire_on_account_update( - common::Ticker ticker, common::Side side, double price, double quantity, - double capital + common::Ticker ticker, common::Side side, float price, float quantity, + float capital ) const override; private: diff --git a/exchange/src/linter/runtime/runtime.hpp b/exchange/src/linter/runtime/runtime.hpp index f5f67e80..1c2df439 100644 --- a/exchange/src/linter/runtime/runtime.hpp +++ b/exchange/src/linter/runtime/runtime.hpp @@ -12,10 +12,10 @@ namespace nutc::lint { using LimitOrderFunction = std::function; using MarketOrderFunction = - std::function; + std::function; using CancelOrderFunction = std::function; @@ -39,14 +39,14 @@ class Runtime { virtual std::optional init() = 0; virtual void fire_on_trade_update( - common::Ticker ticker, common::Side side, double price, double quantity + common::Ticker ticker, common::Side side, float price, float quantity ) const = 0; virtual void fire_on_orderbook_update( - common::Ticker ticker, common::Side side, double price, double quantity + common::Ticker ticker, common::Side side, float price, float quantity ) const = 0; virtual void fire_on_account_update( - common::Ticker ticker, common::Side side, double price, double quantity, - double buyer_capital + common::Ticker ticker, common::Side side, float price, float quantity, + float buyer_capital ) const = 0; protected: diff --git a/exchange/src/linter/spawner/main.cpp b/exchange/src/linter/spawner/main.cpp index 07d5badf..8866ba07 100644 --- a/exchange/src/linter/spawner/main.cpp +++ b/exchange/src/linter/spawner/main.cpp @@ -1,3 +1,4 @@ +#include "common/types/decimal.hpp" #include "common/types/ticker.hpp" #include "common/util.hpp" #include "linter/lint/lint.hpp" @@ -18,7 +19,7 @@ namespace { bool -mock_limit_func(nutc::common::Side, nutc::common::Ticker, float, float, bool) +mock_limit_func(nutc::common::Side, nutc::common::Ticker, float, float, bool = false) { return true; } @@ -44,11 +45,14 @@ main(int argc, char* argv[]) return 1; } - std::string algo_code; - std::string line; - while (std::getline(std::cin, line)) { - algo_code += line + '\n'; - } + std::cerr << "TESTING" << std::endl; + + std::string algo_code_base64; + std::getline(std::cin, algo_code_base64); + std::cerr << "gotline" << std::endl; + + std::string algo_code = nutc::common::base64_decode(algo_code_base64); + std::cerr << "decoded\n" << algo_code << std::endl; nutc::lint::lint_result lint_result; std::string flag = argv[1]; @@ -59,17 +63,31 @@ main(int argc, char* argv[]) lint_result = nutc::lint::lint(runtime); } else if (flag == "-cpp") { + std::cerr << "cpprun" << std::endl; nutc::lint::CppRuntime runtime( algo_code, mock_limit_func, mock_market_func, mock_cancel_func ); + std::cerr << "cpplint" << std::endl; lint_result = nutc::lint::lint(runtime); + std::cerr << "lint output" << std::endl << lint_result.message << std::endl; + std::cerr << "cpplintdone" << std::endl; } else { std::cout << "[linter] no language provided\n"; return 1; } - std::cout << *glz::write_json(lint_result) << "\n"; + auto output = glz::write_json(lint_result); + if (output) { + std::cerr << "output" << *output << std::endl; + std::cout << *output << std::endl; + } + else { + std::cerr << "error" << std::endl; + std::cout << fmt::format( + "[linter] ERROR WRITING LINT RESULT: {}", glz::format_error(output.error()) + ) << std::endl; + } return 0; } diff --git a/exchange/src/linter/spawning/spawning.cpp b/exchange/src/linter/spawning/spawning.cpp index 7a37d4e9..bcb7c02e 100644 --- a/exchange/src/linter/spawning/spawning.cpp +++ b/exchange/src/linter/spawning/spawning.cpp @@ -1,5 +1,6 @@ #include "spawning.hpp" +#include "common/util.hpp" #include "linter/config.h" #include @@ -47,10 +48,10 @@ LintProcessManager::spawn_client(const std::string& algo_code, AlgoLanguage lang auto child = std::make_shared( bp::exe(path), bp::args(get_language_flag(language)), - bp::std_in * in_pipe, io_context + bp::std_in stderr, bp::std_out > *in_pipe, io_context ); - out_pipe << algo_code << std::flush; + out_pipe << common::base64_encode(algo_code) << std::endl; out_pipe.pipe().close(); auto kill_timer = std::make_shared(io_context); diff --git a/web/app/api/protected/db/user/createAlgo/route.ts b/web/app/api/protected/db/user/createAlgo/route.ts index de93c646..b0b528a6 100644 --- a/web/app/api/protected/db/user/createAlgo/route.ts +++ b/web/app/api/protected/db/user/createAlgo/route.ts @@ -15,6 +15,9 @@ export async function POST(req: Request) { ) { return new Response("Not all fields in algo added", { status: 402 }); } + if (algo.language == "C++") { + algo.language = "Cpp"; + } const session = await getSession(); if (!session?.user.sub) { @@ -57,8 +60,11 @@ export async function POST(req: Request) { if (!submission_response.ok) { console.log("Failed to lint/sandbox"); } - console.log(JSON.stringify(await submission_response.json())); - return submission_response; + const resp = await submission_response.text(); + console.log(resp); + return NextResponse.json({ + message: "Linter response: " + resp + }, { status: 200 }); } catch (error) { console.log(error); return NextResponse.json({ message: error }, { status: 500 }); diff --git a/web/app/api/protected/db/user/createUser/generateApplicationEmail.ts b/web/app/api/protected/db/user/createUser/generateApplicationEmail.ts index 6f8be4dc..47182576 100644 --- a/web/app/api/protected/db/user/createUser/generateApplicationEmail.ts +++ b/web/app/api/protected/db/user/createUser/generateApplicationEmail.ts @@ -22,9 +22,8 @@ export async function GenerateApplicationEmail(user: User, profile: Profile) { const link = process.env.AUTH0_BASE_URL + `/api/handleReview?token=${token}`; const accept_link = link + "&accept=true"; const deny_link = link + "&accept=false"; - const resume_link = `${process.env.S3_ENDPOINT}/nutc/${ - s3Key?.Resume?.at(-1)?.s3Key - }`; + const resume_link = `${process.env.EXTERNAL_S3_ENDPOINT}/nutc/${s3Key?.Resume?.at(-1)?.s3Key + }`; const mailOptions = { from: "contact@nutc.io", diff --git a/web/app/dash/submissions/[id]/page.tsx b/web/app/dash/submissions/[id]/page.tsx index 5f3f7dc2..cd722744 100644 --- a/web/app/dash/submissions/[id]/page.tsx +++ b/web/app/dash/submissions/[id]/page.tsx @@ -59,7 +59,7 @@ export default async function SubmissionPage(props: { Download Submission