From f4de0eeb9ff830ac648414cc70e014be4399982a Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 15 Mar 2026 21:10:54 +0000 Subject: [PATCH] good stuff --- docker-compose.yml | 2 +- finder/storage.py | 9 + frontend/public/robots.txt | 9 + frontend/public/sitemap.xml | 23 + frontend/src/App.tsx | 4 + .../src/components/account/AccountPage.tsx | 87 +- frontend/src/components/invite/InvitePage.tsx | 18 +- frontend/src/components/learn/LearnPage.tsx | 250 +- frontend/src/components/map/AiFilterInput.tsx | 16 +- .../components/map/ExternalSearchLinks.tsx | 7 +- frontend/src/components/map/Filters.tsx | 53 +- frontend/src/components/ui/Header.tsx | 13 +- frontend/src/components/ui/UserMenu.tsx | 25 +- frontend/src/hooks/useSavedProperties.ts | 12 + frontend/src/hooks/useSavedSearches.ts | 25 +- frontend/src/lib/external-search.ts | 31 +- grafana-dashboard.json | 3833 +++++++++++++++++ screenshot/src/cache.ts | 4 + screenshot/src/screenshot.ts | 52 +- server-rs/src/aggregation.rs | 17 + server-rs/src/data/postcodes.rs | 75 +- server-rs/src/features.rs | 27 +- server-rs/src/main.rs | 13 +- server-rs/src/metrics.rs | 60 +- server-rs/src/parsing.rs | 2 +- server-rs/src/parsing/filters.rs | 4 + server-rs/src/parsing/h3.rs | 23 + server-rs/src/pocketbase.rs | 226 +- server-rs/src/routes.rs | 2 + server-rs/src/routes/ai_filters.rs | 5 + server-rs/src/routes/hexagon_stats.rs | 9 +- server-rs/src/routes/hexagons.rs | 97 +- server-rs/src/routes/pois.rs | 15 +- server-rs/src/routes/postcodes.rs | 106 +- server-rs/src/routes/properties.rs | 6 +- server-rs/src/routes/screenshot.rs | 9 +- server-rs/src/routes/stats.rs | 265 +- server-rs/src/routes/telemetry.rs | 77 + server-rs/src/state.rs | 2 +- 39 files changed, 5165 insertions(+), 348 deletions(-) create mode 100644 frontend/public/robots.txt create mode 100644 frontend/public/sitemap.xml create mode 100644 grafana-dashboard.json create mode 100644 server-rs/src/routes/telemetry.rs diff --git a/docker-compose.yml b/docker-compose.yml index 2b2ff44..c8126ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,7 +38,7 @@ services: SCREENSHOT_URL: http://screenshot:8002 GEMINI_API_KEY: AIzaSyC2mQDcEwILHM3uOE2C-lxUQbQrKTX9Xi4 GEMINI_MODEL: gemini-3-flash-preview - PUBLIC_URL: https://perfectpostcodes.schmelczer.dev + PUBLIC_URL: https://perfect-postcodes.co.uk GOOGLE_MAPS_API_KEY: "AIzaSyBgBn9LjrxHCjb9j1LZbLYpEdCJj-NkHPY" STRIPE_SECRET_KEY: sk_test_51SyVcePRjj2bdyn1HLkatQ5onwp8kamm41tjMcRdxXnJYWVPsVd9usMTOSNtNdGhrjbsrtNbgTdKXICg2qBiocEn00PvNDC0d3 STRIPE_WEBHOOK_SECRET: whsec_pIkGZblYlcN2VesTxq4pk1cDqdxOQ1y0 diff --git a/finder/storage.py b/finder/storage.py index deda74f..ded3f42 100644 --- a/finder/storage.py +++ b/finder/storage.py @@ -92,5 +92,14 @@ def write_parquet(properties: list[dict], path: Path, channel: str) -> None: }, ) + # Derive asking price per sqm for buy listings + if channel == "buy": + df = df.with_columns( + (pl.col("Asking price") / pl.col("Total floor area (sqm)")) + .round(0) + .cast(pl.Int32, strict=False) + .alias("Asking price per sqm"), + ) + df.write_parquet(path) log.info("Wrote %d properties to %s", len(df), path) diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000..88d5585 --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,9 @@ +User-agent: * +Allow: / +Disallow: /api/ +Disallow: /metrics +Disallow: /health +Disallow: /pb/ +Disallow: /s/ + +Sitemap: https://perfect-postcode.co.uk/sitemap.xml diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml new file mode 100644 index 0000000..da7f388 --- /dev/null +++ b/frontend/public/sitemap.xml @@ -0,0 +1,23 @@ + + + + https://perfect-postcode.co.uk/ + weekly + 1.0 + + + https://perfect-postcode.co.uk/dashboard + daily + 0.9 + + + https://perfect-postcode.co.uk/learn + monthly + 0.7 + + + https://perfect-postcode.co.uk/pricing + monthly + 0.8 + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3b7dbbc..5aedbd5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,6 +18,7 @@ import { INITIAL_VIEW_STATE } from './lib/consts'; import { useTheme } from './hooks/useTheme'; import { useIsMobile } from './hooks/useIsMobile'; import { useAuth } from './hooks/useAuth'; +import { useTelemetry } from './hooks/useTelemetry'; import { useSavedSearches } from './hooks/useSavedSearches'; import { useSavedProperties } from './hooks/useSavedProperties'; @@ -107,6 +108,7 @@ export default function App() { const { theme, toggleTheme } = useTheme(); const isMobile = useIsMobile(); + useTelemetry(); const { user, loading: authLoading, @@ -337,12 +339,14 @@ export default function App() { searches={savedSearches.searches} searchesLoading={savedSearches.loading} onDeleteSearch={savedSearches.deleteSearch} + onUpdateSearchNotes={savedSearches.updateSearchNotes} onOpenSearch={(params) => { window.location.href = `/dashboard?${params}`; }} savedProperties={savedProperties.properties} propertiesLoading={savedProperties.loading} onDeleteProperty={savedProperties.deleteProperty} + onUpdatePropertyNotes={savedProperties.updatePropertyNotes} onOpenProperty={(postcode) => { window.location.href = `/dashboard?pc=${encodeURIComponent(postcode)}`; }} diff --git a/frontend/src/components/account/AccountPage.tsx b/frontend/src/components/account/AccountPage.tsx index e9fa7fe..63b0f8d 100644 --- a/frontend/src/components/account/AccountPage.tsx +++ b/frontend/src/components/account/AccountPage.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import type { AuthUser } from '../../hooks/useAuth'; import type { SavedSearch } from '../../hooks/useSavedSearches'; import type { SavedProperty, SavedPropertyData } from '../../hooks/useSavedProperties'; @@ -71,6 +71,63 @@ function DeleteDialog({ ); } +function NotesInput({ + value, + onSave, +}: { + value: string; + onSave: (notes: string) => void; +}) { + const [text, setText] = useState(value); + const textareaRef = useRef(null); + const timerRef = useRef | null>(null); + + // Sync from parent when value changes externally + useEffect(() => { + setText(value); + }, [value]); + + const autoResize = useCallback(() => { + const el = textareaRef.current; + if (!el) return; + el.style.height = 'auto'; + el.style.height = `${el.scrollHeight}px`; + }, []); + + // Resize on mount and when text changes + useEffect(() => { + autoResize(); + }, [text, autoResize]); + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const newText = e.target.value; + setText(newText); + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => onSave(newText), 600); + }, + [onSave] + ); + + // Save immediately on blur + const handleBlur = useCallback(() => { + if (timerRef.current) clearTimeout(timerRef.current); + if (text !== value) onSave(text); + }, [text, value, onSave]); + + return ( +