This commit is contained in:
Andras Schmelczer 2026-03-15 17:38:26 +00:00
parent 80c093b7ba
commit f72c43a9fa
101 changed files with 2168 additions and 1177 deletions

View file

@ -1,3 +1,5 @@
#![allow(clippy::min_ident_chars)]
mod aggregation;
mod auth;
mod consts;
@ -17,11 +19,11 @@ use std::sync::Arc;
use std::time::Duration;
use anyhow::{bail, Context};
use consts::SERVICE_CALL_TIMEOUT;
use axum::middleware;
use axum::routing::{any, get, patch, post};
use axum::Router;
use clap::Parser;
use consts::SERVICE_CALL_TIMEOUT;
use tower::limit::ConcurrencyLimitLayer;
use tower_http::compression::CompressionLayer;
@ -36,7 +38,10 @@ use tracing_subscriber::EnvFilter;
use state::AppState;
#[derive(Parser)]
#[command(name = "perfect-postcode", about = "Perfect Postcode property map server")]
#[command(
name = "perfect-postcode",
about = "Perfect Postcode property map server"
)]
struct Cli {
/// Path to properties.parquet (one row per historical property)
#[arg(long)]
@ -129,7 +134,6 @@ struct Cli {
/// Google OAuth client secret for PocketBase SSO
#[arg(long, env = "GOOGLE_OAUTH_CLIENT_SECRET")]
google_oauth_client_secret: String,
}
#[tokio::main]
@ -137,8 +141,7 @@ async fn main() -> anyhow::Result<()> {
let file_appender = tracing_appender::rolling::daily("logs", "server.log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(env_filter)
@ -332,10 +335,7 @@ async fn main() -> anyhow::Result<()> {
info!("Gemini configured (model: {})", cli.gemini_model);
let tt_path = &cli.travel_times;
if !tt_path.exists() {
bail!(
"Travel times directory not found: {}",
tt_path.display()
);
bail!("Travel times directory not found: {}", tt_path.display());
}
info!("Loading travel time data from {}", tt_path.display());
let travel_time_store = {
@ -476,7 +476,9 @@ async fn main() -> anyhow::Result<()> {
)
.route(
"/api/travel-destinations",
get(move |query| routes::get_travel_destinations(state_travel_destinations.clone(), query)),
get(move |query| {
routes::get_travel_destinations(state_travel_destinations.clone(), query)
}),
)
.route(
"/api/journey",
@ -490,24 +492,34 @@ async fn main() -> anyhow::Result<()> {
)
.route(
"/api/hexagon-stats",
get(move |ext, query| routes::get_hexagon_stats(state_hexagon_stats.clone(), ext, query)),
get(move |ext, query| {
routes::get_hexagon_stats(state_hexagon_stats.clone(), ext, query)
}),
)
.route(
"/api/postcode-stats",
get(move |ext, query| routes::get_postcode_stats(state_postcode_stats.clone(), ext, query)),
get(move |ext, query| {
routes::get_postcode_stats(state_postcode_stats.clone(), ext, query)
}),
)
.route(
"/api/postcode-properties",
get(move |ext, query| routes::get_postcode_properties(state_postcode_properties.clone(), ext, query)),
get(move |ext, query| {
routes::get_postcode_properties(state_postcode_properties.clone(), ext, query)
}),
)
.route(
"/api/screenshot",
get(move |headers, query| routes::get_screenshot(state_screenshot.clone(), headers, query)),
get(move |headers, query| {
routes::get_screenshot(state_screenshot.clone(), headers, query)
}),
)
.route(
"/api/export",
get(move |headers, ext, query| routes::get_export(state_export.clone(), headers, ext, query))
.layer(ConcurrencyLimitLayer::new(3)),
get(move |headers, ext, query| {
routes::get_export(state_export.clone(), headers, ext, query)
})
.layer(ConcurrencyLimitLayer::new(3)),
)
.route("/api/me", get(routes::get_me))
.route(
@ -525,9 +537,7 @@ async fn main() -> anyhow::Result<()> {
)
.route(
"/api/newsletter",
patch(move |ext, body| {
routes::patch_newsletter(state_newsletter.clone(), ext, body)
}),
patch(move |ext, body| routes::patch_newsletter(state_newsletter.clone(), ext, body)),
)
.route(
"/api/pricing",
@ -546,8 +556,9 @@ async fn main() -> anyhow::Result<()> {
)
.route(
"/api/invites",
get(move |ext| routes::get_invites(state_invites_list.clone(), ext))
.post(move |ext, body| routes::post_invites(state_invites_create.clone(), ext, body)),
get(move |ext| routes::get_invites(state_invites_list.clone(), ext)).post(
move |ext, body| routes::post_invites(state_invites_create.clone(), ext, body),
),
)
.route(
"/api/invite/{code}",
@ -591,35 +602,35 @@ async fn main() -> anyhow::Result<()> {
);
let app = if let Some(ref dist) = cli.dist {
api.fallback_service(
ServeDir::new(dist).fallback(ServeFile::new(dist.join("index.html"))),
)
api.fallback_service(ServeDir::new(dist).fallback(ServeFile::new(dist.join("index.html"))))
} else {
api
}
.layer(middleware::from_fn(metrics::track_metrics))
.layer(middleware::from_fn(auth::auth_middleware))
.layer(middleware::from_fn(
move |req: axum::extract::Request, next: middleware::Next| {
let st = state_crawler.clone();
async move {
// Inject state into request extensions for auth + OG middleware
let (mut parts, body) = req.into_parts();
parts.extensions.insert(st);
let req = axum::extract::Request::from_parts(parts, body);
og_middleware::og_middleware(req, next).await
}
},
))
.layer(cors)
.layer(CompressionLayer::new().zstd(true).gzip(true))
.layer(TraceLayer::new_for_http());
.layer(middleware::from_fn(metrics::track_metrics))
.layer(middleware::from_fn(auth::auth_middleware))
.layer(middleware::from_fn(
move |req: axum::extract::Request, next: middleware::Next| {
let st = state_crawler.clone();
async move {
// Inject state into request extensions for auth + OG middleware
let (mut parts, body) = req.into_parts();
parts.extensions.insert(st);
let req = axum::extract::Request::from_parts(parts, body);
og_middleware::og_middleware(req, next).await
}
},
))
.layer(cors)
.layer(CompressionLayer::new().zstd(true).gzip(true))
.layer(TraceLayer::new_for_http());
// Lock all current and future memory pages to prevent swapping
unsafe {
if libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) != 0 {
let err = std::io::Error::last_os_error();
tracing::warn!("mlockall failed (need CAP_IPC_LOCK or sufficient RLIMIT_MEMLOCK): {err}");
tracing::warn!(
"mlockall failed (need CAP_IPC_LOCK or sufficient RLIMIT_MEMLOCK): {err}"
);
} else {
info!("All memory pages locked (mlockall)");
}