Deploy again
This commit is contained in:
parent
ffe080adef
commit
787428f1a5
18 changed files with 717 additions and 223 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
.venv
|
.venv
|
||||||
**/node_modules
|
**/node_modules
|
||||||
**/dist
|
**/dist
|
||||||
|
r5-java
|
||||||
server-rs/target
|
server-rs/target
|
||||||
.git
|
.git
|
||||||
.task
|
.task
|
||||||
|
|
@ -8,3 +9,6 @@ server-rs/target
|
||||||
__pycache__
|
__pycache__
|
||||||
analyses/
|
analyses/
|
||||||
*.log
|
*.log
|
||||||
|
property-data
|
||||||
|
manual-data
|
||||||
|
!property-data/arcgis_data.parquet
|
||||||
|
|
|
||||||
14
Dockerfile
14
Dockerfile
|
|
@ -21,12 +21,12 @@ WORKDIR /app
|
||||||
COPY --from=server /app/server-rs/target/release/property-map-server ./
|
COPY --from=server /app/server-rs/target/release/property-map-server ./
|
||||||
COPY --from=frontend /app/frontend/dist ./frontend/dist/
|
COPY --from=frontend /app/frontend/dist ./frontend/dist/
|
||||||
|
|
||||||
COPY property-data/wide.parquet ./data/
|
# COPY property-data/wide.parquet ./data/
|
||||||
COPY property-data/filtered_uk_pois.parquet ./data/
|
# COPY property-data/filtered_uk_pois.parquet ./data/
|
||||||
COPY property-data/places.parquet ./data/
|
# COPY property-data/places.parquet ./data/
|
||||||
COPY property-data/uk.pmtiles ./data/
|
# COPY property-data/uk.pmtiles ./data/
|
||||||
COPY manual-data/postcode_boundaries ./data/postcode_boundaries/
|
# COPY manual-data/postcode_boundaries ./data/postcode_boundaries/
|
||||||
COPY property-data/travel-times ./data/travel-times/
|
# COPY property-data/travel-times ./data/travel-times/
|
||||||
|
|
||||||
RUN chown -R appuser:appuser /app
|
RUN chown -R appuser:appuser /app
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|
@ -34,4 +34,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 ["--data", "/app/data/wide.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", "--listings-buy", "/app/data-scraped/online_listings_buy.parquet", "--listings-rent", "/app/data-scraped/online_listings_rent.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"]
|
||||||
|
|
|
||||||
|
|
@ -10,20 +10,27 @@ services:
|
||||||
command: >
|
command: >
|
||||||
bash -c "
|
bash -c "
|
||||||
cargo install cargo-watch &&
|
cargo install cargo-watch &&
|
||||||
cargo watch -i logs/ -x 'run -- --properties /app/data/properties.parquet --postcode-features /app/data/postcode.parquet --listings-buy /app/data/online_listings_buy.parquet --listings-rent /app/data/online_listings_rent.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'
|
cargo watch -i logs/ -x 'run -- --properties /app/data/properties.parquet --postcode-features /app/data/postcode.parquet --listings-buy /app/data-scraped/online_listings_buy.parquet --listings-rent /app/data-scraped/online_listings_rent.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'
|
||||||
"
|
"
|
||||||
ports:
|
ports:
|
||||||
- "8001:8001"
|
- "8001:8001"
|
||||||
networks:
|
networks:
|
||||||
- dev-network
|
- dev-network
|
||||||
|
cap_add:
|
||||||
|
- IPC_LOCK
|
||||||
|
ulimits:
|
||||||
|
memlock:
|
||||||
|
soft: -1
|
||||||
|
hard: -1
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- cargo-registry:/usr/local/cargo/registry
|
- cargo-home:/usr/local/cargo
|
||||||
- cargo-target:/app/server-rs/target
|
- cargo-target:/app/server-rs/target
|
||||||
- ./property-data:/app/data:ro
|
- ./property-data:/app/data:ro
|
||||||
- ./property-data/travel-times:/app/data/travel-times:ro
|
- ./property-data/travel-times:/app/data/travel-times:ro
|
||||||
|
- /volumes/narrowit/property-data/scraped:/app/data-scraped:ro
|
||||||
environment:
|
environment:
|
||||||
POCKETBASE_URL: http://pocketbase:8090
|
POCKETBASE_URL: http://pocketbase:8090
|
||||||
POCKETBASE_ADMIN_EMAIL: *pb-email
|
POCKETBASE_ADMIN_EMAIL: *pb-email
|
||||||
|
|
@ -60,13 +67,6 @@ services:
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
reservations:
|
|
||||||
devices:
|
|
||||||
- driver: nvidia
|
|
||||||
capabilities: [ gpu ]
|
|
||||||
count: 1
|
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
init: true
|
init: true
|
||||||
|
|
@ -151,7 +151,7 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pb-data:
|
pb-data:
|
||||||
cargo-registry:
|
cargo-home:
|
||||||
cargo-target:
|
cargo-target:
|
||||||
frontend-node-modules:
|
frontend-node-modules:
|
||||||
screenshot-cache:
|
screenshot-cache:
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,7 @@ export default function App() {
|
||||||
searchesLoading={savedSearches.loading}
|
searchesLoading={savedSearches.loading}
|
||||||
onDeleteSearch={savedSearches.deleteSearch}
|
onDeleteSearch={savedSearches.deleteSearch}
|
||||||
onOpenSearch={(params) => {
|
onOpenSearch={(params) => {
|
||||||
window.location.href = `/?${params}`;
|
window.location.href = `/dashboard?${params}`;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : activePage === 'invite' && inviteCode ? (
|
) : activePage === 'invite' && inviteCode ? (
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export const MAP_MIN_ZOOM = 5.5;
|
||||||
export const BUFFER_MULTIPLIER = 1.5;
|
export const BUFFER_MULTIPLIER = 1.5;
|
||||||
|
|
||||||
/** Inner London free zone bounds (south, west, north, east) — must match server FREE_ZONE_BOUNDS */
|
/** Inner London free zone bounds (south, west, north, east) — must match server FREE_ZONE_BOUNDS */
|
||||||
export const FREE_ZONE_BOUNDS = { south: 51.48, west: -0.18, north: 51.54, east: -0.02 };
|
export const FREE_ZONE_BOUNDS = { south: 51.42, west: -0.34, north: 51.60, east: 0.14 };
|
||||||
|
|
||||||
export const INITIAL_VIEW_STATE: ViewState = {
|
export const INITIAL_VIEW_STATE: ViewState = {
|
||||||
longitude: (FREE_ZONE_BOUNDS.west + FREE_ZONE_BOUNDS.east) / 2,
|
longitude: (FREE_ZONE_BOUNDS.west + FREE_ZONE_BOUNDS.east) / 2,
|
||||||
|
|
|
||||||
1
server-rs/Cargo.lock
generated
1
server-rs/Cargo.lock
generated
|
|
@ -2450,6 +2450,7 @@ dependencies = [
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
"lasso",
|
"lasso",
|
||||||
|
"libc",
|
||||||
"metrics",
|
"metrics",
|
||||||
"metrics-exporter-prometheus",
|
"metrics-exporter-prometheus",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ hmac = "0.12"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
tower = { version = "0.5", features = ["limit"] }
|
tower = { version = "0.5", features = ["limit"] }
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
min_ident_chars = "warn"
|
min_ident_chars = "warn"
|
||||||
|
|
|
||||||
522
server-rs/logs/server.log.2026-02-19
Normal file
522
server-rs/logs/server.log.2026-02-19
Normal file
|
|
@ -0,0 +1,522 @@
|
||||||
|
2026-02-19T21:26:54.458535Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T21:26:54.458730Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T21:26:54.458738Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T21:26:54.560667Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T21:26:54.560677Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T21:27:01.536771Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
|
||||||
|
2026-02-19T21:27:01.536788Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
|
||||||
|
2026-02-19T21:27:01.858493Z INFO property_map_server::data::property: buy listings joined rows=444605
|
||||||
|
2026-02-19T21:27:01.858503Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
|
||||||
|
2026-02-19T21:27:01.970421Z INFO property_map_server::data::property: rent listings joined rows=125656
|
||||||
|
2026-02-19T21:27:01.970430Z INFO property_map_server::data::property: Concatenating all data sources
|
||||||
|
2026-02-19T21:27:52.277322Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
|
||||||
|
2026-02-19T21:27:52.277425Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
|
||||||
|
2026-02-19T21:27:53.590317Z INFO property_map_server::data::property: Combined data selected rows=15773642
|
||||||
|
2026-02-19T21:27:53.731832Z INFO property_map_server::data::property: Extracting numeric feature columns
|
||||||
|
2026-02-19T21:27:58.340459Z INFO property_map_server::data::property: Computing histograms for numeric features
|
||||||
|
2026-02-19T21:27:59.454079Z INFO property_map_server::data::property: Extracting string columns
|
||||||
|
2026-02-19T21:28:01.589530Z INFO property_map_server::data::property: Building enum features
|
||||||
|
2026-02-19T21:29:17.412343Z INFO property_map_server::data::property: Extracting renovation history
|
||||||
|
2026-02-19T21:29:19.625773Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
|
||||||
|
2026-02-19T21:29:19.625780Z INFO property_map_server::data::property: Extracting listing features
|
||||||
|
2026-02-19T21:29:21.764449Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
|
||||||
|
2026-02-19T21:29:21.764457Z INFO property_map_server::data::property: Sorting rows by spatial locality
|
||||||
|
2026-02-19T21:29:28.223647Z INFO property_map_server::data::property: Building interned strings
|
||||||
|
2026-02-19T21:30:05.730665Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
|
||||||
|
2026-02-19T21:32:02.361349Z INFO property_map_server::data::property: Data loading complete
|
||||||
|
2026-02-19T21:32:03.976002Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
|
||||||
|
2026-02-19T21:32:03.976011Z INFO property_map_server: Building spatial grid index (0.01° cells)
|
||||||
|
2026-02-19T21:32:04.076953Z INFO property_map_server: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T21:32:04.076963Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T21:32:04.487571Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
|
||||||
|
2026-02-19T21:32:04.490452Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
|
||||||
|
2026-02-19T21:32:04.490466Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
|
||||||
|
2026-02-19T21:32:04.740771Z INFO property_map_server::data::poi: Loaded 811937 POIs
|
||||||
|
2026-02-19T21:32:04.865428Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
|
||||||
|
2026-02-19T21:32:04.866050Z INFO property_map_server::data::poi: POI data loading complete.
|
||||||
|
2026-02-19T21:32:04.896188Z INFO property_map_server: POI data loaded pois=811937
|
||||||
|
2026-02-19T21:32:04.896196Z INFO property_map_server: Building POI spatial grid index
|
||||||
|
2026-02-19T21:32:04.903631Z INFO property_map_server: Loading place data from /app/data/places.parquet
|
||||||
|
2026-02-19T21:32:04.908488Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
|
||||||
|
2026-02-19T21:32:04.916283Z INFO property_map_server::data::places: Loaded 90807 places
|
||||||
|
2026-02-19T21:32:04.951763Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
|
||||||
|
2026-02-19T21:32:04.952866Z INFO property_map_server: Place data loaded places=90807
|
||||||
|
2026-02-19T21:32:04.952882Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
|
||||||
|
2026-02-19T21:32:04.952983Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
|
||||||
|
2026-02-19T21:32:04.956401Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
|
||||||
|
2026-02-19T21:32:07.669253Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
|
||||||
|
2026-02-19T21:32:07.669264Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
|
||||||
|
2026-02-19T21:32:07.669278Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
|
||||||
|
2026-02-19T21:32:07.677413Z INFO property_map_server: PMTiles loaded successfully
|
||||||
|
2026-02-19T21:32:07.729382Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
|
||||||
|
2026-02-19T21:32:07.792213Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
|
||||||
|
2026-02-19T21:32:07.792458Z INFO property_map_server: Precomputed features response groups=9
|
||||||
|
2026-02-19T21:32:07.792535Z INFO property_map_server: Precomputed AI filters schema and system prompt
|
||||||
|
2026-02-19T21:32:07.796454Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
|
||||||
|
2026-02-19T21:32:13.118788Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
|
||||||
|
2026-02-19T21:32:13.169893Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
|
||||||
|
2026-02-19T21:32:13.169910Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
|
||||||
|
2026-02-19T21:32:13.169913Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
|
||||||
|
2026-02-19T21:32:13.230971Z WARN property_map_server::pocketbase: PocketBase settings missing oauth2.providers array — cannot configure OAuth
|
||||||
|
2026-02-19T21:32:13.230981Z INFO property_map_server: Ollama configured: http://host.docker.internal:11434 (model: gpt-oss:20b)
|
||||||
|
2026-02-19T21:32:13.230992Z INFO property_map_server: Loading travel time data from /app/data/travel-times
|
||||||
|
2026-02-19T21:32:13.251928Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=76039
|
||||||
|
2026-02-19T21:32:13.272182Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=76039
|
||||||
|
2026-02-19T21:32:13.291900Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=69290
|
||||||
|
2026-02-19T21:32:13.291945Z INFO property_map_server: Travel time store loaded modes=3
|
||||||
|
2026-02-19T21:32:13.296486Z INFO property_map_server: Server listening on 0.0.0.0:8001
|
||||||
|
2026-02-19T21:32:14.023023Z INFO property_map_server::routes::features: GET /api/features
|
||||||
|
2026-02-19T21:32:16.790595Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=5 cells_before_filter=687 cells_after_filter=687 truncated=false bounds=47.0000,-14.0000,57.0000,10.0000 filters=0 filters_raw="-" travel_entries=0 agg_ms=1729.5 total_ms=1748.8
|
||||||
|
2026-02-19T21:32:23.004683Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11
|
||||||
|
2026-02-19T21:32:24.013755Z INFO property_map_server::routes::features: GET /api/features
|
||||||
|
2026-02-19T21:32:24.013792Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11
|
||||||
|
2026-02-19T21:32:25.049834Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=10 cells_before_filter=1419 cells_after_filter=1259 truncated=false bounds=51.4908,-0.1363,51.5292,-0.0637 filters=0 filters_raw="-" travel_entries=0 agg_ms=11.2 total_ms=38.8
|
||||||
|
2026-02-19T21:36:25.033898Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T21:36:25.034069Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T21:36:25.034079Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T21:36:25.129912Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T21:36:25.129921Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T21:36:32.499133Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T21:36:32.499319Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T21:36:32.499330Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T21:36:32.558294Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T21:36:32.558304Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T21:36:34.752324Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
|
||||||
|
2026-02-19T21:36:34.752339Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
|
||||||
|
2026-02-19T21:36:35.020717Z INFO property_map_server::data::property: buy listings joined rows=444605
|
||||||
|
2026-02-19T21:36:35.020727Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
|
||||||
|
2026-02-19T21:36:35.117921Z INFO property_map_server::data::property: rent listings joined rows=125656
|
||||||
|
2026-02-19T21:36:35.117931Z INFO property_map_server::data::property: Concatenating all data sources
|
||||||
|
2026-02-19T21:38:50.238928Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
|
||||||
|
2026-02-19T21:38:50.239009Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
|
||||||
|
2026-02-19T21:38:51.489522Z INFO property_map_server::data::property: Combined data selected rows=15773642
|
||||||
|
2026-02-19T21:38:51.661991Z INFO property_map_server::data::property: Extracting numeric feature columns
|
||||||
|
2026-02-19T21:39:38.714760Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T21:39:38.714936Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T21:39:38.714944Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T21:39:38.790493Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T21:39:38.790504Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T21:39:41.014520Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
|
||||||
|
2026-02-19T21:39:41.014538Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
|
||||||
|
2026-02-19T21:39:41.299979Z INFO property_map_server::data::property: buy listings joined rows=444605
|
||||||
|
2026-02-19T21:39:41.299990Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
|
||||||
|
2026-02-19T21:39:41.400048Z INFO property_map_server::data::property: rent listings joined rows=125656
|
||||||
|
2026-02-19T21:39:41.400059Z INFO property_map_server::data::property: Concatenating all data sources
|
||||||
|
2026-02-19T21:39:47.989385Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
|
||||||
|
2026-02-19T21:39:47.989481Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
|
||||||
|
2026-02-19T21:39:49.243773Z INFO property_map_server::data::property: Combined data selected rows=15773642
|
||||||
|
2026-02-19T21:39:49.422488Z INFO property_map_server::data::property: Extracting numeric feature columns
|
||||||
|
2026-02-19T21:39:54.408464Z INFO property_map_server::data::property: Computing histograms for numeric features
|
||||||
|
2026-02-19T21:39:55.491023Z INFO property_map_server::data::property: Extracting string columns
|
||||||
|
2026-02-19T21:39:57.641424Z INFO property_map_server::data::property: Building enum features
|
||||||
|
2026-02-19T21:40:09.935990Z INFO property_map_server::data::property: Extracting renovation history
|
||||||
|
2026-02-19T21:40:12.074163Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
|
||||||
|
2026-02-19T21:40:12.074172Z INFO property_map_server::data::property: Extracting listing features
|
||||||
|
2026-02-19T21:40:12.681574Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
|
||||||
|
2026-02-19T21:40:12.681588Z INFO property_map_server::data::property: Sorting rows by spatial locality
|
||||||
|
2026-02-19T21:40:18.238722Z INFO property_map_server::data::property: Building interned strings
|
||||||
|
2026-02-19T21:40:24.556588Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
|
||||||
|
2026-02-19T21:40:52.861550Z INFO property_map_server::data::property: Data loading complete
|
||||||
|
2026-02-19T21:40:54.156096Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
|
||||||
|
2026-02-19T21:40:54.156105Z INFO property_map_server: Building spatial grid index (0.01° cells)
|
||||||
|
2026-02-19T21:40:54.550391Z INFO property_map_server: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T21:40:54.550401Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T21:40:54.950194Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
|
||||||
|
2026-02-19T21:40:54.950226Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
|
||||||
|
2026-02-19T21:40:54.950233Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
|
||||||
|
2026-02-19T21:40:54.970688Z INFO property_map_server::data::poi: Loaded 811937 POIs
|
||||||
|
2026-02-19T21:40:55.091891Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
|
||||||
|
2026-02-19T21:40:55.092505Z INFO property_map_server::data::poi: POI data loading complete.
|
||||||
|
2026-02-19T21:40:55.122637Z INFO property_map_server: POI data loaded pois=811937
|
||||||
|
2026-02-19T21:40:55.122650Z INFO property_map_server: Building POI spatial grid index
|
||||||
|
2026-02-19T21:40:55.132909Z INFO property_map_server: Loading place data from /app/data/places.parquet
|
||||||
|
2026-02-19T21:40:55.132919Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
|
||||||
|
2026-02-19T21:40:55.135615Z INFO property_map_server::data::places: Loaded 90807 places
|
||||||
|
2026-02-19T21:40:55.155573Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
|
||||||
|
2026-02-19T21:40:55.157182Z INFO property_map_server: Place data loaded places=90807
|
||||||
|
2026-02-19T21:40:55.157198Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
|
||||||
|
2026-02-19T21:40:55.157202Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
|
||||||
|
2026-02-19T21:40:55.169027Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
|
||||||
|
2026-02-19T21:40:56.700286Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
|
||||||
|
2026-02-19T21:40:56.700297Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
|
||||||
|
2026-02-19T21:40:56.700310Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
|
||||||
|
2026-02-19T21:40:56.711874Z INFO property_map_server: PMTiles loaded successfully
|
||||||
|
2026-02-19T21:40:56.767004Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
|
||||||
|
2026-02-19T21:40:56.793907Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
|
||||||
|
2026-02-19T21:40:56.794046Z INFO property_map_server: Precomputed features response groups=9
|
||||||
|
2026-02-19T21:40:56.794089Z INFO property_map_server: Precomputed AI filters schema and system prompt
|
||||||
|
2026-02-19T21:40:56.794100Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
|
||||||
|
2026-02-19T21:40:56.883854Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
|
||||||
|
2026-02-19T21:40:56.887425Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
|
||||||
|
2026-02-19T21:40:56.887435Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
|
||||||
|
2026-02-19T21:40:56.887438Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
|
||||||
|
2026-02-19T21:40:56.936330Z WARN property_map_server::pocketbase: PocketBase settings missing oauth2.providers array — cannot configure OAuth
|
||||||
|
2026-02-19T21:40:56.936343Z INFO property_map_server: Ollama configured: http://host.docker.internal:11434 (model: gpt-oss:20b)
|
||||||
|
2026-02-19T21:40:56.936362Z INFO property_map_server: Loading travel time data from /app/data/travel-times
|
||||||
|
2026-02-19T21:40:57.078090Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=76039
|
||||||
|
2026-02-19T21:40:57.241363Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=76039
|
||||||
|
2026-02-19T21:40:57.424132Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=69290
|
||||||
|
2026-02-19T21:40:57.424164Z INFO property_map_server: Travel time store loaded modes=3
|
||||||
|
2026-02-19T21:40:57.424333Z INFO property_map_server: Server listening on 0.0.0.0:8001
|
||||||
|
2026-02-19T21:45:48.088981Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T21:45:48.089157Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T21:45:48.089163Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T21:45:48.151222Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T21:45:48.151231Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T21:45:50.419725Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
|
||||||
|
2026-02-19T21:45:50.419740Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
|
||||||
|
2026-02-19T21:45:50.680792Z INFO property_map_server::data::property: buy listings joined rows=444605
|
||||||
|
2026-02-19T21:45:50.680801Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
|
||||||
|
2026-02-19T21:45:50.790235Z INFO property_map_server::data::property: rent listings joined rows=125656
|
||||||
|
2026-02-19T21:45:50.790245Z INFO property_map_server::data::property: Concatenating all data sources
|
||||||
|
2026-02-19T21:45:59.531271Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
|
||||||
|
2026-02-19T21:45:59.531351Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
|
||||||
|
2026-02-19T21:46:00.677779Z INFO property_map_server::data::property: Combined data selected rows=15773642
|
||||||
|
2026-02-19T21:46:00.823682Z INFO property_map_server::data::property: Extracting numeric feature columns
|
||||||
|
2026-02-19T21:46:11.566611Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T21:46:11.566786Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T21:46:11.566792Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T21:46:11.644730Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T21:46:11.644739Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T21:46:17.296113Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T21:46:17.296298Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T21:46:17.296309Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T21:46:17.355178Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T21:46:17.355187Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T21:46:19.508288Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
|
||||||
|
2026-02-19T21:46:19.508307Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
|
||||||
|
2026-02-19T21:46:19.775415Z INFO property_map_server::data::property: buy listings joined rows=444605
|
||||||
|
2026-02-19T21:46:19.775424Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
|
||||||
|
2026-02-19T21:46:19.877913Z INFO property_map_server::data::property: rent listings joined rows=125656
|
||||||
|
2026-02-19T21:46:19.877923Z INFO property_map_server::data::property: Concatenating all data sources
|
||||||
|
2026-02-19T21:46:22.229279Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
|
||||||
|
2026-02-19T21:46:22.229352Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
|
||||||
|
2026-02-19T21:46:23.385234Z INFO property_map_server::data::property: Combined data selected rows=15773642
|
||||||
|
2026-02-19T21:46:23.566673Z INFO property_map_server::data::property: Extracting numeric feature columns
|
||||||
|
2026-02-19T21:46:28.957436Z INFO property_map_server::data::property: Computing histograms for numeric features
|
||||||
|
2026-02-19T21:46:34.625853Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T21:46:34.626033Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T21:46:34.626039Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T21:46:34.683165Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T21:46:34.683174Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T21:46:39.619046Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T21:46:39.619206Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T21:46:39.619211Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T21:46:39.682402Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T21:46:39.682412Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T21:46:41.896969Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
|
||||||
|
2026-02-19T21:46:41.896985Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
|
||||||
|
2026-02-19T21:46:42.158027Z INFO property_map_server::data::property: buy listings joined rows=444605
|
||||||
|
2026-02-19T21:46:42.158037Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
|
||||||
|
2026-02-19T21:46:42.256671Z INFO property_map_server::data::property: rent listings joined rows=125656
|
||||||
|
2026-02-19T21:46:42.256682Z INFO property_map_server::data::property: Concatenating all data sources
|
||||||
|
2026-02-19T21:46:44.596786Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
|
||||||
|
2026-02-19T21:46:44.596858Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
|
||||||
|
2026-02-19T21:46:45.729422Z INFO property_map_server::data::property: Combined data selected rows=15773642
|
||||||
|
2026-02-19T21:46:45.884768Z INFO property_map_server::data::property: Extracting numeric feature columns
|
||||||
|
2026-02-19T21:46:51.133252Z INFO property_map_server::data::property: Computing histograms for numeric features
|
||||||
|
2026-02-19T21:46:52.129246Z INFO property_map_server::data::property: Extracting string columns
|
||||||
|
2026-02-19T21:46:54.247724Z INFO property_map_server::data::property: Building enum features
|
||||||
|
2026-02-19T21:47:06.556048Z INFO property_map_server::data::property: Extracting renovation history
|
||||||
|
2026-02-19T21:47:08.635978Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
|
||||||
|
2026-02-19T21:47:08.635986Z INFO property_map_server::data::property: Extracting listing features
|
||||||
|
2026-02-19T21:47:09.231804Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
|
||||||
|
2026-02-19T21:47:09.231812Z INFO property_map_server::data::property: Sorting rows by spatial locality
|
||||||
|
2026-02-19T21:47:14.793765Z INFO property_map_server::data::property: Building interned strings
|
||||||
|
2026-02-19T21:47:21.074369Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
|
||||||
|
2026-02-19T21:47:49.305297Z INFO property_map_server::data::property: Data loading complete
|
||||||
|
2026-02-19T21:47:50.726389Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
|
||||||
|
2026-02-19T21:47:50.726398Z INFO property_map_server: Building spatial grid index (0.01° cells)
|
||||||
|
2026-02-19T21:47:50.825433Z INFO property_map_server: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T21:47:50.825442Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T21:47:51.183337Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
|
||||||
|
2026-02-19T21:47:51.183366Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
|
||||||
|
2026-02-19T21:47:51.183377Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
|
||||||
|
2026-02-19T21:47:51.239629Z INFO property_map_server::data::poi: Loaded 811937 POIs
|
||||||
|
2026-02-19T21:47:51.358287Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
|
||||||
|
2026-02-19T21:47:51.358921Z INFO property_map_server::data::poi: POI data loading complete.
|
||||||
|
2026-02-19T21:47:51.389530Z INFO property_map_server: POI data loaded pois=811937
|
||||||
|
2026-02-19T21:47:51.389537Z INFO property_map_server: Building POI spatial grid index
|
||||||
|
2026-02-19T21:47:51.397611Z INFO property_map_server: Loading place data from /app/data/places.parquet
|
||||||
|
2026-02-19T21:47:51.397621Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
|
||||||
|
2026-02-19T21:47:51.404147Z INFO property_map_server::data::places: Loaded 90807 places
|
||||||
|
2026-02-19T21:47:51.422097Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
|
||||||
|
2026-02-19T21:47:51.423272Z INFO property_map_server: Place data loaded places=90807
|
||||||
|
2026-02-19T21:47:51.423286Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
|
||||||
|
2026-02-19T21:47:51.423293Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
|
||||||
|
2026-02-19T21:47:51.427524Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
|
||||||
|
2026-02-19T21:47:52.459962Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
|
||||||
|
2026-02-19T21:47:52.459974Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
|
||||||
|
2026-02-19T21:47:52.459991Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
|
||||||
|
2026-02-19T21:47:52.460299Z INFO property_map_server: PMTiles loaded successfully
|
||||||
|
2026-02-19T21:47:52.509697Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
|
||||||
|
2026-02-19T21:47:52.548802Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
|
||||||
|
2026-02-19T21:47:52.548955Z INFO property_map_server: Precomputed features response groups=9
|
||||||
|
2026-02-19T21:47:52.548998Z INFO property_map_server: Precomputed AI filters schema and system prompt
|
||||||
|
2026-02-19T21:47:52.549010Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
|
||||||
|
2026-02-19T21:47:52.651249Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
|
||||||
|
2026-02-19T21:47:52.657303Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
|
||||||
|
2026-02-19T21:47:52.657312Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
|
||||||
|
2026-02-19T21:47:52.657314Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
|
||||||
|
2026-02-19T21:47:52.699918Z WARN property_map_server::pocketbase: PocketBase settings missing oauth2.providers array — cannot configure OAuth
|
||||||
|
2026-02-19T21:47:52.699928Z INFO property_map_server: Ollama configured: http://host.docker.internal:11434 (model: gpt-oss:20b)
|
||||||
|
2026-02-19T21:47:52.699941Z INFO property_map_server: Loading travel time data from /app/data/travel-times
|
||||||
|
2026-02-19T21:47:52.776587Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=76039
|
||||||
|
2026-02-19T21:47:52.892688Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=76039
|
||||||
|
2026-02-19T21:47:52.988301Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=69290
|
||||||
|
2026-02-19T21:47:52.988332Z INFO property_map_server: Travel time store loaded modes=3
|
||||||
|
2026-02-19T21:47:52.988500Z INFO property_map_server: Server listening on 0.0.0.0:8001
|
||||||
|
2026-02-19T21:51:21.780285Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T21:51:21.780462Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T21:51:21.780471Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T21:51:21.851737Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T21:51:21.851746Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T21:51:24.076503Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
|
||||||
|
2026-02-19T21:51:24.076522Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
|
||||||
|
2026-02-19T21:51:24.334934Z INFO property_map_server::data::property: buy listings joined rows=444605
|
||||||
|
2026-02-19T21:51:24.334944Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
|
||||||
|
2026-02-19T21:51:24.438503Z INFO property_map_server::data::property: rent listings joined rows=125656
|
||||||
|
2026-02-19T21:51:24.438513Z INFO property_map_server::data::property: Concatenating all data sources
|
||||||
|
2026-02-19T21:51:35.892299Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
|
||||||
|
2026-02-19T21:51:35.892405Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
|
||||||
|
2026-02-19T21:51:36.985627Z INFO property_map_server::data::property: Combined data selected rows=15773642
|
||||||
|
2026-02-19T21:51:37.144498Z INFO property_map_server::data::property: Extracting numeric feature columns
|
||||||
|
2026-02-19T21:51:42.441195Z INFO property_map_server::data::property: Computing histograms for numeric features
|
||||||
|
2026-02-19T21:51:43.421336Z INFO property_map_server::data::property: Extracting string columns
|
||||||
|
2026-02-19T21:51:45.512575Z INFO property_map_server::data::property: Building enum features
|
||||||
|
2026-02-19T21:51:57.729162Z INFO property_map_server::data::property: Extracting renovation history
|
||||||
|
2026-02-19T21:51:59.870295Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
|
||||||
|
2026-02-19T21:51:59.870303Z INFO property_map_server::data::property: Extracting listing features
|
||||||
|
2026-02-19T21:52:00.496544Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
|
||||||
|
2026-02-19T21:52:00.496553Z INFO property_map_server::data::property: Sorting rows by spatial locality
|
||||||
|
2026-02-19T21:52:05.810063Z INFO property_map_server::data::property: Building interned strings
|
||||||
|
2026-02-19T21:52:12.478733Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
|
||||||
|
2026-02-19T21:52:57.041288Z INFO property_map_server::data::property: Data loading complete
|
||||||
|
2026-02-19T21:52:58.190166Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
|
||||||
|
2026-02-19T21:52:58.190227Z INFO property_map_server: Building spatial grid index (0.01° cells)
|
||||||
|
2026-02-19T21:52:58.612625Z INFO property_map_server: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T21:52:58.612634Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T21:52:59.010323Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
|
||||||
|
2026-02-19T21:52:59.010352Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
|
||||||
|
2026-02-19T21:52:59.010357Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
|
||||||
|
2026-02-19T21:52:59.033154Z INFO property_map_server::data::poi: Loaded 811937 POIs
|
||||||
|
2026-02-19T21:52:59.168962Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
|
||||||
|
2026-02-19T21:52:59.169620Z INFO property_map_server::data::poi: POI data loading complete.
|
||||||
|
2026-02-19T21:52:59.200062Z INFO property_map_server: POI data loaded pois=811937
|
||||||
|
2026-02-19T21:52:59.200069Z INFO property_map_server: Building POI spatial grid index
|
||||||
|
2026-02-19T21:52:59.209004Z INFO property_map_server: Loading place data from /app/data/places.parquet
|
||||||
|
2026-02-19T21:52:59.209026Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
|
||||||
|
2026-02-19T21:52:59.217593Z INFO property_map_server::data::places: Loaded 90807 places
|
||||||
|
2026-02-19T21:52:59.237507Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
|
||||||
|
2026-02-19T21:52:59.238659Z INFO property_map_server: Place data loaded places=90807
|
||||||
|
2026-02-19T21:52:59.238673Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
|
||||||
|
2026-02-19T21:52:59.238677Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
|
||||||
|
2026-02-19T21:52:59.239461Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
|
||||||
|
2026-02-19T21:53:00.577770Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
|
||||||
|
2026-02-19T21:53:00.577784Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
|
||||||
|
2026-02-19T21:53:00.577800Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
|
||||||
|
2026-02-19T21:53:00.578044Z INFO property_map_server: PMTiles loaded successfully
|
||||||
|
2026-02-19T21:53:00.635812Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
|
||||||
|
2026-02-19T21:53:00.661112Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
|
||||||
|
2026-02-19T21:53:00.661276Z INFO property_map_server: Precomputed features response groups=9
|
||||||
|
2026-02-19T21:53:00.661327Z INFO property_map_server: Precomputed AI filters schema and system prompt
|
||||||
|
2026-02-19T21:53:00.661341Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
|
||||||
|
2026-02-19T21:53:00.777123Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
|
||||||
|
2026-02-19T21:53:00.780442Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
|
||||||
|
2026-02-19T21:53:00.780453Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
|
||||||
|
2026-02-19T21:53:00.780455Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
|
||||||
|
2026-02-19T21:53:00.827713Z WARN property_map_server::pocketbase: PocketBase settings missing oauth2.providers array — cannot configure OAuth
|
||||||
|
2026-02-19T21:53:00.827728Z INFO property_map_server: Ollama configured: http://host.docker.internal:11434 (model: gpt-oss:20b)
|
||||||
|
2026-02-19T21:53:00.827756Z INFO property_map_server: Loading travel time data from /app/data/travel-times
|
||||||
|
2026-02-19T21:53:00.853494Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=76039
|
||||||
|
2026-02-19T21:53:00.875117Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=76039
|
||||||
|
2026-02-19T21:53:00.897287Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=69290
|
||||||
|
2026-02-19T21:53:00.897342Z INFO property_map_server: Travel time store loaded modes=3
|
||||||
|
2026-02-19T21:53:00.897521Z WARN property_map_server: mlockall failed (need CAP_IPC_LOCK or sufficient RLIMIT_MEMLOCK): Cannot allocate memory (os error 12)
|
||||||
|
2026-02-19T21:53:00.897564Z INFO property_map_server: Server listening on 0.0.0.0:8001
|
||||||
|
2026-02-19T21:53:43.607361Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T21:53:43.607524Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T21:53:43.607533Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T21:53:43.703745Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T21:53:43.703756Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T21:53:46.126315Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
|
||||||
|
2026-02-19T21:53:46.126336Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
|
||||||
|
2026-02-19T21:53:46.404697Z INFO property_map_server::data::property: buy listings joined rows=444605
|
||||||
|
2026-02-19T21:53:46.404708Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
|
||||||
|
2026-02-19T21:53:46.508191Z INFO property_map_server::data::property: rent listings joined rows=125656
|
||||||
|
2026-02-19T21:53:46.508203Z INFO property_map_server::data::property: Concatenating all data sources
|
||||||
|
2026-02-19T21:54:04.815379Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
|
||||||
|
2026-02-19T21:54:04.815453Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
|
||||||
|
2026-02-19T21:54:05.957615Z INFO property_map_server::data::property: Combined data selected rows=15773642
|
||||||
|
2026-02-19T21:54:06.114182Z INFO property_map_server::data::property: Extracting numeric feature columns
|
||||||
|
2026-02-19T21:54:11.113430Z INFO property_map_server::data::property: Computing histograms for numeric features
|
||||||
|
2026-02-19T21:54:12.014906Z INFO property_map_server::data::property: Extracting string columns
|
||||||
|
2026-02-19T21:54:14.114892Z INFO property_map_server::data::property: Building enum features
|
||||||
|
2026-02-19T21:54:26.216628Z INFO property_map_server::data::property: Extracting renovation history
|
||||||
|
2026-02-19T21:54:28.295021Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
|
||||||
|
2026-02-19T21:54:28.295030Z INFO property_map_server::data::property: Extracting listing features
|
||||||
|
2026-02-19T21:54:28.894967Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
|
||||||
|
2026-02-19T21:54:28.894975Z INFO property_map_server::data::property: Sorting rows by spatial locality
|
||||||
|
2026-02-19T21:54:34.492548Z INFO property_map_server::data::property: Building interned strings
|
||||||
|
2026-02-19T21:54:40.775643Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
|
||||||
|
2026-02-19T21:55:09.150090Z INFO property_map_server::data::property: Data loading complete
|
||||||
|
2026-02-19T21:55:10.482353Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
|
||||||
|
2026-02-19T21:55:10.482362Z INFO property_map_server: Building spatial grid index (0.01° cells)
|
||||||
|
2026-02-19T21:55:10.579983Z INFO property_map_server: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T21:55:10.579992Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T21:55:10.931346Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
|
||||||
|
2026-02-19T21:55:10.931371Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
|
||||||
|
2026-02-19T21:55:10.931376Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
|
||||||
|
2026-02-19T21:55:10.956383Z INFO property_map_server::data::poi: Loaded 811937 POIs
|
||||||
|
2026-02-19T21:55:11.076557Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
|
||||||
|
2026-02-19T21:55:11.077176Z INFO property_map_server::data::poi: POI data loading complete.
|
||||||
|
2026-02-19T21:55:11.107885Z INFO property_map_server: POI data loaded pois=811937
|
||||||
|
2026-02-19T21:55:11.107892Z INFO property_map_server: Building POI spatial grid index
|
||||||
|
2026-02-19T21:55:11.115230Z INFO property_map_server: Loading place data from /app/data/places.parquet
|
||||||
|
2026-02-19T21:55:11.115239Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
|
||||||
|
2026-02-19T21:55:11.117955Z INFO property_map_server::data::places: Loaded 90807 places
|
||||||
|
2026-02-19T21:55:11.135514Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
|
||||||
|
2026-02-19T21:55:11.136623Z INFO property_map_server: Place data loaded places=90807
|
||||||
|
2026-02-19T21:55:11.136637Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
|
||||||
|
2026-02-19T21:55:11.136641Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
|
||||||
|
2026-02-19T21:55:11.138240Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
|
||||||
|
2026-02-19T21:55:12.410370Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
|
||||||
|
2026-02-19T21:55:12.410381Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
|
||||||
|
2026-02-19T21:55:12.410398Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
|
||||||
|
2026-02-19T21:55:12.410625Z INFO property_map_server: PMTiles loaded successfully
|
||||||
|
2026-02-19T21:55:12.462925Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
|
||||||
|
2026-02-19T21:55:12.494298Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
|
||||||
|
2026-02-19T21:55:12.494439Z INFO property_map_server: Precomputed features response groups=9
|
||||||
|
2026-02-19T21:55:12.494482Z INFO property_map_server: Precomputed AI filters schema and system prompt
|
||||||
|
2026-02-19T21:55:12.494496Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
|
||||||
|
2026-02-19T21:55:12.541473Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
|
||||||
|
2026-02-19T21:55:12.543245Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
|
||||||
|
2026-02-19T21:55:12.543253Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
|
||||||
|
2026-02-19T21:55:12.543255Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
|
||||||
|
2026-02-19T21:55:12.586588Z WARN property_map_server::pocketbase: PocketBase settings missing oauth2.providers array — cannot configure OAuth
|
||||||
|
2026-02-19T21:55:12.586599Z INFO property_map_server: Ollama configured: http://host.docker.internal:11434 (model: gpt-oss:20b)
|
||||||
|
2026-02-19T21:55:12.586612Z INFO property_map_server: Loading travel time data from /app/data/travel-times
|
||||||
|
2026-02-19T21:55:12.608664Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=76039
|
||||||
|
2026-02-19T21:55:12.629502Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=76039
|
||||||
|
2026-02-19T21:55:12.648844Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=69290
|
||||||
|
2026-02-19T21:55:12.648881Z INFO property_map_server: Travel time store loaded modes=3
|
||||||
|
2026-02-19T21:55:12.649064Z WARN property_map_server: mlockall failed (need CAP_IPC_LOCK or sufficient RLIMIT_MEMLOCK): Cannot allocate memory (os error 12)
|
||||||
|
2026-02-19T21:55:12.649102Z INFO property_map_server: Server listening on 0.0.0.0:8001
|
||||||
|
2026-02-19T21:56:27.019313Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T21:56:27.019464Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T21:56:27.019473Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T21:56:27.077642Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T21:56:27.077651Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T21:56:29.376382Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
|
||||||
|
2026-02-19T21:56:29.376400Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
|
||||||
|
2026-02-19T21:56:29.643314Z INFO property_map_server::data::property: buy listings joined rows=444605
|
||||||
|
2026-02-19T21:56:29.643325Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
|
||||||
|
2026-02-19T21:56:29.750729Z INFO property_map_server::data::property: rent listings joined rows=125656
|
||||||
|
2026-02-19T21:56:29.750740Z INFO property_map_server::data::property: Concatenating all data sources
|
||||||
|
2026-02-19T21:56:36.764563Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
|
||||||
|
2026-02-19T21:56:36.764643Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
|
||||||
|
2026-02-19T21:56:37.882852Z INFO property_map_server::data::property: Combined data selected rows=15773642
|
||||||
|
2026-02-19T21:56:38.041464Z INFO property_map_server::data::property: Extracting numeric feature columns
|
||||||
|
2026-02-19T21:56:43.567623Z INFO property_map_server::data::property: Computing histograms for numeric features
|
||||||
|
2026-02-19T21:56:44.547629Z INFO property_map_server::data::property: Extracting string columns
|
||||||
|
2026-02-19T21:56:46.638073Z INFO property_map_server::data::property: Building enum features
|
||||||
|
2026-02-19T21:56:58.743576Z INFO property_map_server::data::property: Extracting renovation history
|
||||||
|
2026-02-19T21:57:00.886254Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
|
||||||
|
2026-02-19T21:57:00.886263Z INFO property_map_server::data::property: Extracting listing features
|
||||||
|
2026-02-19T21:57:01.500069Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
|
||||||
|
2026-02-19T21:57:01.500077Z INFO property_map_server::data::property: Sorting rows by spatial locality
|
||||||
|
2026-02-19T21:57:07.085398Z INFO property_map_server::data::property: Building interned strings
|
||||||
|
2026-02-19T21:57:13.333355Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
|
||||||
|
2026-02-19T21:57:42.416818Z INFO property_map_server::data::property: Data loading complete
|
||||||
|
2026-02-19T21:57:43.429909Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
|
||||||
|
2026-02-19T21:57:43.429917Z INFO property_map_server: Building spatial grid index (0.01° cells)
|
||||||
|
2026-02-19T21:57:43.809875Z INFO property_map_server: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T21:57:43.809884Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T21:57:44.161951Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
|
||||||
|
2026-02-19T21:57:44.161990Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
|
||||||
|
2026-02-19T21:57:44.161997Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
|
||||||
|
2026-02-19T21:57:44.184832Z INFO property_map_server::data::poi: Loaded 811937 POIs
|
||||||
|
2026-02-19T21:57:44.303890Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
|
||||||
|
2026-02-19T21:57:44.304500Z INFO property_map_server::data::poi: POI data loading complete.
|
||||||
|
2026-02-19T21:57:44.335104Z INFO property_map_server: POI data loaded pois=811937
|
||||||
|
2026-02-19T21:57:44.335113Z INFO property_map_server: Building POI spatial grid index
|
||||||
|
2026-02-19T21:57:44.342807Z INFO property_map_server: Loading place data from /app/data/places.parquet
|
||||||
|
2026-02-19T21:57:44.342817Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
|
||||||
|
2026-02-19T21:57:44.345902Z INFO property_map_server::data::places: Loaded 90807 places
|
||||||
|
2026-02-19T21:57:44.365611Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
|
||||||
|
2026-02-19T21:57:44.368113Z INFO property_map_server: Place data loaded places=90807
|
||||||
|
2026-02-19T21:57:44.368128Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
|
||||||
|
2026-02-19T21:57:44.368133Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
|
||||||
|
2026-02-19T21:57:44.368931Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
|
||||||
|
2026-02-19T21:57:45.407981Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
|
||||||
|
2026-02-19T21:57:45.407992Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
|
||||||
|
2026-02-19T21:57:45.408009Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
|
||||||
|
2026-02-19T21:57:45.408231Z INFO property_map_server: PMTiles loaded successfully
|
||||||
|
2026-02-19T21:57:45.458216Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
|
||||||
|
2026-02-19T21:57:45.483167Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
|
||||||
|
2026-02-19T21:57:45.483325Z INFO property_map_server: Precomputed features response groups=9
|
||||||
|
2026-02-19T21:57:45.483362Z INFO property_map_server: Precomputed AI filters schema and system prompt
|
||||||
|
2026-02-19T21:57:45.483372Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
|
||||||
|
2026-02-19T21:57:45.527603Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
|
||||||
|
2026-02-19T21:57:45.529274Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
|
||||||
|
2026-02-19T21:57:45.529282Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
|
||||||
|
2026-02-19T21:57:45.529284Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
|
||||||
|
2026-02-19T22:00:02.316188Z INFO property_map_server: Prometheus metrics initialized
|
||||||
|
2026-02-19T22:00:02.316363Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet
|
||||||
|
2026-02-19T22:00:02.316376Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet"
|
||||||
|
2026-02-19T22:00:02.374613Z INFO property_map_server::data::property: Postcode features loaded rows=1262367
|
||||||
|
2026-02-19T22:00:02.374622Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet"
|
||||||
|
2026-02-19T22:00:04.644372Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203381
|
||||||
|
2026-02-19T22:00:04.644386Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet"
|
||||||
|
2026-02-19T22:00:04.911541Z INFO property_map_server::data::property: buy listings joined rows=444605
|
||||||
|
2026-02-19T22:00:04.911554Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet"
|
||||||
|
2026-02-19T22:00:05.012469Z INFO property_map_server::data::property: rent listings joined rows=125656
|
||||||
|
2026-02-19T22:00:05.012480Z INFO property_map_server::data::property: Concatenating all data sources
|
||||||
|
2026-02-19T22:00:22.135033Z INFO property_map_server::data::property: All data sources combined properties=15203381 buy_listings=444605 rent_listings=125656 total=15773642
|
||||||
|
2026-02-19T22:00:22.135120Z INFO property_map_server::data::property: Feature columns from config numeric=55 enums=12 total=67
|
||||||
|
2026-02-19T22:00:23.338993Z INFO property_map_server::data::property: Combined data selected rows=15773642
|
||||||
|
2026-02-19T22:00:23.510508Z INFO property_map_server::data::property: Extracting numeric feature columns
|
||||||
|
2026-02-19T22:00:28.484323Z INFO property_map_server::data::property: Computing histograms for numeric features
|
||||||
|
2026-02-19T22:00:29.357751Z INFO property_map_server::data::property: Extracting string columns
|
||||||
|
2026-02-19T22:00:31.494852Z INFO property_map_server::data::property: Building enum features
|
||||||
|
2026-02-19T22:00:43.668748Z INFO property_map_server::data::property: Extracting renovation history
|
||||||
|
2026-02-19T22:00:45.723371Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829807
|
||||||
|
2026-02-19T22:00:45.723379Z INFO property_map_server::data::property: Extracting listing features
|
||||||
|
2026-02-19T22:00:46.332508Z INFO property_map_server::data::property: Listing features extracted properties_with_features=508871
|
||||||
|
2026-02-19T22:00:46.332517Z INFO property_map_server::data::property: Sorting rows by spatial locality
|
||||||
|
2026-02-19T22:00:51.842163Z INFO property_map_server::data::property: Building interned strings
|
||||||
|
2026-02-19T22:00:57.978323Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted)
|
||||||
|
2026-02-19T22:01:26.614095Z INFO property_map_server::data::property: Data loading complete
|
||||||
|
2026-02-19T22:01:27.675542Z INFO property_map_server: Property data loaded rows=15773642 features=67 enums=12
|
||||||
|
2026-02-19T22:01:27.675552Z INFO property_map_server: Building spatial grid index (0.01° cells)
|
||||||
|
2026-02-19T22:01:28.060580Z INFO property_map_server: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T22:01:28.060590Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12
|
||||||
|
2026-02-19T22:01:28.424039Z INFO property_map_server::data::property: H3 precomputation complete (15773642 cells)
|
||||||
|
2026-02-19T22:01:28.424101Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet
|
||||||
|
2026-02-19T22:01:28.424183Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"...
|
||||||
|
2026-02-19T22:01:28.448672Z INFO property_map_server::data::poi: Loaded 811937 POIs
|
||||||
|
2026-02-19T22:01:28.566180Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71
|
||||||
|
2026-02-19T22:01:28.566791Z INFO property_map_server::data::poi: POI data loading complete.
|
||||||
|
2026-02-19T22:01:28.596675Z INFO property_map_server: POI data loaded pois=811937
|
||||||
|
2026-02-19T22:01:28.596685Z INFO property_map_server: Building POI spatial grid index
|
||||||
|
2026-02-19T22:01:28.603824Z INFO property_map_server: Loading place data from /app/data/places.parquet
|
||||||
|
2026-02-19T22:01:28.603830Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"...
|
||||||
|
2026-02-19T22:01:28.606465Z INFO property_map_server::data::places: Loaded 90807 places
|
||||||
|
2026-02-19T22:01:28.623823Z INFO property_map_server::data::places: Place data loaded places=90807 types=11 with_population=2112 with_city=87577
|
||||||
|
2026-02-19T22:01:28.624990Z INFO property_map_server: Place data loaded places=90807
|
||||||
|
2026-02-19T22:01:28.625002Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries
|
||||||
|
2026-02-19T22:01:28.625006Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries"
|
||||||
|
2026-02-19T22:01:28.637363Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361
|
||||||
|
2026-02-19T22:01:29.656030Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140
|
||||||
|
2026-02-19T22:01:29.656042Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140
|
||||||
|
2026-02-19T22:01:29.656058Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles
|
||||||
|
2026-02-19T22:01:29.656288Z INFO property_map_server: PMTiles loaded successfully
|
||||||
|
2026-02-19T22:01:29.705260Z INFO property_map_server: No --dist provided; static serving and OG injection disabled
|
||||||
|
2026-02-19T22:01:29.738938Z INFO property_map_server: Screenshot service configured: http://screenshot:8002
|
||||||
|
2026-02-19T22:01:29.739087Z INFO property_map_server: Precomputed features response groups=9
|
||||||
|
2026-02-19T22:01:29.739137Z INFO property_map_server: Precomputed AI filters schema and system prompt
|
||||||
|
2026-02-19T22:01:29.739149Z INFO property_map_server: PocketBase configured: http://pocketbase:8090
|
||||||
|
2026-02-19T22:01:29.786529Z INFO property_map_server::pocketbase: PocketBase users collection already has is_admin, subscription, and newsletter fields
|
||||||
|
2026-02-19T22:01:29.788719Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated
|
||||||
|
2026-02-19T22:01:29.788726Z INFO property_map_server::pocketbase: PocketBase collection 'invites' already exists
|
||||||
|
2026-02-19T22:01:29.788728Z INFO property_map_server::pocketbase: PocketBase collection 'short_urls' already exists
|
||||||
|
|
@ -19,7 +19,7 @@ pub const AI_FILTERS_TEMPERATURE: f32 = 0.0;
|
||||||
|
|
||||||
/// Inner London free zone bounds (south, west, north, east) — roughly zones 1–2.
|
/// Inner London free zone bounds (south, west, north, east) — roughly zones 1–2.
|
||||||
/// Users without a license can only query data within these bounds.
|
/// Users without a license can only query data within these bounds.
|
||||||
pub const FREE_ZONE_BOUNDS: (f64, f64, f64, f64) = (51.48, -0.18, 51.54, -0.02);
|
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
|
/// 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.
|
/// when the center of the requested bounds is within DEMO_CENTER_TOLERANCE of this point.
|
||||||
|
|
|
||||||
|
|
@ -437,82 +437,11 @@ impl PropertyData {
|
||||||
tracing::info!(rows = prop_count, "Properties joined with postcodes");
|
tracing::info!(rows = prop_count, "Properties joined with postcodes");
|
||||||
|
|
||||||
// Load online listings (buy + rent) — these have their own lat/lon.
|
// Load online listings (buy + rent) — these have their own lat/lon.
|
||||||
// Normalize column names from finder output to server-expected names.
|
// Expects the new finder parquet format with human-readable column names.
|
||||||
// strict=false: columns already using the new name are silently skipped.
|
|
||||||
let load_listings = |path: &Path, label: &str| -> anyhow::Result<DataFrame> {
|
let load_listings = |path: &Path, label: &str| -> anyhow::Result<DataFrame> {
|
||||||
tracing::info!("Loading {} listings from {:?}", label, path);
|
tracing::info!("Loading {} listings from {:?}", label, path);
|
||||||
let mut lf = LazyFrame::scan_parquet(path, Default::default())
|
let lf = LazyFrame::scan_parquet(path, Default::default())
|
||||||
.with_context(|| format!("Failed to scan {label} listings parquet"))?;
|
.with_context(|| format!("Failed to scan {label} listings parquet"))?;
|
||||||
let schema = lf
|
|
||||||
.collect_schema()
|
|
||||||
.with_context(|| format!("Failed to read {label} listings schema"))?;
|
|
||||||
|
|
||||||
// Rename raw finder columns → server-expected names (no-op if already renamed)
|
|
||||||
let lf = lf.rename(
|
|
||||||
[
|
|
||||||
"postcode",
|
|
||||||
"address",
|
|
||||||
"latitude",
|
|
||||||
"longitude",
|
|
||||||
"bedrooms",
|
|
||||||
"bathrooms",
|
|
||||||
"total_rooms",
|
|
||||||
"tenure",
|
|
||||||
"property_type",
|
|
||||||
"property_sub_type",
|
|
||||||
"price_qualifier",
|
|
||||||
"floorspace_sqm",
|
|
||||||
"url",
|
|
||||||
"features",
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Postcode",
|
|
||||||
"Address per Property Register",
|
|
||||||
"lat",
|
|
||||||
"lon",
|
|
||||||
"Bedrooms",
|
|
||||||
"Bathrooms",
|
|
||||||
"Number of bedrooms & living rooms",
|
|
||||||
"Leashold/Freehold",
|
|
||||||
"Property type",
|
|
||||||
"Property sub-type",
|
|
||||||
"Price qualifier",
|
|
||||||
"Total floor area (sqm)",
|
|
||||||
"Listing URL",
|
|
||||||
"Listing features",
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Derive missing columns for raw finder output that doesn't have them
|
|
||||||
let listing_status = if label == "buy" {
|
|
||||||
"For sale"
|
|
||||||
} else {
|
|
||||||
"For rent"
|
|
||||||
};
|
|
||||||
let lf = if schema.get("Listing status").is_none() {
|
|
||||||
lf.with_column(lit(listing_status).alias("Listing status"))
|
|
||||||
} else {
|
|
||||||
lf
|
|
||||||
};
|
|
||||||
let lf = if schema.get("Asking price").is_none() && schema.get("price").is_some() {
|
|
||||||
if label == "buy" {
|
|
||||||
lf.with_column(col("price").alias("Asking price"))
|
|
||||||
} else {
|
|
||||||
// Normalize rent to monthly: weekly×52/12, yearly÷12
|
|
||||||
lf.with_column(
|
|
||||||
when(col("price_frequency").eq(lit("weekly")))
|
|
||||||
.then(col("price").cast(DataType::Float64) * lit(52.0 / 12.0))
|
|
||||||
.when(col("price_frequency").eq(lit("yearly")))
|
|
||||||
.then(col("price").cast(DataType::Float64) / lit(12.0))
|
|
||||||
.otherwise(col("price").cast(DataType::Float64))
|
|
||||||
.cast(DataType::Int64)
|
|
||||||
.alias("Asking rent (monthly)"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lf
|
|
||||||
};
|
|
||||||
|
|
||||||
// Join with postcodes for area features (listings have their own lat/lon)
|
// Join with postcodes for area features (listings have their own lat/lon)
|
||||||
let pc_no_coords = postcode_df.clone().lazy().drop(["lat", "lon"]);
|
let pc_no_coords = postcode_df.clone().lazy().drop(["lat", "lon"]);
|
||||||
|
|
|
||||||
|
|
@ -478,7 +478,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/export",
|
"/api/export",
|
||||||
get(move |ext, query| routes::get_export(state_export.clone(), ext, query))
|
get(move |headers, ext, query| routes::get_export(state_export.clone(), headers, ext, query))
|
||||||
.layer(ConcurrencyLimitLayer::new(3)),
|
.layer(ConcurrencyLimitLayer::new(3)),
|
||||||
)
|
)
|
||||||
.route("/api/me", get(routes::get_me))
|
.route("/api/me", get(routes::get_me))
|
||||||
|
|
@ -592,6 +592,16 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.layer(CompressionLayer::new().zstd(true).gzip(true))
|
.layer(CompressionLayer::new().zstd(true).gzip(true))
|
||||||
.layer(TraceLayer::new_for_http());
|
.layer(TraceLayer::new_for_http());
|
||||||
|
|
||||||
|
// Lock all current and future memory pages to prevent swapping
|
||||||
|
unsafe {
|
||||||
|
if libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) != 0 {
|
||||||
|
let err = std::io::Error::last_os_error();
|
||||||
|
tracing::warn!("mlockall failed (need CAP_IPC_LOCK or sufficient RLIMIT_MEMLOCK): {err}");
|
||||||
|
} else {
|
||||||
|
info!("All memory pages locked (mlockall)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let addr = consts::SERVER_ADDRESS;
|
let addr = consts::SERVER_ADDRESS;
|
||||||
let listener = tokio::net::TcpListener::bind(addr)
|
let listener = tokio::net::TcpListener::bind(addr)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use crate::state::AppState;
|
||||||
const OG_PLACEHOLDER: &str = r#"<meta name="x-og-placeholder" content="__PERFECT_POSTCODE_OG_TAGS__"/>"#;
|
const OG_PLACEHOLDER: &str = r#"<meta name="x-og-placeholder" content="__PERFECT_POSTCODE_OG_TAGS__"/>"#;
|
||||||
|
|
||||||
pub async fn og_middleware(request: Request, next: Next) -> Response {
|
pub async fn og_middleware(request: Request, next: Next) -> Response {
|
||||||
|
let path = request.uri().path().to_string();
|
||||||
// Capture the query string before passing the request through
|
// Capture the query string before passing the request through
|
||||||
let query_string = request.uri().query().unwrap_or("").to_string();
|
let query_string = request.uri().query().unwrap_or("").to_string();
|
||||||
|
|
||||||
|
|
@ -19,6 +20,11 @@ pub async fn og_middleware(request: Request, next: Next) -> Response {
|
||||||
|
|
||||||
let response = next.run(request).await;
|
let response = next.run(request).await;
|
||||||
|
|
||||||
|
// Only inject OG tags into SPA HTML responses, not proxied PocketBase responses
|
||||||
|
if path.starts_with("/pb/") || path.starts_with("/api/") {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
let content_type = response
|
let content_type = response
|
||||||
.headers()
|
.headers()
|
||||||
.get(header::CONTENT_TYPE)
|
.get(header::CONTENT_TYPE)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::{info, warn};
|
use tracing::info;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct AuthResponse {
|
struct AuthResponse {
|
||||||
|
|
@ -432,29 +432,25 @@ pub async fn ensure_oauth_providers(
|
||||||
// Update OAuth2 providers
|
// Update OAuth2 providers
|
||||||
let providers = settings
|
let providers = settings
|
||||||
.pointer_mut("/oauth2/providers")
|
.pointer_mut("/oauth2/providers")
|
||||||
.and_then(|v| v.as_array_mut());
|
.and_then(|v| v.as_array_mut())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("PocketBase settings missing oauth2.providers array — cannot configure OAuth"))?;
|
||||||
|
|
||||||
if let Some(providers) = providers {
|
let google = match providers
|
||||||
for provider in providers.iter_mut() {
|
.iter()
|
||||||
let name = provider
|
.position(|p| p.get("name").and_then(|n| n.as_str()) == Some("google"))
|
||||||
.get("name")
|
{
|
||||||
.and_then(|n| n.as_str())
|
Some(idx) => &mut providers[idx],
|
||||||
.unwrap_or("");
|
None => {
|
||||||
|
info!("Google provider not found in PocketBase settings — adding it");
|
||||||
|
providers.push(serde_json::json!({"name": "google"}));
|
||||||
|
providers.last_mut().expect("just pushed")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match name {
|
google["clientId"] = serde_json::json!(google_client_id);
|
||||||
"google" => {
|
google["clientSecret"] = serde_json::json!(google_client_secret);
|
||||||
provider["clientId"] = serde_json::json!(google_client_id);
|
google["enabled"] = serde_json::json!(true);
|
||||||
provider["clientSecret"] = serde_json::json!(google_client_secret);
|
|
||||||
provider["enabled"] = serde_json::json!(true);
|
|
||||||
info!("Configured Google OAuth provider");
|
info!("Configured Google OAuth provider");
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warn!("PocketBase settings missing oauth2.providers array — cannot configure OAuth");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// PATCH settings back
|
// PATCH settings back
|
||||||
let patch_resp = client
|
let patch_resp = client
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ pub use pois::{get_poi_categories, get_pois};
|
||||||
pub use postcode_stats::get_postcode_stats;
|
pub use postcode_stats::get_postcode_stats;
|
||||||
pub use postcodes::{get_postcode_lookup, get_postcodes};
|
pub use postcodes::{get_postcode_lookup, get_postcodes};
|
||||||
pub use properties::get_hexagon_properties;
|
pub use properties::get_hexagon_properties;
|
||||||
pub use screenshot::get_screenshot;
|
pub use screenshot::{fetch_screenshot_bytes, get_screenshot};
|
||||||
pub use shorten::{get_short_url, post_shorten};
|
pub use shorten::{get_short_url, post_shorten};
|
||||||
pub use streetview::get_streetview;
|
pub use streetview::get_streetview;
|
||||||
pub use invites::{get_invite, post_invites, post_redeem_invite};
|
pub use invites::{get_invite, post_invites, post_redeem_invite};
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,10 @@ pub fn build_ollama_schema(_features: &FeaturesResponse) -> Value {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": { "type": "string" },
|
"name": { "type": "string" },
|
||||||
"min": { "type": "number" },
|
"bound": { "type": "string", "enum": ["min", "max"] },
|
||||||
"max": { "type": "number" }
|
"value": { "type": "number" }
|
||||||
},
|
},
|
||||||
"required": ["name"]
|
"required": ["name", "bound", "value"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enum_filters": {
|
"enum_filters": {
|
||||||
|
|
@ -80,11 +80,11 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
|
||||||
Rules:\n\
|
Rules:\n\
|
||||||
- ONLY set filters the user explicitly mentioned or clearly implied.\n\
|
- ONLY set filters the user explicitly mentioned or clearly implied.\n\
|
||||||
- Leave out any filter the user did not mention. Empty arrays are fine.\n\
|
- Leave out any filter the user did not mention. Empty arrays are fine.\n\
|
||||||
- For numeric filters, omit \"min\" to leave the lower bound open, \
|
- Each numeric filter sets ONE bound only: \"min\" (at least this value) \
|
||||||
omit \"max\" to leave the upper bound open.\n\
|
or \"max\" (at most this value). Never set two filters on the same feature.\n\
|
||||||
- Use EXACT feature names from the list — spelling, capitalisation, and punctuation must match.\n\
|
- Use EXACT feature names from the list — spelling, capitalisation, and punctuation must match.\n\
|
||||||
- \"cheap\" / \"affordable\" = lower price range. \"expensive\" = higher price range.\n\
|
- \"cheap\" / \"affordable\" = lower price range. \"expensive\" = higher price range.\n\
|
||||||
- \"low crime\" / \"safe\" = low values on crime features. \
|
- \"low crime\" / \"safe\" = low values on Serious crime and Minor crime summary features. \
|
||||||
\"quiet\" = low Noise (dB). \"green\" / \"near parks\" = high Number of parks within 2km.\n\
|
\"quiet\" = low Noise (dB). \"green\" / \"near parks\" = high Number of parks within 2km.\n\
|
||||||
- When the user says a number like \"under 400k\", interpret it as 400000.\n\
|
- When the user says a number like \"under 400k\", interpret it as 400000.\n\
|
||||||
- When the user says \"3 bed\" or \"3 bedroom\", use Number of bedrooms & living rooms \
|
- When the user says \"3 bed\" or \"3 bedroom\", use Number of bedrooms & living rooms \
|
||||||
|
|
@ -98,6 +98,10 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
|
||||||
// Feature catalogue
|
// Feature catalogue
|
||||||
parts.push("\n--- AVAILABLE FEATURES ---\n".to_string());
|
parts.push("\n--- AVAILABLE FEATURES ---\n".to_string());
|
||||||
for group in &features.groups {
|
for group in &features.groups {
|
||||||
|
// Skip individual crime features — only expose "Crime summary" aggregates
|
||||||
|
if group.name == "Crime" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
parts.push(format!("## {}", group.name));
|
parts.push(format!("## {}", group.name));
|
||||||
for feature in &group.features {
|
for feature in &group.features {
|
||||||
match feature {
|
match feature {
|
||||||
|
|
@ -141,7 +145,7 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
|
||||||
|
|
||||||
parts.push(
|
parts.push(
|
||||||
"User: \"cheap freehold house under 400k\"\n\
|
"User: \"cheap freehold house under 400k\"\n\
|
||||||
Output: {\"numeric_filters\": [{\"name\": \"Last known price\", \"max\": 400000}], \
|
Output: {\"numeric_filters\": [{\"name\": \"Last known price\", \"bound\": \"max\", \"value\": 400000}], \
|
||||||
\"enum_filters\": [{\"name\": \"Leashold/Freehold\", \"values\": [\"Freehold\"]}, \
|
\"enum_filters\": [{\"name\": \"Leashold/Freehold\", \"values\": [\"Freehold\"]}, \
|
||||||
{\"name\": \"Property type\", \"values\": [\"Detached\", \"Semi-Detached\", \"Terraced\"]}], \
|
{\"name\": \"Property type\", \"values\": [\"Detached\", \"Semi-Detached\", \"Terraced\"]}], \
|
||||||
\"notes\": \"\"}"
|
\"notes\": \"\"}"
|
||||||
|
|
@ -151,12 +155,12 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
|
||||||
parts.push(
|
parts.push(
|
||||||
"\nUser: \"safe quiet area with good schools and parks\"\n\
|
"\nUser: \"safe quiet area with good schools and parks\"\n\
|
||||||
Output: {\"numeric_filters\": [\
|
Output: {\"numeric_filters\": [\
|
||||||
{\"name\": \"Violence and sexual offences (avg/yr)\", \"max\": 20}, \
|
{\"name\": \"Serious crime (avg/yr)\", \"bound\": \"max\", \"value\": 20}, \
|
||||||
{\"name\": \"Burglary (avg/yr)\", \"max\": 10}, \
|
{\"name\": \"Minor crime (avg/yr)\", \"bound\": \"max\", \"value\": 50}, \
|
||||||
{\"name\": \"Noise (dB)\", \"max\": 55}, \
|
{\"name\": \"Noise (dB)\", \"bound\": \"max\", \"value\": 55}, \
|
||||||
{\"name\": \"Good+ primary schools within 5km\", \"min\": 5}, \
|
{\"name\": \"Good+ primary schools within 5km\", \"bound\": \"min\", \"value\": 5}, \
|
||||||
{\"name\": \"Good+ secondary schools within 5km\", \"min\": 2}, \
|
{\"name\": \"Good+ secondary schools within 5km\", \"bound\": \"min\", \"value\": 2}, \
|
||||||
{\"name\": \"Number of parks within 2km\", \"min\": 3}], \
|
{\"name\": \"Number of parks within 2km\", \"bound\": \"min\", \"value\": 3}], \
|
||||||
\"enum_filters\": [], \"notes\": \"\"}"
|
\"enum_filters\": [], \"notes\": \"\"}"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
|
@ -164,9 +168,9 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
|
||||||
parts.push(
|
parts.push(
|
||||||
"\nUser: \"3 bed flat under 300k with fast broadband near the beach\"\n\
|
"\nUser: \"3 bed flat under 300k with fast broadband near the beach\"\n\
|
||||||
Output: {\"numeric_filters\": [\
|
Output: {\"numeric_filters\": [\
|
||||||
{\"name\": \"Last known price\", \"max\": 300000}, \
|
{\"name\": \"Last known price\", \"bound\": \"max\", \"value\": 300000}, \
|
||||||
{\"name\": \"Number of bedrooms & living rooms\", \"min\": 4}, \
|
{\"name\": \"Number of bedrooms & living rooms\", \"bound\": \"min\", \"value\": 4}, \
|
||||||
{\"name\": \"Max available download speed (Mbps)\", \"min\": 100}], \
|
{\"name\": \"Max available download speed (Mbps)\", \"bound\": \"min\", \"value\": 100}], \
|
||||||
\"enum_filters\": [{\"name\": \"Property type\", \"values\": [\"Flat\"]}], \
|
\"enum_filters\": [{\"name\": \"Property type\", \"values\": [\"Flat\"]}], \
|
||||||
\"notes\": \"No filter for: beach proximity\"}"
|
\"notes\": \"No filter for: beach proximity\"}"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
|
@ -175,9 +179,9 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
|
||||||
parts.push(
|
parts.push(
|
||||||
"\nUser: \"large family home with a garden near restaurants\"\n\
|
"\nUser: \"large family home with a garden near restaurants\"\n\
|
||||||
Output: {\"numeric_filters\": [\
|
Output: {\"numeric_filters\": [\
|
||||||
{\"name\": \"Total floor area (sqm)\", \"min\": 100}, \
|
{\"name\": \"Total floor area (sqm)\", \"bound\": \"min\", \"value\": 100}, \
|
||||||
{\"name\": \"Number of bedrooms & living rooms\", \"min\": 5}, \
|
{\"name\": \"Number of bedrooms & living rooms\", \"bound\": \"min\", \"value\": 5}, \
|
||||||
{\"name\": \"Number of restaurants within 2km\", \"min\": 10}], \
|
{\"name\": \"Number of restaurants within 2km\", \"bound\": \"min\", \"value\": 10}], \
|
||||||
\"enum_filters\": [{\"name\": \"Property type\", \
|
\"enum_filters\": [{\"name\": \"Property type\", \
|
||||||
\"values\": [\"Detached\", \"Semi-Detached\"]}], \
|
\"values\": [\"Detached\", \"Semi-Detached\"]}], \
|
||||||
\"notes\": \"No filter for: garden\"}"
|
\"notes\": \"No filter for: garden\"}"
|
||||||
|
|
@ -187,7 +191,7 @@ pub fn build_system_prompt(features: &FeaturesResponse) -> String {
|
||||||
// Output format reminder
|
// Output format reminder
|
||||||
parts.push(
|
parts.push(
|
||||||
"\n--- OUTPUT FORMAT ---\n\
|
"\n--- OUTPUT FORMAT ---\n\
|
||||||
{\"numeric_filters\": [...], \"enum_filters\": [...], \"notes\": \"...\"}\n\
|
{\"numeric_filters\": [{\"name\": \"...\", \"bound\": \"min\"|\"max\", \"value\": N}, ...], \"enum_filters\": [...], \"notes\": \"...\"}\n\
|
||||||
Respond with ONLY the JSON object. No explanation."
|
Respond with ONLY the JSON object. No explanation."
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
|
@ -244,10 +248,10 @@ pub async fn post_ai_filters(
|
||||||
|
|
||||||
/// Validate LLM output against feature metadata and convert to FeatureFilters format.
|
/// Validate LLM output against feature metadata and convert to FeatureFilters format.
|
||||||
///
|
///
|
||||||
/// Input format (array-based, grammar-friendly):
|
/// Input format (array-based, each numeric filter sets one bound):
|
||||||
/// ```json
|
/// ```json
|
||||||
/// {
|
/// {
|
||||||
/// "numeric_filters": [{"name": "Last known price", "min": 0, "max": 300000}],
|
/// "numeric_filters": [{"name": "Last known price", "bound": "max", "value": 300000}],
|
||||||
/// "enum_filters": [{"name": "Leashold/Freehold", "values": ["Freehold"]}]
|
/// "enum_filters": [{"name": "Leashold/Freehold", "values": ["Freehold"]}]
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -278,7 +282,7 @@ fn validate_and_convert(raw: &Value, features: &FeaturesResponse) -> Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process numeric filters
|
// Process numeric filters — each sets one bound (min or max)
|
||||||
if let Some(arr) = raw.get("numeric_filters").and_then(|val| val.as_array()) {
|
if let Some(arr) = raw.get("numeric_filters").and_then(|val| val.as_array()) {
|
||||||
for item in arr {
|
for item in arr {
|
||||||
let name = match item.get("name").and_then(|val| val.as_str()) {
|
let name = match item.get("name").and_then(|val| val.as_str()) {
|
||||||
|
|
@ -289,16 +293,19 @@ fn validate_and_convert(raw: &Value, features: &FeaturesResponse) -> Value {
|
||||||
Some(range) => *range,
|
Some(range) => *range,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
let filter_min = item
|
let bound = match item.get("bound").and_then(|val| val.as_str()) {
|
||||||
.get("min")
|
Some(b) => b,
|
||||||
.and_then(|val| val.as_f64())
|
None => continue,
|
||||||
.map(|num| num.max(feat_min as f64).min(feat_max as f64) as f32)
|
};
|
||||||
.unwrap_or(feat_min);
|
let value = match item.get("value").and_then(|val| val.as_f64()) {
|
||||||
let filter_max = item
|
Some(v) => v.max(feat_min as f64).min(feat_max as f64) as f32,
|
||||||
.get("max")
|
None => continue,
|
||||||
.and_then(|val| val.as_f64())
|
};
|
||||||
.map(|num| num.max(feat_min as f64).min(feat_max as f64) as f32)
|
let (filter_min, filter_max) = match bound {
|
||||||
.unwrap_or(feat_max);
|
"min" => (value, feat_max),
|
||||||
|
"max" => (feat_min, value),
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
// Only include if range is narrower than full range
|
// Only include if range is narrower than full range
|
||||||
if filter_min > feat_min || filter_max < feat_max {
|
if filter_min > feat_min || filter_max < feat_max {
|
||||||
result.insert(name.to_string(), json!([filter_min, filter_max]));
|
result.insert(name.to_string(), json!([filter_min, filter_max]));
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use std::hash::{Hash, Hasher};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::extract::Query;
|
use axum::extract::Query;
|
||||||
use axum::http::{header, StatusCode};
|
use axum::http::{header, HeaderMap, StatusCode};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
use rust_xlsxwriter::{Format, FormatAlign, FormatBorder, Image, Url, Workbook};
|
use rust_xlsxwriter::{Format, FormatAlign, FormatBorder, Image, Url, Workbook};
|
||||||
|
|
@ -14,7 +14,7 @@ use tracing::{info, warn};
|
||||||
use crate::auth::OptionalUser;
|
use crate::auth::OptionalUser;
|
||||||
use crate::licensing::check_license_bounds;
|
use crate::licensing::check_license_bounds;
|
||||||
use crate::parsing::{parse_field_indices, parse_filters, require_bounds, row_passes_filters};
|
use crate::parsing::{parse_field_indices, parse_filters, require_bounds, row_passes_filters};
|
||||||
use crate::routes::FeatureInfo;
|
use crate::routes::{fetch_screenshot_bytes, FeatureInfo};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
const MAX_EXPORT_POSTCODES: usize = 250;
|
const MAX_EXPORT_POSTCODES: usize = 250;
|
||||||
|
|
@ -120,39 +120,9 @@ fn build_frontend_params(
|
||||||
parts.join("&")
|
parts.join("&")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch a screenshot image from the screenshot service for Excel export.
|
|
||||||
async fn fetch_screenshot(
|
|
||||||
state: &AppState,
|
|
||||||
frontend_params: &str,
|
|
||||||
) -> Option<Vec<u8>> {
|
|
||||||
let screenshot_base = &state.screenshot_url;
|
|
||||||
|
|
||||||
let url = format!("{}/screenshot?{}", screenshot_base, frontend_params);
|
|
||||||
|
|
||||||
match state.http_client.get(&url).send().await {
|
|
||||||
Ok(resp) if resp.status().is_success() => match resp.bytes().await {
|
|
||||||
Ok(bytes) => {
|
|
||||||
info!(bytes = bytes.len(), "Fetched screenshot for export");
|
|
||||||
Some(bytes.to_vec())
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Failed to read screenshot response for export: {err}");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ok(resp) => {
|
|
||||||
warn!(status = %resp.status(), "Screenshot service returned error for export");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Failed to reach screenshot service for export: {err}");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_export(
|
pub async fn get_export(
|
||||||
state: Arc<AppState>,
|
state: Arc<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
Extension(user): Extension<OptionalUser>,
|
Extension(user): Extension<OptionalUser>,
|
||||||
Query(params): Query<ExportParams>,
|
Query(params): Query<ExportParams>,
|
||||||
) -> Result<impl IntoResponse, axum::response::Response> {
|
) -> Result<impl IntoResponse, axum::response::Response> {
|
||||||
|
|
@ -186,7 +156,18 @@ pub async fn get_export(
|
||||||
build_frontend_params(center_lat, center_lon, zoom, filters_str.as_deref());
|
build_frontend_params(center_lat, center_lon, zoom, filters_str.as_deref());
|
||||||
|
|
||||||
// Fetch screenshot (async, before spawn_blocking)
|
// Fetch screenshot (async, before spawn_blocking)
|
||||||
let screenshot_bytes = fetch_screenshot(&state, &frontend_params).await;
|
let auth_header = headers.get(header::AUTHORIZATION);
|
||||||
|
let screenshot_bytes = match fetch_screenshot_bytes(&state, &frontend_params, auth_header).await
|
||||||
|
{
|
||||||
|
Ok(bytes) => {
|
||||||
|
info!(bytes = bytes.len(), "Fetched screenshot for export");
|
||||||
|
Some(bytes)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Screenshot failed for export: {err}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Build feature name → description map from the precomputed features response
|
// Build feature name → description map from the precomputed features response
|
||||||
let feature_descriptions: FxHashMap<String, String> = state
|
let feature_descriptions: FxHashMap<String, String> = state
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, LazyLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::extract::Request;
|
use axum::extract::Request;
|
||||||
|
|
@ -8,6 +9,17 @@ use tracing::warn;
|
||||||
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
/// Dedicated HTTP client for proxying — does not follow redirects so 3xx
|
||||||
|
/// responses are passed through to the browser (needed for OAuth flows).
|
||||||
|
static PROXY_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
|
.timeout(Duration::from_secs(30))
|
||||||
|
.connect_timeout(Duration::from_secs(5))
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build proxy HTTP client")
|
||||||
|
});
|
||||||
|
|
||||||
pub async fn proxy_to_pocketbase(state: Arc<AppState>, req: Request) -> impl IntoResponse {
|
pub async fn proxy_to_pocketbase(state: Arc<AppState>, req: Request) -> impl IntoResponse {
|
||||||
let pb_url = state.pocketbase_url.trim_end_matches('/');
|
let pb_url = state.pocketbase_url.trim_end_matches('/');
|
||||||
|
|
||||||
|
|
@ -21,7 +33,7 @@ pub async fn proxy_to_pocketbase(state: Arc<AppState>, req: Request) -> impl Int
|
||||||
let url = format!("{pb_url}{target_path}{query}");
|
let url = format!("{pb_url}{target_path}{query}");
|
||||||
|
|
||||||
let method = req.method().clone();
|
let method = req.method().clone();
|
||||||
let mut builder = state.http_client.request(method, &url);
|
let mut builder = PROXY_CLIENT.request(method, &url);
|
||||||
|
|
||||||
// Forward only safe headers (allowlist)
|
// Forward only safe headers (allowlist)
|
||||||
const ALLOWED_HEADERS: &[&str] = &[
|
const ALLOWED_HEADERS: &[&str] = &[
|
||||||
|
|
@ -37,6 +49,21 @@ pub async fn proxy_to_pocketbase(state: Arc<AppState>, req: Request) -> impl Int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forward client IP so PocketBase rate-limits per-user, not per-server.
|
||||||
|
// Prefer existing X-Forwarded-For (from reverse proxy), fall back to X-Real-IP.
|
||||||
|
if let Some(xff) = req.headers().get("x-forwarded-for") {
|
||||||
|
builder = builder.header("X-Forwarded-For", xff.clone());
|
||||||
|
// First IP in the chain is the original client
|
||||||
|
if let Ok(s) = xff.to_str() {
|
||||||
|
if let Some(client_ip) = s.split(',').next().map(str::trim) {
|
||||||
|
builder = builder.header("X-Real-IP", client_ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(real_ip) = req.headers().get("x-real-ip") {
|
||||||
|
builder = builder.header("X-Forwarded-For", real_ip.clone());
|
||||||
|
builder = builder.header("X-Real-IP", real_ip.clone());
|
||||||
|
}
|
||||||
|
|
||||||
// Forward body
|
// Forward body
|
||||||
let body_bytes = match axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await {
|
let body_bytes = match axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,53 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::http::header::HeaderValue;
|
||||||
use axum::http::{header, HeaderMap, StatusCode, Uri};
|
use axum::http::{header, HeaderMap, StatusCode, Uri};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
/// Fetch a PNG screenshot from the screenshot service.
|
||||||
|
/// Used by both the `/api/screenshot` proxy and the xlsx export.
|
||||||
|
pub async fn fetch_screenshot_bytes(
|
||||||
|
state: &AppState,
|
||||||
|
query_string: &str,
|
||||||
|
auth_header: Option<&HeaderValue>,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
|
let url = format!("{}/screenshot?{}", state.screenshot_url, query_string);
|
||||||
|
info!("Fetching screenshot from: {}", url);
|
||||||
|
|
||||||
|
let mut req = state.http_client.get(&url);
|
||||||
|
if let Some(auth) = auth_header {
|
||||||
|
req = req.header(header::AUTHORIZATION, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
match req.send().await {
|
||||||
|
Ok(resp) if resp.status().is_success() => resp
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map(|b| b.to_vec())
|
||||||
|
.map_err(|err| format!("Failed to read screenshot response: {err}")),
|
||||||
|
Ok(resp) => {
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
Err(format!(
|
||||||
|
"Screenshot service returned {status}: {body}"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(err) => Err(format!("Failed to reach screenshot service: {err}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_screenshot(
|
pub async fn get_screenshot(
|
||||||
state: Arc<AppState>,
|
state: Arc<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let screenshot_base = &state.screenshot_url;
|
let qs = uri.query().unwrap_or_default();
|
||||||
|
let auth = headers.get(header::AUTHORIZATION);
|
||||||
|
|
||||||
let qs = uri
|
match fetch_screenshot_bytes(&state, qs, auth).await {
|
||||||
.query()
|
|
||||||
.map(|q| format!("?{q}"))
|
|
||||||
.unwrap_or_default();
|
|
||||||
let url = format!("{screenshot_base}/screenshot{qs}");
|
|
||||||
info!("Proxying screenshot request to: {}", url);
|
|
||||||
|
|
||||||
let mut req = state.http_client.get(&url);
|
|
||||||
if let Some(auth) = headers.get(header::AUTHORIZATION) {
|
|
||||||
req = req.header(header::AUTHORIZATION, auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
match req.send().await {
|
|
||||||
Ok(resp) if resp.status().is_success() => match resp.bytes().await {
|
|
||||||
Ok(bytes) => (
|
Ok(bytes) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
[
|
[
|
||||||
|
|
@ -37,19 +58,8 @@ pub async fn get_screenshot(
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("Failed to read screenshot response: {}", err);
|
warn!("{err}");
|
||||||
(StatusCode::BAD_GATEWAY, "Failed to read screenshot").into_response()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ok(resp) => {
|
|
||||||
let status = resp.status();
|
|
||||||
let body = resp.text().await.unwrap_or_default();
|
|
||||||
warn!("Screenshot service returned status {}: {}", status, body);
|
|
||||||
(StatusCode::BAD_GATEWAY, "Screenshot service error").into_response()
|
(StatusCode::BAD_GATEWAY, "Screenshot service error").into_response()
|
||||||
}
|
}
|
||||||
Err(err) => {
|
|
||||||
warn!("Failed to reach screenshot service: {}", err);
|
|
||||||
(StatusCode::BAD_GATEWAY, "Screenshot service unavailable").into_response()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue