Changes
This commit is contained in:
parent
3a3f899ea2
commit
128b3191e7
68 changed files with 28060 additions and 1152 deletions
|
|
@ -6,6 +6,7 @@ mod features;
|
|||
mod metrics;
|
||||
mod og_middleware;
|
||||
pub mod parsing;
|
||||
mod pocketbase;
|
||||
mod routes;
|
||||
mod state;
|
||||
pub mod utils;
|
||||
|
|
@ -23,7 +24,7 @@ use tower_http::compression::CompressionLayer;
|
|||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{info, warn};
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use state::AppState;
|
||||
|
|
@ -39,6 +40,10 @@ struct Cli {
|
|||
#[arg(long)]
|
||||
pois: PathBuf,
|
||||
|
||||
/// Path to the places parquet file
|
||||
#[arg(long)]
|
||||
places: PathBuf,
|
||||
|
||||
/// Path to the postcode boundaries directory
|
||||
#[arg(long)]
|
||||
postcodes: PathBuf,
|
||||
|
|
@ -56,28 +61,36 @@ struct Cli {
|
|||
screenshot_url: String,
|
||||
|
||||
/// Public-facing URL for absolute og:image URLs
|
||||
#[arg(
|
||||
long,
|
||||
env = "PUBLIC_URL",
|
||||
default_value = "https://perfectpostcodes.schmelczer.dev"
|
||||
)]
|
||||
#[arg(long, env = "PUBLIC_URL")]
|
||||
public_url: String,
|
||||
|
||||
/// PocketBase server URL for authentication (e.g. http://localhost:8090)
|
||||
#[arg(long, env = "POCKETBASE_URL")]
|
||||
pocketbase_url: String,
|
||||
|
||||
/// PocketBase superuser email (for auto-creating collections at startup)
|
||||
#[arg(long, env = "POCKETBASE_ADMIN_EMAIL")]
|
||||
pocketbase_admin_email: Option<String>,
|
||||
|
||||
/// PocketBase superuser password (for auto-creating collections at startup)
|
||||
#[arg(long, env = "POCKETBASE_ADMIN_PASSWORD")]
|
||||
pocketbase_admin_password: Option<String>,
|
||||
|
||||
/// Ollama server URL for AI area summaries (e.g. http://ollama:11434)
|
||||
#[arg(long, env = "OLLAMA_URL")]
|
||||
ollama_url: String,
|
||||
|
||||
/// Ollama model name for area summaries
|
||||
#[arg(long, env = "OLLAMA_MODEL", default_value = "gemma3:12b")]
|
||||
#[arg(long, env = "OLLAMA_MODEL")]
|
||||
ollama_model: String,
|
||||
|
||||
/// R5 routing service URL for real-time travel times (e.g. http://r5:8003)
|
||||
#[arg(long, env = "R5_URL", default_value = "")]
|
||||
r5_url: String,
|
||||
/// R5 routing service URL for all travel times (e.g. http://r5:8003)
|
||||
#[arg(long, env = "R5_URL")]
|
||||
r5_url: Option<String>,
|
||||
|
||||
/// Google Maps API key for Street View metadata lookups
|
||||
#[arg(long, env = "GOOGLE_MAPS_API_KEY")]
|
||||
google_maps_api_key: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -138,6 +151,15 @@ async fn main() -> anyhow::Result<()> {
|
|||
info!("Building POI spatial grid index");
|
||||
let poi_grid = utils::GridIndex::build(&poi_data.lat, &poi_data.lng, consts::GRID_CELL_SIZE);
|
||||
|
||||
// Load place data
|
||||
let places_path = &cli.places;
|
||||
if !places_path.exists() {
|
||||
bail!("Places parquet file not found: {}", places_path.display());
|
||||
}
|
||||
info!("Loading place data from {}", places_path.display());
|
||||
let place_data = data::PlaceData::load(places_path)?;
|
||||
info!(places = place_data.name.len(), "Place data loaded");
|
||||
|
||||
// Load postcode boundaries
|
||||
let postcodes_path = &cli.postcodes;
|
||||
if !postcodes_path.exists() {
|
||||
|
|
@ -191,26 +213,15 @@ async fn main() -> anyhow::Result<()> {
|
|||
let poi_category_groups = poi_data.category_groups()?;
|
||||
|
||||
// Read index.html at startup for crawler OG injection
|
||||
let frontend_dist = cli
|
||||
.dist
|
||||
.unwrap_or_else(|| PathBuf::from("frontend/dist"));
|
||||
|
||||
let index_html = {
|
||||
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) => {
|
||||
warn!(
|
||||
"Could not read {}: {} (OG injection disabled)",
|
||||
index_path.display(),
|
||||
err
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
let (frontend_dist, index_html) = if let Some(dist) = cli.dist {
|
||||
let index_path = dist.join("index.html");
|
||||
let html = std::fs::read_to_string(&index_path)
|
||||
.with_context(|| format!("Failed to read {}", index_path.display()))?;
|
||||
info!("Loaded index.html for OG injection");
|
||||
(Some(dist), Some(html))
|
||||
} else {
|
||||
info!("No --dist provided, static serving and OG injection disabled");
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let http_client = reqwest::Client::new();
|
||||
|
|
@ -223,6 +234,10 @@ async fn main() -> anyhow::Result<()> {
|
|||
"Precomputed features response"
|
||||
);
|
||||
|
||||
let ai_filters_schema = routes::build_ollama_schema(&features_response);
|
||||
let ai_filters_system_prompt = routes::build_system_prompt(&features_response);
|
||||
info!("Precomputed AI filters schema and system prompt");
|
||||
|
||||
// Record data loading metrics
|
||||
metrics::record_data_stats(
|
||||
property_data.lat.len(),
|
||||
|
|
@ -231,12 +246,21 @@ async fn main() -> anyhow::Result<()> {
|
|||
);
|
||||
|
||||
info!("PocketBase configured: {}", cli.pocketbase_url);
|
||||
|
||||
if let (Some(ref email), Some(ref password)) =
|
||||
(&cli.pocketbase_admin_email, &cli.pocketbase_admin_password)
|
||||
{
|
||||
pocketbase::ensure_collections(&http_client, &cli.pocketbase_url, email, password).await?;
|
||||
} else {
|
||||
info!("PocketBase admin credentials not set — skipping collection auto-creation");
|
||||
}
|
||||
|
||||
info!(
|
||||
"Ollama configured: {} (model: {})",
|
||||
cli.ollama_url, cli.ollama_model
|
||||
);
|
||||
if !cli.r5_url.is_empty() {
|
||||
info!("R5 routing service configured: {}", cli.r5_url);
|
||||
if let Some(ref url) = cli.r5_url {
|
||||
info!("R5 routing service configured: {}", url);
|
||||
} else {
|
||||
info!("R5 routing service not configured (travel time queries disabled)");
|
||||
}
|
||||
|
|
@ -249,6 +273,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
h3_cells,
|
||||
poi_data,
|
||||
poi_grid,
|
||||
place_data,
|
||||
postcode_data,
|
||||
feature_name_to_index,
|
||||
min_keys,
|
||||
|
|
@ -265,6 +290,9 @@ async fn main() -> anyhow::Result<()> {
|
|||
ollama_model: cli.ollama_model,
|
||||
r5_url: cli.r5_url,
|
||||
token_cache,
|
||||
ai_filters_schema,
|
||||
ai_filters_system_prompt,
|
||||
google_maps_api_key: cli.google_maps_api_key,
|
||||
});
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
|
|
@ -286,8 +314,11 @@ async fn main() -> anyhow::Result<()> {
|
|||
let state_pb = state.clone();
|
||||
let state_postcode_stats = state.clone();
|
||||
let state_area_summary = state.clone();
|
||||
let state_places = state.clone();
|
||||
let state_shorten = state.clone();
|
||||
let state_short_url = state.clone();
|
||||
let state_ai_filters = state.clone();
|
||||
let state_streetview = state.clone();
|
||||
|
||||
let api = Router::new()
|
||||
.route(
|
||||
|
|
@ -314,6 +345,10 @@ async fn main() -> anyhow::Result<()> {
|
|||
"/api/poi-categories",
|
||||
get(move || routes::get_poi_categories(state_poi_categories.clone())),
|
||||
)
|
||||
.route(
|
||||
"/api/places",
|
||||
get(move |query| routes::get_places(state_places.clone(), query)),
|
||||
)
|
||||
.route(
|
||||
"/api/hexagon-properties",
|
||||
get(move |query| {
|
||||
|
|
@ -345,6 +380,14 @@ async fn main() -> anyhow::Result<()> {
|
|||
"/api/shorten",
|
||||
post(move |body| routes::post_shorten(state_shorten.clone(), body)),
|
||||
)
|
||||
.route(
|
||||
"/api/ai-filters",
|
||||
post(move |body| routes::post_ai_filters(state_ai_filters.clone(), body)),
|
||||
)
|
||||
.route(
|
||||
"/api/streetview",
|
||||
get(move |query| routes::get_streetview(state_streetview.clone(), query)),
|
||||
)
|
||||
.route(
|
||||
"/s/{code}",
|
||||
get(move |path| routes::get_short_url(state_short_url.clone(), path)),
|
||||
|
|
@ -364,6 +407,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
routes::get_style(axum::extract::State(reader_style.clone()), headers, query)
|
||||
}),
|
||||
)
|
||||
.route("/health", get(|| async { "ok" }))
|
||||
.route(
|
||||
"/metrics",
|
||||
get(move || metrics::metrics_handler(metrics_handle.clone())),
|
||||
|
|
@ -373,10 +417,9 @@ async fn main() -> anyhow::Result<()> {
|
|||
any(move |req| routes::proxy_to_pocketbase(state_pb.clone(), req)),
|
||||
);
|
||||
|
||||
let app = if frontend_dist.exists() {
|
||||
let app = if let Some(ref dist) = frontend_dist {
|
||||
api.fallback_service(
|
||||
ServeDir::new(&frontend_dist)
|
||||
.not_found_service(ServeFile::new(frontend_dist.join("index.html"))),
|
||||
ServeDir::new(dist).not_found_service(ServeFile::new(dist.join("index.html"))),
|
||||
)
|
||||
} else {
|
||||
api
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue