diff --git a/server/src/main.rs b/server/src/main.rs index 74cd573..59d447d 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,3 +1,4 @@ +use axum::http::HeaderValue; use axum::{ Router, body::Body, @@ -8,62 +9,116 @@ use axum::{ }; use std::net::SocketAddr; use std::path::PathBuf; -use tokio::fs; +use tokio::{fs, sync::broadcast}; use tower_http::services::ServeDir; use tracing::{info, warn}; #[derive(Clone)] struct AppState { website_root: PathBuf, + shutdown_tx: broadcast::Sender<()>, } async fn git_pull(State(state): State) -> impl IntoResponse { - let output = tokio::process::Command::new("git") - .arg("pull") + let fetch = tokio::process::Command::new("git") + .args(["fetch", "origin"]) .current_dir(&state.website_root) .output() .await; - match output { - Ok(o) => { - let stdout = String::from_utf8_lossy(&o.stdout); - let stderr = String::from_utf8_lossy(&o.stderr); - let status = if o.status.success() { "ok" } else { "error" }; + let reset = tokio::process::Command::new("git") + .args(["reset", "--hard", "origin/main"]) + .current_dir(&state.website_root) + .output() + .await; - info!("git pull stdout: {}", stdout.trim()); - if !stderr.is_empty() { - warn!("git pull stderr: {}", stderr.trim()); + match (fetch, reset) { + (Ok(fetch_out), Ok(reset_out)) => { + let success = fetch_out.status.success() && reset_out.status.success(); + + let fetch_stdout = String::from_utf8_lossy(&fetch_out.stdout); + let fetch_stderr = String::from_utf8_lossy(&fetch_out.stderr); + + let reset_stdout = String::from_utf8_lossy(&reset_out.stdout); + let reset_stderr = String::from_utf8_lossy(&reset_out.stderr); + + if !fetch_stdout.trim().is_empty() { + info!("git fetch stdout: {}", fetch_stdout.trim()); + } + if !fetch_stderr.trim().is_empty() { + warn!("git fetch stderr: {}", fetch_stderr.trim()); } + if !reset_stdout.trim().is_empty() { + info!("git reset stdout: {}", reset_stdout.trim()); + } + if !reset_stderr.trim().is_empty() { + warn!("git reset stderr: {}", reset_stderr.trim()); + } + + let body = format!( + "status: {}\n\n--- fetch ---\nstdout:\n{}\nstderr:\n{}\n\n--- reset ---\nstdout:\n{}\nstderr:\n{}", + if success { "ok" } else { "error" }, + fetch_stdout, + fetch_stderr, + reset_stdout, + reset_stderr + ); + ( - if o.status.success() { + if success { StatusCode::OK } else { StatusCode::INTERNAL_SERVER_ERROR }, + body, + ) + } + + (Err(fetch_err), Ok(_)) => { + warn!("failed to run git fetch: {}", fetch_err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("git fetch failed: {}", fetch_err), + ) + } + + (Ok(_), Err(reset_err)) => { + warn!("failed to run git reset: {}", reset_err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("git reset failed: {}", reset_err), + ) + } + + (Err(fetch_err), Err(reset_err)) => { + warn!("failed to run git fetch: {}", fetch_err); + warn!("failed to run git reset: {}", reset_err); + ( + StatusCode::INTERNAL_SERVER_ERROR, format!( - "status: {}\n\nstdout:\n{}\nstderr:\n{}", - status, stdout, stderr + "both commands failed:\nfetch: {}\nreset: {}", + fetch_err, reset_err ), ) } - Err(e) => { - warn!("failed to spawn git pull: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("failed to run git pull: {}", e), - ) - } } } +async fn shutdown(State(state): State) -> StatusCode { + let _ = state.shutdown_tx.send(()); + StatusCode::OK +} + #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); let website_root = PathBuf::from("../website"); + let (shutdown_tx, _) = broadcast::channel(1); let state = AppState { website_root: website_root.clone(), + shutdown_tx: shutdown_tx.clone(), }; let app = Router::new() @@ -74,26 +129,35 @@ async fn main() { .route("/api/teams.json", get(serve_teams)) .route("/api/posts.json", get(serve_posts)) .route("/api/post/{file}", get(serve_post_md)) - .route("/api/update", get(git_pull)) + .route("/api/update", get(git_pull)) // I know this is risky + .route("/api/shutdown", get(shutdown)) // once propper auth & workflows exist this should be removed .route("/directs/{name}", get(serve_direct)) .nest_service("/assets", ServeDir::new(website_root.join("assets"))) .nest_service("/posts", ServeDir::new(website_root.join("posts"))) .fallback(get(serve_static)) .with_state(state.clone()); - // HTTP on port 80 - let http_app = app.clone(); + let mut shutdown_rx = shutdown_tx.subscribe(); let http_handle = tokio::spawn(async move { let addr = SocketAddr::from(([0, 0, 0, 0], 1200)); info!("HTTP server listening on {}", addr); + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - axum::serve(listener, http_app).await.unwrap(); + + axum::serve(listener, app) + .with_graceful_shutdown(async move { + let _ = shutdown_rx.recv().await; + info!("Shutdown signal received"); + }) + .await + .unwrap(); }); http_handle.await.unwrap(); } async fn serve_index(State(state): State) -> impl IntoResponse { + info!("Serving index.html"); serve_file(state.website_root.join("index.html")).await } @@ -101,22 +165,27 @@ async fn serve_post_page( State(state): State, Path(_file): Path, ) -> impl IntoResponse { + info!("Serving post.html"); serve_file(state.website_root.join("post.html")).await } async fn serve_backdrops(State(state): State) -> impl IntoResponse { + info!("Serving backdrops.json"); serve_file(state.website_root.join("api/backdrops.json")).await } async fn serve_staff(State(state): State) -> impl IntoResponse { + info!("Serving staff.json"); serve_file(state.website_root.join("api/staff.json")).await } async fn serve_teams(State(state): State) -> impl IntoResponse { + info!("Serving teams.json"); serve_file(state.website_root.join("api/teams.json")).await } async fn serve_posts(State(state): State) -> impl IntoResponse { + info!("Serving posts.json"); serve_file(state.website_root.join("api/posts.json")).await } @@ -124,6 +193,7 @@ async fn serve_post_md( State(state): State, Path(file): Path, ) -> impl IntoResponse { + info!("Serving {}", file); let path = state.website_root.join("posts").join(&file); if path.exists() { let content = fs::read_to_string(path).await.unwrap_or_default(); @@ -139,6 +209,7 @@ async fn serve_post_md( async fn serve_static(uri: Uri, State(state): State) -> impl IntoResponse { let path = uri.path().trim_start_matches('/'); let file_path = state.website_root.join(path); + info!("Serving static file, {}", path); if file_path.exists() && file_path.is_file() { serve_file(file_path).await @@ -175,12 +246,11 @@ async fn serve_file(path: PathBuf) -> Response { .unwrap() } -use axum::http::HeaderValue; - async fn serve_direct( State(state): State, Path(name): Path, ) -> impl IntoResponse { + info!("Serving redirect {}", name); if name.contains('/') || name.contains('\\') || name.starts_with('.') { return StatusCode::BAD_REQUEST.into_response(); } diff --git a/website/api/backdrops.json b/website/api/backdrops.json index 072de6e..1aada53 100644 --- a/website/api/backdrops.json +++ b/website/api/backdrops.json @@ -1,7 +1,7 @@ [ { "name": "closingin", - "destructive": false + "destructive": true }, { "name": "horse_fog", @@ -21,13 +21,11 @@ }, { "name": "outpost", - "destructive": true, - "time": 8 + "destructive": false }, { "name": "tower_small", - "destructive": true, - "time": 8 + "destructive": false }, { "name": "tower_large", @@ -38,11 +36,11 @@ "destructive": false }, { - "name": "mountain_horse", + "name": "armoury", "destructive": false }, { - "name": "armoury", + "name": "mountain_horse", "destructive": false }, { @@ -71,6 +69,6 @@ }, { "name": "secondsbeforedisaster", - "destructive": true + "destructive": false } ] diff --git a/website/app.js b/website/app.js index 5061362..128a136 100644 --- a/website/app.js +++ b/website/app.js @@ -35,7 +35,7 @@ function initParallax() { update(); } -// ─── Hero Slideshow with Destructive Theme ─── +// ─── Hero Slideshow ─── let currentBackdrop = null; const NEAR_TOP_THRESHOLD = 1800; @@ -63,8 +63,19 @@ function initHeroSlideshow(allBackdrops, usedNames) { let idx = 0; - function showSlide(index) { - const backdrop = slides[index]; + function pickRandomSlide(exclude) { + if (slides.length === 1) return slides[0]; + + let pick; + do { + pick = slides[Math.floor(Math.random() * slides.length)]; + } while (pick.name === exclude?.name); + + return pick; + } + + function showSlide() { + const backdrop = pickRandomSlide(currentBackdrop); currentBackdrop = backdrop; layer.style.opacity = "0"; @@ -76,18 +87,17 @@ function initHeroSlideshow(allBackdrops, usedNames) { }, 600); const delayMs = (backdrop.time || 6) * 1000; - idx = (index + 1) % slides.length; - setTimeout(() => showSlide(idx), delayMs); + setTimeout(showSlide, delayMs); } - currentBackdrop = slides[0]; - layer.style.backgroundImage = `url('/assets/${slides[0].name}.png')`; + const first = pickRandomSlide(); + currentBackdrop = first; + + layer.style.backgroundImage = `url('/assets/${first.name}.png')`; layer.style.opacity = "1"; checkDestructiveTheme(); - idx = 1; - const initialDelay = (slides[0].time || 6) * 1000; - setTimeout(() => showSlide(idx), initialDelay); + setTimeout(showSlide, (first.time || 6) * 1000); } // ─── Load Backdrops ─── @@ -124,7 +134,7 @@ async function loadBackdrops() { } } -// ─── Load Staff (in Socials section) ─── +// ─── Load Staff ─── async function loadStaff() { try { const response = await fetch("/api/staff.json");