Various changes
This commit is contained in:
parent
a42591c701
commit
c388059f68
19 changed files with 1373 additions and 87 deletions
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue