This commit is contained in:
Asger Juul Brunshøj 2025-01-25 12:56:55 +01:00
parent 09d09159f2
commit b49c6d475b
16 changed files with 333 additions and 152 deletions

89
Cargo.lock generated
View File

@ -107,6 +107,7 @@ dependencies = [
"axum", "axum",
"camino", "camino",
"clap", "clap",
"confik",
"console_error_panic_hook", "console_error_panic_hook",
"derive_more", "derive_more",
"http 1.2.0", "http 1.2.0",
@ -130,6 +131,7 @@ dependencies = [
"type-toppings", "type-toppings",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
"xdg",
] ]
[[package]] [[package]]
@ -314,6 +316,9 @@ name = "camino"
version = "1.1.9" version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -433,6 +438,33 @@ dependencies = [
"toml", "toml",
] ]
[[package]]
name = "confik"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb80c5315aacb4f2a7ef6ddb6b2fe7818191e78098f5ac597372647b9958039"
dependencies = [
"camino",
"cfg-if",
"confik-macros",
"envious",
"serde",
"thiserror 1.0.69",
"toml",
]
[[package]]
name = "confik-macros"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "370f641e4f578df8c52e7c37300d5d9d2b12be6fb6f4732b62a794e2b647d1a2"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "console_error_panic_hook" name = "console_error_panic_hook"
version = "0.1.7" version = "0.1.7"
@ -490,6 +522,41 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "darling"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.1.0" version = "6.1.0"
@ -577,6 +644,16 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "envious"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e52788a407588138195a40c991f500621fea2cffa87e7345d86dbab77287dc7"
dependencies = [
"serde",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -1215,6 +1292,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "1.0.3" version = "1.0.3"
@ -3077,6 +3160,12 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "xdg"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
[[package]] [[package]]
name = "xxhash-rust" name = "xxhash-rust"
version = "0.8.15" version = "0.8.15"

View File

@ -3,4 +3,3 @@
<p align="center"> <p align="center">
<img src="docs/ascend.jpg" alt="Logo" width="100%"> <img src="docs/ascend.jpg" alt="Logo" width="100%">
</p> </p>

View File

@ -34,6 +34,8 @@ ron = { version = "0.8" }
rand = { version = "0.8", optional = true } rand = { version = "0.8", optional = true }
web-sys = { version = "0.3.76", features = ["File", "FileList"] } web-sys = { version = "0.3.76", features = ["File", "FileList"] }
smart-default = "0.7.1" smart-default = "0.7.1"
confik = { version = "0.12", optional = true, features = ["camino"] }
xdg = { version = "2.5", optional = true }
[dev-dependencies.serde_json] [dev-dependencies.serde_json]
version = "1" version = "1"
@ -47,6 +49,8 @@ ssr = [
"dep:tower", "dep:tower",
"dep:tower-http", "dep:tower-http",
"dep:leptos_axum", "dep:leptos_axum",
"dep:confik",
"dep:xdg",
"dep:camino", "dep:camino",
"dep:moonboard-parser", "dep:moonboard-parser",
"leptos/ssr", "leptos/ssr",

View File

@ -0,0 +1,9 @@
use leptos::prelude::*;
use web_sys::MouseEvent;
#[component]
pub fn Button(#[prop(into)] text: String, onclick: impl Fn(MouseEvent) -> () + 'static) -> impl IntoView {
view! {
<button on:click=onclick type="button" class="text-black bg-orange-300 hover:bg-orange-400 focus:ring-4 focus:ring-orange-500 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none">{ text }</button>
}
}

View File

@ -14,7 +14,10 @@ pub struct HeaderItem {
/// Header with background color etc. /// Header with background color etc.
#[component] #[component]
pub fn StyledHeader(items: HeaderItems) -> impl IntoView { pub fn StyledHeader(items: HeaderItems) -> impl IntoView {
view! { let fancy = false;
if fancy {
view! {
<div class="flex"> <div class="flex">
// Left gradient chunk // Left gradient chunk
<div class="flex-grow"> <div class="flex-grow">
@ -53,6 +56,17 @@ pub fn StyledHeader(items: HeaderItems) -> impl IntoView {
/> />
</div> </div>
</div> </div>
}
.into_any()
} else {
view! {
<div class="bg-orange-300 text-black border-b-2 border-b-orange-400">
<div class="container mx-auto" >
<Header items />
</div>
</div>
}
.into_any()
} }
} }
@ -84,11 +98,7 @@ pub fn Header(items: HeaderItems) -> impl IntoView {
#[component] #[component]
fn Items(items: Vec<HeaderItem>) -> impl IntoView { fn Items(items: Vec<HeaderItem>) -> impl IntoView {
let items = items.into_iter().map(|item| view! { <Item item /> }).collect_view(); let items = items.into_iter().map(|item| view! { <Item item /> }).collect_view();
view! { view! { <div class="flex gap-4">{items}</div> }
<div class="flex gap-4">
{ items }
</div>
}
} }
#[component] #[component]

View File

@ -5,6 +5,7 @@ pub mod pages {
pub mod wall; pub mod wall;
} }
pub mod components { pub mod components {
pub mod button;
pub mod header; pub mod header;
} }

View File

@ -6,7 +6,6 @@ use leptos::prelude::*;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::ops::Deref; use std::ops::Deref;
use std::path::PathBuf;
#[component] #[component]
pub fn Routes() -> impl leptos::IntoView { pub fn Routes() -> impl leptos::IntoView {
@ -44,11 +43,15 @@ pub fn Routes() -> impl leptos::IntoView {
fn Ready(data: InitialData) -> impl leptos::IntoView { fn Ready(data: InitialData) -> impl leptos::IntoView {
tracing::debug!("ready"); tracing::debug!("ready");
// let import_from_mini_moonboard = Action::from(ServerAction::<ImportFromMiniMoonboard>::new()); let import_from_mini_moonboard = Action::from(ServerAction::<ImportFromMiniMoonboard>::new());
let onclick = move |_mouse_event| {
import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard {});
};
view! { view! {
// <p>"Import problems from"</p> <p>"Import problems from"</p>
// <button on:click=import_from_mini_moonboard>"Mini Moonboard"</button> <button on:click=onclick>"Mini Moonboard"</button>
} }
} }
@ -60,26 +63,24 @@ async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
// use crate::server::state::State; // use crate::server::state::State;
// let state = expect_context::<State>(); // let state = expect_context::<State>();
// TODO: provide info on current routes set
Ok(RonCodec::new(InitialData {})) Ok(RonCodec::new(InitialData {}))
} }
#[server(name = ImportFromMiniMoonboard)] #[server(name = ImportFromMiniMoonboard)]
#[tracing::instrument] #[tracing::instrument]
async fn import_from_mini_moonboard() -> Result<(), ServerFnError> { async fn import_from_mini_moonboard() -> Result<(), ServerFnError> {
use crate::server::config::Config;
use crate::server::state::State;
tracing::info!("Importing mini moonboard problems"); tracing::info!("Importing mini moonboard problems");
let file_path: PathBuf = todo!(); let config = expect_context::<Config>();
let problems = crate::server::import_mini_moonboard_problems(&file_path).await?;
use crate::server::state::State;
let state = expect_context::<State>(); let state = expect_context::<State>();
state
.persistent
.update(|s| {
s.problems.problems.extend(problems);
})
.await?;
crate::server::operations::import_mini_moonboard_problems(&config, &state).await?;
// TODO: Return information about what was done
Ok(()) Ok(())
} }

View File

@ -1,4 +1,5 @@
use crate::codec::ron::RonCodec; use crate::codec::ron::RonCodec;
use crate::components::button::Button;
use crate::components::header::HeaderItem; use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems; use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader; use crate::components::header::StyledHeader;
@ -20,7 +21,7 @@ pub fn Wall() -> impl leptos::IntoView {
let header_items = HeaderItems { let header_items = HeaderItems {
left: vec![], left: vec![],
middle: vec![HeaderItem { middle: vec![HeaderItem {
text: "Ascend".to_string(), text: "ASCEND".to_string(),
link: None, link: None,
}], }],
right: vec![ right: vec![
@ -37,9 +38,9 @@ pub fn Wall() -> impl leptos::IntoView {
leptos::view! { leptos::view! {
<div class="min-w-screen min-h-screen bg-slate-900"> <div class="min-w-screen min-h-screen bg-slate-900">
<StyledHeader items=header_items /> <StyledHeader items=header_items />
<div class="container mx-auto mt-2"> <div class="mx-auto m-2">
<Await future=load let:data> <Await future=load let:data>
<Ready data=data.deref().to_owned() /> <Ready data=data.deref().to_owned() />
</Await> </Await>
@ -74,8 +75,14 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", data.wall.rows, data.wall.cols); let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", data.wall.rows, data.wall.cols);
view! { view! {
<div class=move || { grid_classes.clone() }>{cells}</div> <div class="grid grid-cols sm:grid-cols-2 gap-8 grid-cols-[auto,1fr]">
<button on:click=move |_| problem_fetcher.mark_dirty()>"Random problem"</button> // Render the wall
<div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }>{cells}</div>
<div>
<Button onclick=move |_| problem_fetcher.mark_dirty() text="Next problem ➤" />
</ div>
</div>
} }
} }

View File

@ -1,80 +1,24 @@
//! Server-only features //! Server-only features
use crate::models; use cli::Cli;
use models::HoldPosition; use config::Config;
use models::HoldRole; use confik::Configuration;
use models::Problem; use confik::EnvSource;
use persistence::Persistent; use persistence::Persistent;
use state::PersistentState; use state::PersistentState;
use state::State; use state::State;
use std::collections::BTreeMap;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use type_toppings::ResultExt; use type_toppings::ResultExt;
pub mod cli { mod cli;
//! Server CLI interface pub mod config;
mod migrations;
use std::path::PathBuf; pub mod operations;
#[derive(clap::Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(clap::Subcommand, Default)]
pub enum Command {
#[default]
Serve,
/// Resets state, replacing it with defaults
ResetState,
ImportMiniMoonboardProblems {
file_path: PathBuf,
},
}
}
pub mod state {
//! Server state
const STATE_VERSION: u64 = 1;
use super::persistence::Persistent;
use crate::models;
use crate::models::Wall;
use serde::Deserialize;
use serde::Serialize;
use smart_default::SmartDefault;
use std::collections::BTreeSet;
#[derive(Clone, Debug)]
pub struct State {
pub persistent: Persistent<PersistentState>,
}
#[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)]
pub struct PersistentState {
/// State schema version
#[default(STATE_VERSION)]
pub version: u64,
pub wall: Wall,
pub problems: Problems,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct Problems {
pub problems: BTreeSet<models::Problem>,
}
}
pub mod persistence; pub mod persistence;
pub mod state;
pub const STATE_FILE: &str = "datastore/private/state.ron"; pub const STATE_FILE: &str = "datastore/private/state.ron";
@ -96,19 +40,9 @@ pub async fn main() {
.init(); .init();
let cli = Cli::parse(); let cli = Cli::parse();
match cli.command { match cli.command {
Command::Serve => serve().await.unwrap_or_report(), Command::Serve => serve(cli).await.unwrap_or_report(),
Command::ImportMiniMoonboardProblems { file_path } => {
let problems = import_mini_moonboard_problems(&file_path).await.unwrap_or_report();
let state = load_state().await.unwrap_or_report();
state
.persistent
.update(|s| {
s.problems.problems.extend(problems);
})
.await
.unwrap_or_report();
}
Command::ResetState => { Command::ResetState => {
let s = PersistentState::default(); let s = PersistentState::default();
let p = Path::new(STATE_FILE); let p = Path::new(STATE_FILE);
@ -118,8 +52,8 @@ pub async fn main() {
} }
} }
#[tracing::instrument(err)] #[tracing::instrument(skip(cli), err)]
async fn serve() -> Result<(), Error> { async fn serve(cli: Cli) -> Result<(), Error> {
use crate::app::App; use crate::app::App;
use crate::app::shell; use crate::app::shell;
use axum::Router; use axum::Router;
@ -127,25 +61,34 @@ async fn serve() -> Result<(), Error> {
use leptos_axum::LeptosRoutes; use leptos_axum::LeptosRoutes;
use leptos_axum::generate_route_list; use leptos_axum::generate_route_list;
run_migrations().await.map_err(self::Error::Migration)?; migrations::run_migrations().await;
// Setting get_configuration(None) means we'll be using cargo-leptos's env values // Setting get_configuration(None) means we'll be using cargo-leptos's env values
// For deployment these variables are: // For deployment these variables are:
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain> // <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
// Alternately a file can be specified such as Some("Cargo.toml") // Alternately a file can be specified such as Some("Cargo.toml")
// The file would need to be included with the executable when moved to deployment // The file would need to be included with the executable when moved to deployment
let conf = get_configuration(None).unwrap_or_report(); let leptos_conf_file = get_configuration(None).unwrap_or_report();
let leptos_options = conf.leptos_options; let leptos_options = leptos_conf_file.leptos_options;
let addr = leptos_options.site_addr; let addr = leptos_options.site_addr;
let routes = generate_route_list(App); let routes = generate_route_list(App);
let config = load_config(cli)?;
let server_state = load_state().await?; let server_state = load_state().await?;
let app = Router::new() let app = Router::new()
.leptos_routes_with_context(&leptos_options, routes, move || provide_context(server_state.clone()), { .leptos_routes_with_context(
let leptos_options = leptos_options.clone(); &leptos_options,
move || shell(leptos_options.clone()) routes,
}) move || {
provide_context(server_state.clone());
provide_context(config.clone())
},
{
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
},
)
.nest_service("/files", file_service("datastore/public")) .nest_service("/files", file_service("datastore/public"))
.fallback(leptos_axum::file_and_error_handler(shell)) .fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options); .with_state(leptos_options);
@ -179,46 +122,17 @@ async fn load_state() -> Result<State, Error> {
Ok(State { persistent }) Ok(State { persistent })
} }
#[tracing::instrument] fn load_config(cli: Cli) -> Result<Config, Error> {
pub(crate) async fn import_mini_moonboard_problems(file_path: &Path) -> Result<Vec<Problem>, Error> { let mut builder = config::Config::builder();
let mut problems = Vec::new(); if cli
.config
tracing::info!("Parsing mini moonboard problems from {}", file_path.display()); .try_exists()
let mini_moonboard = moonboard_parser::mini_moonboard::parse(file_path).await?; .expect_or_report_with(|| format!("Failed to look up config file at {}", cli.config))
for problem in mini_moonboard.problems {
let mut holds = BTreeMap::<HoldPosition, HoldRole>::new();
for mv in problem.moves {
let row = mv.description.row();
let col = mv.description.column();
let hold_position = HoldPosition { row, col };
let role = match (mv.is_start, mv.is_end) {
(true, true) => unreachable!(),
(true, false) => HoldRole::Start,
(false, true) => HoldRole::End,
(false, false) => HoldRole::Normal,
};
holds.insert(hold_position, role);
}
let route = Problem { holds };
problems.push(route);
}
Ok(problems)
}
async fn run_migrations() -> Result<(), Box<dyn std::error::Error>> {
// State file moved to datastore/private
{ {
let m = PathBuf::from("state.ron"); builder.override_with(confik::FileSource::new(cli.config));
if m.try_exists()? {
tracing::warn!("MIGRATING STATE FILE");
let p = PathBuf::from(STATE_FILE);
tokio::fs::create_dir_all(p.parent().unwrap()).await?;
tokio::fs::rename(m, &p).await?;
}
} }
let config = builder.override_with(EnvSource::new().allow_secrets()).try_build()?;
Ok(()) Ok(config)
} }
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
@ -230,4 +144,6 @@ pub enum Error {
#[display("Failed migration")] #[display("Failed migration")]
Migration(Box<dyn std::error::Error>), Migration(Box<dyn std::error::Error>),
Confik(confik::Error),
} }

View File

@ -0,0 +1,33 @@
//! Server CLI interface
#[derive(clap::Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
/// Path to configuration file.
#[arg(long, default_value_t = default_config_location())]
pub config: camino::Utf8PathBuf,
}
#[derive(clap::Subcommand, Default)]
pub enum Command {
#[default]
Serve,
/// Resets state, replacing it with defaults
ResetState,
}
fn default_config_location() -> camino::Utf8PathBuf {
let xdg_dirs = xdg::BaseDirectories::with_prefix("ascend").unwrap();
let config_path = xdg_dirs.get_config_file("config.toml");
camino::Utf8PathBuf::from_path_buf(config_path).unwrap()
}
#[cfg(test)]
#[test]
fn verify_cli() {
<Cli as clap::CommandFactory>::command().debug_assert()
}

View File

@ -0,0 +1,7 @@
use camino::Utf8PathBuf;
#[derive(Clone, Debug, confik::Configuration)]
pub struct Config {
/// The location of the moonboard problems directory.
pub moonboard_problems: Utf8PathBuf,
}

View File

@ -0,0 +1,20 @@
use crate::server::STATE_FILE;
use std::path::PathBuf;
use type_toppings::ResultExt;
#[tracing::instrument]
pub async fn run_migrations() {
migrate_state_file().await;
}
/// State file moved to datastore/private
#[tracing::instrument]
async fn migrate_state_file() {
let m = PathBuf::from("state.ron");
if m.try_exists().expect_or_report_with(|| format!("Failed to read {}", m.display())) {
tracing::warn!("MIGRATING STATE FILE");
let p = PathBuf::from(STATE_FILE);
tokio::fs::create_dir_all(p.parent().unwrap()).await.unwrap_or_report();
tokio::fs::rename(m, &p).await.unwrap_or_report();
}
}

View File

@ -0,0 +1,54 @@
//! Server lib module to host re-usable server operations.
use crate::models::HoldPosition;
use crate::models::HoldRole;
use crate::models::Problem;
use crate::server::config::Config;
use crate::server::persistence;
use crate::server::state::State;
use std::collections::BTreeMap;
#[tracing::instrument(skip(state))]
pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &State) -> Result<(), Error> {
let mut problems = Vec::new();
let file_name = "problems Mini MoonBoard 2020 40.json";
let file_path = config.moonboard_problems.join(file_name);
tracing::info!("Parsing mini moonboard problems from {file_path}");
let mini_moonboard = moonboard_parser::mini_moonboard::parse(file_path.as_std_path()).await?;
for problem in mini_moonboard.problems {
let mut holds = BTreeMap::<HoldPosition, HoldRole>::new();
for mv in problem.moves {
let row = mv.description.row();
let col = mv.description.column();
let hold_position = HoldPosition { row, col };
let role = match (mv.is_start, mv.is_end) {
(true, true) => unreachable!(),
(true, false) => HoldRole::Start,
(false, true) => HoldRole::End,
(false, false) => HoldRole::Normal,
};
holds.insert(hold_position, role);
}
let route = Problem { holds };
problems.push(route);
}
state
.persistent
.update(|s| {
s.problems.problems.extend(problems);
})
.await?;
Ok(())
}
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
pub enum Error {
Parser(moonboard_parser::Error),
Persistence(persistence::Error),
}

View File

@ -0,0 +1,31 @@
//! Server state
const STATE_VERSION: u64 = 1;
use super::persistence::Persistent;
use crate::models;
use crate::models::Wall;
use serde::Deserialize;
use serde::Serialize;
use smart_default::SmartDefault;
use std::collections::BTreeSet;
#[derive(Clone, Debug)]
pub struct State {
pub persistent: Persistent<PersistentState>,
}
#[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)]
pub struct PersistentState {
/// State schema version
#[default(STATE_VERSION)]
pub version: u64,
pub wall: Wall,
pub problems: Problems,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct Problems {
pub problems: BTreeSet<models::Problem>,
}

View File

@ -148,6 +148,7 @@
]; ];
env.RUST_LOG = "info,ascend=trace"; env.RUST_LOG = "info,ascend=trace";
env.MOONBOARD_PROBLEMS = "moonboard-problems";
}; };
}; };
} }

View File

@ -25,7 +25,6 @@ run-release:
reset-state: reset-state:
cargo run --features ssr -- reset-state cargo run --features ssr -- reset-state
cargo run --features ssr -- import-mini-moonboard-problems "moonboard-problems/problems Mini MoonBoard 2020 40.json"
# Open firewall port for development # Open firewall port for development
open-firewall: open-firewall: