All required

This commit is contained in:
Andras Schmelczer 2026-06-06 10:45:35 +01:00
parent 44b4e0d72f
commit df63764a9f
7 changed files with 45 additions and 128 deletions

View file

@ -47,4 +47,4 @@ EXPOSE 8001
HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=3 \
CMD curl -f http://localhost:8001/health || exit 1 CMD curl -f http://localhost:8001/health || exit 1
ENTRYPOINT ["./property-map-server"] ENTRYPOINT ["./property-map-server"]
CMD ["--properties", "/app/data/properties.parquet", "--postcode-features", "/app/data/postcode.parquet", "--pois", "/app/data/filtered_uk_pois.parquet", "--places", "/app/data/places.parquet", "--tiles", "/app/data/uk.pmtiles", "--postcodes", "/app/data/postcode_boundaries", "--travel-times", "/app/data/travel-times", "--dist", "/app/frontend/dist"] CMD ["--properties", "/app/data/properties.parquet", "--postcode-features", "/app/data/postcode.parquet", "--pois", "/app/data/filtered_uk_pois.parquet", "--places", "/app/data/places.parquet", "--tiles", "/app/data/uk.pmtiles", "--postcodes", "/app/data/postcode_boundaries", "--travel-times", "/app/data/travel-times", "--satellite-tiles", "/app/data/satellite.pmtiles", "--satellite-highres-tiles", "/app/data/satellite_highres.pmtiles", "--noise-overlay-tiles", "/app/data/noise_lden_10m.pmtiles", "--crime-hotspot-tiles", "/app/data/crime_hotspots.pmtiles", "--tree-overlay-tiles", "/app/data/trees_outside_woodlands.pmtiles", "--property-border-tiles", "/app/data/property_borders.pmtiles", "--dist", "/app/frontend/dist"]

View file

@ -11,7 +11,7 @@ services:
command: > command: >
bash -c " bash -c "
cargo install cargo-watch && cargo install cargo-watch &&
cargo watch --poll -i logs/ -x 'run -- --properties /app/property-data4/properties.parquet --postcode-features /app/property-data4/postcode.parquet --pois /app/property-data4/filtered_uk_pois.parquet --places /app/property-data4/places.parquet --tiles /app/property-data4/uk.pmtiles --postcodes /app/property-data4/postcode_boundaries --travel-times /app/property-data4/travel-times' cargo watch --poll -i logs/ -x 'run -- --properties /app/property-data4/properties.parquet --postcode-features /app/property-data4/postcode.parquet --pois /app/property-data4/filtered_uk_pois.parquet --places /app/property-data4/places.parquet --tiles /app/property-data4/uk.pmtiles --postcodes /app/property-data4/postcode_boundaries --travel-times /app/property-data4/travel-times --satellite-tiles /app/property-data4/satellite.pmtiles --satellite-highres-tiles /app/property-data4/satellite_highres.pmtiles --noise-overlay-tiles /app/property-data4/noise_lden_10m.pmtiles --crime-hotspot-tiles /app/property-data4/crime_hotspots.pmtiles --tree-overlay-tiles /app/property-data4/trees_outside_woodlands.pmtiles --property-border-tiles /app/property-data4/property_borders.pmtiles'
" "
ports: ports:
- "8001:8001" - "8001:8001"
@ -51,8 +51,6 @@ services:
BUGSINK_ENVIRONMENT: ${BUGSINK_ENVIRONMENT:-development} BUGSINK_ENVIRONMENT: ${BUGSINK_ENVIRONMENT:-development}
BUGSINK_RELEASE: ${BUGSINK_RELEASE:-} BUGSINK_RELEASE: ${BUGSINK_RELEASE:-}
BUGSINK_SEND_DEFAULT_PII: ${BUGSINK_SEND_DEFAULT_PII:-false} BUGSINK_SEND_DEFAULT_PII: ${BUGSINK_SEND_DEFAULT_PII:-false}
ACTUAL_LISTINGS_PATH: /app/finder/data/online_listings_buy_enriched.parquet
CRIME_BY_YEAR_PATH: /app/property-data4/crime_by_postcode_by_year.parquet
depends_on: depends_on:
screenshot: screenshot:
condition: service_healthy condition: service_healthy

