[Fix] clean & true randomness
This commit is contained in:
parent
353f75bf5d
commit
89feb1bc32
3 changed files with 122 additions and 44 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
use axum::http::HeaderValue;
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
body::Body,
|
body::Body,
|
||||||
|
|
@ -8,53 +9,105 @@ use axum::{
|
||||||
};
|
};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tokio::fs;
|
use tokio::{fs, sync::broadcast};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
website_root: PathBuf,
|
website_root: PathBuf,
|
||||||
|
shutdown_tx: broadcast::Sender<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn git_pull(State(state): State<AppState>) -> impl IntoResponse {
|
async fn git_pull(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
let output = tokio::process::Command::new("git")
|
let fetch = tokio::process::Command::new("git")
|
||||||
.arg("pull")
|
.args(["fetch", "origin"])
|
||||||
.current_dir(&state.website_root)
|
.current_dir(&state.website_root)
|
||||||
.output()
|
.output()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match output {
|
let reset = tokio::process::Command::new("git")
|
||||||
Ok(o) => {
|
.args(["reset", "--hard", "origin/main"])
|
||||||
let stdout = String::from_utf8_lossy(&o.stdout);
|
.current_dir(&state.website_root)
|
||||||
let stderr = String::from_utf8_lossy(&o.stderr);
|
.output()
|
||||||
let status = if o.status.success() { "ok" } else { "error" };
|
.await;
|
||||||
|
|
||||||
info!("git pull stdout: {}", stdout.trim());
|
match (fetch, reset) {
|
||||||
if !stderr.is_empty() {
|
(Ok(fetch_out), Ok(reset_out)) => {
|
||||||
warn!("git pull stderr: {}", stderr.trim());
|
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
|
StatusCode::OK
|
||||||
} else {
|
} else {
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
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!(
|
format!(
|
||||||
"status: {}\n\nstdout:\n{}\nstderr:\n{}",
|
"both commands failed:\nfetch: {}\nreset: {}",
|
||||||
status, stdout, stderr
|
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<AppState>) -> StatusCode {
|
||||||
|
let _ = state.shutdown_tx.send(());
|
||||||
|
StatusCode::OK
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -62,8 +115,10 @@ async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let website_root = PathBuf::from("../website");
|
let website_root = PathBuf::from("../website");
|
||||||
|
let (shutdown_tx, _) = broadcast::channel(1);
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
website_root: website_root.clone(),
|
website_root: website_root.clone(),
|
||||||
|
shutdown_tx: shutdown_tx.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
|
@ -74,26 +129,35 @@ async fn main() {
|
||||||
.route("/api/teams.json", get(serve_teams))
|
.route("/api/teams.json", get(serve_teams))
|
||||||
.route("/api/posts.json", get(serve_posts))
|
.route("/api/posts.json", get(serve_posts))
|
||||||
.route("/api/post/{file}", get(serve_post_md))
|
.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))
|
.route("/directs/{name}", get(serve_direct))
|
||||||
.nest_service("/assets", ServeDir::new(website_root.join("assets")))
|
.nest_service("/assets", ServeDir::new(website_root.join("assets")))
|
||||||
.nest_service("/posts", ServeDir::new(website_root.join("posts")))
|
.nest_service("/posts", ServeDir::new(website_root.join("posts")))
|
||||||
.fallback(get(serve_static))
|
.fallback(get(serve_static))
|
||||||
.with_state(state.clone());
|
.with_state(state.clone());
|
||||||
|
|
||||||
// HTTP on port 80
|
let mut shutdown_rx = shutdown_tx.subscribe();
|
||||||
let http_app = app.clone();
|
|
||||||
let http_handle = tokio::spawn(async move {
|
let http_handle = tokio::spawn(async move {
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], 1200));
|
let addr = SocketAddr::from(([0, 0, 0, 0], 1200));
|
||||||
info!("HTTP server listening on {}", addr);
|
info!("HTTP server listening on {}", addr);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
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();
|
http_handle.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_index(State(state): State<AppState>) -> impl IntoResponse {
|
async fn serve_index(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
info!("Serving index.html");
|
||||||
serve_file(state.website_root.join("index.html")).await
|
serve_file(state.website_root.join("index.html")).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,22 +165,27 @@ async fn serve_post_page(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(_file): Path<String>,
|
Path(_file): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
info!("Serving post.html");
|
||||||
serve_file(state.website_root.join("post.html")).await
|
serve_file(state.website_root.join("post.html")).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_backdrops(State(state): State<AppState>) -> impl IntoResponse {
|
async fn serve_backdrops(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
info!("Serving backdrops.json");
|
||||||
serve_file(state.website_root.join("api/backdrops.json")).await
|
serve_file(state.website_root.join("api/backdrops.json")).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_staff(State(state): State<AppState>) -> impl IntoResponse {
|
async fn serve_staff(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
info!("Serving staff.json");
|
||||||
serve_file(state.website_root.join("api/staff.json")).await
|
serve_file(state.website_root.join("api/staff.json")).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_teams(State(state): State<AppState>) -> impl IntoResponse {
|
async fn serve_teams(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
info!("Serving teams.json");
|
||||||
serve_file(state.website_root.join("api/teams.json")).await
|
serve_file(state.website_root.join("api/teams.json")).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_posts(State(state): State<AppState>) -> impl IntoResponse {
|
async fn serve_posts(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
info!("Serving posts.json");
|
||||||
serve_file(state.website_root.join("api/posts.json")).await
|
serve_file(state.website_root.join("api/posts.json")).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,6 +193,7 @@ async fn serve_post_md(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(file): Path<String>,
|
Path(file): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
info!("Serving {}", file);
|
||||||
let path = state.website_root.join("posts").join(&file);
|
let path = state.website_root.join("posts").join(&file);
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
let content = fs::read_to_string(path).await.unwrap_or_default();
|
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<AppState>) -> impl IntoResponse {
|
async fn serve_static(uri: Uri, State(state): State<AppState>) -> impl IntoResponse {
|
||||||
let path = uri.path().trim_start_matches('/');
|
let path = uri.path().trim_start_matches('/');
|
||||||
let file_path = state.website_root.join(path);
|
let file_path = state.website_root.join(path);
|
||||||
|
info!("Serving static file, {}", path);
|
||||||
|
|
||||||
if file_path.exists() && file_path.is_file() {
|
if file_path.exists() && file_path.is_file() {
|
||||||
serve_file(file_path).await
|
serve_file(file_path).await
|
||||||
|
|
@ -175,12 +246,11 @@ async fn serve_file(path: PathBuf) -> Response {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
use axum::http::HeaderValue;
|
|
||||||
|
|
||||||
async fn serve_direct(
|
async fn serve_direct(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
info!("Serving redirect {}", name);
|
||||||
if name.contains('/') || name.contains('\\') || name.starts_with('.') {
|
if name.contains('/') || name.contains('\\') || name.starts_with('.') {
|
||||||
return StatusCode::BAD_REQUEST.into_response();
|
return StatusCode::BAD_REQUEST.into_response();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "closingin",
|
"name": "closingin",
|
||||||
"destructive": false
|
"destructive": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "horse_fog",
|
"name": "horse_fog",
|
||||||
|
|
@ -21,13 +21,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "outpost",
|
"name": "outpost",
|
||||||
"destructive": true,
|
"destructive": false
|
||||||
"time": 8
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tower_small",
|
"name": "tower_small",
|
||||||
"destructive": true,
|
"destructive": false
|
||||||
"time": 8
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tower_large",
|
"name": "tower_large",
|
||||||
|
|
@ -38,11 +36,11 @@
|
||||||
"destructive": false
|
"destructive": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mountain_horse",
|
"name": "armoury",
|
||||||
"destructive": false
|
"destructive": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "armoury",
|
"name": "mountain_horse",
|
||||||
"destructive": false
|
"destructive": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -71,6 +69,6 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "secondsbeforedisaster",
|
"name": "secondsbeforedisaster",
|
||||||
"destructive": true
|
"destructive": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ function initParallax() {
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Hero Slideshow with Destructive Theme ───
|
// ─── Hero Slideshow ───
|
||||||
let currentBackdrop = null;
|
let currentBackdrop = null;
|
||||||
const NEAR_TOP_THRESHOLD = 1800;
|
const NEAR_TOP_THRESHOLD = 1800;
|
||||||
|
|
||||||
|
|
@ -63,8 +63,19 @@ function initHeroSlideshow(allBackdrops, usedNames) {
|
||||||
|
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
|
|
||||||
function showSlide(index) {
|
function pickRandomSlide(exclude) {
|
||||||
const backdrop = slides[index];
|
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;
|
currentBackdrop = backdrop;
|
||||||
|
|
||||||
layer.style.opacity = "0";
|
layer.style.opacity = "0";
|
||||||
|
|
@ -76,18 +87,17 @@ function initHeroSlideshow(allBackdrops, usedNames) {
|
||||||
}, 600);
|
}, 600);
|
||||||
|
|
||||||
const delayMs = (backdrop.time || 6) * 1000;
|
const delayMs = (backdrop.time || 6) * 1000;
|
||||||
idx = (index + 1) % slides.length;
|
setTimeout(showSlide, delayMs);
|
||||||
setTimeout(() => showSlide(idx), delayMs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentBackdrop = slides[0];
|
const first = pickRandomSlide();
|
||||||
layer.style.backgroundImage = `url('/assets/${slides[0].name}.png')`;
|
currentBackdrop = first;
|
||||||
|
|
||||||
|
layer.style.backgroundImage = `url('/assets/${first.name}.png')`;
|
||||||
layer.style.opacity = "1";
|
layer.style.opacity = "1";
|
||||||
checkDestructiveTheme();
|
checkDestructiveTheme();
|
||||||
|
|
||||||
idx = 1;
|
setTimeout(showSlide, (first.time || 6) * 1000);
|
||||||
const initialDelay = (slides[0].time || 6) * 1000;
|
|
||||||
setTimeout(() => showSlide(idx), initialDelay);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Load Backdrops ───
|
// ─── Load Backdrops ───
|
||||||
|
|
@ -124,7 +134,7 @@ async function loadBackdrops() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Load Staff (in Socials section) ───
|
// ─── Load Staff ───
|
||||||
async function loadStaff() {
|
async function loadStaff() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/staff.json");
|
const response = await fetch("/api/staff.json");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue