better transit times

This commit is contained in:
Andras Schmelczer 2026-02-22 11:13:39 +00:00
parent 974f005549
commit 205302dbb8
22 changed files with 247 additions and 69 deletions

View file

@ -95,6 +95,9 @@ async fn validate_token(
.ok()?;
if !res.status().is_success() {
let status = res.status();
let body = res.text().await.unwrap_or_default();
warn!("PocketBase auth-refresh returned {status}: {body}");
return None;
}

View file

@ -17,6 +17,9 @@ pub const POSTCODE_SEARCH_OFFSET: f64 = 0.02;
pub const AI_FILTERS_MAX_TOKENS: usize = 2000;
pub const AI_FILTERS_TEMPERATURE: f32 = 0.0;
/// Timeout for outbound HTTP service calls (seconds).
pub const SERVICE_CALL_TIMEOUT: u64 = 120;
/// Inner London free zone bounds (south, west, north, east) — roughly zones 12.
/// Users without a license can only query data within these bounds.
pub const FREE_ZONE_BOUNDS: (f64, f64, f64, f64) = (51.42, -0.34, 51.60, 0.14);
@ -24,5 +27,5 @@ pub const FREE_ZONE_BOUNDS: (f64, f64, f64, f64) = (51.42, -0.34, 51.60, 0.14);
/// Homepage demo center (lat, lng). Unlicensed hexagon requests are allowed
/// when the center of the requested bounds is within DEMO_CENTER_TOLERANCE of this point.
/// Must match DEMO_VIEW_START in ScrollStory.tsx.
pub const DEMO_CENTER: (f64, f64) = (52.2, -1.9);
pub const DEMO_CENTER: (f64, f64) = (51.51, -0.12);
pub const DEMO_CENTER_TOLERANCE: f64 = 1.0;

View file

@ -17,6 +17,7 @@ use std::sync::Arc;
use std::time::Duration;
use anyhow::{bail, Context};
use consts::SERVICE_CALL_TIMEOUT;
use axum::middleware;
use axum::routing::{any, get, patch, post};
use axum::Router;
@ -286,7 +287,7 @@ async fn main() -> anyhow::Result<()> {
};
let http_client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.timeout(Duration::from_secs(SERVICE_CALL_TIMEOUT))
.connect_timeout(Duration::from_secs(5))
.build()
.context("Failed to build HTTP client")?;

View file

@ -17,7 +17,7 @@ pub struct ParsedEnumFilter {
pub allowed: FxHashSet<u32>,
}
/// Parse comma-separated filter string into numeric and enum filters.
/// Parse `;;`-separated filter string into numeric and enum filters.
/// Numeric format: `name:min:max`
/// Enum format: `name:val1|val2|val3` (pipe-separated string values)
///
@ -35,7 +35,7 @@ pub fn parse_filters(
None => return Ok((numeric, enums)),
};
for entry in input.split(',') {
for entry in input.split(";;") {
let parts: Vec<&str> = entry.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(format!("Malformed filter entry (missing ':'): '{entry}'"));
@ -234,7 +234,7 @@ mod tests {
#[test]
fn parse_multiple_numeric_filters() {
let (numeric, _enums) = parse_filters(
Some("Price:100000:500000,Area:50:200"),
Some("Price:100000:500000;;Area:50:200"),
&extended_feature_map(),
&extended_enum_values(),
)
@ -248,7 +248,7 @@ mod tests {
#[test]
fn parse_mixed_filters() {
let (numeric, enums) = parse_filters(
Some("Price:100000:500000,Type:Semi|Terraced"),
Some("Price:100000:500000;;Type:Semi|Terraced"),
&extended_feature_map(),
&extended_enum_values(),
)
@ -288,7 +288,7 @@ mod tests {
#[test]
fn parse_filter_with_whitespace() {
let (numeric, enums) = parse_filters(
Some("Price : 100000 : 500000 , Type : Detached | Flats/Maisonettes"),
Some("Price : 100000 : 500000 ;; Type : Detached | Flats/Maisonettes"),
&extended_feature_map(),
&extended_enum_values(),
)

View file

@ -84,7 +84,7 @@ fn extract_filter_feature_names(filters_str: Option<&str>) -> Vec<String> {
None => return Vec::new(),
};
let mut names = Vec::new();
for entry in input.split(',') {
for entry in input.split(";;") {
let parts: Vec<&str> = entry.splitn(2, ':').collect();
if parts.len() == 2 {
let name = parts[0].trim().to_string();
@ -110,7 +110,7 @@ fn build_frontend_params(
];
if let Some(fs) = filters_str {
if !fs.is_empty() {
for entry in fs.split(',') {
for entry in fs.split(";;") {
if !entry.is_empty() {
parts.push(format!("filter={}", urlencoding::encode(entry.trim())));
}

View file

@ -30,7 +30,7 @@ pub struct HexagonsResponse {
pub struct HexagonParams {
resolution: u8,
bounds: Option<String>,
/// Comma-separated filters: `name:min:max,...`
/// `;;`-separated filters: `name:min:max;;...`
filters: Option<String>,
/// Comma-separated feature names to include in min/max aggregation.
fields: Option<String>,
@ -191,7 +191,7 @@ pub async fn get_hexagons(
require_bounds(params.bounds).map_err(IntoResponse::into_response)?;
// Allow the homepage demo: check if the center of the requested bounds
// is near the demo view center (52.2, -1.9).
// is near the demo view center (51.51, -0.12).
let center_lat = (south + north) / 2.0;
let center_lng = (west + east) / 2.0;
let is_demo_view = (center_lat - DEMO_CENTER.0).abs() <= DEMO_CENTER_TOLERANCE

View file

@ -27,7 +27,7 @@ pub struct PostcodesResponse {
#[derive(Deserialize)]
pub struct PostcodeParams {
bounds: Option<String>,
/// Comma-separated filters: `name:min:max,...`
/// `;;`-separated filters: `name:min:max;;...`
filters: Option<String>,
/// Comma-separated feature names to include in min/max aggregation.
fields: Option<String>,

View file

@ -11,7 +11,7 @@ use crate::state::AppState;
/// Pricing tiers: (cumulative user cap, price in pence).
const TIERS: &[(u64, u64)] = &[
(10, 0), // First 10 users: free
(1, 0), // First 10 users: free
(20, 1000), // Next 10: £10
(45, 2500), // Next 25: £25
(95, 5000), // Next 50: £50