View file

@ -21,10 +21,6 @@ pub struct FrontendConfig {
pub send_default_pii: bool, pub send_default_pii: bool,
} }
pub fn env_nonempty(name: &str) -> Option<String> {
std::env::var(name).ok().and_then(nonempty)
}
pub fn nonempty(value: String) -> Option<String> { pub fn nonempty(value: String) -> Option<String> {
let trimmed = value.trim(); let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.to_owned()) (!trimmed.is_empty()).then(|| trimmed.to_owned())

View file

@ -40,14 +40,6 @@ pub struct CrimeByYearData {
} }
impl CrimeByYearData { impl CrimeByYearData {
pub fn empty() -> Self {
Self {
crime_types: Vec::new(),
years_by_type: Vec::new(),
series_by_postcode: FxHashMap::default(),
}
}
pub fn load(path: &Path) -> anyhow::Result<Self> { pub fn load(path: &Path) -> anyhow::Result<Self> {
run_polars_io(|| Self::load_inner(path)) run_polars_io(|| Self::load_inner(path))
} }

View file

@ -251,29 +251,29 @@ struct Cli {
#[arg(long)] #[arg(long)]
tiles: PathBuf, tiles: PathBuf,
/// Optional PMTiles raster basemap for satellite imagery. /// PMTiles raster basemap for satellite imagery.
#[arg(long, env = "SATELLITE_TILES")] #[arg(long, env = "SATELLITE_TILES")]
satellite_tiles: Option<PathBuf>, satellite_tiles: PathBuf,
/// Optional PMTiles raster overlay for high-resolution EA aerial photography. /// PMTiles raster overlay for high-resolution EA aerial photography.
#[arg(long, env = "SATELLITE_HIGHRES_TILES")] #[arg(long, env = "SATELLITE_HIGHRES_TILES")]
satellite_highres_tiles: Option<PathBuf>, satellite_highres_tiles: PathBuf,
/// Optional PMTiles raster overlay for high-resolution strategic noise. /// PMTiles raster overlay for high-resolution strategic noise.
#[arg(long, env = "NOISE_OVERLAY_TILES")] #[arg(long, env = "NOISE_OVERLAY_TILES")]
noise_overlay_tiles: Option<PathBuf>, noise_overlay_tiles: PathBuf,
/// Optional PMTiles vector overlay for crime heatmap points. /// PMTiles vector overlay for crime heatmap points.
#[arg(long, env = "CRIME_HOTSPOT_TILES")] #[arg(long, env = "CRIME_HOTSPOT_TILES")]
crime_hotspot_tiles: Option<PathBuf>, crime_hotspot_tiles: PathBuf,
/// Optional PMTiles vector overlay for Trees Outside Woodland polygons. /// PMTiles vector overlay for Trees Outside Woodland polygons.
#[arg(long, env = "TREE_OVERLAY_TILES")] #[arg(long, env = "TREE_OVERLAY_TILES")]
tree_overlay_tiles: Option<PathBuf>, tree_overlay_tiles: PathBuf,
/// Optional PMTiles vector overlay for INSPIRE property-border polygons. /// PMTiles vector overlay for INSPIRE property-border polygons.
#[arg(long, env = "PROPERTY_BORDER_TILES")] #[arg(long, env = "PROPERTY_BORDER_TILES")]
property_border_tiles: Option<PathBuf>, property_border_tiles: PathBuf,
/// Path to the frontend dist directory (optional; disables static serving and OG injection when omitted) /// Path to the frontend dist directory (optional; disables static serving and OG injection when omitted)
#[arg(long)] #[arg(long)]
@ -311,13 +311,13 @@ struct Cli {
#[arg(long, env = "TRAVEL_TIMES")] #[arg(long, env = "TRAVEL_TIMES")]
travel_times: PathBuf, travel_times: PathBuf,
/// Optional path to a parquet of live online listings (Rightmove etc.) to overlay on the map. /// Path to a parquet of live online listings (Rightmove etc.) to overlay on the map.
#[arg(long, env = "ACTUAL_LISTINGS_PATH")] #[arg(long, env = "ACTUAL_LISTINGS_PATH")]
actual_listings_path: Option<PathBuf>, actual_listings_path: PathBuf,
/// Optional path to the per-LSOA per-year crime parquet (display-only side table for the right pane). /// Path to the per-LSOA per-year crime parquet (display-only side table for the right pane).
#[arg(long, env = "CRIME_BY_YEAR_PATH")] #[arg(long, env = "CRIME_BY_YEAR_PATH")]
crime_by_year_path: Option<PathBuf>, crime_by_year_path: PathBuf,
/// Google Maps API key for Street View metadata lookups /// Google Maps API key for Street View metadata lookups
#[arg(long, env = "GOOGLE_MAPS_API_KEY")] #[arg(long, env = "GOOGLE_MAPS_API_KEY")]
@ -345,22 +345,22 @@ struct Cli {
/// Bugsink DSN for backend error reporting /// Bugsink DSN for backend error reporting
#[arg(long, env = "BUGSINK_DSN", hide_env_values = true)] #[arg(long, env = "BUGSINK_DSN", hide_env_values = true)]
bugsink_dsn: Option<String>, bugsink_dsn: String,
/// Bugsink DSN injected into the browser app; falls back to BUGSINK_DSN when omitted /// Bugsink DSN injected into the browser app
#[arg(long, env = "FRONTEND_BUGSINK_DSN", hide_env_values = true)] #[arg(long, env = "FRONTEND_BUGSINK_DSN", hide_env_values = true)]
frontend_bugsink_dsn: Option<String>, frontend_bugsink_dsn: String,
/// Bugsink/Sentry environment name /// Bugsink/Sentry environment name
#[arg(long, env = "BUGSINK_ENVIRONMENT")] #[arg(long, env = "BUGSINK_ENVIRONMENT")]
bugsink_environment: Option<String>, bugsink_environment: String,
/// Bugsink/Sentry release name /// Bugsink/Sentry release name
#[arg(long, env = "BUGSINK_RELEASE")] #[arg(long, env = "BUGSINK_RELEASE")]
bugsink_release: Option<String>, bugsink_release: String,
/// Include default PII in Bugsink events /// Include default PII in Bugsink events
#[arg(long, env = "BUGSINK_SEND_DEFAULT_PII", default_value_t = false)] #[arg(long, env = "BUGSINK_SEND_DEFAULT_PII", action = clap::ArgAction::Set)]
bugsink_send_default_pii: bool, bugsink_send_default_pii: bool,
} }
@ -404,49 +404,19 @@ async fn init_required_tile_reader(
Ok(Arc::new(routes::init_tile_reader(path).await?)) Ok(Arc::new(routes::init_tile_reader(path).await?))
} }
fn configured_or_default_overlay_path(
configured: &Option<PathBuf>,
tiles_path: &Path,
file_name: &str,
) -> PathBuf {
if let Some(path) = configured {
return path.clone();
}
tiles_path
.parent()
.map(|parent| parent.join(file_name))
.unwrap_or_else(|| PathBuf::from(file_name))
}
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
let bugsink_environment = cli
.bugsink_environment
.clone()
.or_else(|| bugsink::env_nonempty("SENTRY_ENVIRONMENT"));
let bugsink_release = cli
.bugsink_release
.clone()
.or_else(|| bugsink::env_nonempty("SENTRY_RELEASE"));
let backend_bugsink_dsn = cli
.bugsink_dsn
.clone()
.or_else(|| bugsink::env_nonempty("SENTRY_DSN"));
let _bugsink_guard = bugsink::init_backend(&bugsink::BackendConfig { let _bugsink_guard = bugsink::init_backend(&bugsink::BackendConfig {
dsn: backend_bugsink_dsn.clone(), dsn: Some(cli.bugsink_dsn.clone()),
environment: bugsink_environment.clone(), environment: Some(cli.bugsink_environment.clone()),
release: bugsink_release.clone(), release: Some(cli.bugsink_release.clone()),
send_default_pii: cli.bugsink_send_default_pii, send_default_pii: cli.bugsink_send_default_pii,
}); });
let bugsink_frontend_config = bugsink::frontend_config( let bugsink_frontend_config = bugsink::frontend_config(
cli.frontend_bugsink_dsn Some(cli.frontend_bugsink_dsn.clone()),
.clone() Some(cli.bugsink_environment.clone()),
.or_else(|| bugsink::env_nonempty("PUBLIC_BUGSINK_DSN")) Some(cli.bugsink_release.clone()),
.or(backend_bugsink_dsn),
bugsink_environment.clone(),
bugsink_release.clone(),
cli.bugsink_send_default_pii, cli.bugsink_send_default_pii,
); );
@ -569,44 +539,17 @@ async fn main() -> anyhow::Result<()> {
let tile_reader = Arc::new(routes::init_tile_reader(tiles_path).await?); let tile_reader = Arc::new(routes::init_tile_reader(tiles_path).await?);
info!("PMTiles loaded successfully"); info!("PMTiles loaded successfully");
let noise_overlay_tiles = configured_or_default_overlay_path( let noise_overlay_reader =
&cli.noise_overlay_tiles, init_required_tile_reader("Noise", &cli.noise_overlay_tiles).await?;
tiles_path, let satellite_reader = init_required_tile_reader("Satellite", &cli.satellite_tiles).await?;
"noise_lden_10m.pmtiles",
);
let satellite_tiles =
configured_or_default_overlay_path(&cli.satellite_tiles, tiles_path, "satellite.pmtiles");
let satellite_highres_tiles = configured_or_default_overlay_path(
&cli.satellite_highres_tiles,
tiles_path,
"satellite_highres.pmtiles",
);
let crime_hotspot_tiles = configured_or_default_overlay_path(
&cli.crime_hotspot_tiles,
tiles_path,
"crime_hotspots.pmtiles",
);
let tree_overlay_tiles = configured_or_default_overlay_path(
&cli.tree_overlay_tiles,
tiles_path,
"trees_outside_woodlands.pmtiles",
);
let property_border_tiles = configured_or_default_overlay_path(
&cli.property_border_tiles,
tiles_path,
"property_borders.pmtiles",
);
let noise_overlay_reader = init_required_tile_reader("Noise", &noise_overlay_tiles).await?;
let satellite_reader = init_required_tile_reader("Satellite", &satellite_tiles).await?;
let satellite_highres_reader = let satellite_highres_reader =
init_required_tile_reader("Satellite high-res", &satellite_highres_tiles).await?; init_required_tile_reader("Satellite high-res", &cli.satellite_highres_tiles).await?;
let crime_hotspot_reader = let crime_hotspot_reader =
init_required_tile_reader("Crime hotspots", &crime_hotspot_tiles).await?; init_required_tile_reader("Crime hotspots", &cli.crime_hotspot_tiles).await?;
let tree_overlay_reader = let tree_overlay_reader =
init_required_tile_reader("Trees outside woodland", &tree_overlay_tiles).await?; init_required_tile_reader("Trees outside woodland", &cli.tree_overlay_tiles).await?;
let property_border_reader = let property_border_reader =
init_required_tile_reader("Property borders", &property_border_tiles).await?; init_required_tile_reader("Property borders", &cli.property_border_tiles).await?;
let feature_name_to_index: rustc_hash::FxHashMap<String, usize> = property_data let feature_name_to_index: rustc_hash::FxHashMap<String, usize> = property_data
.feature_names .feature_names
@ -720,7 +663,8 @@ async fn main() -> anyhow::Result<()> {
let superuser_token_cache = Arc::new(pocketbase::SuperuserTokenCache::new()); let superuser_token_cache = Arc::new(pocketbase::SuperuserTokenCache::new());
let share_cache = Arc::new(licensing::ShareBoundsCache::new()); let share_cache = Arc::new(licensing::ShareBoundsCache::new());
let actual_listings = if let Some(path) = cli.actual_listings_path.as_ref() { let actual_listings = {
let path = &cli.actual_listings_path;
if !path.exists() { if !path.exists() {
bail!("Actual listings parquet not found: {}", path.display()); bail!("Actual listings parquet not found: {}", path.display());
} }
@ -728,22 +672,17 @@ async fn main() -> anyhow::Result<()> {
let listings = data::ActualListingData::load(path, &property_data)?; let listings = data::ActualListingData::load(path, &property_data)?;
trim_allocator("actual listings load"); trim_allocator("actual listings load");
info!(rows = listings.lat.len(), "Actual listings loaded"); info!(rows = listings.lat.len(), "Actual listings loaded");
Some(Arc::new(listings)) Arc::new(listings)
} else {
info!("ACTUAL_LISTINGS_PATH not set; live listings overlay disabled");
None
}; };
let crime_by_year = if let Some(path) = cli.crime_by_year_path.as_ref() { let crime_by_year = {
let path = &cli.crime_by_year_path;
if !path.exists() { if !path.exists() {
bail!("Crime-by-year parquet not found: {}", path.display()); bail!("Crime-by-year parquet not found: {}", path.display());
} }
let data = data::CrimeByYearData::load(path)?; let data = data::CrimeByYearData::load(path)?;
trim_allocator("crime-by-year load"); trim_allocator("crime-by-year load");
Arc::new(data) Arc::new(data)
} else {
info!("CRIME_BY_YEAR_PATH not set; crime-over-time chart disabled");
Arc::new(data::CrimeByYearData::empty())
}; };
let app_state = AppState { let app_state = AppState {

View file

@ -65,14 +65,7 @@ pub async fn get_actual_listings(
); );
} }
let Some(actual_listings) = state.actual_listings.clone() else { let actual_listings = state.actual_listings.clone();
return Ok(Json(ActualListingsResponse {
listings: Vec::new(),
total: 0,
offset,
truncated: false,
}));
};
let (south, west, north, east) = let (south, west, north, east) =
require_bounds(params.bounds).map_err(IntoResponse::into_response)?; require_bounds(params.bounds).map_err(IntoResponse::into_response)?;

View file

@ -44,10 +44,9 @@ pub struct AppState {
pub poi_category_groups: Arc<Vec<POICategoryGroup>>, pub poi_category_groups: Arc<Vec<POICategoryGroup>>,
/// Precomputed travel time data store /// Precomputed travel time data store
pub travel_time_store: Arc<TravelTimeStore>, pub travel_time_store: Arc<TravelTimeStore>,
/// Optional real-world listings (e.g. Rightmove / Zoopla data) loaded from ACTUAL_LISTINGS_PATH. /// Real-world listings (e.g. Rightmove / Zoopla data) loaded from ACTUAL_LISTINGS_PATH.
pub actual_listings: Option<Arc<ActualListingData>>, pub actual_listings: Arc<ActualListingData>,
/// Per-LSOA per-year crime counts used by the right pane to plot trends. /// Per-LSOA per-year crime counts used by the right pane to plot trends.
/// Empty when the side parquet was not supplied.
pub crime_by_year: Arc<CrimeByYearData>, pub crime_by_year: Arc<CrimeByYearData>,
/// Token validation cache (60s TTL) /// Token validation cache (60s TTL)
pub token_cache: Arc<TokenCache>, pub token_cache: Arc<TokenCache>,