+ DEMO MODE - Limited to 6 simulations with auto-rotation. Manual deletion disabled.
Rust Backend
SoccerCloud Web Control Room
diff --git a/src/main.rs b/src/main.rs
index 2ecef92..b8962b4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -35,6 +35,9 @@ struct Cli {
#[arg(long, global = true)]
listen_open: bool,
+ #[arg(long, global = true)]
+ demo: bool,
+
#[command(subcommand)]
command: Option,
}
@@ -93,7 +96,14 @@ fn main() -> io::Result<()> {
"--web cannot be combined with subcommands",
));
}
- return run_web_server(base_seed, cli.speed, cli.listen_open);
+ return run_web_server(base_seed, cli.speed, cli.listen_open, cli.demo);
+ }
+
+ if cli.demo {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "--demo can only be used with --web",
+ ));
}
match cli.command {
diff --git a/src/web.rs b/src/web.rs
index f42a236..22a322e 100644
--- a/src/web.rs
+++ b/src/web.rs
@@ -23,15 +23,17 @@ struct SharedState {
struct WebState {
base_seed: u64,
speed: Speed,
+ demo: bool,
next_id: usize,
instances: Vec,
}
impl WebState {
- fn new(base_seed: u64, speed: Speed) -> Self {
+ fn new(base_seed: u64, speed: Speed, demo: bool) -> Self {
Self {
base_seed,
speed,
+ demo,
next_id: 0,
instances: Vec::new(),
}
@@ -118,6 +120,11 @@ struct CreateSimulationResponse {
id: usize,
}
+#[derive(Debug, Serialize)]
+struct ConfigResponse {
+ demo: bool,
+}
+
fn sim_type_label(sim_type: SimulationType) -> &'static str {
match sim_type {
SimulationType::Single => "Single Match",
@@ -275,6 +282,18 @@ async fn sc_logo_jpg() -> impl Responder {
.body(include_bytes!("../sc-logo.jpg").as_slice())
}
+async fn api_config(state: web::Data) -> impl Responder {
+ let guard = match state.inner.lock() {
+ Ok(g) => g,
+ Err(_) => {
+ return HttpResponse::InternalServerError().json(ErrorDto {
+ error: "state lock poisoned".to_string(),
+ })
+ }
+ };
+ HttpResponse::Ok().json(ConfigResponse { demo: guard.demo })
+}
+
async fn api_teams() -> impl Responder {
let items: Vec = TEAMS
.iter()
@@ -340,6 +359,13 @@ async fn api_create_simulation(
});
};
+ // Demo mode: FIFO rotation - remove oldest if at limit
+ const DEMO_MAX_INSTANCES: usize = 6;
+ if guard.demo && guard.instances.len() >= DEMO_MAX_INSTANCES {
+ // Remove the oldest simulation (first in the vec)
+ guard.instances.remove(0);
+ }
+
let id = guard.next_id;
let seed = guard.next_seed();
let auto_fill = payload.auto_fill.unwrap_or(true);
@@ -399,6 +425,13 @@ async fn api_clone_simulation(
});
};
+ // Demo mode: FIFO rotation - remove oldest if at limit
+ const DEMO_MAX_INSTANCES: usize = 6;
+ if guard.demo && guard.instances.len() >= DEMO_MAX_INSTANCES {
+ // Remove the oldest simulation (first in the vec)
+ guard.instances.remove(0);
+ }
+
let new_id = guard.next_id;
let new_seed = guard.next_seed();
let clone = existing.clone_as(new_id, new_seed);
@@ -422,6 +455,13 @@ async fn api_delete_simulation(
}
};
+ // Disable manual deletion in demo mode
+ if guard.demo {
+ return HttpResponse::Forbidden().json(ErrorDto {
+ error: "Manual deletion is disabled in demo mode".to_string(),
+ });
+ }
+
if guard.remove_simulation(id) {
return HttpResponse::NoContent().finish();
}
@@ -463,9 +503,9 @@ async fn api_export_csv(path: web::Path, state: web::Data) -
.body(csv)
}
-pub fn run_web_server(base_seed: u64, speed: Speed, listen_open: bool) -> io::Result<()> {
+pub fn run_web_server(base_seed: u64, speed: Speed, listen_open: bool, demo: bool) -> io::Result<()> {
let shared = SharedState {
- inner: Arc::new(Mutex::new(WebState::new(base_seed, speed))),
+ inner: Arc::new(Mutex::new(WebState::new(base_seed, speed, demo))),
};
let ticker = shared.clone();
@@ -506,6 +546,7 @@ pub fn run_web_server(base_seed: u64, speed: Speed, listen_open: bool) -> io::Re
.route("/sc-logo.jpg", web::get().to(sc_logo_jpg))
.service(
web::scope("/api")
+ .route("/config", web::get().to(api_config))
.route("/teams", web::get().to(api_teams))
.route("/simulations", web::get().to(api_list_simulations))
.route("/simulations", web::post().to(api_create_simulation))