4.1 KB
raw
mod alerts;
mod app;
mod checker;
mod crawler;
mod db;
mod lighthouse;
mod middleware;
mod migrate;
mod models;
mod pdf;
mod render;
mod routes;
mod scheduler;
mod templates;
pub use app::{AppState, Config};
use std::net::SocketAddr;
use std::path::PathBuf;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info,sqlx=warn")),
)
.init();
// rustls 0.23 doesn't pick a CryptoProvider on its own when neither
// the `ring` nor the `aws-lc-rs` feature is enabled at the rustls
// crate level. The phased prober (src/checker.rs) builds rustls
// ClientConfigs directly, which would panic on first probe without
// this. Install once at startup; reqwest/lettre's TLS paths are
// unaffected because they ship their own provider via hyper-rustls.
let _ = rustls::crypto::ring::default_provider().install_default();
// Subcommand dispatch. Anything besides a known subcommand falls through to the server.
let mut argv = std::env::args().skip(1);
if let Some(first) = argv.next() {
match first.as_str() {
"migrate" => return run_migrate(argv.collect()).await,
"preview-email" => return run_preview_email(argv.collect()),
"--help" | "-h" => {
print_usage();
return Ok(());
}
other if !other.is_empty() => {
eprintln!("unknown subcommand: {other}");
print_usage();
std::process::exit(2);
}
_ => {}
}
}
let port: u16 = std::env::var("PORT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(8000);
let state = AppState::from_env().await?;
// Reset wedged crawl/lighthouse rows from a prior crash, then spawn the
// scheduler loop alongside the HTTP server.
scheduler::reset_states_on_boot(&state.pool).await?;
let scheduler_handle = scheduler::spawn(state.pool.clone(), state.config.clone());
let router = app::router(state);
let addr = SocketAddr::from(([0, 0, 0, 0], port));
let listener = tokio::net::TcpListener::bind(addr).await?;
tracing::info!("status listening on http://{addr}");
axum::serve(listener, router).await?;
drop(scheduler_handle);
Ok(())
}
fn print_usage() {
eprintln!(
"status: single-binary axum uptime monitor\n\
\n\
Usage:\n \
status run the HTTP server + scheduler\n \
status migrate <path> [--force] import a Django status SQLite\n \
status preview-email <down|recovery> render an alert HTML to stdout\n"
);
}
fn run_preview_email(args: Vec<String>) -> anyhow::Result<()> {
let kind = args
.first()
.map(String::as_str)
.ok_or_else(|| anyhow::anyhow!("usage: status preview-email <down|recovery>"))?;
let base_url = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
let html = alerts::render_preview_html(kind, &base_url)?;
print!("{html}");
Ok(())
}
async fn run_migrate(args: Vec<String>) -> anyhow::Result<()> {
let mut source: Option<PathBuf> = None;
let mut force = false;
for arg in args {
match arg.as_str() {
"--force" | "-f" => force = true,
"--help" | "-h" => {
eprintln!("Usage: status migrate <path-to-django-sqlite3> [--force]");
return Ok(());
}
v if v.starts_with("--") => anyhow::bail!("unknown migrate flag: {v}"),
v => {
if source.is_some() {
anyhow::bail!("migrate takes a single source path");
}
source = Some(PathBuf::from(v));
}
}
}
let source = source.ok_or_else(|| {
anyhow::anyhow!("usage: status migrate <path-to-django-sqlite3> [--force]")
})?;
migrate::run(source, force).await
}