mirror of
https://github.com/Xevion/banner.git
synced 2025-12-05 23:14:20 -06:00
feat: add formatter CLI argument, setup asset embedding in release mode
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,3 +2,6 @@
|
||||
/target
|
||||
/go/
|
||||
.cargo/config.toml
|
||||
|
||||
**/*.md
|
||||
!/README.md
|
||||
209
Cargo.lock
generated
209
Cargo.lock
generated
@@ -41,6 +41,56 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.99"
|
||||
@@ -175,6 +225,7 @@ dependencies = [
|
||||
"axum",
|
||||
"bitflags 2.9.4",
|
||||
"chrono",
|
||||
"clap",
|
||||
"compile-time",
|
||||
"cookie",
|
||||
"dashmap 6.1.0",
|
||||
@@ -184,6 +235,7 @@ dependencies = [
|
||||
"futures",
|
||||
"governor",
|
||||
"http 1.3.1",
|
||||
"mime_guess",
|
||||
"num-format",
|
||||
"once_cell",
|
||||
"poise",
|
||||
@@ -191,6 +243,7 @@ dependencies = [
|
||||
"regex",
|
||||
"reqwest 0.12.23",
|
||||
"reqwest-middleware",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
@@ -200,6 +253,7 @@ dependencies = [
|
||||
"time",
|
||||
"tl",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
@@ -247,6 +301,16 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
@@ -337,6 +401,52 @@ dependencies = [
|
||||
"windows-link 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "command_attr"
|
||||
version = "0.5.3"
|
||||
@@ -571,9 +681,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
@@ -947,6 +1057,19 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.10.1"
|
||||
@@ -1129,6 +1252,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range-header"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
@@ -1440,6 +1569,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
@@ -1730,6 +1865,12 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.73"
|
||||
@@ -2259,6 +2400,41 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.106",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594"
|
||||
dependencies = [
|
||||
"globset",
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.26"
|
||||
@@ -3097,12 +3273,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.41"
|
||||
version = "0.3.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||
checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
@@ -3112,15 +3287,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.22"
|
||||
version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
@@ -3334,14 +3509,24 @@ checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"iri-string",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3577,6 +3762,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uwl"
|
||||
version = "0.6.0"
|
||||
|
||||
@@ -34,7 +34,7 @@ sqlx = { version = "0.8.6", features = [
|
||||
"macros",
|
||||
] }
|
||||
thiserror = "2.0.16"
|
||||
time = "0.3.41"
|
||||
time = "0.3.43"
|
||||
tokio = { version = "1.47.1", features = ["full"] }
|
||||
tl = "0.7.8"
|
||||
tracing = "0.1.41"
|
||||
@@ -44,5 +44,9 @@ governor = "0.10.1"
|
||||
once_cell = "1.21.3"
|
||||
serde_path_to_error = "0.1.17"
|
||||
num-format = "0.4.4"
|
||||
tower-http = { version = "0.6.0", features = ["fs", "cors", "trace"] }
|
||||
rust-embed = { version = "8.0", features = ["debug-embed", "include-exclude"] }
|
||||
mime_guess = "2.0"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct Config {
|
||||
/// Defaults to "info" if not specified
|
||||
#[serde(default = "default_log_level")]
|
||||
pub log_level: String,
|
||||
/// Port for the web server
|
||||
/// Port for the web server (default: 8080)
|
||||
#[serde(default = "default_port")]
|
||||
pub port: u16,
|
||||
/// Database connection URL
|
||||
@@ -51,9 +51,9 @@ fn default_log_level() -> String {
|
||||
"info".to_string()
|
||||
}
|
||||
|
||||
/// Default port of 3000
|
||||
/// Default port of 8080
|
||||
fn default_port() -> u16 {
|
||||
3000
|
||||
8080
|
||||
}
|
||||
|
||||
/// Default shutdown timeout of 8 seconds
|
||||
|
||||
@@ -24,14 +24,14 @@ const TIMESTAMP_FORMAT: &[FormatItem<'static>] =
|
||||
/// A custom formatter with enhanced timestamp formatting
|
||||
///
|
||||
/// Re-implementation of the Full formatter with improved timestamp display.
|
||||
pub struct CustomFormatter;
|
||||
pub struct CustomPrettyFormatter;
|
||||
|
||||
/// A custom JSON formatter that flattens fields to root level
|
||||
///
|
||||
/// Outputs logs in the format: { "message": "...", "level": "...", "customAttribute": "..." }
|
||||
pub struct CustomJsonFormatter;
|
||||
|
||||
impl<S, N> FormatEvent<S, N> for CustomFormatter
|
||||
impl<S, N> FormatEvent<S, N> for CustomPrettyFormatter
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
N: for<'a> FormatFields<'a> + 'static,
|
||||
|
||||
52
src/main.rs
52
src/main.rs
@@ -1,3 +1,4 @@
|
||||
use clap::Parser;
|
||||
use figment::value::UncasedStr;
|
||||
use num_format::{Locale, ToFormattedString};
|
||||
use serenity::all::{ActivityData, ClientBuilder, Context, GatewayIntents};
|
||||
@@ -28,6 +29,25 @@ mod services;
|
||||
mod state;
|
||||
mod web;
|
||||
|
||||
/// Banner Discord Bot - Course availability monitoring
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// Log formatter to use
|
||||
#[arg(long, value_enum, default_value_t = LogFormatter::Auto)]
|
||||
formatter: LogFormatter,
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Debug)]
|
||||
enum LogFormatter {
|
||||
/// Use pretty formatter (default in debug mode)
|
||||
Pretty,
|
||||
/// Use JSON formatter (default in release mode)
|
||||
Json,
|
||||
/// Auto-select based on build mode (debug=pretty, release=json)
|
||||
Auto,
|
||||
}
|
||||
|
||||
async fn update_bot_status(ctx: &Context, app_state: &AppState) -> Result<(), anyhow::Error> {
|
||||
let course_count = app_state.get_course_count().await?;
|
||||
|
||||
@@ -44,6 +64,9 @@ async fn update_bot_status(ctx: &Context, app_state: &AppState) -> Result<(), an
|
||||
async fn main() {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
// Parse CLI arguments
|
||||
let args = Args::parse();
|
||||
|
||||
// Load configuration first to get log level
|
||||
let config: Config = Figment::new()
|
||||
.merge(Env::raw().map(|k| {
|
||||
@@ -64,22 +87,31 @@ async fn main() {
|
||||
base_level
|
||||
))
|
||||
});
|
||||
let subscriber = {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
|
||||
// Select formatter based on CLI args
|
||||
let use_pretty = match args.formatter {
|
||||
LogFormatter::Pretty => true,
|
||||
LogFormatter::Json => false,
|
||||
LogFormatter::Auto => cfg!(debug_assertions),
|
||||
};
|
||||
|
||||
let subscriber: Box<dyn tracing::Subscriber + Send + Sync> = if use_pretty {
|
||||
Box::new(
|
||||
FmtSubscriber::builder()
|
||||
.with_target(true)
|
||||
.event_format(formatter::CustomFormatter)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
.event_format(formatter::CustomPrettyFormatter)
|
||||
.with_env_filter(filter)
|
||||
.finish(),
|
||||
)
|
||||
} else {
|
||||
Box::new(
|
||||
FmtSubscriber::builder()
|
||||
.with_target(true)
|
||||
.event_format(formatter::CustomJsonFormatter)
|
||||
}
|
||||
}
|
||||
.with_env_filter(filter)
|
||||
.finish();
|
||||
.finish(),
|
||||
)
|
||||
};
|
||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||
|
||||
// Log application startup context
|
||||
|
||||
64
src/web/assets.rs
Normal file
64
src/web/assets.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
//! Embedded assets for the web frontend
|
||||
//!
|
||||
//! This module handles serving static assets that are embedded into the binary
|
||||
//! at compile time using rust-embed.
|
||||
|
||||
use axum::{
|
||||
extract::Path,
|
||||
http::{StatusCode, header},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
/// Embedded web assets from the dist directory
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "web/dist/"]
|
||||
#[include = "*"]
|
||||
#[exclude = "*.map"]
|
||||
pub struct WebAssets;
|
||||
|
||||
/// Serve embedded static assets
|
||||
pub async fn serve_asset(Path(path): Path<String>) -> Response {
|
||||
let path = path.trim_start_matches('/');
|
||||
|
||||
match WebAssets::get(path) {
|
||||
Some(content) => {
|
||||
let mime_type = mime_guess::from_path(path).first_or_text_plain();
|
||||
let data = content.data.to_vec();
|
||||
([(header::CONTENT_TYPE, mime_type.as_ref())], data).into_response()
|
||||
}
|
||||
None => (StatusCode::NOT_FOUND, "Asset not found").into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serve the main SPA index.html for client-side routing
|
||||
pub async fn serve_spa_index() -> Response {
|
||||
match WebAssets::get("index.html") {
|
||||
Some(content) => {
|
||||
let data = content.data.to_vec();
|
||||
let html_content = String::from_utf8_lossy(&data).to_string();
|
||||
Html(html_content).into_response()
|
||||
}
|
||||
None => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to load index.html",
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
const ASSET_EXTENSIONS: &[&str] = &[
|
||||
"js", "css", "png", "jpg", "jpeg", "gif", "svg", "ico", "woff", "woff2", "ttf", "eot",
|
||||
];
|
||||
|
||||
/// Check if a path should be served as a static asset
|
||||
pub fn is_asset_path(path: &str) -> bool {
|
||||
if !path.starts_with("/assets/") {
|
||||
return path.eq("index.html");
|
||||
}
|
||||
|
||||
match path.split_once('.') {
|
||||
Some((_, extension)) => ASSET_EXTENSIONS.contains(&extension),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Web API module for the banner application.
|
||||
|
||||
pub mod assets;
|
||||
pub mod routes;
|
||||
|
||||
pub use routes::*;
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
//! Web API endpoints for Banner bot monitoring and metrics.
|
||||
|
||||
use axum::{Router, extract::State, response::Json, routing::get};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::State,
|
||||
http::{StatusCode, Uri},
|
||||
response::{IntoResponse, Json, Response},
|
||||
routing::{any, get},
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::sync::Arc;
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
trace::TraceLayer,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::web::assets::{is_asset_path, serve_asset, serve_spa_index};
|
||||
|
||||
use crate::banner::BannerApi;
|
||||
|
||||
/// Shared application state for web server
|
||||
@@ -15,24 +27,72 @@ pub struct BannerState {
|
||||
|
||||
/// Creates the web server router
|
||||
pub fn create_router(state: BannerState) -> Router {
|
||||
Router::new()
|
||||
.route("/", get(root))
|
||||
let api_router = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/status", get(status))
|
||||
.route("/metrics", get(metrics))
|
||||
.with_state(state)
|
||||
.with_state(state);
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
// Development mode: API routes only, frontend served by Vite dev server
|
||||
Router::new()
|
||||
.route("/", get(root))
|
||||
.nest("/api", api_router)
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any),
|
||||
)
|
||||
.layer(TraceLayer::new_for_http())
|
||||
} else {
|
||||
// Production mode: serve embedded assets and handle SPA routing
|
||||
Router::new()
|
||||
.route("/", get(root))
|
||||
.nest("/api", api_router)
|
||||
.route("/assets/{*path}", any(serve_asset))
|
||||
.fallback(handle_spa_fallback)
|
||||
.layer(TraceLayer::new_for_http())
|
||||
}
|
||||
}
|
||||
|
||||
async fn root() -> Json<Value> {
|
||||
async fn root() -> Response {
|
||||
if cfg!(debug_assertions) {
|
||||
// Development mode: return API info
|
||||
Json(json!({
|
||||
"message": "Banner Discord Bot API",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.1",
|
||||
"mode": "development",
|
||||
"frontend": "http://localhost:3000",
|
||||
"endpoints": {
|
||||
"health": "/health",
|
||||
"status": "/status",
|
||||
"metrics": "/metrics"
|
||||
"health": "/api/health",
|
||||
"status": "/api/status",
|
||||
"metrics": "/api/metrics"
|
||||
}
|
||||
}))
|
||||
.into_response()
|
||||
} else {
|
||||
// Production mode: serve the SPA index.html
|
||||
handle_spa_fallback(Uri::from_static("/")).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles SPA routing by serving index.html for non-API, non-asset requests
|
||||
async fn handle_spa_fallback(uri: Uri) -> Response {
|
||||
let path = uri.path();
|
||||
|
||||
// Don't serve index.html for API routes or asset requests
|
||||
if path.starts_with("/api/") || is_asset_path(path) {
|
||||
return (StatusCode::NOT_FOUND, "Not Found").into_response();
|
||||
}
|
||||
|
||||
// In production, serve embedded index.html for SPA routing
|
||||
if cfg!(not(debug_assertions)) {
|
||||
return serve_spa_index().await;
|
||||
}
|
||||
|
||||
// Development fallback (shouldn't reach here in production)
|
||||
(StatusCode::NOT_FOUND, "Not Found").into_response()
|
||||
}
|
||||
|
||||
/// Health check endpoint
|
||||
|
||||
Reference in New Issue
Block a user