Update data

This commit is contained in:
Andras Schmelczer 2026-05-14 08:17:10 +01:00
parent a4103b0896
commit 273d7a83ee
15 changed files with 716 additions and 316 deletions

View file

@ -215,6 +215,14 @@ struct Cli {
#[arg(long, env = "STRIPE_REFERRAL_COUPON_ID")]
stripe_referral_coupon_id: String,
/// Bearer token required to scrape /metrics.
#[arg(long, env = "METRICS_BEARER_TOKEN")]
metrics_bearer_token: Option<String>,
/// Allow unauthenticated /metrics scraping when no METRICS_BEARER_TOKEN is set.
#[arg(long, env = "ALLOW_PUBLIC_METRICS", default_value_t = false)]
allow_public_metrics: bool,
/// Google OAuth client ID for PocketBase SSO
#[arg(long, env = "GOOGLE_OAUTH_CLIENT_ID")]
google_oauth_client_id: String,
@ -246,6 +254,8 @@ async fn main() -> anyhow::Result<()> {
info!("Prometheus metrics initialized");
let cli = Cli::parse();
let metrics_bearer_token = cli.metrics_bearer_token.clone();
let allow_public_metrics = cli.allow_public_metrics;
for (label, path) in [
("Properties", &cli.properties),
@ -510,7 +520,10 @@ async fn main() -> anyhow::Result<()> {
let public_url_tiles = initial_state.public_url.clone();
let api = Router::new()
.route("/api/features", get(routes::get_features))
.route(
"/api/features",
get(routes::get_features).layer(ConcurrencyLimitLayer::new(20)),
)
.route(
"/api/hexagons",
get(routes::get_hexagons).layer(ConcurrencyLimitLayer::new(20)),
@ -519,30 +532,57 @@ async fn main() -> anyhow::Result<()> {
"/api/postcodes",
get(routes::get_postcodes).layer(ConcurrencyLimitLayer::new(20)),
)
.route("/api/postcode/{postcode}", get(routes::get_postcode_lookup))
.route("/api/nearest-postcode", get(routes::get_nearest_postcode))
.route(
"/api/postcode/{postcode}",
get(routes::get_postcode_lookup).layer(ConcurrencyLimitLayer::new(20)),
)
.route(
"/api/nearest-postcode",
get(routes::get_nearest_postcode).layer(ConcurrencyLimitLayer::new(10)),
)
.route(
"/api/pois",
get(routes::get_pois).layer(ConcurrencyLimitLayer::new(20)),
)
.route("/api/poi-categories", get(routes::get_poi_categories))
.route("/api/places", get(routes::get_places))
.route("/api/travel-modes", get(routes::get_travel_modes))
.route(
"/api/poi-categories",
get(routes::get_poi_categories).layer(ConcurrencyLimitLayer::new(20)),
)
.route(
"/api/places",
get(routes::get_places).layer(ConcurrencyLimitLayer::new(10)),
)
.route(
"/api/travel-modes",
get(routes::get_travel_modes).layer(ConcurrencyLimitLayer::new(20)),
)
.route(
"/api/travel-destinations",
get(routes::get_travel_destinations),
get(routes::get_travel_destinations).layer(ConcurrencyLimitLayer::new(10)),
)
.route(
"/api/journey",
get(routes::get_journey).layer(ConcurrencyLimitLayer::new(10)),
)
.route("/api/journey", get(routes::get_journey))
.route(
"/api/hexagon-properties",
get(routes::get_hexagon_properties),
get(routes::get_hexagon_properties).layer(ConcurrencyLimitLayer::new(10)),
)
.route(
"/api/filter-counts",
get(routes::get_filter_counts).layer(ConcurrencyLimitLayer::new(5)),
)
.route(
"/api/hexagon-stats",
get(routes::get_hexagon_stats).layer(ConcurrencyLimitLayer::new(5)),
)
.route(
"/api/postcode-stats",
get(routes::get_postcode_stats).layer(ConcurrencyLimitLayer::new(5)),
)
.route("/api/filter-counts", get(routes::get_filter_counts))
.route("/api/hexagon-stats", get(routes::get_hexagon_stats))
.route("/api/postcode-stats", get(routes::get_postcode_stats))
.route(
"/api/postcode-properties",
get(routes::get_postcode_properties),
get(routes::get_postcode_properties).layer(ConcurrencyLimitLayer::new(10)),
)
.route(
"/api/screenshot",
@ -552,13 +592,26 @@ async fn main() -> anyhow::Result<()> {
"/api/export",
get(routes::get_export).layer(ConcurrencyLimitLayer::new(3)),
)
.route("/api/me", get(routes::get_me))
.route("/api/shorten", post(routes::post_shorten))
.route(
"/api/me",
get(routes::get_me).layer(ConcurrencyLimitLayer::new(20)),
)
.route(
"/api/shorten",
post(routes::post_shorten).layer(ConcurrencyLimitLayer::new(5)),
)
.route(
"/api/share-links",
get(routes::get_share_links).layer(ConcurrencyLimitLayer::new(10)),
)
.route(
"/api/ai-filters",
post(routes::post_ai_filters).layer(ConcurrencyLimitLayer::new(5)),
)
.route("/api/streetview", get(routes::get_streetview))
.route(
"/api/streetview",
get(routes::get_streetview).layer(ConcurrencyLimitLayer::new(5)),
)
.route(
"/api/rightmove-search",
get(routes::get_rightmove_redirect).layer(ConcurrencyLimitLayer::new(10)),
@ -567,23 +620,44 @@ async fn main() -> anyhow::Result<()> {
"/api/newsletter",
patch(routes::patch_newsletter).layer(ConcurrencyLimitLayer::new(10)),
)
.route("/api/pricing", get(routes::get_pricing))
.route(
"/api/pricing",
get(routes::get_pricing).layer(ConcurrencyLimitLayer::new(20)),
)
.route(
"/api/checkout",
post(routes::post_checkout).layer(ConcurrencyLimitLayer::new(10)),
)
.route("/api/stripe-webhook", post(routes::post_stripe_webhook))
.route(
"/api/stripe-webhook",
post(routes::post_stripe_webhook).layer(ConcurrencyLimitLayer::new(10)),
)
.route(
"/api/invites",
get(routes::get_invites).post(routes::post_invites),
get(routes::get_invites)
.post(routes::post_invites)
.layer(ConcurrencyLimitLayer::new(5)),
)
.route(
"/api/invite/{code}",
get(routes::get_invite).layer(ConcurrencyLimitLayer::new(5)),
)
.route(
"/api/redeem-invite",
post(routes::post_redeem_invite).layer(ConcurrencyLimitLayer::new(5)),
)
.route(
"/s/{code}",
get(routes::get_short_url).layer(ConcurrencyLimitLayer::new(10)),
)
.route("/api/invite/{code}", get(routes::get_invite))
.route("/api/redeem-invite", post(routes::post_redeem_invite))
.route("/s/{code}", get(routes::get_short_url))
.route(
"/api/telemetry",
post(routes::post_telemetry).layer(ConcurrencyLimitLayer::new(20)),
)
.route(
"/pb/api/realtime",
any(routes::proxy_to_pocketbase).layer(ConcurrencyLimitLayer::new(50)),
)
.route(
"/pb/{*rest}",
any(routes::proxy_to_pocketbase).layer(ConcurrencyLimitLayer::new(10)),
@ -591,19 +665,28 @@ async fn main() -> anyhow::Result<()> {
// Tile routes use a different state type — kept as closures
.route(
"/api/tiles/{z}/{x}/{y}",
get(move |path| routes::get_tile(axum::extract::State(reader_tile.clone()), path)),
get(move |path| routes::get_tile(axum::extract::State(reader_tile.clone()), path))
.layer(ConcurrencyLimitLayer::new(30)),
)
.route(
"/api/tiles/style.json",
get(move |query| {
let pu = public_url_tiles.clone();
routes::get_style(axum::extract::State(reader_style.clone()), pu, query)
}),
})
.layer(ConcurrencyLimitLayer::new(20)),
)
.route("/health", get(|| async { "ok" }))
.route(
"/metrics",
get(move || metrics::metrics_handler(metrics_handle.clone())),
get(move |headers| {
metrics::metrics_handler(
metrics_handle.clone(),
metrics_bearer_token.clone(),
allow_public_metrics,
headers,
)
}),
)
.with_state(shared.clone());

View file

@ -8,7 +8,7 @@ use serde::Deserialize;
use tracing::{info, warn};
use crate::auth::OptionalUser;
use crate::consts::{DEFAULT_PROPERTIES_LIMIT, MAX_PROPERTIES_LIMIT, POSTCODE_SEARCH_OFFSET};
use crate::consts::{DEFAULT_PROPERTIES_LIMIT, POSTCODE_SEARCH_OFFSET};
use crate::licensing::{check_license_point, resolve_share_code};
use crate::parsing::{parse_filters_with_poi, row_passes_filters, row_passes_poi_filters};
use crate::state::SharedState;
@ -151,10 +151,7 @@ pub async fn get_postcode_properties(
});
let total = matching_rows.len();
let limit = params
.limit
.unwrap_or(DEFAULT_PROPERTIES_LIMIT)
.min(MAX_PROPERTIES_LIMIT);
let limit = params.limit.unwrap_or(DEFAULT_PROPERTIES_LIMIT);
let page_offset = params.offset.unwrap_or(0);
let truncated = total > page_offset + limit;

View file

@ -12,7 +12,6 @@ use tracing::info;
use crate::aggregation::{Aggregator, EnumDistConfig, PoiAggregator};
use crate::auth::OptionalUser;
use crate::consts::MAX_CELLS_PER_REQUEST;
use crate::data::travel_time::TravelData;
use crate::licensing::{check_license_bounds, resolve_share_code};
use crate::parsing::{
@ -354,73 +353,61 @@ pub async fn get_postcodes(
features.push(feature);
included_postcodes.insert(pc_idx);
if features.len() >= MAX_CELLS_PER_REQUEST {
break;
}
}
if features.len() < MAX_CELLS_PER_REQUEST {
for pc_idx in selectable_postcodes {
if included_postcodes.contains(&pc_idx) {
continue;
}
let (pc_south, pc_west, pc_north, pc_east) = postcode_data.aabbs[pc_idx];
if !bounds_intersect(
pc_south as f64,
pc_west as f64,
pc_north as f64,
pc_east as f64,
south,
west,
north,
east,
) {
filtered_out += 1;
continue;
}
let geometry = postcode_data.geometry_geojson(pc_idx);
let centroid = postcode_data.centroids[pc_idx];
let mut props = Map::new();
props.insert(
"postcode".into(),
Value::String(postcode_data.postcodes[pc_idx].clone()),
);
props.insert("count".into(), Value::from(0));
props.insert(
"centroid".into(),
Value::Array(vec![
Value::from(centroid.1 as f64),
Value::from(centroid.0 as f64),
]),
);
let mut feature = Map::new();
feature.insert("type".into(), Value::String("Feature".into()));
feature.insert("geometry".into(), geometry);
feature.insert("properties".into(), Value::Object(props));
features.push(feature);
if features.len() >= MAX_CELLS_PER_REQUEST {
break;
}
for pc_idx in selectable_postcodes {
if included_postcodes.contains(&pc_idx) {
continue;
}
let (pc_south, pc_west, pc_north, pc_east) = postcode_data.aabbs[pc_idx];
if !bounds_intersect(
pc_south as f64,
pc_west as f64,
pc_north as f64,
pc_east as f64,
south,
west,
north,
east,
) {
filtered_out += 1;
continue;
}
let geometry = postcode_data.geometry_geojson(pc_idx);
let centroid = postcode_data.centroids[pc_idx];
let mut props = Map::new();
props.insert(
"postcode".into(),
Value::String(postcode_data.postcodes[pc_idx].clone()),
);
props.insert("count".into(), Value::from(0));
props.insert(
"centroid".into(),
Value::Array(vec![
Value::from(centroid.1 as f64),
Value::from(centroid.0 as f64),
]),
);
let mut feature = Map::new();
feature.insert("type".into(), Value::String("Feature".into()));
feature.insert("geometry".into(), geometry);
feature.insert("properties".into(), Value::Object(props));
features.push(feature);
}
histogram!("postcodes_response_count").record(features.len() as f64);
let truncated = features.len() >= MAX_CELLS_PER_REQUEST;
let t_total = t0.elapsed();
info!(
postcodes_before_filter,
matching_postcodes,
postcodes_after_filter = features.len(),
filtered_out,
truncated,
bounds = format_args!("{:.6},{:.6},{:.6},{:.6}", south, west, north, east),
filters = num_filters,
filters_raw = filters_str.as_deref().unwrap_or("-"),