Various changes

This commit is contained in:
Andras Schmelczer 2026-02-03 19:26:34 +00:00
parent a42591c701
commit c388059f68
19 changed files with 1373 additions and 87 deletions

View file

@ -3,6 +3,7 @@ mod data;
mod features;
mod filter;
mod grid_index;
mod og_middleware;
mod routes;
mod state;
#[cfg(test)]
@ -12,6 +13,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{bail, Context};
use axum::middleware;
use axum::routing::get;
use axum::Router;
use clap::Parser;
@ -38,6 +40,18 @@ struct Cli {
/// Path to the frontend dist directory
#[arg(long)]
dist: Option<PathBuf>,
/// URL of the OG screenshot sidecar (e.g. http://og-screenshot:8002)
#[arg(long, env = "OG_SIDECAR_URL")]
og_sidecar_url: Option<String>,
/// Public-facing URL for absolute og:image URLs
#[arg(
long,
env = "PUBLIC_URL",
default_value = "https://narrowit.schmelczer.dev"
)]
public_url: String,
}
#[tokio::main]
@ -69,7 +83,11 @@ async fn main() -> anyhow::Result<()> {
);
info!("Building spatial grid index (0.01° cells)");
let grid = grid_index::GridIndex::build(&property_data.lat, &property_data.lon, consts::GRID_CELL_SIZE);
let grid = grid_index::GridIndex::build(
&property_data.lat,
&property_data.lon,
consts::GRID_CELL_SIZE,
);
info!(
"Precomputing H3 cells at resolution {}",
@ -88,7 +106,8 @@ async fn main() -> anyhow::Result<()> {
info!(pois = poi_data.lat.len(), "POI data loaded");
info!("Building POI spatial grid index");
let poi_grid = grid_index::GridIndex::build(&poi_data.lat, &poi_data.lng, consts::GRID_CELL_SIZE);
let poi_grid =
grid_index::GridIndex::build(&poi_data.lat, &poi_data.lng, consts::GRID_CELL_SIZE);
let min_keys: Vec<String> = property_data
.feature_names
@ -119,10 +138,7 @@ async fn main() -> anyhow::Result<()> {
for row in 0..num_pois {
let category = poi_data.category.get(row).to_string();
let group = poi_data.group.get(row).to_string();
group_cats
.entry(group)
.or_default()
.insert(category);
group_cats.entry(group).or_default().insert(category);
}
// Validate that data groups match the hardcoded order exactly
let expected: std::collections::HashSet<&str> =
@ -137,11 +153,17 @@ async fn main() -> anyhow::Result<()> {
missing_from_data, missing_from_order
);
}
consts::POI_GROUP_ORDER.iter().map(|group_name| group_name.to_string()).collect::<Vec<_>>()
consts::POI_GROUP_ORDER
.iter()
.map(|group_name| group_name.to_string())
.collect::<Vec<_>>()
.into_iter()
.map(|name| {
let mut categories: Vec<String> =
group_cats.remove(&name).context("POI group validated but missing from map")?.into_iter().collect();
let mut categories: Vec<String> = group_cats
.remove(&name)
.context("POI group validated but missing from map")?
.into_iter()
.collect();
categories.sort();
Ok(state::POICategoryGroup { name, categories })
})
@ -156,6 +178,45 @@ async fn main() -> anyhow::Result<()> {
.map(|(index, enum_feature)| (enum_feature.name.clone(), index))
.collect();
// Read index.html at startup for crawler OG injection
let frontend_dist = cli.dist.unwrap_or_else(|| {
if let Ok(executable) = std::env::current_exe() {
let executable_dir = executable
.parent()
.unwrap_or_else(|| std::path::Path::new("."));
let dist_next_to_binary = executable_dir.join("dist");
if dist_next_to_binary.exists() {
return dist_next_to_binary;
}
}
PathBuf::from("frontend/dist")
});
let index_html = if frontend_dist.exists() {
let index_path = frontend_dist.join("index.html");
match std::fs::read_to_string(&index_path) {
Ok(html) => {
info!("Loaded index.html for OG injection");
Some(html)
}
Err(err) => {
tracing::warn!("Could not read index.html: {}", err);
None
}
}
} else {
None
};
let http_client = reqwest::Client::new();
if cli.og_sidecar_url.is_some() {
info!(
"OG sidecar configured: {}",
cli.og_sidecar_url.as_deref().unwrap()
);
}
let state = Arc::new(AppState {
data: property_data,
grid,
@ -168,6 +229,10 @@ async fn main() -> anyhow::Result<()> {
enum_max_keys,
poi_category_groups,
enum_name_to_idx,
og_sidecar_url: cli.og_sidecar_url,
public_url: cli.public_url,
index_html,
http_client,
});
let cors = CorsLayer::new()
@ -181,6 +246,8 @@ async fn main() -> anyhow::Result<()> {
let state_poi_categories = state.clone();
let state_hexagon_properties = state.clone();
let state_hexagon_stats = state.clone();
let state_og_image = state.clone();
let state_crawler = state.clone();
let api = Router::new()
.route(
@ -208,26 +275,31 @@ async fn main() -> anyhow::Result<()> {
.route(
"/api/hexagon-stats",
get(move |query| routes::get_hexagon_stats(state_hexagon_stats.clone(), query)),
)
.route(
"/api/og-image",
get(move |query| routes::get_og_image(state_og_image.clone(), query)),
);
let frontend_dist = cli.dist.unwrap_or_else(|| {
// Check next to the binary first, then fall back to working directory
if let Ok(executable) = std::env::current_exe() {
let executable_dir = executable.parent().unwrap_or_else(|| std::path::Path::new("."));
let dist_next_to_binary = executable_dir.join("dist");
if dist_next_to_binary.exists() {
return dist_next_to_binary;
}
}
PathBuf::from("frontend/dist")
});
let app = if frontend_dist.exists() {
api.fallback_service(ServeDir::new(frontend_dist))
api.fallback_service(ServeDir::new(&frontend_dist))
} else {
api
};
let app = app
.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 the 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());
@ -237,8 +309,6 @@ async fn main() -> anyhow::Result<()> {
.await
.with_context(|| format!("Failed to bind to {addr}"))?;
info!("Server listening on {}", addr);
axum::serve(listener, app)
.await
.context("Server error")?;
axum::serve(listener, app).await.context("Server error")?;
Ok(())
}