Hacky demo changes
This commit is contained in:
parent
7cba369308
commit
ea7afd618c
39 changed files with 2041 additions and 745 deletions
1
check.sh
1
check.sh
|
|
@ -23,6 +23,7 @@ step "Python unit tests" uv run pytest \
|
|||
step "Frontend lint: ESLint" npm run lint
|
||||
step "Frontend format check: Prettier" npm run format:check
|
||||
step "Frontend typecheck: TypeScript" npm run typecheck
|
||||
step "Frontend i18n completeness" npm run check:i18n
|
||||
step "Frontend unit tests: Vitest" npm run test
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
|
||||
"format:check": "prettier --check \"src/**/*.{ts,tsx,css}\""
|
||||
"format:check": "prettier --check \"src/**/*.{ts,tsx,css}\"",
|
||||
"check:i18n": "node scripts/check-translations.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deck.gl/core": "^9.0.0",
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 370 KiB |
Binary file not shown.
259
frontend/scripts/check-translations.mjs
Normal file
259
frontend/scripts/check-translations.mjs
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
#!/usr/bin/env node
|
||||
// Validates that every translation file under src/i18n is complete and consistent.
|
||||
//
|
||||
// Checks:
|
||||
// 1. Locales declared in SUPPORTED_LANGUAGES (index.ts) match the files in locales/
|
||||
// and the language records in descriptions.ts / details.ts.
|
||||
// 2. Every leaf key in en.ts is present and non-empty in every other locale.
|
||||
// 3. Every {{placeholder}} and HTML tag in an English string also appears,
|
||||
// with the same multiset, in the translated string.
|
||||
// 4. descriptions.ts and details.ts: the union of feature-name keys across
|
||||
// languages is treated as canonical; every language must cover all of them.
|
||||
//
|
||||
// The script parses the TypeScript source with the compiler API and walks the
|
||||
// AST — no runtime import, no transpilation, no temp files. Run it with:
|
||||
// node frontend/scripts/check-translations.mjs
|
||||
|
||||
import { readFileSync, readdirSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import ts from 'typescript';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const I18N_DIR = join(__dirname, '..', 'src', 'i18n');
|
||||
const LOCALES_DIR = join(I18N_DIR, 'locales');
|
||||
|
||||
const PLACEHOLDER_RE = /\{\{\s*[a-zA-Z_][\w]*\s*\}\}/g;
|
||||
const HTML_TAG_RE = /<\/?[a-zA-Z][\w]*\b[^>]*>/g;
|
||||
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const fail = (msg) => errors.push(msg);
|
||||
const warn = (msg) => warnings.push(msg);
|
||||
|
||||
function parseFile(path) {
|
||||
const src = readFileSync(path, 'utf8');
|
||||
return ts.createSourceFile(path, src, ts.ScriptTarget.Latest, true);
|
||||
}
|
||||
|
||||
// Recursively turn a TS literal expression into a plain JS value.
|
||||
// Returns undefined for nodes we don't understand — callers must check.
|
||||
function literalToJs(node) {
|
||||
if (!node) return undefined;
|
||||
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
||||
if (ts.isAsExpression(node) || ts.isParenthesizedExpression(node)) {
|
||||
return literalToJs(node.expression);
|
||||
}
|
||||
if (ts.isObjectLiteralExpression(node)) {
|
||||
const out = {};
|
||||
for (const prop of node.properties) {
|
||||
if (!ts.isPropertyAssignment(prop)) continue;
|
||||
const k = prop.name;
|
||||
let key;
|
||||
if (ts.isIdentifier(k)) key = k.text;
|
||||
else if (ts.isStringLiteral(k) || ts.isNoSubstitutionTemplateLiteral(k)) key = k.text;
|
||||
else continue;
|
||||
out[key] = literalToJs(prop.initializer);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (ts.isArrayLiteralExpression(node)) {
|
||||
return node.elements.map((e) => literalToJs(e));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findVarInitializer(sourceFile, name) {
|
||||
let result;
|
||||
function visit(node) {
|
||||
if (ts.isVariableStatement(node)) {
|
||||
for (const decl of node.declarationList.declarations) {
|
||||
if (ts.isIdentifier(decl.name) && decl.name.text === name) {
|
||||
result = decl.initializer;
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(sourceFile);
|
||||
return result;
|
||||
}
|
||||
|
||||
function readSupportedLanguages() {
|
||||
const sf = parseFile(join(I18N_DIR, 'index.ts'));
|
||||
const init = findVarInitializer(sf, 'SUPPORTED_LANGUAGES');
|
||||
if (!init) throw new Error('Could not find SUPPORTED_LANGUAGES in index.ts');
|
||||
const arr = literalToJs(init);
|
||||
if (!Array.isArray(arr)) throw new Error('SUPPORTED_LANGUAGES is not an array literal');
|
||||
return arr.map((entry) => entry.code);
|
||||
}
|
||||
|
||||
function readLocale(code) {
|
||||
const path = join(LOCALES_DIR, `${code}.ts`);
|
||||
const sf = parseFile(path);
|
||||
const init = findVarInitializer(sf, code);
|
||||
if (!init) throw new Error(`Could not find const ${code} in ${path}`);
|
||||
const obj = literalToJs(init);
|
||||
if (!obj || typeof obj !== 'object') throw new Error(`${code}.ts: not an object literal`);
|
||||
return obj;
|
||||
}
|
||||
|
||||
function readNamedRecord(file, varName) {
|
||||
const sf = parseFile(join(I18N_DIR, file));
|
||||
const init = findVarInitializer(sf, varName);
|
||||
if (!init) throw new Error(`Could not find ${varName} in ${file}`);
|
||||
const obj = literalToJs(init);
|
||||
if (!obj || typeof obj !== 'object') throw new Error(`${file}: ${varName} is not an object`);
|
||||
return obj;
|
||||
}
|
||||
|
||||
function flatten(obj, prefix = '', out = new Map()) {
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
const path = prefix ? `${prefix}.${k}` : k;
|
||||
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
||||
flatten(v, path, out);
|
||||
} else {
|
||||
out.set(path, v);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function tokenMultiset(s, re) {
|
||||
const matches = String(s).match(re) || [];
|
||||
// Normalise whitespace inside placeholders so '{{ count }}' == '{{count}}'.
|
||||
return matches.map((t) => t.replace(/\s+/g, '')).sort();
|
||||
}
|
||||
|
||||
function multisetsEqual(a, b) {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkLeafConsistency(path, enValue, trValue, lang) {
|
||||
if (typeof trValue !== 'string') {
|
||||
fail(`[${lang}] ${path}: missing translation`);
|
||||
return;
|
||||
}
|
||||
if (trValue.trim() === '') {
|
||||
fail(`[${lang}] ${path}: empty translation`);
|
||||
return;
|
||||
}
|
||||
for (const [re, label] of [
|
||||
[PLACEHOLDER_RE, 'placeholder'],
|
||||
[HTML_TAG_RE, 'HTML tag'],
|
||||
]) {
|
||||
const want = tokenMultiset(enValue, re);
|
||||
const got = tokenMultiset(trValue, re);
|
||||
if (!multisetsEqual(want, got)) {
|
||||
fail(
|
||||
`[${lang}] ${path}: ${label} mismatch — en=${JSON.stringify(want)} ` +
|
||||
`${lang}=${JSON.stringify(got)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkLocales(supportedCodes) {
|
||||
const localeFiles = readdirSync(LOCALES_DIR)
|
||||
.filter((f) => f.endsWith('.ts'))
|
||||
.map((f) => f.replace(/\.ts$/, ''));
|
||||
|
||||
for (const code of supportedCodes) {
|
||||
if (!localeFiles.includes(code)) {
|
||||
fail(`SUPPORTED_LANGUAGES lists "${code}" but locales/${code}.ts is missing`);
|
||||
}
|
||||
}
|
||||
for (const code of localeFiles) {
|
||||
if (!supportedCodes.includes(code)) {
|
||||
warn(`locales/${code}.ts exists but is not listed in SUPPORTED_LANGUAGES`);
|
||||
}
|
||||
}
|
||||
|
||||
const en = flatten(readLocale('en'));
|
||||
for (const code of supportedCodes) {
|
||||
if (code === 'en') continue;
|
||||
if (!localeFiles.includes(code)) continue;
|
||||
const tr = flatten(readLocale(code));
|
||||
for (const [path, enValue] of en) {
|
||||
checkLeafConsistency(path, enValue, tr.get(path), code);
|
||||
}
|
||||
for (const path of tr.keys()) {
|
||||
if (!en.has(path)) warn(`[${code}] ${path}: extra key not in en.ts`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkRecordCoverage(file, varName, supportedCodes, serverKeys) {
|
||||
const record = readNamedRecord(file, varName);
|
||||
const expected = supportedCodes.filter((c) => c !== 'en');
|
||||
const present = Object.keys(record);
|
||||
|
||||
for (const code of expected) {
|
||||
if (!present.includes(code)) {
|
||||
fail(`${file}: missing language record "${code}"`);
|
||||
}
|
||||
}
|
||||
for (const code of present) {
|
||||
if (!expected.includes(code)) {
|
||||
warn(`${file}: unexpected language record "${code}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Use the union of feature-name keys across languages as canonical.
|
||||
const union = new Set();
|
||||
for (const code of expected) {
|
||||
if (record[code]) for (const k of Object.keys(record[code])) union.add(k);
|
||||
}
|
||||
|
||||
for (const code of expected) {
|
||||
const langKeys = new Set(Object.keys(record[code] ?? {}));
|
||||
for (const key of union) {
|
||||
if (!langKeys.has(key)) {
|
||||
fail(`${file} [${code}]: missing translation for "${key}"`);
|
||||
} else {
|
||||
const v = record[code][key];
|
||||
if (typeof v !== 'string' || v.trim() === '') {
|
||||
fail(`${file} [${code}]: empty translation for "${key}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every key here must also be a translatable feature name in en.ts > server.
|
||||
// Otherwise the description is unreachable — ts() looks up server.${name}.
|
||||
for (const key of union) {
|
||||
if (!serverKeys.has(key)) {
|
||||
fail(`${file}: key "${key}" has no matching entry in en.ts > server`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
let supportedCodes;
|
||||
try {
|
||||
supportedCodes = readSupportedLanguages();
|
||||
} catch (e) {
|
||||
console.error(`fatal: ${e.message}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
checkLocales(supportedCodes);
|
||||
const en = readLocale('en');
|
||||
const serverKeys = new Set(Object.keys(en.server ?? {}));
|
||||
checkRecordCoverage('descriptions.ts', 'descriptions', supportedCodes, serverKeys);
|
||||
checkRecordCoverage('details.ts', 'details', supportedCodes, serverKeys);
|
||||
|
||||
for (const w of warnings) console.warn(`warn: ${w}`);
|
||||
if (errors.length > 0) {
|
||||
for (const e of errors) console.error(`error: ${e}`);
|
||||
console.error(`\n${errors.length} translation error(s).`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
`i18n OK — ${supportedCodes.length} languages, ${warnings.length} warning(s).`
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -15,11 +15,13 @@ import { TickerValue } from '../ui/TickerValue';
|
|||
import {
|
||||
ChartBarIcon,
|
||||
CheckIcon,
|
||||
ChevronIcon,
|
||||
ClipboardIcon,
|
||||
DownloadIcon,
|
||||
FilterIcon,
|
||||
LogoIcon,
|
||||
MapPinIcon,
|
||||
PlayIcon,
|
||||
RouteIcon,
|
||||
} from '../ui/icons';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
|
|
@ -84,7 +86,9 @@ function highlightBrandText(text: string) {
|
|||
|
||||
function ProductDemoVideo() {
|
||||
const sectionRef = useRef<HTMLDivElement | null>(null);
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const [shouldLoadVideo, setShouldLoadVideo] = useState(false);
|
||||
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const section = sectionRef.current;
|
||||
|
|
@ -108,14 +112,30 @@ function ProductDemoVideo() {
|
|||
return () => observer.disconnect();
|
||||
}, [shouldLoadVideo]);
|
||||
|
||||
const playVideo = () => {
|
||||
const video = videoRef.current;
|
||||
setShouldLoadVideo(true);
|
||||
if (!video) return;
|
||||
|
||||
if (!video.getAttribute('src')) {
|
||||
video.src = PRODUCT_DEMO_VIDEO_SRC;
|
||||
video.load();
|
||||
}
|
||||
|
||||
void video.play().catch(() => {
|
||||
setIsVideoPlaying(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id={PRODUCT_DEMO_SECTION_ID}
|
||||
ref={sectionRef}
|
||||
className={`${HOME_SECTION_CONTAINER_CLASS} pt-8 md:pt-12 pb-2`}
|
||||
>
|
||||
<div className="overflow-hidden rounded-lg border border-warm-200 bg-navy-950 shadow-sm dark:border-warm-700">
|
||||
<div className="relative overflow-hidden rounded-lg border border-warm-200 bg-navy-950 shadow-sm dark:border-warm-700">
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={shouldLoadVideo ? PRODUCT_DEMO_VIDEO_SRC : undefined}
|
||||
poster={PRODUCT_DEMO_POSTER_SRC}
|
||||
controls
|
||||
|
|
@ -123,7 +143,22 @@ function ProductDemoVideo() {
|
|||
preload={shouldLoadVideo ? 'metadata' : 'none'}
|
||||
className="block aspect-video w-full bg-navy-950 object-contain"
|
||||
aria-label="Perfect Postcode product demo"
|
||||
onPlay={() => setIsVideoPlaying(true)}
|
||||
onPause={() => setIsVideoPlaying(false)}
|
||||
onEnded={() => setIsVideoPlaying(false)}
|
||||
/>
|
||||
{!isVideoPlaying && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-navy-950/15 transition-colors">
|
||||
<button
|
||||
type="button"
|
||||
onClick={playVideo}
|
||||
className="pointer-events-auto group flex h-20 w-20 items-center justify-center rounded-full bg-white/95 text-coral-500 shadow-2xl shadow-navy-950/40 ring-1 ring-white/60 transition-transform hover:scale-105 focus:outline-none focus-visible:scale-105 focus-visible:ring-4 focus-visible:ring-teal-300/75 md:h-24 md:w-24"
|
||||
aria-label="Play Perfect Postcode product demo"
|
||||
>
|
||||
<PlayIcon className="h-11 w-11 -translate-x-0.5 md:h-14 md:w-14" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -131,7 +166,7 @@ function ProductDemoVideo() {
|
|||
|
||||
const DEMO_FEATURES: FeatureMeta[] = [
|
||||
{
|
||||
name: 'Last known price',
|
||||
name: 'Estimated price',
|
||||
type: 'numeric',
|
||||
group: 'Properties',
|
||||
min: 0,
|
||||
|
|
@ -165,13 +200,13 @@ const DEMO_FEATURES: FeatureMeta[] = [
|
|||
suffix: ' dB',
|
||||
},
|
||||
{
|
||||
name: 'Distance to nearest train or tube station (km)',
|
||||
name: 'Travel time to nearest train or tube station (min)',
|
||||
type: 'numeric',
|
||||
group: 'Transport',
|
||||
min: 0,
|
||||
max: 5,
|
||||
step: 0.1,
|
||||
suffix: ' km',
|
||||
max: 60,
|
||||
step: 1,
|
||||
suffix: ' min',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -234,15 +269,6 @@ const HOUSE_PRICE_BREAKDOWN = [
|
|||
{ label: '£/sq ft', value: '£872', helper: 'local median' },
|
||||
];
|
||||
|
||||
const RECENT_SALES = [
|
||||
{ address: '2-bed flat, Lexham Gardens', price: '£612k', meta: 'Mar 2025 · 710 sq ft' },
|
||||
{ address: '1-bed flat, Earl\'s Court Road', price: '£485k', meta: 'Feb 2025 · 544 sq ft' },
|
||||
{ address: '3-bed maisonette, Warwick Road', price: '£918k', meta: 'Jan 2025 · 1,020 sq ft' },
|
||||
{ address: 'Studio, Courtfield Gardens', price: '£365k', meta: 'Dec 2024 · 361 sq ft' },
|
||||
{ address: '2-bed flat, Collingham Place', price: '£755k', meta: 'Nov 2024 · 814 sq ft' },
|
||||
{ address: '4-bed terrace, Nevern Square', price: '£1.64m', meta: 'Oct 2024 · 1,780 sq ft' },
|
||||
];
|
||||
|
||||
const PRICE_SIGNALS = [
|
||||
['5-year change', '+22%'],
|
||||
['Last 12 months', '+2.1%'],
|
||||
|
|
@ -295,9 +321,9 @@ const ENGLAND_SHOWCASE_POLYGON: number[][] = [
|
|||
];
|
||||
|
||||
const SHOWCASE_MAP_START_VIEW: ViewState = {
|
||||
longitude: -1.52,
|
||||
latitude: 52.92,
|
||||
zoom: 5.55,
|
||||
longitude: -1.7,
|
||||
latitude: 52.7,
|
||||
zoom: 6.6,
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
};
|
||||
|
|
@ -386,10 +412,10 @@ function interpolateViewState(progress: number): ViewState {
|
|||
}
|
||||
|
||||
function demoSliderStep(feature: FeatureMeta): number {
|
||||
if (feature.name === 'Last known price') return 1000;
|
||||
if (feature.name === 'Estimated price') return 1000;
|
||||
if (feature.name === 'Noise (dB)') return 0.05;
|
||||
if (feature.name === 'Good+ primary schools within 2km') return 0.01;
|
||||
if (feature.name === 'Distance to nearest train or tube station (km)') return 0.01;
|
||||
if (feature.name === 'Travel time to nearest train or tube station (min)') return 0.1;
|
||||
return feature.step ?? 1;
|
||||
}
|
||||
|
||||
|
|
@ -420,7 +446,7 @@ function FilterPreviewRow({
|
|||
feature,
|
||||
value,
|
||||
rangeLabel,
|
||||
withoutLabel,
|
||||
withoutCount,
|
||||
index,
|
||||
isTightened,
|
||||
onValueChange,
|
||||
|
|
@ -428,19 +454,21 @@ function FilterPreviewRow({
|
|||
feature: FeatureMeta;
|
||||
value: [number, number];
|
||||
rangeLabel: string;
|
||||
withoutLabel: string;
|
||||
withoutCount: number;
|
||||
index: number;
|
||||
isTightened: boolean;
|
||||
onValueChange: (value: [number, number]) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const style = FILTER_ROW_STYLES[index % FILTER_ROW_STYLES.length];
|
||||
const shortLabel =
|
||||
{
|
||||
'Last known price': 'Price',
|
||||
'Noise (dB)': 'Noise',
|
||||
'Good+ primary schools within 2km': 'Schools',
|
||||
'Distance to nearest train or tube station (km)': 'Station',
|
||||
}[feature.name] ?? feature.name;
|
||||
const shortLabelKeys = {
|
||||
'Estimated price': 'home.showcaseFeaturePriceShort',
|
||||
'Noise (dB)': 'home.showcaseFeatureNoiseShort',
|
||||
'Good+ primary schools within 2km': 'home.showcaseFeatureSchoolsShort',
|
||||
'Travel time to nearest train or tube station (min)': 'home.showcaseFeatureTravelShort',
|
||||
} as const;
|
||||
const shortLabelKey = shortLabelKeys[feature.name as keyof typeof shortLabelKeys];
|
||||
const shortLabel = shortLabelKey ? t(shortLabelKey) : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -453,7 +481,7 @@ function FilterPreviewRow({
|
|||
<div className="min-w-0">
|
||||
<FeatureLabel feature={feature} size="sm" className="hidden sm:flex" />
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-warm-700 dark:text-warm-300 sm:hidden">
|
||||
<FeatureLabel feature={{ ...feature, name: shortLabel }} size="sm" />
|
||||
<FeatureLabel feature={feature} size="sm" label={shortLabel} />
|
||||
</div>
|
||||
<div className="mt-1 text-sm font-medium text-warm-500 dark:text-warm-400">
|
||||
{rangeLabel}
|
||||
|
|
@ -462,7 +490,9 @@ function FilterPreviewRow({
|
|||
<span
|
||||
className={`w-fit shrink-0 rounded-md px-2.5 py-1 text-xs font-bold leading-none ${style.chip}`}
|
||||
>
|
||||
{withoutLabel}
|
||||
+
|
||||
<span className="font-mono tabular-nums">{withoutCount.toLocaleString()}</span>
|
||||
{' without this filter'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 px-2 pl-4 sm:mt-5">
|
||||
|
|
@ -484,7 +514,7 @@ function formatCompactCurrency(value: number): string {
|
|||
}
|
||||
|
||||
function formatDemoRange(feature: FeatureMeta, value: [number, number]): string {
|
||||
if (feature.name === 'Last known price') {
|
||||
if (feature.name === 'Estimated price') {
|
||||
return `${formatCompactCurrency(value[0])} - ${formatCompactCurrency(value[1])}`;
|
||||
}
|
||||
if (feature.name === 'Noise (dB)') {
|
||||
|
|
@ -493,8 +523,8 @@ function formatDemoRange(feature: FeatureMeta, value: [number, number]): string
|
|||
if (feature.name === 'Good+ primary schools within 2km') {
|
||||
return `${Math.round(value[0])}+ good primaries nearby`;
|
||||
}
|
||||
if (feature.name === 'Distance to nearest train or tube station (km)') {
|
||||
return `Within ${value[1].toFixed(1)} km of rail`;
|
||||
if (feature.name === 'Travel time to nearest train or tube station (min)') {
|
||||
return `Within ${Math.round(value[1])} min of rail`;
|
||||
}
|
||||
return `${value[0]} - ${value[1]}`;
|
||||
}
|
||||
|
|
@ -539,32 +569,36 @@ function FilterOnlyScreen({ isActive }: { isActive: boolean }) {
|
|||
[240000, 535000],
|
||||
[135000, 485000],
|
||||
[285000, 610000],
|
||||
[0, 650000],
|
||||
] as [number, number][],
|
||||
without: [41820, 50622, 24860, 18645, 29796],
|
||||
without: [41820, 50622, 24860, 18645, 29796, 41820],
|
||||
},
|
||||
{
|
||||
feature: DEMO_FEATURES[3],
|
||||
values: [
|
||||
[40, 58],
|
||||
[43, 52],
|
||||
[40, 58],
|
||||
] as [number, number][],
|
||||
without: [19412, 8706],
|
||||
without: [19412, 8706, 19412],
|
||||
},
|
||||
{
|
||||
feature: DEMO_FEATURES[4],
|
||||
values: [
|
||||
[0, 60],
|
||||
[5, 25],
|
||||
[0, 60],
|
||||
] as [number, number][],
|
||||
without: [11209, 4118, 11209],
|
||||
},
|
||||
{
|
||||
feature: DEMO_FEATURES[2],
|
||||
values: [
|
||||
[1, 8],
|
||||
[2, 6],
|
||||
[1, 8],
|
||||
] as [number, number][],
|
||||
without: [13608, 6944],
|
||||
},
|
||||
{
|
||||
feature: DEMO_FEATURES[4],
|
||||
values: [
|
||||
[0, 1.4],
|
||||
[0.25, 0.9],
|
||||
] as [number, number][],
|
||||
without: [11209, 4118],
|
||||
without: [13608, 6944, 13608],
|
||||
},
|
||||
],
|
||||
[]
|
||||
|
|
@ -617,15 +651,15 @@ function FilterOnlyScreen({ isActive }: { isActive: boolean }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto bg-white scrollbar-hide dark:bg-navy-950">
|
||||
<div className="mx-auto grid min-h-full max-w-3xl content-start gap-2 p-2.5 sm:content-between sm:gap-3 sm:p-4">
|
||||
<div className="h-full overflow-y-auto bg-white scrollbar-hide dark:bg-navy-950/50 dark:backdrop-blur-sm">
|
||||
<div className="mx-auto grid min-h-full max-w-3xl content-start gap-2 p-2.5 sm:content-center sm:gap-3 sm:p-4">
|
||||
{rows.map((row, index) => (
|
||||
<div key={row.feature.name} className={index >= 3 ? 'hidden sm:block' : undefined}>
|
||||
<FilterPreviewRow
|
||||
feature={row.feature}
|
||||
value={filterState[index].value}
|
||||
rangeLabel={formatDemoRange(row.feature, filterState[index].value)}
|
||||
withoutLabel={`+${filterState[index].without.toLocaleString()} without this filter`}
|
||||
withoutCount={filterState[index].without}
|
||||
index={index}
|
||||
isTightened
|
||||
onValueChange={(value) => updateFilter(index, value)}
|
||||
|
|
@ -671,7 +705,7 @@ function EnglandHexMapScreen({ isActive }: { isActive: boolean }) {
|
|||
}, [isActive]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none relative h-full overflow-hidden bg-warm-100 dark:bg-navy-950">
|
||||
<div className="pointer-events-none relative h-full overflow-hidden bg-warm-100 dark:bg-navy-950/50">
|
||||
<ProductMap
|
||||
data={SHOWCASE_MAP_DATA}
|
||||
postcodeData={EMPTY_SHOWCASE_POSTCODES}
|
||||
|
|
@ -758,9 +792,9 @@ function RightPaneOnlyScreen({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-hidden bg-white dark:bg-navy-900">
|
||||
<div className="h-full overflow-hidden bg-white dark:bg-navy-900/60 dark:backdrop-blur-sm">
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<div className="bg-white px-3 py-3 shadow-sm dark:bg-navy-900 sm:px-5 sm:py-4">
|
||||
<div className="bg-white px-3 py-3 shadow-sm dark:bg-navy-900/65 sm:px-5 sm:py-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-teal-50 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300 sm:h-9 sm:w-9">
|
||||
<MapPinIcon className="h-4 w-4 sm:h-5 sm:w-5" />
|
||||
|
|
@ -771,22 +805,6 @@ function RightPaneOnlyScreen({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-3 divide-x divide-warm-200 rounded-md bg-warm-50 text-center dark:divide-navy-700 dark:bg-navy-950 sm:mt-4">
|
||||
{[
|
||||
['Sales', '1,284'],
|
||||
['Median sold', '£492k'],
|
||||
['Scout rank', '#3'],
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="px-1.5 py-1.5 sm:px-2 sm:py-2">
|
||||
<div className="text-xs font-bold uppercase text-warm-400">
|
||||
{label}
|
||||
</div>
|
||||
<div className="mt-0.5 text-sm font-black text-navy-950 dark:text-warm-100">
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={scrollerRef}
|
||||
|
|
@ -795,7 +813,7 @@ function RightPaneOnlyScreen({
|
|||
onTouchStart={markUserScrolled}
|
||||
onWheel={markUserScrolled}
|
||||
>
|
||||
<div className="rounded-lg bg-warm-50 p-3 dark:bg-navy-950 sm:p-4">
|
||||
<div className="rounded-lg bg-warm-50 p-3 dark:bg-navy-950/50 sm:p-4">
|
||||
<div className="mb-1 flex items-center justify-between gap-3">
|
||||
<FeatureLabel feature={DEMO_FEATURES[0]} size="sm" />
|
||||
<span className="shrink-0 text-xs font-black text-teal-700 dark:text-teal-300">
|
||||
|
|
@ -807,7 +825,7 @@ function RightPaneOnlyScreen({
|
|||
{HOUSE_PRICE_BREAKDOWN.map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
className="rounded-md bg-white px-2.5 py-2 shadow-sm shadow-navy-950/5 dark:bg-navy-900 sm:px-3"
|
||||
className="rounded-md bg-white px-2.5 py-2 shadow-sm shadow-navy-950/5 dark:bg-navy-900/60 sm:px-3"
|
||||
>
|
||||
<div className="text-xs font-bold uppercase text-warm-400">
|
||||
{item.label}
|
||||
|
|
@ -823,35 +841,7 @@ function RightPaneOnlyScreen({
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 rounded-md bg-white p-3 shadow-sm shadow-navy-950/5 dark:bg-navy-900">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<span className="text-xs font-black text-navy-950 dark:text-warm-100">
|
||||
Recent sold prices
|
||||
</span>
|
||||
<span className="text-xs font-bold uppercase text-warm-400">within 0.5 mi</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{RECENT_SALES.map((sale, index) => (
|
||||
<div
|
||||
key={sale.address}
|
||||
className={`items-baseline justify-between gap-3 ${
|
||||
index >= 3 ? 'hidden sm:flex' : 'flex'
|
||||
}`}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-xs font-semibold text-warm-700 dark:text-warm-300">
|
||||
{sale.address}
|
||||
</div>
|
||||
<div className="text-xs font-medium text-warm-400">{sale.meta}</div>
|
||||
</div>
|
||||
<div className="shrink-0 text-xs font-black text-navy-950 dark:text-warm-100">
|
||||
{sale.price}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-3 divide-x divide-warm-200 rounded-md bg-white text-center shadow-sm shadow-navy-950/5 dark:divide-navy-700 dark:bg-navy-900">
|
||||
<div className="mt-3 grid grid-cols-3 divide-x divide-warm-200 rounded-md bg-white text-center shadow-sm shadow-navy-950/5 dark:divide-navy-700 dark:bg-navy-900/60">
|
||||
{PRICE_SIGNALS.map(([label, value]) => (
|
||||
<div key={label} className="px-2 py-2">
|
||||
<div className="text-xs font-bold uppercase text-warm-400">{label}</div>
|
||||
|
|
@ -862,7 +852,7 @@ function RightPaneOnlyScreen({
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-warm-50 p-3 dark:bg-navy-950 sm:p-4">
|
||||
<div className="rounded-lg bg-warm-50 p-3 dark:bg-navy-950/50 sm:p-4">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-warm-700 dark:text-warm-300">
|
||||
<RouteIcon className="h-4 w-4 text-teal-600 dark:text-teal-400" />
|
||||
<span>Journey routes</span>
|
||||
|
|
@ -876,7 +866,7 @@ function RightPaneOnlyScreen({
|
|||
showGoogleMapsLink={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-lg bg-warm-50 p-3 dark:bg-navy-950 sm:p-4">
|
||||
<div className="rounded-lg bg-warm-50 p-3 dark:bg-navy-950/50 sm:p-4">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<FeatureLabel feature={DEMO_FEATURES[2]} size="sm" />
|
||||
<span className="shrink-0 text-xs font-black text-teal-700 dark:text-teal-300">
|
||||
|
|
@ -892,14 +882,14 @@ function RightPaneOnlyScreen({
|
|||
formatLabel={(value) => value.toFixed(0)}
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-lg bg-warm-50 p-3 dark:bg-navy-950 sm:p-4">
|
||||
<div className="rounded-lg bg-warm-50 p-3 dark:bg-navy-950/50 sm:p-4">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-warm-700 dark:text-warm-300">
|
||||
<ChartBarIcon className="h-4 w-4 text-teal-600 dark:text-teal-400" />
|
||||
<span>{t('home.showcaseStep3Stat2Label')}</span>
|
||||
</div>
|
||||
<StackedBarChart segments={CRIME_SEGMENTS} total={82} />
|
||||
</div>
|
||||
<div className="rounded-lg bg-warm-50 p-3 dark:bg-navy-950 sm:p-4">
|
||||
<div className="rounded-lg bg-warm-50 p-3 dark:bg-navy-950/50 sm:p-4">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm font-semibold text-warm-700 dark:text-warm-300">
|
||||
<ChartBarIcon className="h-4 w-4 shrink-0 text-teal-600 dark:text-teal-400" />
|
||||
|
|
@ -928,10 +918,10 @@ function ScoutScreen() {
|
|||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full min-h-0 flex-col overflow-hidden bg-[#f7f3ed] p-3 dark:bg-navy-950 sm:p-5">
|
||||
<div className="relative z-10 mt-5 rounded-lg bg-white p-3 shadow-2xl shadow-navy-950/10 dark:bg-navy-900 sm:mt-9 sm:p-5">
|
||||
<div className="relative flex h-full min-h-0 flex-col overflow-hidden bg-[#f7f3ed] p-3 dark:bg-navy-950/45 sm:p-5">
|
||||
<div className="relative z-10 mt-5 rounded-lg bg-white p-3 shadow-2xl shadow-navy-950/10 dark:bg-navy-900/65 dark:backdrop-blur-sm sm:mt-9 sm:p-5">
|
||||
<div className="grid grid-cols-2 gap-2 sm:gap-4">
|
||||
<div className="cursor-default select-none rounded-lg border border-warm-200 bg-warm-50 p-3 shadow-sm dark:border-navy-700 dark:bg-navy-950 sm:p-4">
|
||||
<div className="cursor-default select-none rounded-lg border border-warm-200 bg-warm-50 p-3 shadow-sm dark:border-navy-700 dark:bg-navy-950/50 sm:p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-teal-50 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300 sm:h-10 sm:w-10">
|
||||
<ClipboardIcon className="h-4 w-4 sm:h-5 sm:w-5" />
|
||||
|
|
@ -973,13 +963,16 @@ function ScoutScreen() {
|
|||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M50 4 L50 78"
|
||||
d="M50 4 L50 92"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeDasharray="4 4"
|
||||
/>
|
||||
<polygon points="50,92 39,76 61,76" fill="currentColor" />
|
||||
<polygon
|
||||
points="50,92 44.5,84 55.5,84"
|
||||
className="fill-teal-500 dark:fill-teal-300"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
className="pointer-events-none absolute right-14 top-[8.75rem] z-20 h-36 w-20 text-teal-500/80 dark:text-teal-300/80 sm:right-12 sm:top-[11rem] sm:h-56 sm:w-72"
|
||||
|
|
@ -988,21 +981,20 @@ function ScoutScreen() {
|
|||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M50 4 L50 78"
|
||||
d="M50 4 L50 92"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeDasharray="4 4"
|
||||
/>
|
||||
<polygon points="50,92 39,76 61,76" fill="currentColor" />
|
||||
<polygon
|
||||
points="50,92 44.5,84 55.5,84"
|
||||
className="fill-teal-500 dark:fill-teal-300"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div className="relative z-10 mt-auto rounded-lg bg-navy-950 p-4 text-white shadow-2xl shadow-navy-950/20 sm:p-5">
|
||||
<div className="inline-flex items-center gap-2 rounded-md bg-teal-400/10 px-2.5 py-1 text-xs font-bold uppercase text-teal-200">
|
||||
<RouteIcon className="h-3.5 w-3.5" />
|
||||
{t('home.showcaseStep4Title')}
|
||||
</div>
|
||||
<div className="mt-3 text-lg font-black leading-tight sm:mt-4">
|
||||
<div className="relative z-10 mt-auto rounded-lg bg-navy-950/55 p-4 text-white shadow-2xl shadow-navy-950/20 backdrop-blur-sm sm:p-5">
|
||||
<div className="text-lg font-black leading-tight">
|
||||
{t('home.showcaseStep4Conclusion')}
|
||||
</div>
|
||||
<div className="mt-4 grid gap-2 text-xs font-medium leading-relaxed text-warm-300 sm:mt-5 sm:gap-3 sm:text-sm">
|
||||
|
|
@ -1045,9 +1037,9 @@ function DashboardShowcase({
|
|||
const showStageHeader = activeStep !== 3;
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden bg-warm-100 text-navy-950 dark:bg-navy-950 dark:text-warm-100">
|
||||
<div className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden bg-warm-100 text-navy-950 dark:bg-navy-950/45 dark:text-warm-100">
|
||||
{showStageHeader && (
|
||||
<div className="shrink-0 bg-navy-950 px-3 py-2 text-white shadow-sm sm:px-4 sm:py-3">
|
||||
<div className="shrink-0 bg-navy-950/55 px-3 py-2 text-white shadow-sm backdrop-blur-sm sm:px-4 sm:py-3">
|
||||
<div className="flex items-start gap-2 sm:gap-3">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-teal-400/10 text-teal-200 sm:h-9 sm:w-9">
|
||||
<ActiveIcon className="h-4 w-4" />
|
||||
|
|
@ -1086,23 +1078,9 @@ function DashboardShowcase({
|
|||
function HeroProductShowcase() {
|
||||
const { t } = useTranslation();
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const isStagePausedRef = useRef(false);
|
||||
const [isStagePaused, setIsStagePaused] = useState(false);
|
||||
const inspectUserScrolledRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const prefersReducedMotion =
|
||||
typeof window !== 'undefined' &&
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (prefersReducedMotion) return;
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
if (isStagePausedRef.current) return;
|
||||
setActiveStep((step) => (step + 1) % SHOWCASE_STEP_COUNT);
|
||||
}, SHOWCASE_INTERVAL_MS);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const steps: ShowcaseStep[] = [
|
||||
{
|
||||
tab: t('home.showcaseStep1Tab'),
|
||||
|
|
@ -1134,22 +1112,14 @@ function HeroProductShowcase() {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="dark relative w-full min-w-0 max-w-[58rem]"
|
||||
onMouseEnter={() => {
|
||||
isStagePausedRef.current = true;
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
isStagePausedRef.current = false;
|
||||
}}
|
||||
onFocus={() => {
|
||||
isStagePausedRef.current = true;
|
||||
}}
|
||||
onBlur={() => {
|
||||
isStagePausedRef.current = false;
|
||||
}}
|
||||
className="dark relative w-full min-w-0 max-w-[58rem] justify-self-center lg:max-w-none lg:justify-self-stretch"
|
||||
onMouseEnter={() => setIsStagePaused(true)}
|
||||
onMouseLeave={() => setIsStagePaused(false)}
|
||||
onFocus={() => setIsStagePaused(true)}
|
||||
onBlur={() => setIsStagePaused(false)}
|
||||
aria-label={t('home.showcaseContext')}
|
||||
>
|
||||
<div className="flex h-[34rem] flex-col overflow-hidden rounded-lg bg-[#080d19] shadow-2xl shadow-black/40 sm:h-[46rem] md:h-[50rem] lg:h-[45.5rem] xl:h-[43rem]">
|
||||
<div className="flex h-[34rem] flex-col overflow-hidden rounded-lg bg-navy-950/50 shadow-2xl shadow-black/40 backdrop-blur-sm ring-1 ring-white/10 sm:h-[46rem] md:h-[50rem] lg:h-[47rem] xl:h-[46rem]">
|
||||
<div className="shrink-0 bg-white/[0.035] p-1.5 sm:p-2 md:p-3">
|
||||
<div className="grid grid-cols-4 gap-1 sm:gap-2">
|
||||
{steps.map((step, index) => {
|
||||
|
|
@ -1184,7 +1154,11 @@ function HeroProductShowcase() {
|
|||
className="showcase-progress block h-full origin-left bg-teal-400"
|
||||
style={{
|
||||
animationDuration: `${SHOWCASE_INTERVAL_MS}ms`,
|
||||
animationPlayState: isStagePaused ? 'paused' : 'running',
|
||||
}}
|
||||
onAnimationEnd={() =>
|
||||
setActiveStep((step) => (step + 1) % SHOWCASE_STEP_COUNT)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
|
|
@ -1255,15 +1229,39 @@ export default function HomePage({
|
|||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const scrollToProductDemoVideo = () => {
|
||||
const target = document.getElementById(PRODUCT_DEMO_SECTION_ID);
|
||||
if (!target) return;
|
||||
const scroller = target.closest('.overflow-y-auto') as HTMLElement | null;
|
||||
if (!scroller) return;
|
||||
const start = scroller.scrollTop;
|
||||
const end =
|
||||
start +
|
||||
target.getBoundingClientRect().top -
|
||||
scroller.getBoundingClientRect().top +
|
||||
24;
|
||||
const distance = end - start;
|
||||
const duration = 1200;
|
||||
let startTime: number;
|
||||
const step = (time: number) => {
|
||||
if (!startTime) startTime = time;
|
||||
const p = Math.min((time - startTime) / duration, 1);
|
||||
const ease = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
|
||||
scroller.scrollTop = start + distance * ease;
|
||||
if (p < 1) requestAnimationFrame(step);
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-warm-50 dark:bg-navy-950 relative">
|
||||
<div className="relative" style={{ zIndex: 1 }}>
|
||||
{/* Hero */}
|
||||
<div className="relative overflow-hidden bg-gradient-to-b from-navy-950 via-navy-900 to-navy-900 dark:from-navy-950 dark:via-navy-900 dark:to-navy-950 min-h-[calc(100dvh-3rem)] flex flex-col">
|
||||
<HexCanvas isDark={theme === 'dark'} animated={false} />
|
||||
<div className="relative z-10 max-w-[96rem] mx-auto w-full px-6 md:px-10 pt-6 pb-8 md:pt-24 lg:pb-0 backdrop-blur-[2px] flex-1 flex flex-col">
|
||||
<div className="grid gap-x-8 gap-y-6 lg:grid-cols-[0.85fr_1.15fr] lg:gap-12 items-center">
|
||||
<div className="min-w-0 max-w-4xl">
|
||||
<div className="relative z-10 mx-auto flex w-full max-w-[104rem] flex-1 flex-col px-6 pb-8 pt-6 backdrop-blur-[2px] md:px-10 md:py-10 lg:py-12">
|
||||
<div className="hero-roomy-lift grid flex-1 items-center gap-x-8 gap-y-6 lg:grid-cols-[minmax(0,0.82fr)_minmax(38rem,1.18fr)] lg:gap-12 xl:grid-cols-[minmax(0,0.78fr)_minmax(44rem,1.22fr)] xl:gap-16">
|
||||
<div className="min-w-0 max-w-4xl lg:max-w-[42rem] xl:max-w-[45rem]">
|
||||
<p className="text-sm font-semibold text-teal-300 mb-3">{t('home.heroEyebrow')}</p>
|
||||
<h1 className="text-3xl md:text-5xl font-extrabold text-white mb-4 leading-[1.1]">
|
||||
{t('home.heroTitle1')}{' '}
|
||||
|
|
@ -1290,27 +1288,7 @@ export default function HomePage({
|
|||
<button
|
||||
onClick={() => {
|
||||
trackEvent('CTA Click', { location: 'hero', label: 'see_difference' });
|
||||
const target = document.getElementById(PRODUCT_DEMO_SECTION_ID);
|
||||
if (!target) return;
|
||||
const scroller = target.closest('.overflow-y-auto') as HTMLElement | null;
|
||||
if (!scroller) return;
|
||||
const start = scroller.scrollTop;
|
||||
const end =
|
||||
start +
|
||||
target.getBoundingClientRect().top -
|
||||
scroller.getBoundingClientRect().top -
|
||||
48;
|
||||
const distance = end - start;
|
||||
const duration = 1200;
|
||||
let startTime: number;
|
||||
const step = (time: number) => {
|
||||
if (!startTime) startTime = time;
|
||||
const p = Math.min((time - startTime) / duration, 1);
|
||||
const ease = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
|
||||
scroller.scrollTop = start + distance * ease;
|
||||
if (p < 1) requestAnimationFrame(step);
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
scrollToProductDemoVideo();
|
||||
}}
|
||||
className="px-7 py-3 border-2 border-teal-400 text-teal-400 rounded-lg font-semibold hover:bg-teal-400/10 transition-colors text-base text-center"
|
||||
>
|
||||
|
|
@ -1340,8 +1318,18 @@ export default function HomePage({
|
|||
</div>
|
||||
<HeroProductShowcase />
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="hero-scroll-chevron absolute bottom-4 left-1/2 z-20 -translate-x-1/2 items-center justify-center rounded-full text-white transition-colors hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-white/50"
|
||||
aria-label="Scroll to product demo"
|
||||
onClick={() => {
|
||||
trackEvent('CTA Click', { location: 'hero_chevron', label: 'scroll_down' });
|
||||
scrollToProductDemoVideo();
|
||||
}}
|
||||
>
|
||||
<ChevronIcon direction="down" className="h-14 w-14" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="home-content-surface relative overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ interface MapProps {
|
|||
currentLocation?: { lat: number; lng: number } | null;
|
||||
bounds?: Bounds | null;
|
||||
hideLegend?: boolean;
|
||||
hideLocationSearch?: boolean;
|
||||
travelTimeEntries?: TravelTimeEntry[];
|
||||
densityLabel?: string;
|
||||
totalCount?: number;
|
||||
|
|
@ -173,6 +174,7 @@ export default memo(function Map({
|
|||
currentLocation,
|
||||
bounds: viewportBounds,
|
||||
hideLegend = false,
|
||||
hideLocationSearch = false,
|
||||
travelTimeEntries = EMPTY_TRAVEL_ENTRIES,
|
||||
densityLabel: densityLabelProp,
|
||||
totalCount: totalCountProp,
|
||||
|
|
@ -231,6 +233,7 @@ export default memo(function Map({
|
|||
}, [viewState, dimensions, onViewChange]);
|
||||
|
||||
const handleMove = useCallback((evt: { viewState: ViewState }) => {
|
||||
if (window.__demoRecording) window.__demoMapIdle = false;
|
||||
setInternalViewState((prev) => {
|
||||
const next = evt.viewState;
|
||||
// Skip re-render when viewport values haven't changed (e.g. container resize
|
||||
|
|
@ -249,6 +252,14 @@ export default memo(function Map({
|
|||
});
|
||||
}, []);
|
||||
|
||||
const handleIdle = useCallback(() => {
|
||||
if (screenshotMode) window.__map_idle = true;
|
||||
if (window.__demoRecording) {
|
||||
window.__demoMapIdle = true;
|
||||
window.__demoMapIdleVersion = (window.__demoMapIdleVersion ?? 0) + 1;
|
||||
}
|
||||
}, [screenshotMode]);
|
||||
|
||||
const handleFlyTo = useCallback((lat: number, lng: number, zoom: number) => {
|
||||
setInternalViewState((prev) => ({ ...prev, latitude: lat, longitude: lng, zoom }));
|
||||
}, []);
|
||||
|
|
@ -293,13 +304,7 @@ export default memo(function Map({
|
|||
{...viewState}
|
||||
onMove={handleMove}
|
||||
onLoad={undefined}
|
||||
onIdle={
|
||||
screenshotMode
|
||||
? () => {
|
||||
window.__map_idle = true;
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onIdle={handleIdle}
|
||||
mapStyle={mapStyle}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
attributionControl={false}
|
||||
|
|
@ -362,12 +367,14 @@ export default memo(function Map({
|
|||
) : (
|
||||
<>
|
||||
<div className="absolute top-3 left-3 right-3 z-[60] flex flex-wrap items-start justify-between gap-2 pointer-events-none">
|
||||
<LocationSearch
|
||||
onFlyTo={handleFlyTo}
|
||||
onLocationSearched={onLocationSearched}
|
||||
onCurrentLocationFound={onCurrentLocationFound}
|
||||
onMouseEnter={handleMouseLeave}
|
||||
/>
|
||||
{!hideLocationSearch && (
|
||||
<LocationSearch
|
||||
onFlyTo={handleFlyTo}
|
||||
onLocationSearched={onLocationSearched}
|
||||
onCurrentLocationFound={onCurrentLocationFound}
|
||||
onMouseEnter={handleMouseLeave}
|
||||
/>
|
||||
)}
|
||||
{!hideLegend &&
|
||||
(viewFeature && colorRange ? (
|
||||
viewFeature.startsWith('tt_') ? (
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import { apiUrl, authHeaders, buildFilterString, logNonAbortError } from '../../
|
|||
import { useFilterCounts } from '../../hooks/useFilterCounts';
|
||||
import { ts } from '../../i18n/server';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { canWheelScrollInsideTarget } from '../../lib/dom-scroll';
|
||||
import { INITIAL_VIEW_STATE } from '../../lib/consts';
|
||||
import { getSchoolBackendFeatureName } from '../../lib/school-filter';
|
||||
import { useLicense } from '../../hooks/useLicense';
|
||||
|
|
@ -54,6 +55,18 @@ const MapPageSelectionPane = lazy(() =>
|
|||
const UpgradeModal = lazy(() => import('../ui/UpgradeModal'));
|
||||
const Joyride = lazy(() => import('react-joyride'));
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__demoRecording?: boolean;
|
||||
__demoOpenBestHexagon?: () => string | null;
|
||||
__demoMapSettled?: boolean;
|
||||
__demoMapSettleVersion?: number;
|
||||
__demoMapIdle?: boolean;
|
||||
__demoMapIdleVersion?: number;
|
||||
__demoSelectionReady?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
function MapFallback() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-warm-100 dark:bg-navy-950">
|
||||
|
|
@ -218,6 +231,9 @@ export default function MapPage({
|
|||
travelTimeEntries: entries,
|
||||
shareCode,
|
||||
});
|
||||
const demoMapHasData = mapData.usePostcodeView
|
||||
? mapData.postcodeData.length > 0
|
||||
: mapData.data.length > 0;
|
||||
|
||||
const handleAiFilterSubmit = useCallback(
|
||||
async (query: string) => {
|
||||
|
|
@ -416,6 +432,48 @@ export default function MapPage({
|
|||
setRightPaneTab(initialTab);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.__demoRecording) return;
|
||||
void import('./MapPageSelectionPane');
|
||||
void import('./AreaPane');
|
||||
void import('./PropertiesPane');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.__demoRecording) return;
|
||||
window.__demoMapSettled = !mapData.loading && demoMapHasData;
|
||||
if (window.__demoMapSettled) {
|
||||
window.__demoMapSettleVersion = (window.__demoMapSettleVersion ?? 0) + 1;
|
||||
}
|
||||
return () => {
|
||||
window.__demoMapSettled = false;
|
||||
};
|
||||
}, [demoMapHasData, mapData.loading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.__demoRecording) return;
|
||||
window.__demoSelectionReady = Boolean(selectedHexagon && areaStats && !loadingAreaStats);
|
||||
return () => {
|
||||
window.__demoSelectionReady = false;
|
||||
};
|
||||
}, [areaStats, loadingAreaStats, selectedHexagon]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.__demoRecording) return;
|
||||
window.__demoOpenBestHexagon = () => {
|
||||
const best = mapData.data.reduce<(typeof mapData.data)[number] | null>((winner, item) => {
|
||||
if (!winner || item.count > winner.count) return item;
|
||||
return winner;
|
||||
}, null);
|
||||
if (!best) return null;
|
||||
handleHexagonClick(best.h3);
|
||||
return best.h3;
|
||||
};
|
||||
return () => {
|
||||
delete window.__demoOpenBestHexagon;
|
||||
};
|
||||
}, [handleHexagonClick, mapData.data]);
|
||||
|
||||
// Navigate to a specific postcode on mount (e.g. from saved properties)
|
||||
useEffect(() => {
|
||||
if (!initialPostcode) return;
|
||||
|
|
@ -451,7 +509,10 @@ export default function MapPage({
|
|||
// Prevent browser back/forward navigation from horizontal trackpad swipes
|
||||
useEffect(() => {
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
|
||||
if (
|
||||
Math.abs(e.deltaX) > Math.abs(e.deltaY) &&
|
||||
!canWheelScrollInsideTarget(e.target, e.deltaX, e.deltaY)
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
|
@ -873,6 +934,7 @@ export default function MapPage({
|
|||
currentLocation={currentLocation}
|
||||
bounds={mapData.bounds}
|
||||
hideLegend
|
||||
hideLocationSearch={mobileDrawerOpen && !!selectedHexagon}
|
||||
travelTimeEntries={entries}
|
||||
/>
|
||||
</Suspense>
|
||||
|
|
@ -898,7 +960,7 @@ export default function MapPage({
|
|||
</button>
|
||||
|
||||
{poiPaneOpen && (
|
||||
<div className="absolute top-14 right-3 left-3 z-20 max-h-[45dvh] overflow-hidden flex flex-col rounded-lg shadow-xl border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-900">
|
||||
<div className="absolute top-14 right-3 left-3 z-20 flex h-[45dvh] min-h-0 flex-col overflow-hidden rounded-lg border border-warm-200 bg-white shadow-xl dark:border-warm-700 dark:bg-warm-900">
|
||||
{renderPOIPane()}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1045,7 +1107,7 @@ export default function MapPage({
|
|||
</button>
|
||||
{/* Floating POI panel */}
|
||||
{poiPaneOpen && (
|
||||
<div className="absolute bottom-14 right-4 z-10 w-80 max-h-[60vh] overflow-hidden flex flex-col rounded-lg shadow-xl border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-900">
|
||||
<div className="absolute bottom-14 right-4 z-10 flex h-[60vh] min-h-0 w-80 flex-col overflow-hidden rounded-lg border border-warm-200 bg-white shadow-xl dark:border-warm-700 dark:bg-warm-900">
|
||||
{renderPOIPane()}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1062,6 +1124,7 @@ export default function MapPage({
|
|||
onClose={handleCloseSelection}
|
||||
renderAreaPane={renderAreaPane}
|
||||
renderPropertiesPane={renderPropertiesPane}
|
||||
demoReady={Boolean(areaStats && !loadingAreaStats)}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ interface MapPageSelectionPaneProps {
|
|||
onClose: () => void;
|
||||
renderAreaPane: () => ReactNode;
|
||||
renderPropertiesPane: () => ReactNode;
|
||||
demoReady?: boolean;
|
||||
}
|
||||
|
||||
export function MapPageSelectionPane({
|
||||
|
|
@ -29,10 +30,12 @@ export function MapPageSelectionPane({
|
|||
onClose,
|
||||
renderAreaPane,
|
||||
renderPropertiesPane,
|
||||
demoReady = false,
|
||||
}: MapPageSelectionPaneProps) {
|
||||
return (
|
||||
<div
|
||||
data-tutorial="right-pane"
|
||||
data-demo-ready={demoReady ? 'true' : 'false'}
|
||||
className="flex bg-white dark:bg-navy-950 shadow-lg z-10"
|
||||
style={{ width }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export default function POIPane({
|
|||
const selectedCount = selectedCategories.size;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-white dark:bg-warm-900 shadow-lg overflow-hidden">
|
||||
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-white shadow-lg dark:bg-warm-900">
|
||||
<div className="flex-shrink-0 px-3 pt-3 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-semibold text-warm-500 dark:text-warm-400 uppercase tracking-wide">
|
||||
|
|
@ -150,7 +150,7 @@ export default function POIPane({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto border-t border-warm-200 dark:border-warm-700">
|
||||
<div className="flex-1 min-h-0 overflow-y-auto overscroll-contain border-t border-warm-200 dark:border-warm-700">
|
||||
<div className="px-3 pt-2 pb-1">
|
||||
<SearchInput
|
||||
value={searchTerm}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ interface FeatureLabelProps {
|
|||
className?: string;
|
||||
size?: 'xs' | 'sm';
|
||||
description?: string;
|
||||
label?: string;
|
||||
hideIconOnMobile?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ export function FeatureLabel({
|
|||
className = '',
|
||||
size = 'xs',
|
||||
description,
|
||||
label,
|
||||
hideIconOnMobile,
|
||||
}: FeatureLabelProps) {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -29,7 +31,7 @@ export function FeatureLabel({
|
|||
const featureIcon = getFeatureIcon(feature.name, iconClass);
|
||||
const GroupIcon = !featureIcon && feature.group ? getGroupIcon(feature.group) : null;
|
||||
|
||||
const translatedName = ts(feature.name);
|
||||
const translatedName = label ?? ts(feature.name);
|
||||
const translatedDesc = description ? tsDesc(feature.name, description) : undefined;
|
||||
|
||||
const nameContent = (
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ interface PillGroupProps {
|
|||
export function PillGroup({ children, className = '' }: PillGroupProps) {
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-nowrap overflow-x-auto gap-1.5 md:flex-wrap md:overflow-x-visible scrollbar-hide ${className}`}
|
||||
className={`flex min-w-0 max-w-full flex-nowrap gap-1.5 overflow-x-auto overscroll-x-contain touch-pan-x touch-pan-y scrollbar-hide md:flex-wrap md:overflow-x-visible ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
|||
11
frontend/src/components/ui/icons/PlayIcon.tsx
Normal file
11
frontend/src/components/ui/icons/PlayIcon.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
interface IconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PlayIcon({ className = 'w-4 h-4' }: IconProps) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M6.75 4.35v15.3a1.05 1.05 0 0 0 1.6.9l12.55-7.65a1.05 1.05 0 0 0 0-1.8L8.35 3.45a1.05 1.05 0 0 0-1.6.9z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ export { LogoIcon } from './LogoIcon';
|
|||
export { MapPinIcon } from './MapPinIcon';
|
||||
export { MenuIcon } from './MenuIcon';
|
||||
export { MoonIcon } from './MoonIcon';
|
||||
export { PlayIcon } from './PlayIcon';
|
||||
export { PlusIcon } from './PlusIcon';
|
||||
export { RouteIcon } from './RouteIcon';
|
||||
export { SearchIcon } from './SearchIcon';
|
||||
|
|
|
|||
|
|
@ -348,6 +348,10 @@ const de: Translations = {
|
|||
seeTheDifference: 'So funktioniert es',
|
||||
showcaseHeader: 'So funktioniert es',
|
||||
showcaseContext: 'So funktioniert Perfect Postcode',
|
||||
showcaseFeaturePriceShort: 'Preis',
|
||||
showcaseFeatureNoiseShort: 'Lärm',
|
||||
showcaseFeatureSchoolsShort: 'Schulen',
|
||||
showcaseFeatureTravelShort: 'Fahrzeit',
|
||||
showcaseStep1Tab: 'Filtern',
|
||||
showcaseStep1Title: 'Aus vagen Wünschen eine präzise Suche machen',
|
||||
showcaseStep1Body:
|
||||
|
|
@ -801,6 +805,7 @@ const de: Translations = {
|
|||
'Property type': 'Immobilientyp',
|
||||
'Leasehold/Freehold': 'Erbbaurecht/Volleigentum',
|
||||
'Last known price': 'Letzter bekannter Preis',
|
||||
'Estimated price': 'Geschätzter Preis',
|
||||
'Estimated current price': 'Geschätzter aktueller Preis',
|
||||
'Price per sqm': 'Preis pro m²',
|
||||
'Est. price per sqm': 'Gesch. Preis pro m²',
|
||||
|
|
@ -817,6 +822,8 @@ const de: Translations = {
|
|||
// ─ Feature names (Transport) ─
|
||||
'Distance to nearest train or tube station (km)':
|
||||
'Entfernung zum nächsten Bahn- oder U-Bahnhof (km)',
|
||||
'Travel time to nearest train or tube station (min)':
|
||||
'Fahrzeit zum nächsten Bahn- oder U-Bahnhof (Min.)',
|
||||
|
||||
// ─ Feature names (Education) ─
|
||||
'Good+ primary schools within 2km': 'Gute+ Grundschulen im Umkreis von 2 km',
|
||||
|
|
|
|||
|
|
@ -343,6 +343,10 @@ const en = {
|
|||
seeTheDifference: 'See how it works',
|
||||
showcaseHeader: 'How it works',
|
||||
showcaseContext: 'How Perfect Postcode works',
|
||||
showcaseFeaturePriceShort: 'Price',
|
||||
showcaseFeatureNoiseShort: 'Noise',
|
||||
showcaseFeatureSchoolsShort: 'Schools',
|
||||
showcaseFeatureTravelShort: 'Travel',
|
||||
showcaseStep1Tab: 'Filter',
|
||||
showcaseStep1Title: 'Turn vague needs into a tight search',
|
||||
showcaseStep1Body:
|
||||
|
|
@ -788,6 +792,7 @@ const en = {
|
|||
'Property type': 'Property type',
|
||||
'Leasehold/Freehold': 'Leasehold/Freehold',
|
||||
'Last known price': 'Last known price',
|
||||
'Estimated price': 'Estimated price',
|
||||
'Estimated current price': 'Estimated current price',
|
||||
'Price per sqm': 'Price per sqm',
|
||||
'Est. price per sqm': 'Est. price per sqm',
|
||||
|
|
@ -804,6 +809,8 @@ const en = {
|
|||
// ─ Feature names (Transport) ─
|
||||
'Distance to nearest train or tube station (km)':
|
||||
'Distance to nearest train or tube station (km)',
|
||||
'Travel time to nearest train or tube station (min)':
|
||||
'Travel time to nearest train or tube station (min)',
|
||||
|
||||
// ─ Feature names (Education) ─
|
||||
'Good+ primary schools within 2km': 'Good+ primary schools within 2km',
|
||||
|
|
|
|||
|
|
@ -351,6 +351,10 @@ const fr: Translations = {
|
|||
seeTheDifference: 'Voir comment ça marche',
|
||||
showcaseHeader: 'Comment ça marche',
|
||||
showcaseContext: 'Comment fonctionne Perfect Postcode',
|
||||
showcaseFeaturePriceShort: 'Prix',
|
||||
showcaseFeatureNoiseShort: 'Bruit',
|
||||
showcaseFeatureSchoolsShort: 'Écoles',
|
||||
showcaseFeatureTravelShort: 'Trajet',
|
||||
showcaseStep1Tab: 'Filtrer',
|
||||
showcaseStep1Title: 'Transformez des besoins vagues en recherche précise',
|
||||
showcaseStep1Body:
|
||||
|
|
@ -802,6 +806,7 @@ const fr: Translations = {
|
|||
'Property type': 'Type de bien',
|
||||
'Leasehold/Freehold': 'Bail/Pleine propriété',
|
||||
'Last known price': 'Dernier prix connu',
|
||||
'Estimated price': 'Prix estimé',
|
||||
'Estimated current price': 'Prix actuel estimé',
|
||||
'Price per sqm': 'Prix au m²',
|
||||
'Est. price per sqm': 'Prix estimé au m²',
|
||||
|
|
@ -818,6 +823,8 @@ const fr: Translations = {
|
|||
// ─ Feature names (Transport) ─
|
||||
'Distance to nearest train or tube station (km)':
|
||||
'Distance à la gare ou station de métro la plus proche (km)',
|
||||
'Travel time to nearest train or tube station (min)':
|
||||
'Temps de trajet jusqu’à la gare ou station de métro la plus proche (min)',
|
||||
|
||||
// ─ Feature names (Education) ─
|
||||
'Good+ primary schools within 2km': 'Écoles primaires Bien+ dans un rayon de 2 km',
|
||||
|
|
|
|||
|
|
@ -323,6 +323,10 @@ const hi: Translations = {
|
|||
seeTheDifference: 'देखें यह कैसे काम करता है',
|
||||
showcaseHeader: 'यह कैसे काम करता है',
|
||||
showcaseContext: 'Perfect Postcode कैसे काम करता है',
|
||||
showcaseFeaturePriceShort: 'कीमत',
|
||||
showcaseFeatureNoiseShort: 'शोर',
|
||||
showcaseFeatureSchoolsShort: 'स्कूल',
|
||||
showcaseFeatureTravelShort: 'यात्रा',
|
||||
showcaseStep1Tab: 'फिल्टर',
|
||||
showcaseStep1Title: 'अस्पष्ट जरूरतों को सटीक खोज में बदलें',
|
||||
showcaseStep1Body:
|
||||
|
|
@ -738,6 +742,7 @@ const hi: Translations = {
|
|||
'Property type': 'संपत्ति प्रकार',
|
||||
'Leasehold/Freehold': 'लीजहोल्ड/फ्रीहोल्ड',
|
||||
'Last known price': 'अंतिम ज्ञात कीमत',
|
||||
'Estimated price': 'अनुमानित कीमत',
|
||||
'Estimated current price': 'अनुमानित मौजूदा कीमत',
|
||||
'Price per sqm': 'प्रति वर्ग मी कीमत',
|
||||
'Est. price per sqm': 'अनु. प्रति वर्ग मी कीमत',
|
||||
|
|
@ -751,6 +756,8 @@ const hi: Translations = {
|
|||
'Potential energy rating': 'संभावित ऊर्जा रेटिंग',
|
||||
'Interior height (m)': 'भीतरी ऊंचाई (मी)',
|
||||
'Distance to nearest train or tube station (km)': 'निकटतम ट्रेन या ट्यूब स्टेशन तक दूरी (किमी)',
|
||||
'Travel time to nearest train or tube station (min)':
|
||||
'निकटतम ट्रेन या ट्यूब स्टेशन तक यात्रा समय (मिनट)',
|
||||
'Good+ primary schools within 2km': '2 किमी के अंदर Good+ प्राथमिक स्कूल',
|
||||
'Good+ secondary schools within 2km': '2 किमी के अंदर Good+ सेकेंडरी स्कूल',
|
||||
'Good+ primary schools within 5km': '5 किमी के अंदर Good+ प्राथमिक स्कूल',
|
||||
|
|
|
|||
|
|
@ -345,6 +345,10 @@ const hu: Translations = {
|
|||
seeTheDifference: 'Így működik',
|
||||
showcaseHeader: 'Így működik',
|
||||
showcaseContext: 'Így működik a Perfect Postcode',
|
||||
showcaseFeaturePriceShort: 'Ár',
|
||||
showcaseFeatureNoiseShort: 'Zaj',
|
||||
showcaseFeatureSchoolsShort: 'Iskolák',
|
||||
showcaseFeatureTravelShort: 'Utazás',
|
||||
showcaseStep1Tab: 'Szűrés',
|
||||
showcaseStep1Title: 'A homályos igényekből pontos keresés lesz',
|
||||
showcaseStep1Body:
|
||||
|
|
@ -796,6 +800,7 @@ const hu: Translations = {
|
|||
'Property type': 'Ingatlantípus',
|
||||
'Leasehold/Freehold': 'Bérleti/Tulajdonjog',
|
||||
'Last known price': 'Utolsó ismert ár',
|
||||
'Estimated price': 'Becsült ár',
|
||||
'Estimated current price': 'Becsült jelenlegi ár',
|
||||
'Price per sqm': 'Ár per nm',
|
||||
'Est. price per sqm': 'Becsült ár per nm',
|
||||
|
|
@ -812,6 +817,8 @@ const hu: Translations = {
|
|||
// ─ Feature names (Transport) ─
|
||||
'Distance to nearest train or tube station (km)':
|
||||
'Távolság a legközelebbi vonat- vagy metróállomástól (km)',
|
||||
'Travel time to nearest train or tube station (min)':
|
||||
'Utazási idő a legközelebbi vonat- vagy metróállomásig (perc)',
|
||||
|
||||
// ─ Feature names (Education) ─
|
||||
'Good+ primary schools within 2km': 'Jó+ általános iskolák 2 km-en belül',
|
||||
|
|
|
|||
|
|
@ -340,6 +340,10 @@ const zh: Translations = {
|
|||
seeTheDifference: '查看使用方式',
|
||||
showcaseHeader: '工作原理',
|
||||
showcaseContext: 'Perfect Postcode 的工作流程',
|
||||
showcaseFeaturePriceShort: '价格',
|
||||
showcaseFeatureNoiseShort: '噪声',
|
||||
showcaseFeatureSchoolsShort: '学校',
|
||||
showcaseFeatureTravelShort: '出行',
|
||||
showcaseStep1Tab: '筛选',
|
||||
showcaseStep1Title: '把模糊需求变成精准搜索',
|
||||
showcaseStep1Body:
|
||||
|
|
@ -772,6 +776,7 @@ const zh: Translations = {
|
|||
'Property type': '房产类型',
|
||||
'Leasehold/Freehold': '租赁产权/永久产权',
|
||||
'Last known price': '上次成交价',
|
||||
'Estimated price': '估计价格',
|
||||
'Estimated current price': '估计当前价格',
|
||||
'Price per sqm': '每平方米价格',
|
||||
'Est. price per sqm': '估计每平方米价格',
|
||||
|
|
@ -787,6 +792,7 @@ const zh: Translations = {
|
|||
|
||||
// ─ Feature names (Transport) ─
|
||||
'Distance to nearest train or tube station (km)': '到最近火车或地铁站的距离(公里)',
|
||||
'Travel time to nearest train or tube station (min)': '到最近火车或地铁站的出行时间(分钟)',
|
||||
|
||||
// ─ Feature names (Education) ─
|
||||
'Good+ primary schools within 2km': '2公里内良好+小学数量',
|
||||
|
|
|
|||
|
|
@ -135,10 +135,42 @@ h3 {
|
|||
.showcase-progress {
|
||||
animation-name: showcase-progress;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.hero-roomy-lift {
|
||||
transform: translateY(0);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.hero-scroll-chevron {
|
||||
display: none;
|
||||
bottom: 0.75rem;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) and (min-height: 900px) {
|
||||
.hero-roomy-lift {
|
||||
transform: translateY(-2.5rem);
|
||||
}
|
||||
|
||||
.hero-scroll-chevron {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) and (min-height: 1040px) {
|
||||
.hero-roomy-lift {
|
||||
transform: translateY(-3.5rem);
|
||||
}
|
||||
|
||||
.hero-scroll-chevron {
|
||||
bottom: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scout-export-click {
|
||||
0%,
|
||||
52%,
|
||||
|
|
|
|||
58
frontend/src/lib/dom-scroll.test.ts
Normal file
58
frontend/src/lib/dom-scroll.test.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { canWheelScrollInsideTarget } from './dom-scroll';
|
||||
|
||||
function setElementMetrics(
|
||||
element: HTMLElement,
|
||||
metrics: Partial<{
|
||||
clientHeight: number;
|
||||
clientWidth: number;
|
||||
scrollHeight: number;
|
||||
scrollWidth: number;
|
||||
}>
|
||||
) {
|
||||
for (const [key, value] of Object.entries(metrics)) {
|
||||
Object.defineProperty(element, key, { configurable: true, value });
|
||||
}
|
||||
}
|
||||
|
||||
describe('canWheelScrollInsideTarget', () => {
|
||||
it('allows horizontal wheel gestures inside a horizontal scroller', () => {
|
||||
const scroller = document.createElement('div');
|
||||
scroller.style.overflowX = 'auto';
|
||||
scroller.scrollLeft = 20;
|
||||
setElementMetrics(scroller, { clientWidth: 100, scrollWidth: 240 });
|
||||
|
||||
const child = document.createElement('button');
|
||||
scroller.appendChild(child);
|
||||
document.body.appendChild(scroller);
|
||||
|
||||
expect(canWheelScrollInsideTarget(child, 40, 0)).toBe(true);
|
||||
scroller.remove();
|
||||
});
|
||||
|
||||
it('allows vertical wheel gestures inside a vertical scroller', () => {
|
||||
const scroller = document.createElement('div');
|
||||
scroller.style.overflowY = 'auto';
|
||||
scroller.scrollTop = 20;
|
||||
setElementMetrics(scroller, { clientHeight: 100, scrollHeight: 240 });
|
||||
|
||||
const child = document.createElement('button');
|
||||
scroller.appendChild(child);
|
||||
document.body.appendChild(scroller);
|
||||
|
||||
expect(canWheelScrollInsideTarget(child, 60, 20)).toBe(true);
|
||||
scroller.remove();
|
||||
});
|
||||
|
||||
it('does not allow horizontal gestures at the edge of a scroller', () => {
|
||||
const scroller = document.createElement('div');
|
||||
scroller.style.overflowX = 'auto';
|
||||
scroller.scrollLeft = 0;
|
||||
setElementMetrics(scroller, { clientWidth: 100, scrollWidth: 240 });
|
||||
|
||||
document.body.appendChild(scroller);
|
||||
|
||||
expect(canWheelScrollInsideTarget(scroller, -40, 0)).toBe(false);
|
||||
scroller.remove();
|
||||
});
|
||||
});
|
||||
45
frontend/src/lib/dom-scroll.ts
Normal file
45
frontend/src/lib/dom-scroll.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
const SCROLLABLE_OVERFLOW = new Set(['auto', 'scroll', 'overlay']);
|
||||
|
||||
function canScrollHorizontally(element: HTMLElement, deltaX: number): boolean {
|
||||
if (deltaX === 0) return false;
|
||||
|
||||
const style = window.getComputedStyle(element);
|
||||
if (!SCROLLABLE_OVERFLOW.has(style.overflowX)) return false;
|
||||
if (element.scrollWidth <= element.clientWidth) return false;
|
||||
|
||||
const maxScrollLeft = element.scrollWidth - element.clientWidth;
|
||||
if (deltaX < 0) return element.scrollLeft > 0;
|
||||
return element.scrollLeft < maxScrollLeft - 1;
|
||||
}
|
||||
|
||||
function canScrollVertically(element: HTMLElement, deltaY: number): boolean {
|
||||
if (deltaY === 0) return false;
|
||||
|
||||
const style = window.getComputedStyle(element);
|
||||
if (!SCROLLABLE_OVERFLOW.has(style.overflowY)) return false;
|
||||
if (element.scrollHeight <= element.clientHeight) return false;
|
||||
|
||||
const maxScrollTop = element.scrollHeight - element.clientHeight;
|
||||
if (deltaY < 0) return element.scrollTop > 0;
|
||||
return element.scrollTop < maxScrollTop - 1;
|
||||
}
|
||||
|
||||
export function canWheelScrollInsideTarget(
|
||||
target: EventTarget | null,
|
||||
deltaX: number,
|
||||
deltaY: number
|
||||
): boolean {
|
||||
let element = target instanceof Element ? target : null;
|
||||
|
||||
while (element && element !== document.body && element !== document.documentElement) {
|
||||
if (
|
||||
element instanceof HTMLElement &&
|
||||
(canScrollHorizontally(element, deltaX) || canScrollVertically(element, deltaY))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -19,6 +19,12 @@ const FEATURE_ICON_PATHS: Record<string, ReactNode> = {
|
|||
<polyline points="15 6 21 6 21 12" />
|
||||
</>
|
||||
),
|
||||
'Estimated price': (
|
||||
<>
|
||||
<polyline points="4 16 8 12 13 15 20 6" />
|
||||
<polyline points="15 6 21 6 21 12" />
|
||||
</>
|
||||
),
|
||||
'Price per sqm': (
|
||||
<>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
|
|
@ -109,6 +115,18 @@ const FEATURE_ICON_PATHS: Record<string, ReactNode> = {
|
|||
<line x1="12" y1="18" x2="12" y2="15" />
|
||||
</>
|
||||
),
|
||||
'Travel time to nearest train or tube station (min)': (
|
||||
<>
|
||||
<rect x="5" y="3" width="14" height="10" rx="2" />
|
||||
<path d="M8 17h8" />
|
||||
<path d="M9 13l-2 4" />
|
||||
<path d="M15 13l2 4" />
|
||||
<circle cx="8.5" cy="8" r="1" fill="currentColor" />
|
||||
<circle cx="15.5" cy="8" r="1" fill="currentColor" />
|
||||
<path d="M12 18v3" />
|
||||
<circle cx="12" cy="21" r="1.5" />
|
||||
</>
|
||||
),
|
||||
|
||||
// ── Education ────────────────────────────────
|
||||
'Education, Skills and Training Score': (
|
||||
|
|
|
|||
|
|
@ -31,9 +31,11 @@ module.exports = {
|
|||
950: '#003330',
|
||||
},
|
||||
coral: {
|
||||
50: '#fff7ed',
|
||||
400: '#fb923c',
|
||||
500: '#f97316',
|
||||
600: '#ea580c',
|
||||
700: '#c2410c',
|
||||
},
|
||||
warm: {
|
||||
50: '#fafaf9',
|
||||
|
|
|
|||
|
|
@ -2409,3 +2409,569 @@
|
|||
2026-05-05T21:26:06.465961Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:26:06.773040Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=11318 parallel=false cells_before_filter=380 cells_after_filter=363 truncated=false bounds=50.9954,-1.3207,51.0982,-1.0405 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.3 json_ms=0.3 total_ms=0.6
|
||||
2026-05-05T21:26:08.686005Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=291392 parallel=true cells_before_filter=2485 cells_after_filter=2481 truncated=false bounds=50.8884,-1.6525,51.2556,-0.6513 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=3.8 json_ms=1.6 total_ms=5.3
|
||||
2026-05-05T21:35:19.535962Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:35:19.536004Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:35:20.928669Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:35:20.943703Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:35:21.260328Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:35:21.263080Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:35:22.518336Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:35:22.519413Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:35:22.552338Z INFO property_map_server::routes::postcodes: GET /api/postcode/M44FZ postcode=M4 4FZ
|
||||
2026-05-05T21:35:22.579079Z INFO property_map_server::routes::postcode_stats: GET /api/postcode-stats postcode=M4 4FZ total_count=144 filters=0 filters_raw="-" ms=0.4
|
||||
2026-05-05T21:35:22.579978Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
|
||||
2026-05-05T21:35:22.595218Z INFO property_map_server::routes::postcodes: GET /api/postcode/M4 4FZ postcode=M4 4FZ
|
||||
2026-05-05T21:35:22.684822Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=881951b74bfffff resolution=8 total_count=2958 filters=0 filters_raw="-" ms=1.1
|
||||
2026-05-05T21:35:23.324836Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=1241 filters=0 filters_raw="-" ms=0.6
|
||||
2026-05-05T21:35:23.507421Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.4548,53.5912,-2.0354 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.9 json_ms=0.1 total_ms=3.1
|
||||
2026-05-05T21:35:23.866219Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=1 travel=1 total=41544 filters_raw="-" ms=11.3
|
||||
2026-05-05T21:35:32.108784Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.7
|
||||
2026-05-05T21:35:32.135015Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=310 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=0.3
|
||||
2026-05-05T21:35:32.136509Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.7
|
||||
2026-05-05T21:35:33.790810Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.0 json_ms=0.2 total_ms=4.1
|
||||
2026-05-05T21:35:33.818308Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=20.4
|
||||
2026-05-05T21:35:37.753394Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=2156 cells_after_filter=2155 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=7.4 json_ms=2.0 total_ms=9.4
|
||||
2026-05-05T21:35:39.940308Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.9 json_ms=0.1 total_ms=4.0
|
||||
2026-05-05T21:35:40.981422Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.6 json_ms=0.1 total_ms=4.7
|
||||
2026-05-05T21:35:41.681395Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=23.1
|
||||
2026-05-05T21:36:50.130617Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:36:50.137912Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:37:36.248757Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:37:36.255038Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:37:36.856875Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:37:36.856933Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:37:36.873429Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:37:36.880771Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:40:03.751467Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:40:09.196030Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:40:10.589453Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:40:10.589833Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:40:10.622151Z INFO property_map_server::routes::postcodes: GET /api/postcode/M44FZ postcode=M4 4FZ
|
||||
2026-05-05T21:40:10.650954Z INFO property_map_server::routes::postcode_stats: GET /api/postcode-stats postcode=M4 4FZ total_count=144 filters=0 filters_raw="-" ms=0.3
|
||||
2026-05-05T21:40:10.651985Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
|
||||
2026-05-05T21:40:10.699279Z INFO property_map_server::routes::postcodes: GET /api/postcode/M4 4FZ postcode=M4 4FZ
|
||||
2026-05-05T21:40:10.712262Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=881951b74bfffff resolution=8 total_count=2958 filters=0 filters_raw="-" ms=1.1
|
||||
2026-05-05T21:40:10.882037Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=1241 filters=0 filters_raw="-" ms=0.6
|
||||
2026-05-05T21:40:11.183074Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.4548,53.5912,-2.0354 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.5 json_ms=0.1 total_ms=3.6
|
||||
2026-05-05T21:40:11.556734Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=1 travel=1 total=41544 filters_raw="-" ms=12.9
|
||||
2026-05-05T21:40:20.767960Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.5
|
||||
2026-05-05T21:40:20.796799Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=310 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=0.4
|
||||
2026-05-05T21:40:20.798737Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.5
|
||||
2026-05-05T21:40:21.381833Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=7.7 json_ms=0.2 total_ms=7.9
|
||||
2026-05-05T21:40:21.399664Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=20.6
|
||||
2026-05-05T21:40:24.897526Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=2156 cells_after_filter=2155 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=10.6 json_ms=1.9 total_ms=12.5
|
||||
2026-05-05T21:40:25.086271Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.0 json_ms=0.1 total_ms=4.1
|
||||
2026-05-05T21:40:27.093792Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.6 json_ms=0.1 total_ms=2.7
|
||||
2026-05-05T21:40:27.109216Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=17.8
|
||||
2026-05-05T21:41:49.677712Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:41:51.102585Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:41:51.103971Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:41:51.136237Z INFO property_map_server::routes::postcodes: GET /api/postcode/M44FZ postcode=M4 4FZ
|
||||
2026-05-05T21:41:51.164096Z INFO property_map_server::routes::postcode_stats: GET /api/postcode-stats postcode=M4 4FZ total_count=144 filters=0 filters_raw="-" ms=0.3
|
||||
2026-05-05T21:41:51.165332Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.8
|
||||
2026-05-05T21:41:51.181286Z INFO property_map_server::routes::postcodes: GET /api/postcode/M4 4FZ postcode=M4 4FZ
|
||||
2026-05-05T21:41:51.269962Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=881951b74bfffff resolution=8 total_count=2958 filters=0 filters_raw="-" ms=1.1
|
||||
2026-05-05T21:41:51.920774Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=1241 filters=0 filters_raw="-" ms=0.5
|
||||
2026-05-05T21:41:52.111652Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.4548,53.5912,-2.0354 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.5 json_ms=0.1 total_ms=3.6
|
||||
2026-05-05T21:41:52.396733Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=1 travel=1 total=41544 filters_raw="-" ms=12.1
|
||||
2026-05-05T21:42:01.835682Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.9
|
||||
2026-05-05T21:42:01.856925Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=310 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=0.4
|
||||
2026-05-05T21:42:01.861410Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=4.9
|
||||
2026-05-05T21:42:02.025694Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4548,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=11.1 json_ms=0.1 total_ms=11.3
|
||||
2026-05-05T21:42:03.370679Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.5 json_ms=0.2 total_ms=3.7
|
||||
2026-05-05T21:42:03.390285Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=23.1
|
||||
2026-05-05T21:42:07.747532Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=2156 cells_after_filter=2155 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=7.0 json_ms=2.2 total_ms=9.3
|
||||
2026-05-05T21:42:07.971145Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.8 json_ms=0.1 total_ms=4.9
|
||||
2026-05-05T21:42:10.063960Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.0 json_ms=0.1 total_ms=4.1
|
||||
2026-05-05T21:42:13.030016Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=22.1
|
||||
2026-05-05T21:43:23.988313Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:43:23.989906Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:43:38.937614Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:43:40.739899Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:43:40.741294Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:43:40.836366Z INFO property_map_server::routes::postcodes: GET /api/postcode/M44FZ postcode=M4 4FZ
|
||||
2026-05-05T21:43:40.871628Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.7
|
||||
2026-05-05T21:43:40.871677Z INFO property_map_server::routes::postcode_stats: GET /api/postcode-stats postcode=M4 4FZ total_count=144 filters=0 filters_raw="-" ms=0.3
|
||||
2026-05-05T21:43:40.963658Z INFO property_map_server::routes::postcodes: GET /api/postcode/M4 4FZ postcode=M4 4FZ
|
||||
2026-05-05T21:43:40.972553Z INFO property_map_server::routes::postcodes: GET /api/postcode/M4 4FZ postcode=M4 4FZ
|
||||
2026-05-05T21:43:41.228623Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.8 json_ms=0.2 total_ms=3.0
|
||||
2026-05-05T21:43:41.589568Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=1241 filters=0 filters_raw="-" ms=0.9
|
||||
2026-05-05T21:43:41.611940Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=15.0
|
||||
2026-05-05T21:43:42.106439Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.4548,53.5912,-2.0354 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.9 json_ms=0.2 total_ms=5.1
|
||||
2026-05-05T21:43:42.148975Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=1 travel=1 total=41544 filters_raw="-" ms=12.9
|
||||
2026-05-05T21:43:50.925280Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.7
|
||||
2026-05-05T21:43:50.950197Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=310 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=0.4
|
||||
2026-05-05T21:43:50.952039Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.3
|
||||
2026-05-05T21:43:51.901986Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.9 json_ms=0.1 total_ms=3.1
|
||||
2026-05-05T21:43:51.952950Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=23.4
|
||||
2026-05-05T21:43:56.136201Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=2156 cells_after_filter=2155 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=6.7 json_ms=1.6 total_ms=8.3
|
||||
2026-05-05T21:43:56.549574Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.0 json_ms=0.2 total_ms=4.1
|
||||
2026-05-05T21:43:58.005257Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.6 json_ms=0.1 total_ms=3.7
|
||||
2026-05-05T21:43:58.316362Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=21.2
|
||||
2026-05-05T21:43:59.526091Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:43:59.526363Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:44:05.897144Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:44:05.899027Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:44:23.329571Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=223412 parallel=true cells_before_filter=740 cells_after_filter=698 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.4 total_ms=2.0
|
||||
2026-05-05T21:45:36.770349Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:45:36.770355Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:46:55.780524Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:46:57.151058Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:46:57.152528Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:46:57.183975Z INFO property_map_server::routes::postcodes: GET /api/postcode/M44FZ postcode=M4 4FZ
|
||||
2026-05-05T21:46:57.210662Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
|
||||
2026-05-05T21:46:57.210978Z INFO property_map_server::routes::postcode_stats: GET /api/postcode-stats postcode=M4 4FZ total_count=144 filters=0 filters_raw="-" ms=0.3
|
||||
2026-05-05T21:46:57.229878Z INFO property_map_server::routes::postcodes: GET /api/postcode/M4 4FZ postcode=M4 4FZ
|
||||
2026-05-05T21:46:57.313945Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=881951b74bfffff resolution=8 total_count=2958 filters=0 filters_raw="-" ms=1.1
|
||||
2026-05-05T21:46:57.497295Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=1241 filters=0 filters_raw="-" ms=0.8
|
||||
2026-05-05T21:46:57.674945Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.4548,53.5912,-2.0354 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.9 json_ms=0.1 total_ms=3.0
|
||||
2026-05-05T21:46:58.291511Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=1 travel=1 total=41544 filters_raw="-" ms=12.0
|
||||
2026-05-05T21:47:08.638543Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:47:08.640080Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:47:08.693213Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:47:08.696020Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:47:08.698555Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:47:08.698706Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:47:08.708540Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:47:08.712376Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:47:08.881143Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.7
|
||||
2026-05-05T21:47:08.922170Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.5
|
||||
2026-05-05T21:47:08.922265Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=310 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=0.4
|
||||
2026-05-05T21:47:09.053354Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4548,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.7 json_ms=0.1 total_ms=2.8
|
||||
2026-05-05T21:47:09.585970Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.7 json_ms=0.1 total_ms=2.8
|
||||
2026-05-05T21:47:09.606306Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=23.2
|
||||
2026-05-05T21:47:14.368249Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=2156 cells_after_filter=2155 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=9.1 json_ms=2.6 total_ms=11.7
|
||||
2026-05-05T21:47:15.268987Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.2 json_ms=0.1 total_ms=4.4
|
||||
2026-05-05T21:47:16.742521Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:47:16.742526Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:47:19.144031Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.8 json_ms=0.1 total_ms=4.9
|
||||
2026-05-05T21:47:21.329311Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=18.9
|
||||
2026-05-05T21:48:19.552426Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:48:20.910287Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:48:20.911597Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:48:20.944503Z INFO property_map_server::routes::postcodes: GET /api/postcode/M44FZ postcode=M4 4FZ
|
||||
2026-05-05T21:48:20.970972Z INFO property_map_server::routes::postcode_stats: GET /api/postcode-stats postcode=M4 4FZ total_count=144 filters=0 filters_raw="-" ms=0.3
|
||||
2026-05-05T21:48:20.971485Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
|
||||
2026-05-05T21:48:20.988965Z INFO property_map_server::routes::postcodes: GET /api/postcode/M4 4FZ postcode=M4 4FZ
|
||||
2026-05-05T21:48:21.070355Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=881951b74bfffff resolution=8 total_count=2958 filters=0 filters_raw="-" ms=1.3
|
||||
2026-05-05T21:48:21.194673Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=1241 filters=0 filters_raw="-" ms=0.7
|
||||
2026-05-05T21:48:21.406038Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.4548,53.5912,-2.0354 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=5.1 json_ms=0.2 total_ms=5.3
|
||||
2026-05-05T21:48:22.149166Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=1 travel=1 total=41544 filters_raw="-" ms=11.6
|
||||
2026-05-05T21:48:26.000886Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:48:26.000890Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:48:27.742269Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:48:27.743395Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:48:27.877066Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:48:27.877153Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:48:27.877463Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:48:27.882051Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:48:27.891902Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:48:27.892061Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:48:31.303985Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.2
|
||||
2026-05-05T21:48:31.324969Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=310 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=0.3
|
||||
2026-05-05T21:48:31.326996Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.4
|
||||
2026-05-05T21:48:31.688134Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.8 json_ms=0.1 total_ms=5.0
|
||||
2026-05-05T21:48:32.516497Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=18.0
|
||||
2026-05-05T21:48:38.027758Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=2156 cells_after_filter=2155 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=6.0 json_ms=1.7 total_ms=7.7
|
||||
2026-05-05T21:48:38.546001Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.2 json_ms=0.1 total_ms=3.3
|
||||
2026-05-05T21:48:40.622421Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.1 json_ms=0.1 total_ms=4.2
|
||||
2026-05-05T21:48:42.032479Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=22.6
|
||||
2026-05-05T21:49:10.338613Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:49:10.339888Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:50:46.231350Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:50:46.232494Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:50:46.232853Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:50:46.234575Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:50:46.261419Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:50:46.261843Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:50:46.286185Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:50:46.287577Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:50:47.072067Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:50:47.072362Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:50:47.073288Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:50:47.076801Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:50:47.117663Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:50:47.117695Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:50:47.118146Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:50:47.125177Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:50:52.820159Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:50:52.826259Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:50:52.826495Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:50:52.836857Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:50:52.858218Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:50:52.858230Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:50:52.894119Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:50:52.895473Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:52:16.280628Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:52:17.757767Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:52:17.759242Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:52:17.784497Z INFO property_map_server::routes::postcodes: GET /api/postcode/M44FZ postcode=M4 4FZ
|
||||
2026-05-05T21:52:17.795311Z INFO property_map_server::routes::postcode_stats: GET /api/postcode-stats postcode=M4 4FZ total_count=144 filters=0 filters_raw="-" ms=0.4
|
||||
2026-05-05T21:52:17.821652Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
|
||||
2026-05-05T21:52:17.830328Z INFO property_map_server::routes::postcodes: GET /api/postcode/M4 4FZ postcode=M4 4FZ
|
||||
2026-05-05T21:52:17.906952Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=881951b74bfffff resolution=8 total_count=2958 filters=0 filters_raw="-" ms=1.4
|
||||
2026-05-05T21:52:18.280692Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=1241 filters=0 filters_raw="-" ms=0.6
|
||||
2026-05-05T21:52:18.442852Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.4548,53.5912,-2.0354 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.3 json_ms=0.1 total_ms=2.4
|
||||
2026-05-05T21:52:18.775913Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=1 travel=1 total=41544 filters_raw="-" ms=12.3
|
||||
2026-05-05T21:52:29.653339Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:52:29.661372Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:52:29.947627Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.0
|
||||
2026-05-05T21:52:29.968963Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b74a3ffff resolution=9 total_count=310 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=0.3
|
||||
2026-05-05T21:52:29.970311Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.8
|
||||
2026-05-05T21:52:30.307589Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.7 json_ms=0.1 total_ms=3.8
|
||||
2026-05-05T21:52:30.524199Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:52:30.524204Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:52:30.624729Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=26.4
|
||||
2026-05-05T21:52:35.559305Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=2156 cells_after_filter=2155 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=9.0 json_ms=2.1 total_ms=11.1
|
||||
2026-05-05T21:52:35.822961Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.7 json_ms=0.1 total_ms=2.9
|
||||
2026-05-05T21:52:37.260802Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.5 json_ms=0.1 total_ms=3.5
|
||||
2026-05-05T21:52:37.768202Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=21.9
|
||||
2026-05-05T21:53:03.831217Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:53:03.831449Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:53:03.848657Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:53:03.848697Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:53:03.880252Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:53:03.880653Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:53:03.890238Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:53:03.891728Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:53:03.901154Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:53:03.902246Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:53:18.725683Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:53:18.725685Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:54:05.076079Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:54:05.076294Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:56:34.822373Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:56:36.141237Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:56:36.142948Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:56:36.207992Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.8
|
||||
2026-05-05T21:56:36.504745Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.2 json_ms=0.2 total_ms=3.3
|
||||
2026-05-05T21:56:36.733128Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=13.1
|
||||
2026-05-05T21:56:40.571881Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.3
|
||||
2026-05-05T21:56:40.583234Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.5
|
||||
2026-05-05T21:56:40.892631Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.5 json_ms=0.2 total_ms=4.6
|
||||
2026-05-05T21:56:41.062742Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=24.6
|
||||
2026-05-05T21:56:43.955287Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=7.9 json_ms=2.5 total_ms=10.4
|
||||
2026-05-05T21:56:44.345169Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=9.5 json_ms=0.1 total_ms=9.7
|
||||
2026-05-05T21:56:46.004109Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=5.2 json_ms=0.1 total_ms=5.2
|
||||
2026-05-05T21:56:46.078279Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=24.5
|
||||
2026-05-05T21:56:51.868367Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=268290 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4198,-2.4014,53.5395,-2.1022 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.3 json_ms=0.1 total_ms=2.4
|
||||
2026-05-05T21:56:51.879357Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=268290 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=13.3
|
||||
2026-05-05T21:56:52.896449Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=173011 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4360,-2.3633,53.5235,-2.1444 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.8 json_ms=0.1 total_ms=1.8
|
||||
2026-05-05T21:56:52.901548Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=173011 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=6.9
|
||||
2026-05-05T21:57:14.298418Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b7467ffff resolution=9 total_count=149 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=0.3
|
||||
2026-05-05T21:57:14.913263Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:57:15.416546Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=141822 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4360,-2.3359,53.5235,-2.1718 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.8 json_ms=0.1 total_ms=1.9
|
||||
2026-05-05T21:57:15.455008Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=141822 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=8.2
|
||||
2026-05-05T21:57:18.611959Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:57:18.611965Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:57:18.724225Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.3
|
||||
2026-05-05T21:57:19.334648Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=8.3 json_ms=0.2 total_ms=8.5
|
||||
2026-05-05T21:57:20.412105Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=26.8
|
||||
2026-05-05T21:57:23.399746Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.1
|
||||
2026-05-05T21:57:23.425315Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.1
|
||||
2026-05-05T21:57:23.643747Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=7.9 json_ms=0.1 total_ms=8.0
|
||||
2026-05-05T21:57:25.019448Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=27.7
|
||||
2026-05-05T21:57:27.456478Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=21.8 json_ms=2.6 total_ms=24.5
|
||||
2026-05-05T21:57:27.801520Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=7.9 json_ms=0.2 total_ms=8.0
|
||||
2026-05-05T21:57:29.430108Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=5.0 json_ms=0.1 total_ms=5.1
|
||||
2026-05-05T21:57:29.984292Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=27.1
|
||||
2026-05-05T21:57:37.788237Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=268290 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4198,-2.4014,53.5395,-2.1022 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.6 json_ms=0.1 total_ms=4.7
|
||||
2026-05-05T21:57:38.031795Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=173011 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4360,-2.3633,53.5235,-2.1444 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.3 json_ms=0.1 total_ms=4.4
|
||||
2026-05-05T21:57:39.262206Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=173011 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=9.9
|
||||
2026-05-05T21:57:58.793587Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b7467ffff resolution=9 total_count=149 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=0.2
|
||||
2026-05-05T21:57:59.886649Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=141822 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4360,-2.3359,53.5235,-2.1718 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.2 json_ms=0.1 total_ms=2.3
|
||||
2026-05-05T21:57:59.945458Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=141822 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=6.3
|
||||
2026-05-05T21:58:10.755620Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:58:10.757205Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T21:58:26.903364Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T21:58:26.904448Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:01:32.049909Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:01:34.012407Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:01:34.014056Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:01:34.073888Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.9
|
||||
2026-05-05T22:01:34.604384Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.2 json_ms=0.2 total_ms=3.3
|
||||
2026-05-05T22:01:35.318029Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=14.1
|
||||
2026-05-05T22:01:38.545948Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.1
|
||||
2026-05-05T22:01:38.559798Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.9
|
||||
2026-05-05T22:01:39.087439Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.9 json_ms=0.1 total_ms=4.1
|
||||
2026-05-05T22:01:39.111716Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=21.4
|
||||
2026-05-05T22:01:42.572213Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=9.2 json_ms=2.5 total_ms=11.7
|
||||
2026-05-05T22:01:42.812709Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.5 json_ms=0.1 total_ms=3.6
|
||||
2026-05-05T22:01:44.895449Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.4 json_ms=0.1 total_ms=4.5
|
||||
2026-05-05T22:01:44.915751Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=21.0
|
||||
2026-05-05T22:02:00.539658Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=0.6
|
||||
2026-05-05T22:02:01.209769Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.1 json_ms=0.1 total_ms=4.2
|
||||
2026-05-05T22:02:01.410662Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=21.9
|
||||
2026-05-05T22:02:30.280215Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:02:30.280220Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:02:30.494339Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:02:30.494338Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:03:04.445153Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:03:05.814766Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:03:05.816218Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:03:05.877835Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.5
|
||||
2026-05-05T22:03:06.171149Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.1 json_ms=0.2 total_ms=3.3
|
||||
2026-05-05T22:03:06.416637Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=13.0
|
||||
2026-05-05T22:03:10.243319Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
|
||||
2026-05-05T22:03:10.254863Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.3
|
||||
2026-05-05T22:03:11.600208Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.8 json_ms=0.1 total_ms=4.9
|
||||
2026-05-05T22:03:11.620435Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=24.2
|
||||
2026-05-05T22:03:14.007173Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=6.0 json_ms=1.9 total_ms=7.9
|
||||
2026-05-05T22:03:17.458780Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.2 json_ms=0.2 total_ms=3.5
|
||||
2026-05-05T22:03:18.845895Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.8 json_ms=0.1 total_ms=4.9
|
||||
2026-05-05T22:03:18.864525Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=22.8
|
||||
2026-05-05T22:03:35.601665Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=0.6
|
||||
2026-05-05T22:03:36.222150Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.7 json_ms=0.1 total_ms=3.8
|
||||
2026-05-05T22:03:36.513454Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=23.9
|
||||
2026-05-05T22:04:11.176851Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:04:12.558667Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:04:12.559951Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:04:12.628604Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.5
|
||||
2026-05-05T22:04:12.929976Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.4 json_ms=0.2 total_ms=3.6
|
||||
2026-05-05T22:04:13.173110Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=13.1
|
||||
2026-05-05T22:04:16.988146Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
|
||||
2026-05-05T22:04:17.000016Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
|
||||
2026-05-05T22:04:17.925005Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=5.9 json_ms=0.1 total_ms=6.0
|
||||
2026-05-05T22:04:17.940327Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=21.2
|
||||
2026-05-05T22:04:20.338637Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=6.4 json_ms=1.9 total_ms=8.3
|
||||
2026-05-05T22:04:20.819388Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.2 json_ms=0.2 total_ms=3.4
|
||||
2026-05-05T22:04:23.030591Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.9 json_ms=0.1 total_ms=4.9
|
||||
2026-05-05T22:04:23.052922Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=27.3
|
||||
2026-05-05T22:04:40.188074Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=1.0
|
||||
2026-05-05T22:04:40.729235Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.9 json_ms=0.1 total_ms=4.0
|
||||
2026-05-05T22:04:40.995511Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=25.3
|
||||
2026-05-05T22:05:34.435389Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:05:35.713246Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:05:35.713807Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:05:35.775163Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.8
|
||||
2026-05-05T22:05:36.061950Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.9 json_ms=0.1 total_ms=4.1
|
||||
2026-05-05T22:05:36.320303Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=12.8
|
||||
2026-05-05T22:05:40.134229Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.3
|
||||
2026-05-05T22:05:40.151547Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
|
||||
2026-05-05T22:05:41.222363Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.0 json_ms=0.1 total_ms=4.1
|
||||
2026-05-05T22:05:41.245295Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=27.0
|
||||
2026-05-05T22:05:44.655480Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=8.1 json_ms=2.4 total_ms=10.5
|
||||
2026-05-05T22:05:45.759197Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.4 json_ms=0.1 total_ms=3.5
|
||||
2026-05-05T22:05:46.783618Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.7 json_ms=0.1 total_ms=3.7
|
||||
2026-05-05T22:05:47.179663Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=26.6
|
||||
2026-05-05T22:05:52.156396Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=1.1
|
||||
2026-05-05T22:05:56.697343Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.4 json_ms=0.1 total_ms=3.5
|
||||
2026-05-05T22:06:00.750033Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=29.0
|
||||
2026-05-05T22:07:46.408950Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:07:47.806303Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:07:47.807699Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:07:47.869410Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.5
|
||||
2026-05-05T22:07:48.207181Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.6 json_ms=0.1 total_ms=4.8
|
||||
2026-05-05T22:07:48.419634Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=14.1
|
||||
2026-05-05T22:07:52.350175Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.0
|
||||
2026-05-05T22:07:52.364217Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.0
|
||||
2026-05-05T22:07:52.637745Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.7 json_ms=0.2 total_ms=3.9
|
||||
2026-05-05T22:07:53.569400Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=23.5
|
||||
2026-05-05T22:07:58.242591Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=8.5 json_ms=1.7 total_ms=10.2
|
||||
2026-05-05T22:07:59.705019Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.7 json_ms=0.2 total_ms=3.9
|
||||
2026-05-05T22:08:01.282032Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.0 json_ms=0.1 total_ms=4.1
|
||||
2026-05-05T22:08:01.302333Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=24.3
|
||||
2026-05-05T22:08:07.868528Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=1.1
|
||||
2026-05-05T22:08:11.985786Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=5.5 json_ms=0.1 total_ms=5.6
|
||||
2026-05-05T22:08:14.126304Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=22.2
|
||||
2026-05-05T22:09:36.351097Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:09:37.648717Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:09:37.650429Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:09:37.718577Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
|
||||
2026-05-05T22:09:38.022594Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.2 json_ms=0.2 total_ms=3.4
|
||||
2026-05-05T22:09:38.479616Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=15.1
|
||||
2026-05-05T22:09:42.116891Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.3
|
||||
2026-05-05T22:09:42.129082Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.7
|
||||
2026-05-05T22:09:43.385938Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.5 json_ms=0.1 total_ms=4.6
|
||||
2026-05-05T22:09:43.405845Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=24.7
|
||||
2026-05-05T22:09:45.802330Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=6.0 json_ms=1.7 total_ms=7.7
|
||||
2026-05-05T22:09:46.150374Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.9 json_ms=0.1 total_ms=4.0
|
||||
2026-05-05T22:09:47.635412Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.4 json_ms=0.1 total_ms=4.5
|
||||
2026-05-05T22:09:51.024342Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=26.9
|
||||
2026-05-05T22:09:57.125364Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=1.0
|
||||
2026-05-05T22:10:01.670253Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.0 json_ms=0.1 total_ms=3.1
|
||||
2026-05-05T22:10:03.962206Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=21.3
|
||||
2026-05-05T22:11:29.323311Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:11:29.324866Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:11:29.326169Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:11:29.327416Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:11:29.341892Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:11:29.342825Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:11:29.358511Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:11:29.361667Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:11:31.233803Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:11:31.233808Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:11:31.244858Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:11:31.244881Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:11:31.244950Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:11:31.246951Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:11:31.285478Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:11:31.286881Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:22:58.183462Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.2 total_ms=1.1
|
||||
2026-05-05T22:22:59.659662Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=89195da49c3ffff resolution=9 total_count=238 filters=0 filters_raw="-" ms=0.3
|
||||
2026-05-05T22:23:00.015894Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=64311 parallel=true cells_before_filter=249 cells_after_filter=229 truncated=false bounds=51.4958,-0.1632,51.5342,-0.0968 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.8 json_ms=0.1 total_ms=0.9
|
||||
2026-05-05T22:23:11.413301Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=64311 parallel=true cells_before_filter=249 cells_after_filter=229 truncated=false bounds=51.4958,-0.1632,51.5342,-0.0968 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.2 total_ms=1.0
|
||||
2026-05-05T22:23:18.040570Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=64311 parallel=true cells_before_filter=249 cells_after_filter=229 truncated=false bounds=51.4958,-0.1632,51.5342,-0.0968 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.2 total_ms=1.2
|
||||
2026-05-05T22:43:15.610059Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=83099 parallel=true cells_before_filter=312 cells_after_filter=300 truncated=false bounds=51.4958,-0.1756,51.5342,-0.0844 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.8 json_ms=0.2 total_ms=1.0
|
||||
2026-05-05T22:43:16.903828Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=149 truncated=false bounds=51.4958,-0.1497,51.5342,-0.1103 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.6 json_ms=0.1 total_ms=0.7
|
||||
2026-05-05T22:43:20.549697Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=19171 parallel=false cells_before_filter=105 cells_after_filter=94 truncated=false bounds=51.4978,-0.1407,51.5322,-0.1193 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.4 json_ms=0.1 total_ms=0.4
|
||||
2026-05-05T22:43:21.976727Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=19171 parallel=false cells_before_filter=105 cells_after_filter=100 truncated=false bounds=51.4978,-0.1424,51.5322,-0.1176 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.5 json_ms=0.1 total_ms=0.6
|
||||
2026-05-05T22:43:22.293571Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=40248 parallel=false cells_before_filter=186 cells_after_filter=175 truncated=false bounds=51.4978,-0.1547,51.5322,-0.1053 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.7 json_ms=0.1 total_ms=0.8
|
||||
2026-05-05T22:43:22.845608Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=146 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.6 json_ms=0.1 total_ms=0.6
|
||||
2026-05-05T22:43:23.303420Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=89195da4d23ffff resolution=9 total_count=4 filters=0 filters_raw="-" ms=0.1
|
||||
2026-05-05T22:43:42.565671Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:43:44.001279Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:43:44.001506Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:43:44.076119Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
|
||||
2026-05-05T22:43:44.654476Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.5 json_ms=0.1 total_ms=2.6
|
||||
2026-05-05T22:43:44.894850Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=11.6
|
||||
2026-05-05T22:43:48.729626Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
|
||||
2026-05-05T22:43:48.741364Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
|
||||
2026-05-05T22:43:48.926131Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.1 json_ms=0.2 total_ms=4.3
|
||||
2026-05-05T22:43:49.231122Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=28.6
|
||||
2026-05-05T22:43:51.908240Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=8.9 json_ms=2.3 total_ms=11.2
|
||||
2026-05-05T22:43:53.360428Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.1 json_ms=0.2 total_ms=4.2
|
||||
2026-05-05T22:43:57.147224Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.1 json_ms=0.1 total_ms=4.2
|
||||
2026-05-05T22:43:57.169568Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=26.6
|
||||
2026-05-05T22:44:07.124405Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=1.0
|
||||
2026-05-05T22:44:11.794994Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.1 json_ms=0.1 total_ms=3.2
|
||||
2026-05-05T22:44:15.007697Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=26.1
|
||||
2026-05-05T22:44:33.883430Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:44:33.883631Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:44:33.956903Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:44:33.957097Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:44:34.066338Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:44:34.066345Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:44:34.151042Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:44:34.151631Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:44:34.917734Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=146 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.6 json_ms=0.1 total_ms=0.7
|
||||
2026-05-05T22:45:56.400232Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=146 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.6 json_ms=0.1 total_ms=0.7
|
||||
2026-05-05T22:46:00.243885Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=146 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.1 total_ms=1.7
|
||||
2026-05-05T22:46:02.123616Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=151 truncated=false bounds=51.4957,-0.1512,51.5334,-0.1097 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.1 total_ms=1.6
|
||||
2026-05-05T22:46:03.665847Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=90632 parallel=true cells_before_filter=347 cells_after_filter=312 truncated=false bounds=51.4868,-0.1624,51.5414,-0.1022 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.8 json_ms=0.3 total_ms=2.1
|
||||
2026-05-05T22:46:04.264588Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=414377 parallel=true cells_before_filter=1471 cells_after_filter=1371 truncated=false bounds=51.4530,-0.2043,51.5713,-0.0741 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=3.6 json_ms=1.1 total_ms=4.7
|
||||
2026-05-05T22:46:06.547788Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=146 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.1 total_ms=1.7
|
||||
2026-05-05T22:46:16.636099Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=146 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.1 total_ms=1.6
|
||||
2026-05-05T22:46:23.394449Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=146 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.1 total_ms=1.7
|
||||
2026-05-05T22:46:24.308069Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=162 cells_after_filter=145 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=1 filters_raw="Price per sqm:12:30077.969" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.1 total_ms=1.6
|
||||
2026-05-05T22:46:24.553931Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=32331 filters=1 travel=0 total=26612 filters_raw="Price per sqm:12:30077.969" ms=0.8
|
||||
2026-05-05T22:46:26.959319Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32787 parallel=false cells_before_filter=151 cells_after_filter=134 truncated=false bounds=51.4952,-0.1532,51.5297,-0.1152 filters=1 filters_raw="Price per sqm:12:30077.969" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.1 total_ms=1.7
|
||||
2026-05-05T22:46:27.318926Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=58963 parallel=true cells_before_filter=238 cells_after_filter=206 truncated=false bounds=51.4900,-0.1588,51.5326,-0.1119 filters=1 filters_raw="Price per sqm:12:30077.969" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.2 json_ms=0.2 total_ms=1.4
|
||||
2026-05-05T22:46:27.749837Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=108235 parallel=true cells_before_filter=389 cells_after_filter=374 truncated=false bounds=51.4786,-0.1710,51.5391,-0.1044 filters=1 filters_raw="Price per sqm:12:30077.969" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.4 total_ms=2.0
|
||||
2026-05-05T22:46:28.171700Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=339930 parallel=true cells_before_filter=1157 cells_after_filter=1088 truncated=false bounds=51.4504,-0.2014,51.5554,-0.0858 filters=1 filters_raw="Price per sqm:12:30077.969" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=3.0 json_ms=1.0 total_ms=4.0
|
||||
2026-05-05T22:46:28.601824Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=749457 parallel=true cells_before_filter=3198 cells_after_filter=3059 truncated=false bounds=51.3997,-0.2558,51.5845,-0.0524 filters=1 filters_raw="Price per sqm:12:30077.969" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=6.0 json_ms=2.1 total_ms=8.1
|
||||
2026-05-05T22:46:29.055179Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=1560750 parallel=true cells_before_filter=1545 cells_after_filter=1535 truncated=false bounds=51.3051,-0.3572,51.6387,0.0098 filters=1 filters_raw="Price per sqm:12:30077.969" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=11.5 json_ms=1.0 total_ms=12.5
|
||||
2026-05-05T22:46:29.321372Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=1560750 filters=1 travel=0 total=1299177 filters_raw="Price per sqm:12:30077.969" ms=34.9
|
||||
2026-05-05T22:46:33.087766Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=2261657 parallel=true cells_before_filter=616 cells_after_filter=616 truncated=false bounds=51.1810,-0.4615,51.7329,0.1455 filters=1 filters_raw="Price per sqm:12:30077.969" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=12.7 json_ms=0.4 total_ms=13.1
|
||||
2026-05-05T22:46:33.494231Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=2918823 parallel=true cells_before_filter=1362 cells_after_filter=1362 truncated=false bounds=51.0169,-0.6036,51.8540,0.3166 filters=1 filters_raw="Price per sqm:12:30077.969" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=12.6 json_ms=1.0 total_ms=13.7
|
||||
2026-05-05T22:46:33.752516Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=2918823 filters=1 travel=0 total=2383011 filters_raw="Price per sqm:12:30077.969" ms=62.9
|
||||
2026-05-05T22:46:41.495897Z INFO property_map_server::routes::pois: GET /api/pois results=30 candidates=110817 categories=1 categories_raw="Ferry" ms=1.1
|
||||
2026-05-05T22:46:54.343178Z INFO property_map_server::routes::pois: GET /api/pois results=546 candidates=110817 categories=2 categories_raw="Ferry,Rail station" ms=1.4
|
||||
2026-05-05T22:46:55.441445Z INFO property_map_server::routes::pois: GET /api/pois results=548 candidates=110817 categories=3 categories_raw="Ferry,Rail station,Airport" ms=1.5
|
||||
2026-05-05T22:46:55.794750Z INFO property_map_server::routes::pois: GET /api/pois results=549 candidates=110817 categories=4 categories_raw="Ferry,Rail station,Airport,Bus station" ms=1.5
|
||||
2026-05-05T22:46:56.155646Z INFO property_map_server::routes::pois: GET /api/pois results=10000 candidates=110817 categories=5 categories_raw="Ferry,Rail station,Airport,Bus station,Bus stop" ms=6.7
|
||||
2026-05-05T22:48:05.525357Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=87195db43ffffff resolution=7 total_count=91 filters=1 filters_raw="Price per sqm:12:30077.969" ms=0.1
|
||||
2026-05-05T22:50:01.895537Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=87195db43ffffff resolution=7 total_count=127 filters=0 filters_raw="-" ms=0.1
|
||||
2026-05-05T22:50:02.051971Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=2918823 parallel=true cells_before_filter=1363 cells_after_filter=1363 truncated=false bounds=51.0169,-0.6036,51.8540,0.3166 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=6.3 json_ms=0.6 total_ms=7.0
|
||||
2026-05-05T22:50:31.118058Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=2918823 parallel=true cells_before_filter=1363 cells_after_filter=1363 truncated=false bounds=51.0169,-0.6036,51.8540,0.3166 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=7.1 json_ms=0.6 total_ms=7.8
|
||||
2026-05-05T22:50:31.118295Z INFO property_map_server::routes::pois: GET /api/pois results=10000 candidates=110817 categories=5 categories_raw="Ferry,Rail station,Airport,Bus station,Bus stop" ms=8.0
|
||||
2026-05-05T22:51:30.713271Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=2918823 parallel=true cells_before_filter=1363 cells_after_filter=1363 truncated=false bounds=51.0169,-0.6036,51.8540,0.3166 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=7.2 json_ms=0.7 total_ms=7.9
|
||||
2026-05-05T22:51:30.714292Z INFO property_map_server::routes::pois: GET /api/pois results=10000 candidates=110817 categories=5 categories_raw="Ferry,Rail station,Airport,Bus station,Bus stop" ms=8.8
|
||||
2026-05-05T22:53:32.469648Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:53:32.472277Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:53:49.086905Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=146 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.6 json_ms=0.1 total_ms=0.7
|
||||
2026-05-05T22:54:09.482457Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=146 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.6 json_ms=0.1 total_ms=0.7
|
||||
2026-05-05T22:54:21.493756Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:54:21.496988Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:54:26.683522Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:54:26.689059Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:55:49.934502Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:55:51.273763Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:55:51.274240Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:55:51.342597Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
|
||||
2026-05-05T22:55:51.625779Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.6 json_ms=0.1 total_ms=2.7
|
||||
2026-05-05T22:55:51.883818Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=13.2
|
||||
2026-05-05T22:56:00.674036Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.0
|
||||
2026-05-05T22:56:00.688452Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
|
||||
2026-05-05T22:56:01.660467Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.3 json_ms=0.2 total_ms=3.5
|
||||
2026-05-05T22:56:01.686325Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=29.2
|
||||
2026-05-05T22:56:08.892303Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=10.0 json_ms=2.4 total_ms=12.4
|
||||
2026-05-05T22:56:09.044778Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.5 json_ms=0.2 total_ms=4.6
|
||||
2026-05-05T22:56:27.273066Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.5 json_ms=0.1 total_ms=3.5
|
||||
2026-05-05T22:56:27.290297Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=20.8
|
||||
2026-05-05T22:56:41.380242Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=1.1
|
||||
2026-05-05T22:56:48.559932Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=5.9 json_ms=0.1 total_ms=6.0
|
||||
2026-05-05T22:56:48.598808Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=25.7
|
||||
2026-05-05T22:59:17.850275Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:59:19.683668Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T22:59:19.685209Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T22:59:19.868856Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
|
||||
2026-05-05T22:59:20.700955Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.9 json_ms=0.2 total_ms=3.1
|
||||
2026-05-05T22:59:21.388973Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=18.5
|
||||
2026-05-05T22:59:30.197488Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.0
|
||||
2026-05-05T22:59:30.209877Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.8
|
||||
2026-05-05T22:59:31.524992Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.1 json_ms=0.1 total_ms=4.2
|
||||
2026-05-05T22:59:31.552172Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=31.5
|
||||
2026-05-05T22:59:39.058584Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=8.2 json_ms=2.4 total_ms=10.5
|
||||
2026-05-05T22:59:40.888781Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.3 json_ms=0.2 total_ms=4.4
|
||||
2026-05-05T22:59:49.369015Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.9 json_ms=0.1 total_ms=3.0
|
||||
2026-05-05T22:59:53.656223Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=24.4
|
||||
2026-05-05T23:00:06.632909Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=1.1
|
||||
2026-05-05T23:00:14.326078Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.6 json_ms=0.1 total_ms=4.7
|
||||
2026-05-05T23:00:14.345041Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=24.1
|
||||
2026-05-05T23:02:44.138033Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T23:02:45.527629Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T23:02:45.528980Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T23:02:45.591980Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.7
|
||||
2026-05-05T23:02:45.870217Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.3 json_ms=0.2 total_ms=2.5
|
||||
2026-05-05T23:02:46.138216Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=13.9
|
||||
2026-05-05T23:02:55.255284Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.3
|
||||
2026-05-05T23:02:55.284710Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.3
|
||||
2026-05-05T23:02:56.543228Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=5.1 json_ms=0.2 total_ms=5.2
|
||||
2026-05-05T23:02:56.565466Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=27.6
|
||||
2026-05-05T23:03:04.737677Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=8.4 json_ms=2.3 total_ms=10.7
|
||||
2026-05-05T23:03:06.524250Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.0 json_ms=0.2 total_ms=4.1
|
||||
2026-05-05T23:03:21.842452Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.6 json_ms=0.1 total_ms=4.7
|
||||
2026-05-05T23:03:21.864802Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=27.0
|
||||
2026-05-05T23:03:42.145302Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=1.1
|
||||
2026-05-05T23:03:46.908451Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=499440 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.4549,53.5912,-2.0354 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.5 json_ms=0.1 total_ms=2.6
|
||||
2026-05-05T23:03:47.229906Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=499440 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=24.5
|
||||
2026-05-05T23:05:31.402218Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T23:05:31.414308Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T23:05:31.415566Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T23:05:31.415719Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T23:05:31.436317Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T23:05:31.436387Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T23:05:31.451985Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T23:05:31.452263Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T23:05:31.961090Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=146 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.7 json_ms=0.1 total_ms=0.8
|
||||
2026-05-05T23:06:57.415147Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T23:06:58.764931Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T23:06:58.766642Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T23:06:58.836054Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.5
|
||||
2026-05-05T23:06:59.176972Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.3 json_ms=0.1 total_ms=2.5
|
||||
2026-05-05T23:06:59.430262Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=12.3
|
||||
2026-05-05T23:07:08.481220Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.9
|
||||
2026-05-05T23:07:08.494481Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.8
|
||||
2026-05-05T23:07:09.211888Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.7 json_ms=0.1 total_ms=3.9
|
||||
2026-05-05T23:07:09.235165Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=27.3
|
||||
2026-05-05T23:07:16.474238Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=7.3 json_ms=2.7 total_ms=9.9
|
||||
2026-05-05T23:07:21.423747Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.3 json_ms=0.2 total_ms=4.4
|
||||
2026-05-05T23:07:28.025215Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.0 json_ms=0.1 total_ms=3.1
|
||||
2026-05-05T23:07:30.260151Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=21.9
|
||||
2026-05-05T23:07:40.399802Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=470423 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3848,-2.4911,53.5741,-2.0174 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.0 json_ms=0.1 total_ms=3.1
|
||||
2026-05-05T23:07:42.310630Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=328746 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4065,-2.4487,53.5527,-2.0829 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.3 json_ms=0.1 total_ms=3.4
|
||||
2026-05-05T23:07:42.807273Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=328746 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4065,-2.4487,53.5527,-2.0829 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.1 json_ms=0.1 total_ms=2.2
|
||||
2026-05-05T23:07:42.872719Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=328746 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=17.6
|
||||
2026-05-05T23:07:44.921638Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=1.1
|
||||
2026-05-05T23:07:46.846641Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=283073 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4065,-2.4029,53.5527,-2.1286 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.1 json_ms=0.1 total_ms=3.2
|
||||
2026-05-05T23:07:47.055307Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=283073 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=11.3
|
||||
2026-05-05T23:09:24.286826Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T23:09:24.288081Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T23:09:24.310068Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T23:09:24.310452Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T23:09:24.328925Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T23:09:24.330142Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T23:09:24.376513Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-05T23:09:24.386911Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-05T23:09:24.866051Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=146 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.8 json_ms=0.1 total_ms=0.9
|
||||
|
|
|
|||
78
server-rs/logs/server.log.2026-05-06
Normal file
78
server-rs/logs/server.log.2026-05-06
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
2026-05-06T06:24:15.704164Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-06T06:24:15.851886Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-06T07:12:47.756563Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=149 truncated=false bounds=51.4958,-0.1497,51.5342,-0.1103 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.6 json_ms=0.1 total_ms=0.7
|
||||
2026-05-06T07:12:51.057411Z INFO property_map_server::routes::pois: GET /api/pois results=8114 candidates=8143 categories=94 categories_raw="Airport,Bus station,Bus stop,Ferry,Rail station,Taxi rank,Tube station,Aldi,Asda,Bakery,Booths,Budgens,Butcher & Fishmonger,COOK,Co-op,Convenience Store,Costco,Deli & Specialty,Farmfoods,Greengrocer,Heron Foods,Iceland,Lidl,M&S,Makro,Morrisons,Off-Licence,Planet Organic,Sainsbury's,Spar,Supermarket,Tesco,Waitrose,Whole Foods Market,Bar,Café,Cinema,Entertainment,Fast Food,Live Music & Events,Nightclub,Park,Playground,Pub,Restaurant,Sports Centre,Theatre,School,Care Home,Counselling & Therapy,Dentist,GP Surgery,Hospital & Clinic,Medical & Mobility,Optician,Pharmacy,Physiotherapy,Ambulance Station,Fire Station,Police,Community Centre,EV Charging,Fuel Station,Hotel,Local Business,Offices,Arts Centre,Gallery,Library,Museum,Place of Worship,Tourist Attraction,Zoo,Bank,Car Services,Dry Cleaner & Laundry,Gym & Fitness,Hairdresser & Beauty,Other,Post Office,Travel Agent,Vet & Pet Care,Bookshop,Charity Shop,DIY & Hardware,Department Store,Electronics,Fashion & Clothing,Gift & Hobby,Home & Garden,Newsagent,Pet Shop,Specialist Shop,Sports & Outdoor" ms=2.9
|
||||
2026-05-06T07:12:53.465923Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=109329 parallel=true cells_before_filter=400 cells_after_filter=367 truncated=false bounds=51.4830,-0.1594,51.5444,-0.0964 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.2 total_ms=1.3
|
||||
2026-05-06T07:12:53.470208Z INFO property_map_server::routes::pois: GET /api/pois results=10000 candidates=13896 categories=94 categories_raw="Airport,Bus station,Bus stop,Ferry,Rail station,Taxi rank,Tube station,Aldi,Asda,Bakery,Booths,Budgens,Butcher & Fishmonger,COOK,Co-op,Convenience Store,Costco,Deli & Specialty,Farmfoods,Greengrocer,Heron Foods,Iceland,Lidl,M&S,Makro,Morrisons,Off-Licence,Planet Organic,Sainsbury's,Spar,Supermarket,Tesco,Waitrose,Whole Foods Market,Bar,Café,Cinema,Entertainment,Fast Food,Live Music & Events,Nightclub,Park,Playground,Pub,Restaurant,Sports Centre,Theatre,School,Care Home,Counselling & Therapy,Dentist,GP Surgery,Hospital & Clinic,Medical & Mobility,Optician,Pharmacy,Physiotherapy,Ambulance Station,Fire Station,Police,Community Centre,EV Charging,Fuel Station,Hotel,Local Business,Offices,Arts Centre,Gallery,Library,Museum,Place of Worship,Tourist Attraction,Zoo,Bank,Car Services,Dry Cleaner & Laundry,Gym & Fitness,Hairdresser & Beauty,Other,Post Office,Travel Agent,Vet & Pet Care,Bookshop,Charity Shop,DIY & Hardware,Department Store,Electronics,Fashion & Clothing,Gift & Hobby,Home & Garden,Newsagent,Pet Shop,Specialist Shop,Sports & Outdoor" ms=4.4
|
||||
2026-05-06T07:12:54.729810Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=163973 parallel=true cells_before_filter=583 cells_after_filter=552 truncated=false bounds=51.4759,-0.1656,51.5526,-0.0869 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.3 total_ms=1.5
|
||||
2026-05-06T07:12:54.734619Z INFO property_map_server::routes::pois: GET /api/pois results=10000 candidates=17509 categories=94 categories_raw="Airport,Bus station,Bus stop,Ferry,Rail station,Taxi rank,Tube station,Aldi,Asda,Bakery,Booths,Budgens,Butcher & Fishmonger,COOK,Co-op,Convenience Store,Costco,Deli & Specialty,Farmfoods,Greengrocer,Heron Foods,Iceland,Lidl,M&S,Makro,Morrisons,Off-Licence,Planet Organic,Sainsbury's,Spar,Supermarket,Tesco,Waitrose,Whole Foods Market,Bar,Café,Cinema,Entertainment,Fast Food,Live Music & Events,Nightclub,Park,Playground,Pub,Restaurant,Sports Centre,Theatre,School,Care Home,Counselling & Therapy,Dentist,GP Surgery,Hospital & Clinic,Medical & Mobility,Optician,Pharmacy,Physiotherapy,Ambulance Station,Fire Station,Police,Community Centre,EV Charging,Fuel Station,Hotel,Local Business,Offices,Arts Centre,Gallery,Library,Museum,Place of Worship,Tourist Attraction,Zoo,Bank,Car Services,Dry Cleaner & Laundry,Gym & Fitness,Hairdresser & Beauty,Other,Post Office,Travel Agent,Vet & Pet Care,Bookshop,Charity Shop,DIY & Hardware,Department Store,Electronics,Fashion & Clothing,Gift & Hobby,Home & Garden,Newsagent,Pet Shop,Specialist Shop,Sports & Outdoor" ms=4.8
|
||||
2026-05-06T07:13:00.198267Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=360388 parallel=true cells_before_filter=1242 cells_after_filter=1226 truncated=false bounds=51.4759,-0.2171,51.5526,-0.0354 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.3 json_ms=0.6 total_ms=2.8
|
||||
2026-05-06T07:13:00.201735Z INFO property_map_server::routes::pois: GET /api/pois results=10000 candidates=28781 categories=94 categories_raw="Airport,Bus station,Bus stop,Ferry,Rail station,Taxi rank,Tube station,Aldi,Asda,Bakery,Booths,Budgens,Butcher & Fishmonger,COOK,Co-op,Convenience Store,Costco,Deli & Specialty,Farmfoods,Greengrocer,Heron Foods,Iceland,Lidl,M&S,Makro,Morrisons,Off-Licence,Planet Organic,Sainsbury's,Spar,Supermarket,Tesco,Waitrose,Whole Foods Market,Bar,Café,Cinema,Entertainment,Fast Food,Live Music & Events,Nightclub,Park,Playground,Pub,Restaurant,Sports Centre,Theatre,School,Care Home,Counselling & Therapy,Dentist,GP Surgery,Hospital & Clinic,Medical & Mobility,Optician,Pharmacy,Physiotherapy,Ambulance Station,Fire Station,Police,Community Centre,EV Charging,Fuel Station,Hotel,Local Business,Offices,Arts Centre,Gallery,Library,Museum,Place of Worship,Tourist Attraction,Zoo,Bank,Car Services,Dry Cleaner & Laundry,Gym & Fitness,Hairdresser & Beauty,Other,Post Office,Travel Agent,Vet & Pet Care,Bookshop,Charity Shop,DIY & Hardware,Department Store,Electronics,Fashion & Clothing,Gift & Hobby,Home & Garden,Newsagent,Pet Shop,Specialist Shop,Sports & Outdoor" ms=6.3
|
||||
2026-05-06T07:13:05.333847Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=360388 parallel=true cells_before_filter=1242 cells_after_filter=1226 truncated=false bounds=51.4759,-0.2171,51.5526,-0.0354 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=3.3 json_ms=0.9 total_ms=4.2
|
||||
2026-05-06T07:13:08.713423Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=360388 parallel=true cells_before_filter=1242 cells_after_filter=1226 truncated=false bounds=51.4759,-0.2171,51.5526,-0.0354 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=3.2 json_ms=0.9 total_ms=4.1
|
||||
2026-05-06T07:13:13.136900Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=360388 parallel=true cells_before_filter=1242 cells_after_filter=1226 truncated=false bounds=51.4759,-0.2171,51.5526,-0.0354 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=3.1 json_ms=0.9 total_ms=4.0
|
||||
2026-05-06T07:14:05.699501Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-06T07:14:07.875074Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-06T07:14:07.877346Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-06T07:14:08.019024Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4
|
||||
2026-05-06T07:14:08.941722Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.9 json_ms=0.1 total_ms=3.1
|
||||
2026-05-06T07:14:09.179080Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=12.0
|
||||
2026-05-06T07:14:18.221435Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.1
|
||||
2026-05-06T07:14:18.236889Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.3
|
||||
2026-05-06T07:14:19.840645Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.5 json_ms=0.1 total_ms=4.7
|
||||
2026-05-06T07:14:19.866316Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=24.0
|
||||
2026-05-06T07:14:26.999239Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=8.4 json_ms=2.5 total_ms=10.9
|
||||
2026-05-06T07:14:31.542557Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=5.3 json_ms=0.2 total_ms=5.5
|
||||
2026-05-06T07:14:37.389719Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=418717 parallel=true cells_before_filter=1460 cells_after_filter=1344 truncated=false bounds=51.4738,-0.2209,51.5536,-0.0316 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=5.7 json_ms=1.1 total_ms=6.8
|
||||
2026-05-06T07:14:37.391144Z INFO property_map_server::routes::pois: GET /api/pois results=10000 candidates=29908 categories=94 categories_raw="Airport,Bus station,Bus stop,Ferry,Rail station,Taxi rank,Tube station,Aldi,Asda,Bakery,Booths,Budgens,Butcher & Fishmonger,COOK,Co-op,Convenience Store,Costco,Deli & Specialty,Farmfoods,Greengrocer,Heron Foods,Iceland,Lidl,M&S,Makro,Morrisons,Off-Licence,Planet Organic,Sainsbury's,Spar,Supermarket,Tesco,Waitrose,Whole Foods Market,Bar,Café,Cinema,Entertainment,Fast Food,Live Music & Events,Nightclub,Park,Playground,Pub,Restaurant,Sports Centre,Theatre,School,Care Home,Counselling & Therapy,Dentist,GP Surgery,Hospital & Clinic,Medical & Mobility,Optician,Pharmacy,Physiotherapy,Ambulance Station,Fire Station,Police,Community Centre,EV Charging,Fuel Station,Hotel,Local Business,Offices,Arts Centre,Gallery,Library,Museum,Place of Worship,Tourist Attraction,Zoo,Bank,Car Services,Dry Cleaner & Laundry,Gym & Fitness,Hairdresser & Beauty,Other,Post Office,Travel Agent,Vet & Pet Care,Bookshop,Charity Shop,DIY & Hardware,Department Store,Electronics,Fashion & Clothing,Gift & Hobby,Home & Garden,Newsagent,Pet Shop,Specialist Shop,Sports & Outdoor" ms=7.8
|
||||
2026-05-06T07:14:38.216101Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=234725 parallel=true cells_before_filter=806 cells_after_filter=680 truncated=false bounds=51.4911,-0.1905,51.5453,-0.0620 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=3.7 json_ms=0.8 total_ms=4.5
|
||||
2026-05-06T07:14:38.222170Z INFO property_map_server::routes::pois: GET /api/pois results=10000 candidates=19659 categories=94 categories_raw="Airport,Bus station,Bus stop,Ferry,Rail station,Taxi rank,Tube station,Aldi,Asda,Bakery,Booths,Budgens,Butcher & Fishmonger,COOK,Co-op,Convenience Store,Costco,Deli & Specialty,Farmfoods,Greengrocer,Heron Foods,Iceland,Lidl,M&S,Makro,Morrisons,Off-Licence,Planet Organic,Sainsbury's,Spar,Supermarket,Tesco,Waitrose,Whole Foods Market,Bar,Café,Cinema,Entertainment,Fast Food,Live Music & Events,Nightclub,Park,Playground,Pub,Restaurant,Sports Centre,Theatre,School,Care Home,Counselling & Therapy,Dentist,GP Surgery,Hospital & Clinic,Medical & Mobility,Optician,Pharmacy,Physiotherapy,Ambulance Station,Fire Station,Police,Community Centre,EV Charging,Fuel Station,Hotel,Local Business,Offices,Arts Centre,Gallery,Library,Museum,Place of Worship,Tourist Attraction,Zoo,Bank,Car Services,Dry Cleaner & Laundry,Gym & Fitness,Hairdresser & Beauty,Other,Post Office,Travel Agent,Vet & Pet Care,Bookshop,Charity Shop,DIY & Hardware,Department Store,Electronics,Fashion & Clothing,Gift & Hobby,Home & Garden,Newsagent,Pet Shop,Specialist Shop,Sports & Outdoor" ms=8.5
|
||||
2026-05-06T07:14:44.419446Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=105806 parallel=true cells_before_filter=404 cells_after_filter=317 truncated=false bounds=51.4911,-0.1541,51.5453,-0.0984 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=2.2 json_ms=0.4 total_ms=2.5
|
||||
2026-05-06T07:14:44.423580Z INFO property_map_server::routes::pois: GET /api/pois results=10000 candidates=11596 categories=94 categories_raw="Airport,Bus station,Bus stop,Ferry,Rail station,Taxi rank,Tube station,Aldi,Asda,Bakery,Booths,Budgens,Butcher & Fishmonger,COOK,Co-op,Convenience Store,Costco,Deli & Specialty,Farmfoods,Greengrocer,Heron Foods,Iceland,Lidl,M&S,Makro,Morrisons,Off-Licence,Planet Organic,Sainsbury's,Spar,Supermarket,Tesco,Waitrose,Whole Foods Market,Bar,Café,Cinema,Entertainment,Fast Food,Live Music & Events,Nightclub,Park,Playground,Pub,Restaurant,Sports Centre,Theatre,School,Care Home,Counselling & Therapy,Dentist,GP Surgery,Hospital & Clinic,Medical & Mobility,Optician,Pharmacy,Physiotherapy,Ambulance Station,Fire Station,Police,Community Centre,EV Charging,Fuel Station,Hotel,Local Business,Offices,Arts Centre,Gallery,Library,Museum,Place of Worship,Tourist Attraction,Zoo,Bank,Car Services,Dry Cleaner & Laundry,Gym & Fitness,Hairdresser & Beauty,Other,Post Office,Travel Agent,Vet & Pet Care,Bookshop,Charity Shop,DIY & Hardware,Department Store,Electronics,Fashion & Clothing,Gift & Hobby,Home & Garden,Newsagent,Pet Shop,Specialist Shop,Sports & Outdoor" ms=6.6
|
||||
2026-05-06T07:14:44.661886Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.3 json_ms=0.1 total_ms=4.4
|
||||
2026-05-06T07:14:44.686961Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=29.1
|
||||
2026-05-06T07:14:46.392737Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=32331 parallel=false cells_before_filter=163 cells_after_filter=146 truncated=false bounds=51.4978,-0.1490,51.5322,-0.1110 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.9 json_ms=0.1 total_ms=2.1
|
||||
2026-05-06T07:14:46.394380Z INFO property_map_server::routes::pois: GET /api/pois results=6986 candidates=7010 categories=94 categories_raw="Airport,Bus station,Bus stop,Ferry,Rail station,Taxi rank,Tube station,Aldi,Asda,Bakery,Booths,Budgens,Butcher & Fishmonger,COOK,Co-op,Convenience Store,Costco,Deli & Specialty,Farmfoods,Greengrocer,Heron Foods,Iceland,Lidl,M&S,Makro,Morrisons,Off-Licence,Planet Organic,Sainsbury's,Spar,Supermarket,Tesco,Waitrose,Whole Foods Market,Bar,Café,Cinema,Entertainment,Fast Food,Live Music & Events,Nightclub,Park,Playground,Pub,Restaurant,Sports Centre,Theatre,School,Care Home,Counselling & Therapy,Dentist,GP Surgery,Hospital & Clinic,Medical & Mobility,Optician,Pharmacy,Physiotherapy,Ambulance Station,Fire Station,Police,Community Centre,EV Charging,Fuel Station,Hotel,Local Business,Offices,Arts Centre,Gallery,Library,Museum,Place of Worship,Tourist Attraction,Zoo,Bank,Car Services,Dry Cleaner & Laundry,Gym & Fitness,Hairdresser & Beauty,Other,Post Office,Travel Agent,Vet & Pet Care,Bookshop,Charity Shop,DIY & Hardware,Department Store,Electronics,Fashion & Clothing,Gift & Hobby,Home & Garden,Newsagent,Pet Shop,Specialist Shop,Sports & Outdoor" ms=3.7
|
||||
2026-05-06T07:14:55.383153Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=89195da4d2bffff resolution=9 total_count=166 filters=0 filters_raw="-" ms=0.3
|
||||
2026-05-06T07:14:55.521238Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=470423 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3851,-2.4904,53.5738,-2.0185 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.6 json_ms=0.1 total_ms=2.7
|
||||
2026-05-06T07:14:57.292200Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=387398 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3999,-2.4614,53.5591,-2.0632 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.2 json_ms=0.1 total_ms=3.3
|
||||
2026-05-06T07:14:57.327444Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=387398 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=16.0
|
||||
2026-05-06T07:14:58.285866Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=328746 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4065,-2.4487,53.5527,-2.0829 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.5 json_ms=0.1 total_ms=2.6
|
||||
2026-05-06T07:14:58.350203Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=328746 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=16.5
|
||||
2026-05-06T07:14:58.617181Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=89195da4d63ffff resolution=9 total_count=382 filters=0 filters_raw="-" ms=0.3
|
||||
2026-05-06T07:15:00.740312Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=1.2
|
||||
2026-05-06T07:15:02.197947Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=283073 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4065,-2.4029,53.5527,-2.1286 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.1 json_ms=0.1 total_ms=2.2
|
||||
2026-05-06T07:15:02.491433Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=283073 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=12.8
|
||||
2026-05-06T07:15:13.901971Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-06T07:15:15.283795Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-06T07:15:15.284213Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-06T07:15:15.347797Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6
|
||||
2026-05-06T07:15:15.636170Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=163 cells_after_filter=163 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=0 filters_raw="-" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.9 json_ms=0.1 total_ms=5.1
|
||||
2026-05-06T07:15:15.900377Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=1 travel=1 total=41544 filters_raw="-" ms=13.9
|
||||
2026-05-06T07:15:24.985267Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.1
|
||||
2026-05-06T07:15:25.003392Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=2.1
|
||||
2026-05-06T07:15:25.539775Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.0 json_ms=0.2 total_ms=3.2
|
||||
2026-05-06T07:15:25.556452Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=20439 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=19.8
|
||||
2026-05-06T07:15:27.290918Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=64311 parallel=true cells_before_filter=249 cells_after_filter=229 truncated=false bounds=51.4958,-0.1632,51.5342,-0.0968 filters=0 filters_raw="-" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=2.3 json_ms=0.3 total_ms=2.6
|
||||
2026-05-06T07:15:27.294614Z INFO property_map_server::routes::pois: GET /api/pois results=10000 candidates=11740 categories=94 categories_raw="Airport,Bus station,Bus stop,Ferry,Rail station,Taxi rank,Tube station,Aldi,Asda,Bakery,Booths,Budgens,Butcher & Fishmonger,COOK,Co-op,Convenience Store,Costco,Deli & Specialty,Farmfoods,Greengrocer,Heron Foods,Iceland,Lidl,M&S,Makro,Morrisons,Off-Licence,Planet Organic,Sainsbury's,Spar,Supermarket,Tesco,Waitrose,Whole Foods Market,Bar,Café,Cinema,Entertainment,Fast Food,Live Music & Events,Nightclub,Park,Playground,Pub,Restaurant,Sports Centre,Theatre,School,Care Home,Counselling & Therapy,Dentist,GP Surgery,Hospital & Clinic,Medical & Mobility,Optician,Pharmacy,Physiotherapy,Ambulance Station,Fire Station,Police,Community Centre,EV Charging,Fuel Station,Hotel,Local Business,Offices,Arts Centre,Gallery,Library,Museum,Place of Worship,Tourist Attraction,Zoo,Bank,Car Services,Dry Cleaner & Laundry,Gym & Fitness,Hairdresser & Beauty,Other,Post Office,Travel Agent,Vet & Pet Care,Bookshop,Charity Shop,DIY & Hardware,Department Store,Electronics,Fashion & Clothing,Gift & Hobby,Home & Garden,Newsagent,Pet Shop,Specialist Shop,Sports & Outdoor" ms=6.3
|
||||
2026-05-06T07:15:33.072972Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=2446 cells_after_filter=2433 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=7.3 json_ms=2.6 total_ms=9.9
|
||||
2026-05-06T07:15:33.220203Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=134 cells_after_filter=134 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.7 json_ms=0.1 total_ms=3.9
|
||||
2026-05-06T07:15:47.139336Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=556230 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3675,-2.5248,53.5912,-1.9654 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=4.1 json_ms=0.1 total_ms=4.2
|
||||
2026-05-06T07:15:47.158892Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=556230 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=23.7
|
||||
2026-05-06T07:16:01.664304Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=387398 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.3999,-2.4614,53.5591,-2.0632 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.3 json_ms=0.1 total_ms=3.4
|
||||
2026-05-06T07:16:02.312074Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=338007 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4063,-2.4490,53.5529,-2.0824 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.8 json_ms=0.1 total_ms=2.9
|
||||
2026-05-06T07:16:03.862614Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=328746 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4065,-2.4487,53.5527,-2.0829 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.5 json_ms=0.1 total_ms=2.6
|
||||
2026-05-06T07:16:03.875136Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=328746 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=15.0
|
||||
2026-05-06T07:16:06.187821Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=891951b70abffff resolution=9 total_count=1487 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=1.1
|
||||
2026-05-06T07:16:07.771759Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=283073 parallel=true cells_before_filter=59 cells_after_filter=59 truncated=false bounds=53.4065,-2.4029,53.5527,-2.1286 filters=4 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.4 json_ms=0.1 total_ms=2.4
|
||||
2026-05-06T07:16:07.828161Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=283073 filters=5 travel=1 total=12434 filters_raw="Property type:Flats/Maisonettes|Terraced;;Estimated current price:175000:450000;;Serious crime per 1k residents (avg/yr):0:55;;Noise (dB):50:68" ms=14.7
|
||||
2026-05-06T07:16:41.428217Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-06T07:16:41.429461Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-06T07:16:41.458276Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-06T07:16:41.464802Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-06T07:16:41.466920Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-06T07:16:41.468098Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-06T07:16:41.474421Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-06T07:16:41.475516Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
2026-05-06T18:23:02.915468Z INFO property_map_server::routes::features: GET /api/features
|
||||
2026-05-06T18:23:02.922130Z INFO property_map_server::routes::pois: GET /api/poi-categories count=94 groups=11
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
"record": "tsc && node dist/record.js",
|
||||
"record:vertical": "tsc && ASPECT=9x16 node dist/record.js",
|
||||
"encode": "ffmpeg -y -i output/recording.webm -c:v libx264 -pix_fmt yuv420p -crf 14 -preset fast -movflags +faststart output/recording.mp4",
|
||||
"verify-output": "tsc && node dist/verify.js",
|
||||
"render": "./render.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -23,20 +23,16 @@ PB_ADMIN_EMAIL="${PB_ADMIN_EMAIL:-admin@propertymap.local}"
|
|||
PB_ADMIN_PASSWORD="${PB_ADMIN_PASSWORD:-propertymap-dev-2024}"
|
||||
PB_EMAIL="${PB_EMAIL:-demo-video@local.test}"
|
||||
PB_PASSWORD="${PB_PASSWORD:-DemoVideoPass123!}"
|
||||
MAX_DURATION_S="${MAX_DURATION_S:-15}"
|
||||
RECORD_SCALE="${RECORD_SCALE:-2}" # 2x raw capture -> real 50fps after speed-up
|
||||
OUTPUT_FPS="${OUTPUT_FPS:-50}" # matches RECORD_SCALE=2 output cadence
|
||||
CAPTURE_SCALE="${CAPTURE_SCALE:-1.5}" # sharper than 1x without the 2x software-GL cost
|
||||
AUTH_TTL_HOURS="${AUTH_TTL_HOURS:-24}" # re-auth if auth.json older than this
|
||||
# Where the homepage <video> source lives. Vite copies frontend/public/* into
|
||||
# the built bundle, so updating this path is what makes the new clip appear
|
||||
# on the homepage. Override if the dashboard ever moves.
|
||||
PUBLISH_DIR="${PUBLISH_DIR:-../frontend/public/video}"
|
||||
# When in the *output* timeline (post-speedup) to grab the poster frame.
|
||||
# Right-pane inspection (~10s output) is the clearest paused-state preview:
|
||||
# Right-pane inspection (~16s output) is the clearest paused-state preview:
|
||||
# Manchester map, filters applied, right pane populated, larger narration
|
||||
# caption visible.
|
||||
POSTER_TIME_S="${POSTER_TIME_S:-8}"
|
||||
POSTER_TIME_S="${POSTER_TIME_S:-16}"
|
||||
|
||||
FRESH_AUTH="${FORCE_AUTH:-0}"
|
||||
DO_ENCODE=1
|
||||
|
|
@ -134,13 +130,12 @@ mkdir -p output
|
|||
# Wipe last run's leaking artifacts so the rename step picks up *this* run.
|
||||
rm -f output/recording.webm output/recording.mp4 output/page@*.webm output/page@*.webm.untrimmed
|
||||
|
||||
APP_URL="$APP_URL" MAX_DURATION_S="$MAX_DURATION_S" CAPTURE_SCALE="$CAPTURE_SCALE" \
|
||||
RECORD_SCALE="$RECORD_SCALE" OUTPUT_FPS="$OUTPUT_FPS" \
|
||||
node dist/record.js
|
||||
APP_URL="$APP_URL" node dist/record.js
|
||||
|
||||
if [ ! -s output/recording.webm ]; then
|
||||
fail "recording.webm missing or empty"
|
||||
fi
|
||||
node dist/verify.js output/recording.webm
|
||||
|
||||
# -- encode -------------------------------------------------------------------
|
||||
if [ "$DO_ENCODE" = "1" ]; then
|
||||
|
|
@ -164,6 +159,8 @@ if [ "$DO_ENCODE" = "1" ]; then
|
|||
ffmpeg -y -loglevel warning -i output/recording.mp4 -ss "$POSTER_TIME_S" \
|
||||
-frames:v 1 -update 1 -q:v 2 \
|
||||
output/poster.jpg
|
||||
|
||||
node dist/verify.js output/recording.mp4 output/poster.jpg
|
||||
fi
|
||||
|
||||
# -- publish to homepage ------------------------------------------------------
|
||||
|
|
@ -177,6 +174,7 @@ if [ "$DO_ENCODE" = "1" ]; then
|
|||
say "Publishing to $PUBLISH_DIR"
|
||||
cp output/recording.mp4 "$PUBLISH_DIR/recording.mp4"
|
||||
cp output/poster.jpg "$PUBLISH_DIR/poster.jpg"
|
||||
node dist/verify.js "$PUBLISH_DIR/recording.mp4" "$PUBLISH_DIR/poster.jpg"
|
||||
fi
|
||||
|
||||
# -- report -------------------------------------------------------------------
|
||||
|
|
|
|||
123
video/src/browser.ts
Normal file
123
video/src/browser.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { chromium, type Browser, type BrowserContext } from 'playwright';
|
||||
import { AUTH_STATE_PATH, CAPTURE_SCALE, OUTPUT_DIR, VIDEO_SIZE, VIEWPORT } from './config.js';
|
||||
|
||||
export interface RecordingBrowser {
|
||||
browser: Browser;
|
||||
context: BrowserContext;
|
||||
}
|
||||
|
||||
export async function launchRecordingBrowser(): Promise<RecordingBrowser> {
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--use-gl=angle',
|
||||
'--use-angle=swiftshader',
|
||||
'--enable-unsafe-swiftshader',
|
||||
'--ignore-gpu-blocklist',
|
||||
'--disable-frame-rate-limit',
|
||||
'--disable-gpu-vsync',
|
||||
'--disable-features=CalculateNativeWinOcclusion,IntensiveWakeUpThrottling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
],
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
storageState: AUTH_STATE_PATH,
|
||||
viewport: VIEWPORT,
|
||||
deviceScaleFactor: CAPTURE_SCALE,
|
||||
recordVideo: { dir: OUTPUT_DIR, size: VIDEO_SIZE },
|
||||
});
|
||||
await suppressDevServerNoise(context);
|
||||
return { browser, context };
|
||||
}
|
||||
|
||||
async function suppressDevServerNoise(context: BrowserContext) {
|
||||
await context.addInitScript(() => {
|
||||
(window as typeof window & { __demoRecording?: boolean }).__demoRecording = true;
|
||||
|
||||
const RealWS = window.WebSocket;
|
||||
window.WebSocket = new Proxy(RealWS, {
|
||||
construct(target, args) {
|
||||
const url = String(args[0] ?? '');
|
||||
const proto = (args[1] as string | string[] | undefined) ?? '';
|
||||
const protoStr = Array.isArray(proto) ? proto.join(',') : proto;
|
||||
if (
|
||||
protoStr.includes('vite-hmr') ||
|
||||
protoStr.includes('webpack') ||
|
||||
url.includes('/ws') ||
|
||||
url.includes('sockjs-node')
|
||||
) {
|
||||
const fake = new EventTarget() as WebSocket;
|
||||
Object.defineProperties(fake, {
|
||||
readyState: { value: RealWS.CLOSED },
|
||||
url: { value: url },
|
||||
protocol: { value: '' },
|
||||
extensions: { value: '' },
|
||||
bufferedAmount: { value: 0 },
|
||||
binaryType: { value: 'blob', writable: true },
|
||||
});
|
||||
fake.send = () => {};
|
||||
fake.close = () => fake.dispatchEvent(new Event('close'));
|
||||
queueMicrotask(() => fake.dispatchEvent(new Event('close')));
|
||||
return fake;
|
||||
}
|
||||
return Reflect.construct(target, args);
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(window.location, 'reload', {
|
||||
value: () => {},
|
||||
configurable: true,
|
||||
});
|
||||
window.addEventListener('error', (e) => e.stopImmediatePropagation(), true);
|
||||
window.addEventListener('unhandledrejection', (e) => e.stopImmediatePropagation(), true);
|
||||
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.textContent = `
|
||||
vite-error-overlay,
|
||||
wds-overlay,
|
||||
#webpack-dev-server-client-overlay,
|
||||
#webpack-dev-server-client-overlay-div,
|
||||
iframe[src*="webpack-dev-server"],
|
||||
iframe[id*="webpack"],
|
||||
[id*="webpack-dev-server-client"],
|
||||
[class*="error-overlay"],
|
||||
[class*="webpack-error"] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
`;
|
||||
(document.head ?? document.documentElement).appendChild(styleEl);
|
||||
|
||||
const killOverlay = (node: Element) => {
|
||||
const tag = node.tagName?.toLowerCase();
|
||||
const id = (node as HTMLElement).id?.toLowerCase() ?? '';
|
||||
if (
|
||||
tag === 'vite-error-overlay' ||
|
||||
tag === 'wds-overlay' ||
|
||||
id.includes('webpack-dev-server-client') ||
|
||||
id.includes('webpack-error')
|
||||
) {
|
||||
(node as HTMLElement).remove();
|
||||
}
|
||||
};
|
||||
const obs = new MutationObserver((muts) => {
|
||||
for (const m of muts) {
|
||||
m.addedNodes.forEach((n) => {
|
||||
if (n.nodeType === 1) killOverlay(n as Element);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (document.body) obs.observe(document.body, { childList: true, subtree: true });
|
||||
else {
|
||||
document.addEventListener('DOMContentLoaded', () =>
|
||||
obs.observe(document.body, { childList: true, subtree: true })
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -50,7 +50,6 @@ export const STUBBED_TRAVEL_TIME_FILTERS: {
|
|||
// component renders each travel-time entry with `data-filter-name="tt_${i}"`,
|
||||
// and our stub only sets one entry, so it's tt_0.
|
||||
export const TT_CARD_SELECTOR = '[data-filter-name="tt_0"]';
|
||||
export const TT_SLIDER_MIN = 0;
|
||||
export const TT_SLIDER_MAX = 120;
|
||||
export const TT_DRAG_FROM_MIN = 35; // matches AI stub max above
|
||||
export const TT_DRAG_TO_MIN = 20;
|
||||
|
|
@ -59,12 +58,6 @@ export const TT_DRAG_TO_MIN = 20;
|
|||
// 2.4 fills most of the viewport with the prompt card without blowing up text.
|
||||
export const AI_ZOOM_SCALE = Number(process.env.AI_ZOOM_SCALE ?? 2.4);
|
||||
|
||||
// Cluster scene: how many wheel ticks (deck.gl smooths each one) and the
|
||||
// per-tick delay. ~5 ticks at -120 each gets us +2 zoom levels.
|
||||
export const CLUSTER_ZOOM_TICKS = 5;
|
||||
export const CLUSTER_ZOOM_DELTA = -120;
|
||||
export const CLUSTER_ZOOM_TICK_MS = 90;
|
||||
|
||||
// Initial map view used while we navigate. The AI scene zooms in on the
|
||||
// sidebar so this only matters once we zoom out.
|
||||
export const INITIAL_MAP_VIEW = {
|
||||
|
|
@ -73,29 +66,17 @@ export const INITIAL_MAP_VIEW = {
|
|||
zoom: 11.5,
|
||||
};
|
||||
|
||||
// Postcode pre-selected on page load. The dashboard reads ?pc= and:
|
||||
// 1. fetches /api/postcode/{pc}
|
||||
// 2. mapFlyToRef → zoom 16 over the postcode
|
||||
// 3. handleLocationSearch → opens the right pane populated with that postcode
|
||||
// We use this to guarantee the right pane is open by the time the cluster
|
||||
// scene plays. The visual cursor click is then ceremonial — pane is real,
|
||||
// data is real, only the causation is staged.
|
||||
//
|
||||
// M44FZ is in Ancoats/Northern Quarter: central enough to read as Manchester,
|
||||
// and it still has matching properties after the commute is tightened to 20m.
|
||||
export const PRELOAD_POSTCODE = process.env.PRELOAD_POSTCODE ?? 'M44FZ';
|
||||
// Verification guard only. The renderer no longer uses this as an editing cap:
|
||||
// if the storyboard needs more than 15 seconds to avoid jumps, keep the frames.
|
||||
export const MAX_DURATION_S = Number(process.env.MAX_DURATION_S ?? 45);
|
||||
export const MIN_DURATION_S = Number(process.env.MIN_DURATION_S ?? 10);
|
||||
|
||||
// Hard cap on the trimmed output. Keep the homepage demo tight; the render
|
||||
// trims from the outro if a dev-server hiccup stretches a scene.
|
||||
export const MAX_DURATION_S = Number(process.env.MAX_DURATION_S ?? 15);
|
||||
// Slow down all interactions while recording, then speed the output back up in
|
||||
// ffmpeg. A higher scale makes rendering take longer, but gives the 25fps raw
|
||||
// recorder enough unique frames for a smooth 50fps final without shortcut cuts.
|
||||
export const RECORD_SCALE = Math.max(1, Number(process.env.RECORD_SCALE ?? 3.5));
|
||||
|
||||
// Slow down all interactions while recording, then speed the output back up
|
||||
// in ffmpeg. 2x gives a real 50fps final video from Playwright's 25fps raw
|
||||
// recorder without making the take painfully long.
|
||||
export const RECORD_SCALE = Math.max(1, Number(process.env.RECORD_SCALE ?? 2));
|
||||
|
||||
// Target fps of the FINAL output. With RECORD_SCALE=2 this matches the real
|
||||
// captured frame cadence, so the MP4 does not need synthetic interpolation.
|
||||
// Target fps of the FINAL output.
|
||||
export const OUTPUT_FPS = Number(process.env.OUTPUT_FPS ?? 50);
|
||||
|
||||
// Brand strings for the outro card.
|
||||
|
|
|
|||
194
video/src/dom.ts
194
video/src/dom.ts
|
|
@ -1,4 +1,5 @@
|
|||
import type { Page } from 'playwright';
|
||||
import { RECORD_SCALE } from './config.js';
|
||||
|
||||
/**
|
||||
* Inject a visible cursor that mirrors the real mouse position. The browser's
|
||||
|
|
@ -110,17 +111,19 @@ export async function installCursor(page: Page): Promise<void> {
|
|||
#__demo-outro-card {
|
||||
text-align: center;
|
||||
color: white;
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 0;
|
||||
transform: translateY(12px) scale(0.985);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
animation: __demo-outro-pop 520ms cubic-bezier(0.22,1,0.36,1) both;
|
||||
}
|
||||
#__demo-outro.visible #__demo-outro-card {
|
||||
animation: __demo-outro-pop 620ms cubic-bezier(0.22,1,0.36,1) both;
|
||||
}
|
||||
@keyframes __demo-outro-pop {
|
||||
0% { transform: translateY(10px) scale(0.985); }
|
||||
100% { transform: translateY(0) scale(1); }
|
||||
0% { opacity: 0; transform: translateY(12px) scale(0.985); }
|
||||
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
#__demo-outro-brand {
|
||||
font: 760 72px/1.05 ui-sans-serif, system-ui, sans-serif;
|
||||
|
|
@ -177,6 +180,15 @@ export async function installCursor(page: Page): Promise<void> {
|
|||
},
|
||||
{ passive: true, capture: true }
|
||||
);
|
||||
(window as typeof window & {
|
||||
__demoMoveCursor?: (x: number, y: number, durationMs: number) => void;
|
||||
}).__demoMoveCursor = (x, y, durationMs) => {
|
||||
cursor.style.transition = `transform ${durationMs}ms cubic-bezier(0.22, 1, 0.36, 1), scale 120ms ease-out`;
|
||||
cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`;
|
||||
window.setTimeout(() => {
|
||||
cursor.style.transition = 'transform 60ms linear, scale 120ms ease-out';
|
||||
}, durationMs + 40);
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
'mousedown',
|
||||
|
|
@ -236,6 +248,29 @@ export async function flashRect(
|
|||
}, rect);
|
||||
}
|
||||
|
||||
export async function visualClick(
|
||||
page: Page,
|
||||
point: { x: number; y: number },
|
||||
rippleColor = 'rgba(20, 184, 166, 0.9)'
|
||||
): Promise<void> {
|
||||
await page.evaluate(
|
||||
({ point, rippleColor }) => {
|
||||
const cursor = document.getElementById('__demo-cursor');
|
||||
cursor?.classList.add('click');
|
||||
window.setTimeout(() => cursor?.classList.remove('click'), 140);
|
||||
|
||||
const r = document.createElement('div');
|
||||
r.className = '__demo-ripple';
|
||||
r.style.left = `${point.x}px`;
|
||||
r.style.top = `${point.y}px`;
|
||||
r.style.borderColor = rippleColor;
|
||||
document.body.appendChild(r);
|
||||
window.setTimeout(() => r.remove(), 650);
|
||||
},
|
||||
{ point, rippleColor }
|
||||
);
|
||||
}
|
||||
|
||||
export async function showOutro(
|
||||
page: Page,
|
||||
brand: string,
|
||||
|
|
@ -247,7 +282,6 @@ export async function showOutro(
|
|||
document.getElementById('__demo-caption')?.classList.remove('visible');
|
||||
const el = document.createElement('div');
|
||||
el.id = '__demo-outro';
|
||||
el.className = 'visible';
|
||||
el.innerHTML = `
|
||||
<div id="__demo-outro-card">
|
||||
<div id="__demo-outro-brand">${brand}</div>
|
||||
|
|
@ -255,6 +289,9 @@ export async function showOutro(
|
|||
<div id="__demo-outro-url">${url}</div>
|
||||
</div>`;
|
||||
document.body.appendChild(el);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => el.classList.add('visible'));
|
||||
});
|
||||
},
|
||||
{ brand, tagline, url }
|
||||
);
|
||||
|
|
@ -336,79 +373,30 @@ export async function zoomTo(
|
|||
opts: { scale: number; focusX: number; focusY: number; durationMs?: number }
|
||||
): Promise<void> {
|
||||
const { scale, focusX, focusY, durationMs = 1100 } = opts;
|
||||
const transitionMs = Math.round(durationMs * RECORD_SCALE);
|
||||
const viewport = page.viewportSize() ?? { width: 1920, height: 1080 };
|
||||
const dx = viewport.width / 2 - scale * focusX;
|
||||
const dy = viewport.height / 2 - scale * focusY;
|
||||
await page.evaluate(
|
||||
({ dx, dy, scale, durationMs }) => {
|
||||
({ dx, dy, scale, transitionMs }) => {
|
||||
const wrap = document.getElementById('__demo-zoom-wrap');
|
||||
if (!wrap) return;
|
||||
wrap.style.transition = `transform ${durationMs}ms cubic-bezier(0.22, 1, 0.36, 1)`;
|
||||
wrap.style.transition = `transform ${transitionMs}ms cubic-bezier(0.22, 1, 0.36, 1)`;
|
||||
wrap.style.transform = `translate(${dx}px, ${dy}px) scale(${scale})`;
|
||||
},
|
||||
{ dx, dy, scale, durationMs }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom in such that the focus point STAYS where it is on screen — only the
|
||||
* surroundings expand outward. Use when the cursor is hovering over a target
|
||||
* we want to keep clickable: clicks at the focus point still map to the
|
||||
* same DOM/canvas pixel, both pre- and post-zoom, so deck.gl hit-tests work.
|
||||
*
|
||||
* Contrast with zoomTo, which translates the focus to viewport centre.
|
||||
*/
|
||||
export async function zoomAt(
|
||||
page: Page,
|
||||
opts: { scale: number; focusX: number; focusY: number; durationMs?: number }
|
||||
): Promise<void> {
|
||||
const { scale, focusX, focusY, durationMs = 1100 } = opts;
|
||||
await page.evaluate(
|
||||
({ scale, focusX, focusY, durationMs }) => {
|
||||
const wrap = document.getElementById('__demo-zoom-wrap');
|
||||
if (!wrap) return;
|
||||
wrap.style.transformOrigin = `${focusX}px ${focusY}px`;
|
||||
wrap.style.transition = `transform ${durationMs}ms cubic-bezier(0.22, 1, 0.36, 1)`;
|
||||
wrap.style.transform = `scale(${scale})`;
|
||||
},
|
||||
{ scale, focusX, focusY, durationMs }
|
||||
{ dx, dy, scale, transitionMs }
|
||||
);
|
||||
}
|
||||
|
||||
/** Animate the wrapper back to identity transform. */
|
||||
export async function zoomReset(page: Page, durationMs = 1100): Promise<void> {
|
||||
await page.evaluate((durationMs) => {
|
||||
const transitionMs = Math.round(durationMs * RECORD_SCALE);
|
||||
await page.evaluate((transitionMs) => {
|
||||
const wrap = document.getElementById('__demo-zoom-wrap');
|
||||
if (!wrap) return;
|
||||
wrap.style.transition = `transform ${durationMs}ms cubic-bezier(0.22, 1, 0.36, 1)`;
|
||||
wrap.style.transition = `transform ${transitionMs}ms cubic-bezier(0.22, 1, 0.36, 1)`;
|
||||
wrap.style.transform = `translate(0px, 0px) scale(1)`;
|
||||
}, durationMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap-set the wrapper transform with no transition. Use for the very first
|
||||
* frame so the recording opens already-zoomed instead of zooming in from 1.
|
||||
*/
|
||||
export async function zoomToInstant(
|
||||
page: Page,
|
||||
opts: { scale: number; focusX: number; focusY: number }
|
||||
): Promise<void> {
|
||||
const { scale, focusX, focusY } = opts;
|
||||
const viewport = page.viewportSize() ?? { width: 1920, height: 1080 };
|
||||
const dx = viewport.width / 2 - scale * focusX;
|
||||
const dy = viewport.height / 2 - scale * focusY;
|
||||
await page.evaluate(
|
||||
({ dx, dy, scale }) => {
|
||||
const wrap = document.getElementById('__demo-zoom-wrap');
|
||||
if (!wrap) return;
|
||||
wrap.style.transition = 'none';
|
||||
wrap.style.transform = `translate(${dx}px, ${dy}px) scale(${scale})`;
|
||||
// Force a reflow so the next assignment with a transition actually
|
||||
// animates instead of being collapsed with this one.
|
||||
void wrap.offsetHeight;
|
||||
},
|
||||
{ dx, dy, scale }
|
||||
);
|
||||
}, transitionMs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -447,3 +435,83 @@ export async function scrollPaneTo(
|
|||
{ selector, top }
|
||||
);
|
||||
}
|
||||
|
||||
export async function waitForAnimationFrames(page: Page, frames = 3): Promise<void> {
|
||||
await page.evaluate(
|
||||
(frameCount) =>
|
||||
new Promise<void>((resolve) => {
|
||||
let seen = 0;
|
||||
const tick = () => {
|
||||
seen += 1;
|
||||
if (seen >= frameCount) resolve();
|
||||
else requestAnimationFrame(tick);
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
}),
|
||||
frames
|
||||
);
|
||||
}
|
||||
|
||||
export async function getDemoMapSettleVersion(page: Page): Promise<number> {
|
||||
return page.evaluate(
|
||||
() =>
|
||||
(
|
||||
window as typeof window & {
|
||||
__demoMapSettleVersion?: number;
|
||||
}
|
||||
).__demoMapSettleVersion ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
export async function waitForDemoMapSettled(
|
||||
page: Page,
|
||||
timeoutMs = 12000,
|
||||
afterVersion = -1
|
||||
): Promise<void> {
|
||||
await page.waitForFunction(
|
||||
(version) => {
|
||||
const demo = window as typeof window & {
|
||||
__demoMapSettled?: boolean;
|
||||
__demoMapSettleVersion?: number;
|
||||
__demoMapIdle?: boolean;
|
||||
};
|
||||
return (
|
||||
demo.__demoMapSettled === true &&
|
||||
demo.__demoMapIdle === true &&
|
||||
(demo.__demoMapSettleVersion ?? 0) > version
|
||||
);
|
||||
},
|
||||
afterVersion,
|
||||
{ timeout: timeoutMs }
|
||||
);
|
||||
await waitForAnimationFrames(page, 4);
|
||||
}
|
||||
|
||||
export async function waitForCurrentDemoMapSettled(page: Page, timeoutMs = 12000): Promise<void> {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const demo = window as typeof window & {
|
||||
__demoMapSettled?: boolean;
|
||||
__demoMapIdle?: boolean;
|
||||
};
|
||||
return demo.__demoMapSettled === true && demo.__demoMapIdle === true;
|
||||
},
|
||||
undefined,
|
||||
{ timeout: timeoutMs }
|
||||
);
|
||||
await waitForAnimationFrames(page, 4);
|
||||
}
|
||||
|
||||
export async function waitForDemoSelectionReady(page: Page, timeoutMs = 12000): Promise<void> {
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
(
|
||||
window as typeof window & {
|
||||
__demoSelectionReady?: boolean;
|
||||
}
|
||||
).__demoSelectionReady === true,
|
||||
undefined,
|
||||
{ timeout: timeoutMs }
|
||||
);
|
||||
await waitForAnimationFrames(page, 4);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,30 +12,22 @@ export const sleep = (ms: number) =>
|
|||
export const easeInOut = (t: number): number =>
|
||||
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
|
||||
// Slight overshoot then settle — gives clicks a tactile feel when paired with ripple.
|
||||
export const easeOutBack = (t: number): number => {
|
||||
const c1 = 1.70158;
|
||||
const c3 = c1 + 1;
|
||||
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
||||
};
|
||||
|
||||
interface MoveOptions {
|
||||
durationMs?: number;
|
||||
ease?: (t: number) => number;
|
||||
realMouse?: boolean;
|
||||
/**
|
||||
* Override the per-step CDP cost used to size the loop. Default 35ms is
|
||||
* right for free cursor moves. During a drag, every mouse.move fires a
|
||||
* pointermove → React re-render → thumb position update on the same
|
||||
* thread, pushing effective per-step cost to ~100ms. Pass that for drags
|
||||
* Override the per-step CDP cost used to size the loop. During a drag, every
|
||||
* mouse.move fires a pointermove -> React re-render -> thumb position update
|
||||
* on the same thread, pushing effective per-step cost higher. Pass that for drags
|
||||
* so the loop's wall duration matches `durationMs * RECORD_SCALE`.
|
||||
*/
|
||||
stepBudgetMs?: number;
|
||||
}
|
||||
|
||||
// Empirical Playwright→Chromium CDP roundtrip cost for a mouse.move command
|
||||
// while recording the 4K, software-GL dashboard. It is much higher than a
|
||||
// simple page because every move competes with map rendering and video capture.
|
||||
const CDP_MOVE_MS = 90;
|
||||
// Empirical Playwright-to-Chromium CDP roundtrip cost for a mouse.move command
|
||||
// while recording the software-GL dashboard.
|
||||
const CDP_MOVE_MS = 70;
|
||||
|
||||
/**
|
||||
* Move the real mouse from its current position to (x, y) along an eased path.
|
||||
|
|
@ -51,10 +43,36 @@ export async function smoothMove(
|
|||
page: Page,
|
||||
from: { x: number; y: number },
|
||||
to: { x: number; y: number },
|
||||
{ durationMs = 600, ease = easeInOut, stepBudgetMs = CDP_MOVE_MS }: MoveOptions = {}
|
||||
{
|
||||
durationMs = 600,
|
||||
ease = easeInOut,
|
||||
realMouse = false,
|
||||
stepBudgetMs = CDP_MOVE_MS,
|
||||
}: MoveOptions = {}
|
||||
): Promise<void> {
|
||||
const wallDuration = durationMs * RECORD_SCALE;
|
||||
const steps = Math.max(2, Math.min(28, Math.round(wallDuration / stepBudgetMs)));
|
||||
if (!realMouse) {
|
||||
const animated = await page.evaluate(
|
||||
({ x, y, wallDuration }) => {
|
||||
const move = (
|
||||
window as typeof window & {
|
||||
__demoMoveCursor?: (x: number, y: number, durationMs: number) => void;
|
||||
}
|
||||
).__demoMoveCursor;
|
||||
if (!move) return false;
|
||||
move(x, y, wallDuration);
|
||||
return true;
|
||||
},
|
||||
{ x: to.x, y: to.y, wallDuration }
|
||||
);
|
||||
if (animated) {
|
||||
await new Promise((resolve) => setTimeout(resolve, wallDuration));
|
||||
await page.mouse.move(to.x, to.y);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const steps = Math.max(2, Math.min(96, Math.round(wallDuration / stepBudgetMs)));
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const t = ease(i / steps);
|
||||
const x = from.x + (to.x - from.x) * t;
|
||||
|
|
@ -76,7 +94,7 @@ export async function fakeType(
|
|||
delayMs: number
|
||||
): Promise<void> {
|
||||
const delay = delayMs * RECORD_SCALE;
|
||||
const steps = Math.min(6, text.length);
|
||||
const steps = text.length;
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const end = Math.ceil((text.length * i) / steps);
|
||||
await page.evaluate(
|
||||
|
|
@ -121,14 +139,13 @@ export async function smoothDragSliderThumb(
|
|||
// smoothMove already applies RECORD_SCALE internally; pass human-time durations.
|
||||
await smoothMove(page, fromCursor, { x: thumbCx, y: thumbCy }, { durationMs: 220 });
|
||||
await page.mouse.down();
|
||||
// Keep the drag to a few pointer updates. The map will redraw after commit;
|
||||
// asking React/deck.gl for dozens of intermediate travel-time states is what
|
||||
// made previous renders crawl and look stuttery.
|
||||
// The user explicitly prefers a longer render over stepped motion, so use
|
||||
// enough real pointer updates for the thumb itself to read as continuous.
|
||||
await smoothMove(
|
||||
page,
|
||||
{ x: thumbCx, y: thumbCy },
|
||||
{ x: targetX, y: thumbCy },
|
||||
{ durationMs, stepBudgetMs: 360 }
|
||||
{ durationMs, realMouse: true, stepBudgetMs: 135 }
|
||||
);
|
||||
await page.mouse.up();
|
||||
return { x: targetX, y: thumbCy };
|
||||
|
|
|
|||
|
|
@ -1,241 +1,23 @@
|
|||
import { chromium } from 'playwright';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, statSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
APP_URL,
|
||||
AUTH_STATE_PATH,
|
||||
CAPTURE_SCALE,
|
||||
DASHBOARD_PATH,
|
||||
INITIAL_MAP_VIEW,
|
||||
MAX_DURATION_S,
|
||||
OUTPUT_FPS,
|
||||
OUTPUT_DIR,
|
||||
PRELOAD_POSTCODE,
|
||||
RECORD_SCALE,
|
||||
STUBBED_FILTERS,
|
||||
STUBBED_TRAVEL_TIME_FILTERS,
|
||||
VIDEO_SIZE,
|
||||
VIEWPORT,
|
||||
WEBM_BITRATE,
|
||||
} from './config.js';
|
||||
import { installCursor, installZoomWrapper } from './dom.js';
|
||||
import {
|
||||
preZoomToAiBox,
|
||||
sceneAiCloseUp,
|
||||
sceneClusterClick,
|
||||
sceneExportAndOutro,
|
||||
sceneTravelTimeSlider,
|
||||
sceneZoomOutResults,
|
||||
type SceneCtx,
|
||||
} from './scenes.js';
|
||||
import { sleep } from './motion.js';
|
||||
|
||||
/**
|
||||
* Stub the AI endpoint. The real backend calls Gemini and takes 2–5s; for a
|
||||
* 15-second video we want sub-second response so the map reacts crisply with
|
||||
* the typed prompt still on screen. Returning canned filters also makes every
|
||||
* recording bit-identical.
|
||||
*
|
||||
* The shape MUST match what useAiFilters expects (filters, travel_time_filters,
|
||||
* notes, match_count) — see frontend/src/hooks/useAiFilters.ts.
|
||||
*/
|
||||
async function stubAiFilters(page: import('playwright').Page) {
|
||||
await page.route('**/api/ai-filters', async (route) => {
|
||||
// Small delay so the loading indicator is visible (looks like real AI work).
|
||||
await new Promise((r) => setTimeout(r, 180));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
filters: STUBBED_FILTERS,
|
||||
travel_time_filters: STUBBED_TRAVEL_TIME_FILTERS,
|
||||
notes: '',
|
||||
match_count: 1247,
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function stubExport(page: import('playwright').Page) {
|
||||
await page.route('**/api/export?**', async (route) => {
|
||||
await new Promise((r) => setTimeout(r, 160));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers: {
|
||||
'content-disposition': 'attachment; filename="perfect-postcode-export.xlsx"',
|
||||
},
|
||||
body: Buffer.from('Perfect Postcode demo export\n'),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addInitialTravelTimeParams(params: URLSearchParams) {
|
||||
for (const tt of STUBBED_TRAVEL_TIME_FILTERS) {
|
||||
params.append('tt', `${tt.mode}:${tt.slug}:${tt.label}:${tt.min ?? 0}:${tt.max ?? 120}`);
|
||||
}
|
||||
}
|
||||
import { AUTH_STATE_PATH, OUTPUT_DIR } from './config.js';
|
||||
import { launchRecordingBrowser } from './browser.js';
|
||||
import { installDemoRoutes } from './routes.js';
|
||||
import { prepareTimeline, runTimeline } from './timeline.js';
|
||||
import { trimRecording } from './video.js';
|
||||
|
||||
async function main() {
|
||||
if (!existsSync(AUTH_STATE_PATH)) {
|
||||
console.error(
|
||||
`No ${AUTH_STATE_PATH} found. Run "npm run setup-auth" first to log in once.`
|
||||
);
|
||||
console.error(`No ${AUTH_STATE_PATH} found. Run "npm run setup-auth" first.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!existsSync(OUTPUT_DIR)) mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
// Headless Chromium otherwise loses the WebGL context mid-render when
|
||||
// deck.gl pushes large buffers; SwiftShader is software GL but stable.
|
||||
'--use-gl=angle',
|
||||
'--use-angle=swiftshader',
|
||||
'--enable-unsafe-swiftshader',
|
||||
'--ignore-gpu-blocklist',
|
||||
// Lift Chromium's animation/raster rate caps so RECORD_SCALE actually
|
||||
// gets us extra frames per second of wall time. Without these, Chromium
|
||||
// throttles offscreen rendering and the slow-down is wasted.
|
||||
'--disable-frame-rate-limit',
|
||||
'--disable-gpu-vsync',
|
||||
'--disable-features=CalculateNativeWinOcclusion,IntensiveWakeUpThrottling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
],
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
storageState: AUTH_STATE_PATH,
|
||||
viewport: VIEWPORT,
|
||||
deviceScaleFactor: CAPTURE_SCALE,
|
||||
recordVideo: { dir: OUTPUT_DIR, size: VIDEO_SIZE },
|
||||
});
|
||||
|
||||
// Vite's dev server pushes HMR updates over a "vite-hmr" WebSocket. If a
|
||||
// module isn't accept-marked the client triggers a FULL page reload — which
|
||||
// mid-recording resets the React tree and re-shows "Connecting to server…".
|
||||
// Disable the client-side HMR socket entirely.
|
||||
await context.addInitScript(() => {
|
||||
// Block the vite-hmr WebSocket so HMR push messages never arrive.
|
||||
const RealWS = window.WebSocket;
|
||||
window.WebSocket = new Proxy(RealWS, {
|
||||
construct(target, args) {
|
||||
const url = String(args[0] ?? '');
|
||||
const proto = (args[1] as string | string[] | undefined) ?? '';
|
||||
const protoStr = Array.isArray(proto) ? proto.join(',') : proto;
|
||||
if (
|
||||
protoStr.includes('vite-hmr') ||
|
||||
protoStr.includes('webpack') ||
|
||||
url.includes('/ws') ||
|
||||
url.includes('sockjs-node')
|
||||
) {
|
||||
const fake = new EventTarget() as WebSocket;
|
||||
Object.defineProperties(fake, {
|
||||
readyState: { value: RealWS.CLOSED },
|
||||
url: { value: url },
|
||||
protocol: { value: '' },
|
||||
extensions: { value: '' },
|
||||
bufferedAmount: { value: 0 },
|
||||
binaryType: { value: 'blob', writable: true },
|
||||
});
|
||||
fake.send = () => {};
|
||||
fake.close = () => {
|
||||
fake.dispatchEvent(new Event('close'));
|
||||
};
|
||||
queueMicrotask(() => fake.dispatchEvent(new Event('close')));
|
||||
return fake;
|
||||
}
|
||||
return Reflect.construct(target, args);
|
||||
},
|
||||
});
|
||||
|
||||
// Belt-and-braces: even if an HMR push slips through (e.g. via a different
|
||||
// transport in a later Vite version), neutralize the full-reload fallback.
|
||||
const noop = () => {};
|
||||
Object.defineProperty(window.location, 'reload', { value: noop, configurable: true });
|
||||
|
||||
// Stop runtime errors from reaching Vite's <vite-error-overlay>. We're
|
||||
// recording against a dev server for fast iteration; in prod the overlay
|
||||
// wouldn't exist either way. A stray deck.gl layer-pipeline error covering
|
||||
// the dashboard ruins the take, so we eat the error before Vite can see it.
|
||||
// capture=true → our handler runs before Vite's at the document level.
|
||||
window.addEventListener(
|
||||
'error',
|
||||
(e) => {
|
||||
e.stopImmediatePropagation();
|
||||
},
|
||||
true
|
||||
);
|
||||
window.addEventListener(
|
||||
'unhandledrejection',
|
||||
(e) => {
|
||||
e.stopImmediatePropagation();
|
||||
},
|
||||
true
|
||||
);
|
||||
// CSS fallback covering all the bundlers' diagnostic overlays. Vite
|
||||
// ships <vite-error-overlay>, webpack v5+ uses <wds-overlay> (shadow DOM)
|
||||
// or <div id="webpack-dev-server-client-overlay">, webpack v4 injects an
|
||||
// iframe. Compilation warnings surface as a top-level red banner that
|
||||
// occludes the dashboard.
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.textContent = `
|
||||
vite-error-overlay,
|
||||
wds-overlay,
|
||||
#webpack-dev-server-client-overlay,
|
||||
#webpack-dev-server-client-overlay-div,
|
||||
iframe[src*="webpack-dev-server"],
|
||||
iframe[id*="webpack"],
|
||||
[id*="webpack-dev-server-client"],
|
||||
[class*="error-overlay"],
|
||||
[class*="webpack-error"] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
`;
|
||||
(document.head ?? document.documentElement).appendChild(styleEl);
|
||||
|
||||
// Belt-and-braces: a MutationObserver kills any newly-injected overlay
|
||||
// root by name. Webpack v5 reinjects on each warning batch, so a static
|
||||
// CSS rule alone occasionally races a brief flash of the banner.
|
||||
const killOverlay = (node: Element) => {
|
||||
const tag = node.tagName?.toLowerCase();
|
||||
const id = (node as HTMLElement).id?.toLowerCase() ?? '';
|
||||
if (
|
||||
tag === 'vite-error-overlay' ||
|
||||
tag === 'wds-overlay' ||
|
||||
id.includes('webpack-dev-server-client') ||
|
||||
id.includes('webpack-error')
|
||||
) {
|
||||
(node as HTMLElement).remove();
|
||||
}
|
||||
};
|
||||
const obs = new MutationObserver((muts) => {
|
||||
for (const m of muts)
|
||||
m.addedNodes.forEach((n) => {
|
||||
if (n.nodeType === 1) killOverlay(n as Element);
|
||||
});
|
||||
});
|
||||
if (document.body) obs.observe(document.body, { childList: true, subtree: true });
|
||||
else
|
||||
document.addEventListener('DOMContentLoaded', () =>
|
||||
obs.observe(document.body, { childList: true, subtree: true })
|
||||
);
|
||||
});
|
||||
|
||||
const { browser, context } = await launchRecordingBrowser();
|
||||
const page = await context.newPage();
|
||||
const recordedVideo = page.video();
|
||||
// recordVideo starts the moment the page is created. We want the final clip
|
||||
// to begin once we're zoomed on the AI prompt and ready to type — NOT
|
||||
// include navigation, sidebar mount, or the AI button click. We track scene
|
||||
// start vs record start and ffmpeg-trim post-hoc.
|
||||
const recordStartMs = Date.now();
|
||||
|
||||
page.on('console', (m) => {
|
||||
if (m.type() === 'error' || m.type() === 'warning') {
|
||||
console.log(`[browser ${m.type()}] ${m.text()}`);
|
||||
|
|
@ -251,87 +33,14 @@ async function main() {
|
|||
const u = r.url();
|
||||
if (u.includes('ai-filters')) console.log(`[req] ${r.method()} ${u}`);
|
||||
});
|
||||
await stubAiFilters(page);
|
||||
await stubExport(page);
|
||||
|
||||
// Pre-load with ?pc= so the dashboard auto-opens the right pane and
|
||||
// flies to that postcode at zoom 16 (where postcode polygons render
|
||||
// individually). The cluster click later in the scene becomes purely
|
||||
// visual — the pane is already there, data already loaded.
|
||||
const params = new URLSearchParams({
|
||||
pc: PRELOAD_POSTCODE,
|
||||
lat: String(INITIAL_MAP_VIEW.lat),
|
||||
lon: String(INITIAL_MAP_VIEW.lon),
|
||||
zoom: String(INITIAL_MAP_VIEW.zoom),
|
||||
});
|
||||
addInitialTravelTimeParams(params);
|
||||
const url = `${APP_URL}${DASHBOARD_PATH}?${params}`;
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForLoadState('load', { timeout: 15000 }).catch(() => {});
|
||||
await page
|
||||
.locator('[data-tutorial="ai-filters"]')
|
||||
.waitFor({ state: 'visible', timeout: 15000 });
|
||||
|
||||
// Wait for the right pane to actually mount AND its content to render.
|
||||
// Without this the AI scene's browser-side setInterval (fakeType) gets
|
||||
// throttled by Chromium's scheduler under boot load (deck.gl uploads,
|
||||
// Street View iframe, postcode geometry fetch) and typing stretches 3×.
|
||||
try {
|
||||
await page
|
||||
.locator('[data-tutorial="right-pane"]')
|
||||
.waitFor({ state: 'visible', timeout: 15000 });
|
||||
// Either the area stats or the Street View embed indicates the pane
|
||||
// has finished its first render pass.
|
||||
await page
|
||||
.locator('[data-tutorial="right-pane"] iframe, [data-tutorial="right-pane"] canvas')
|
||||
.first()
|
||||
.waitFor({ state: 'attached', timeout: 2000 })
|
||||
.catch(() => {});
|
||||
} catch {
|
||||
// Pane didn't appear (postcode may not exist on this stack); proceed
|
||||
// anyway — scenes still work, just with no pane content.
|
||||
console.log('[render] right-pane preload did not mount; continuing');
|
||||
}
|
||||
// Final settle. The dashboard's flyTo to the preloaded postcode runs a
|
||||
// ~1.5s maplibre animation; deck.gl's hexagon/postcode buffers upload
|
||||
// through the same window. We wait long enough that all of this completes
|
||||
// before scenes start, otherwise the AI scene's browser-side setInterval
|
||||
// (fakeType) gets throttled to 200ms+ ticks.
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
|
||||
// Wrapper must be installed BEFORE the cursor — the cursor is appended to
|
||||
// <body> and must remain a sibling of the wrapper, not a descendant.
|
||||
await installZoomWrapper(page);
|
||||
await installCursor(page);
|
||||
|
||||
// Park cursor near the AI box (sidebar) at low-y so the first move is short.
|
||||
const ctx: SceneCtx = { page, cursor: { x: 200, y: 240 } };
|
||||
await page.mouse.move(ctx.cursor.x, ctx.cursor.y);
|
||||
|
||||
// Pre-flight (NOT counted in scene wall time): expand the AI prompt and
|
||||
// snap the wrapper to its zoomed-on-AI starting state.
|
||||
await preZoomToAiBox(ctx);
|
||||
await sleep(80);
|
||||
|
||||
const sceneStartMs = Date.now();
|
||||
const t = (label: string, prev: number) => {
|
||||
const now = Date.now();
|
||||
console.log(`[scene] ${label}: ${((now - prev) / 1000).toFixed(2)}s wall`);
|
||||
return now;
|
||||
};
|
||||
let mark = sceneStartMs;
|
||||
await sceneAiCloseUp(ctx); mark = t('AI close-up', mark);
|
||||
await sceneZoomOutResults(ctx); mark = t('Zoom out', mark);
|
||||
await sceneTravelTimeSlider(ctx); mark = t('TT slider', mark);
|
||||
await sceneClusterClick(ctx); mark = t('Cluster click', mark);
|
||||
await sceneExportAndOutro(ctx); mark = t('Export + outro', mark);
|
||||
const sceneEndMs = Date.now();
|
||||
await installDemoRoutes(page);
|
||||
const ctx = await prepareTimeline(page);
|
||||
const timeline = await runTimeline(ctx);
|
||||
|
||||
await page.close();
|
||||
const rawPath = join(OUTPUT_DIR, 'recording.raw.webm');
|
||||
if (recordedVideo) {
|
||||
await recordedVideo.saveAs(rawPath);
|
||||
}
|
||||
if (recordedVideo) await recordedVideo.saveAs(rawPath);
|
||||
await context.close();
|
||||
await browser.close();
|
||||
|
||||
|
|
@ -340,46 +49,10 @@ async function main() {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
const trimmedPath = join(OUTPUT_DIR, 'recording.webm');
|
||||
const sceneSpan = (sceneEndMs - sceneStartMs) / 1000;
|
||||
const maxFinalDuration = Math.max(0.1, MAX_DURATION_S - 0.2);
|
||||
// The trim window is in *recording wall time*, which is RECORD_SCALE× the
|
||||
// visible duration. After ffmpeg setpts speeds it back up, the final clip
|
||||
// will be exactly MAX_DURATION_S seconds (or sceneSpan/RECORD_SCALE if shorter).
|
||||
const wallCap = maxFinalDuration * RECORD_SCALE;
|
||||
const trimEnd = (sceneEndMs - recordStartMs) / 1000;
|
||||
const wallDuration = Math.min(sceneSpan, wallCap);
|
||||
const trimStart = trimEnd - wallDuration;
|
||||
const finalDuration = wallDuration / RECORD_SCALE;
|
||||
if (sceneSpan > wallCap) {
|
||||
console.log(
|
||||
`Scene wall time was ${sceneSpan.toFixed(2)}s (cap ${wallCap.toFixed(2)}s at scale ${RECORD_SCALE}); trimming to last ${maxFinalDuration.toFixed(2)}s (anchored to outro).`
|
||||
);
|
||||
}
|
||||
|
||||
// Trim + speed-up in one pass.
|
||||
// - -ss + -t: trim window in raw recording's wall time.
|
||||
// - setpts=PTS/RECORD_SCALE: speed up the clip back to "human time". Each
|
||||
// raw frame plays for 1/N as long → we get N× the effective fps for free,
|
||||
// no synthetic interpolation needed.
|
||||
// - fps=OUTPUT_FPS gives the MP4 encoder a stable cadence; with the default
|
||||
// RECORD_SCALE=2 this matches the real captured cadence (25fps × 2).
|
||||
execSync(
|
||||
`ffmpeg -y -ss ${trimStart.toFixed(3)} -i "${rawPath}" -t ${wallDuration.toFixed(3)} ` +
|
||||
`-vf "setpts=PTS/${RECORD_SCALE},fps=${OUTPUT_FPS},trim=duration=${finalDuration.toFixed(3)},setpts=PTS-STARTPTS" -fps_mode cfr ` +
|
||||
`-r ${OUTPUT_FPS} -c:v libvpx -b:v ${WEBM_BITRATE} -deadline good -cpu-used 5 ` +
|
||||
`"${trimmedPath}"`,
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
// Drop the untrimmed file once we've extracted the scenes.
|
||||
try {
|
||||
statSync(rawPath) && renameSync(rawPath, rawPath + '.untrimmed');
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
console.log(
|
||||
`Wrote ${trimmedPath} (${finalDuration.toFixed(2)}s, scale=${RECORD_SCALE}, capture=${VIDEO_SIZE.width}x${VIDEO_SIZE.height})`
|
||||
);
|
||||
trimRecording(rawPath, join(OUTPUT_DIR, 'recording.webm'), {
|
||||
recordStartMs,
|
||||
...timeline,
|
||||
});
|
||||
console.log('Run "npm run encode" to produce output/recording.mp4');
|
||||
}
|
||||
|
||||
|
|
|
|||
58
video/src/routes.ts
Normal file
58
video/src/routes.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import type { Page } from 'playwright';
|
||||
import {
|
||||
APP_URL,
|
||||
DASHBOARD_PATH,
|
||||
INITIAL_MAP_VIEW,
|
||||
STUBBED_FILTERS,
|
||||
STUBBED_TRAVEL_TIME_FILTERS,
|
||||
} from './config.js';
|
||||
|
||||
export async function installDemoRoutes(page: Page) {
|
||||
await Promise.all([stubAiFilters(page), stubExport(page)]);
|
||||
}
|
||||
|
||||
export function dashboardUrl(): string {
|
||||
const params = new URLSearchParams({
|
||||
lat: String(INITIAL_MAP_VIEW.lat),
|
||||
lon: String(INITIAL_MAP_VIEW.lon),
|
||||
zoom: String(INITIAL_MAP_VIEW.zoom),
|
||||
});
|
||||
addInitialTravelTimeParams(params);
|
||||
return `${APP_URL}${DASHBOARD_PATH}?${params}`;
|
||||
}
|
||||
|
||||
async function stubAiFilters(page: Page) {
|
||||
await page.route('**/api/ai-filters', async (route) => {
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
filters: STUBBED_FILTERS,
|
||||
travel_time_filters: STUBBED_TRAVEL_TIME_FILTERS,
|
||||
notes: '',
|
||||
match_count: 1247,
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function stubExport(page: Page) {
|
||||
await page.route('**/api/export?**', async (route) => {
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers: {
|
||||
'content-disposition': 'attachment; filename="perfect-postcode-export.xlsx"',
|
||||
},
|
||||
body: Buffer.from('Perfect Postcode demo export\n'),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addInitialTravelTimeParams(params: URLSearchParams) {
|
||||
for (const tt of STUBBED_TRAVEL_TIME_FILTERS) {
|
||||
params.append('tt', `${tt.mode}:${tt.slug}:${tt.label}:${tt.min ?? 0}:${tt.max ?? 120}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,13 +13,16 @@ import {
|
|||
import {
|
||||
clearVignette,
|
||||
flashRect,
|
||||
getDemoMapSettleVersion,
|
||||
hideCaption,
|
||||
scrollPaneTo,
|
||||
showCaption,
|
||||
showOutro,
|
||||
visualClick,
|
||||
waitForDemoMapSettled,
|
||||
waitForCurrentDemoMapSettled,
|
||||
waitForDemoSelectionReady,
|
||||
zoomReset,
|
||||
zoomTo,
|
||||
zoomToInstant,
|
||||
} from './dom.js';
|
||||
import { fakeType, sleep, smoothMove, smoothDragSliderThumb } from './motion.js';
|
||||
|
||||
|
|
@ -29,37 +32,36 @@ export interface SceneCtx {
|
|||
}
|
||||
|
||||
/**
|
||||
* Scene 1: open already zoomed in on the AI prompt card. Caption fades in,
|
||||
* the user types their request, and the already-preloaded filters are revealed
|
||||
* behind the zoomed wrapper. Keeping this beat visual avoids slow dev-server
|
||||
* data refreshes eating the 15-second timeline.
|
||||
*
|
||||
* Pre-conditions (set up by record.ts before scene timer starts):
|
||||
* - The AI box is already expanded (textarea visible, ready to focus).
|
||||
* - The wrapper is already zoomed at AI_ZOOM_SCALE on the AI box centre.
|
||||
* - The vignette is up.
|
||||
* Scene 1: start wide, then zoom into the AI prompt. The AI response is
|
||||
* stubbed, while the map filters and right pane are loaded from the real app.
|
||||
*/
|
||||
export async function sceneAiCloseUp(ctx: SceneCtx): Promise<void> {
|
||||
const { page } = ctx;
|
||||
|
||||
await clearVignette(page);
|
||||
await showCaption(page, 'Brief: flats or terraces under £450k near central Manchester.');
|
||||
await sleep(160);
|
||||
await sleep(180);
|
||||
|
||||
await fakeType(page, '[data-tutorial="ai-filters"] textarea', PROMPT_TEXT, 14);
|
||||
await sleep(120);
|
||||
await zoomToAiBox(page, 720);
|
||||
await sleep(760);
|
||||
|
||||
await fakeType(page, '[data-tutorial="ai-filters"] textarea', PROMPT_TEXT, 18);
|
||||
await sleep(160);
|
||||
const aiResponse = page
|
||||
.waitForResponse(
|
||||
(response) => response.url().includes('/api/ai-filters') && response.status() === 200,
|
||||
{ timeout: 1800 }
|
||||
)
|
||||
.catch(() => null);
|
||||
const mapVersion = await getDemoMapSettleVersion(page);
|
||||
await page.evaluate(() => {
|
||||
document.querySelector<HTMLFormElement>('[data-tutorial="ai-filters"] form')?.requestSubmit();
|
||||
});
|
||||
await aiResponse;
|
||||
await sleep(160);
|
||||
await waitForDemoMapSettled(page, 15000, mapVersion);
|
||||
await showCaption(page, 'The filters are already live on the map.');
|
||||
await sleep(360);
|
||||
await sleep(560);
|
||||
await hideCaption(page);
|
||||
}
|
||||
|
||||
|
|
@ -72,10 +74,10 @@ export async function sceneAiCloseUp(ctx: SceneCtx): Promise<void> {
|
|||
export async function sceneZoomOutResults(ctx: SceneCtx): Promise<void> {
|
||||
const { page } = ctx;
|
||||
await showCaption(page, 'Now the view lands on central Manchester with the filters already set.');
|
||||
await zoomReset(page, 560);
|
||||
await sleep(420);
|
||||
await zoomReset(page, 860);
|
||||
await sleep(980);
|
||||
await hideCaption(page);
|
||||
await sleep(80);
|
||||
await sleep(180);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -107,6 +109,7 @@ export async function sceneTravelTimeSlider(ctx: SceneCtx): Promise<void> {
|
|||
|
||||
// Slider goes 0..120, target = 20 → fraction 0.166...
|
||||
const toFraction = TT_DRAG_TO_MIN / TT_SLIDER_MAX;
|
||||
const mapVersion = await getDemoMapSettleVersion(page);
|
||||
|
||||
ctx.cursor = await smoothDragSliderThumb(
|
||||
page,
|
||||
|
|
@ -114,87 +117,90 @@ export async function sceneTravelTimeSlider(ctx: SceneCtx): Promise<void> {
|
|||
trackSelector,
|
||||
ctx.cursor,
|
||||
toFraction,
|
||||
520
|
||||
1180
|
||||
);
|
||||
|
||||
await sleep(120);
|
||||
await sleep(220);
|
||||
await waitForDemoMapSettled(page, 16000, mapVersion);
|
||||
await showCaption(page, 'The map redraws around the areas that still work.');
|
||||
await sleep(440);
|
||||
await sleep(720);
|
||||
await hideCaption(page);
|
||||
await sleep(60);
|
||||
await sleep(180);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scene 4: zoom into a cluster of filtered postcodes (using deck.gl's own
|
||||
* camera, via wheel events), click one, and as the right pane fills, pan
|
||||
* the framing rightward while scrolling the pane content.
|
||||
*
|
||||
* Why two zoom mechanisms across this scene:
|
||||
* - Pre-click: native deck.gl wheel-zoom. CSS-transforming the wrapper
|
||||
* changes `canvas.getBoundingClientRect()` (scaled rect) without changing
|
||||
* `canvas.width`. deck.gl's hit-test uses the rect for screen→buffer
|
||||
* mapping, returns a partial picked object, and React re-renders mid-paint
|
||||
* leaving a null layer reference that crashes `MapboxLayer.render`.
|
||||
* Native wheel-zoom recomputes deck.gl's camera in-place; layers stay coherent.
|
||||
* - Post-click: CSS transform to pan the framing rightward. By this point
|
||||
* the postcode is selected and layers are stable, so the transform is safe.
|
||||
* Scene 4: after the filtered result map is visible, zoom into Manchester,
|
||||
* click a hexagon, then let the right pane open from that selection.
|
||||
*/
|
||||
export async function sceneClusterClick(ctx: SceneCtx): Promise<void> {
|
||||
const { page } = ctx;
|
||||
const viewport = page.viewportSize() ?? { width: 1920, height: 1080 };
|
||||
|
||||
await showCaption(page, 'Open one promising area and check the detail before shortlisting.');
|
||||
await showCaption(page, 'Zoom into the result clusters, then click a promising hexagon.');
|
||||
|
||||
// Click point: roughly map centre. After AI flew the camera to Manchester, this
|
||||
// sits in the densely-filtered city core where hexagons reliably cover any
|
||||
// pixel. Earlier iterations wheel-zoomed first to "feel cinematic", but
|
||||
// that crossed the hexagon→postcode layer-swap threshold mid-flight and
|
||||
// clicks landed in a layer gap (no pane opened).
|
||||
const cluster = {
|
||||
x: 360 + (viewport.width - 360) * 0.5,
|
||||
y: viewport.height * 0.45,
|
||||
x: 360 + (viewport.width - 360) * 0.35,
|
||||
y: viewport.height * 0.52,
|
||||
};
|
||||
|
||||
await smoothMove(page, ctx.cursor, cluster, { durationMs: 260 });
|
||||
await smoothMove(page, ctx.cursor, cluster, { durationMs: 520 });
|
||||
ctx.cursor = cluster;
|
||||
await sleep(70);
|
||||
await sleep(220);
|
||||
|
||||
// The right pane was opened at page load via ?pc= — no need to drive a
|
||||
// real selection through deck.gl's hit-test, which is flaky in headless
|
||||
// Chromium. The mouse.click here is purely for the visible cursor ripple
|
||||
// animation; the pane is already populated with real postcode data.
|
||||
await page.mouse.click(cluster.x, cluster.y);
|
||||
await sleep(130);
|
||||
await zoomMapWithWheel(page, cluster);
|
||||
|
||||
// NOW zoom in toward the cluster, pan rightward to centre the right pane,
|
||||
// and scroll the pane content — all in parallel. Layers are stable so the
|
||||
// CSS transform is safe.
|
||||
const rightShift = 240;
|
||||
await Promise.all([
|
||||
zoomTo(page, {
|
||||
scale: 1.35,
|
||||
focusX: cluster.x + rightShift,
|
||||
focusY: cluster.y,
|
||||
durationMs: 520,
|
||||
}),
|
||||
scrollPaneTo(page, '[data-tutorial="right-pane"]', 480),
|
||||
]);
|
||||
await sleep(320);
|
||||
const clicked = await clickHexagon(page, cluster);
|
||||
ctx.cursor = clicked;
|
||||
await openDemoHexagon(page);
|
||||
await page.locator('[data-tutorial="right-pane"]').waitFor({ state: 'visible', timeout: 5000 });
|
||||
await waitForDemoSelectionReady(page, 16000);
|
||||
await sleep(360);
|
||||
await showCaption(
|
||||
page,
|
||||
'This is the useful pause: local stats, matching homes, and street context together.'
|
||||
);
|
||||
await sleep(520);
|
||||
await sleep(1000);
|
||||
await hideCaption(page);
|
||||
}
|
||||
|
||||
async function clickHexagon(
|
||||
page: Page,
|
||||
target: { x: number; y: number }
|
||||
): Promise<{ x: number; y: number }> {
|
||||
await visualClick(page, target);
|
||||
await sleep(140);
|
||||
return target;
|
||||
}
|
||||
|
||||
async function zoomMapWithWheel(page: Page, target: { x: number; y: number }): Promise<void> {
|
||||
await page.mouse.move(target.x, target.y);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await page.mouse.wheel(0, -120);
|
||||
await sleep(95);
|
||||
}
|
||||
await waitForCurrentDemoMapSettled(page, 16000);
|
||||
await sleep(260);
|
||||
}
|
||||
|
||||
async function openDemoHexagon(page: Page): Promise<void> {
|
||||
const selected = await page.evaluate(
|
||||
() =>
|
||||
(
|
||||
window as typeof window & {
|
||||
__demoOpenBestHexagon?: () => string | null;
|
||||
}
|
||||
).__demoOpenBestHexagon?.() ?? null
|
||||
);
|
||||
if (!selected) throw new Error('Could not open a demo hexagon selection');
|
||||
}
|
||||
|
||||
/** Export the current shortlist, then reveal the URL. */
|
||||
export async function sceneExportAndOutro(ctx: SceneCtx): Promise<void> {
|
||||
const { page } = ctx;
|
||||
|
||||
await showCaption(page, 'When the shortlist feels right, export the exact filtered view.');
|
||||
await zoomReset(page, 460);
|
||||
await sleep(240);
|
||||
await zoomReset(page, 680);
|
||||
await sleep(520);
|
||||
|
||||
const exportButton = page.locator('button[title="Export to Excel"]').first();
|
||||
await exportButton.waitFor({ state: 'visible', timeout: 4000 });
|
||||
|
|
@ -202,34 +208,25 @@ export async function sceneExportAndOutro(ctx: SceneCtx): Promise<void> {
|
|||
if (!box) throw new Error('Export button has no bounding box');
|
||||
const target = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
||||
|
||||
await smoothMove(page, ctx.cursor, target, { durationMs: 360 });
|
||||
await smoothMove(page, ctx.cursor, target, { durationMs: 620 });
|
||||
ctx.cursor = target;
|
||||
await sleep(70);
|
||||
await sleep(160);
|
||||
|
||||
const download = page.waitForEvent('download', { timeout: 4000 }).catch(() => null);
|
||||
await page.mouse.click(target.x, target.y);
|
||||
await flashRect(page, box);
|
||||
|
||||
await sleep(280);
|
||||
await sleep(680);
|
||||
await hideCaption(page);
|
||||
await showOutro(ctx.page, BRAND_NAME, BRAND_TAGLINE, BRAND_URL);
|
||||
void download;
|
||||
await sleep(2400);
|
||||
await sleep(2200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper used by record.ts: after navigation but BEFORE the scene timer
|
||||
* starts, click the AI-prompt button so its textarea is mounted, then snap
|
||||
* the wrapper to its zoomed-on-AI starting state.
|
||||
*
|
||||
* Splitting this out keeps the scene timer honest: the textarea's mount
|
||||
* animation and the zoom snap don't eat into the 15s budget.
|
||||
*/
|
||||
export async function preZoomToAiBox(ctx: SceneCtx): Promise<void> {
|
||||
/** Open the AI prompt before the timed scene starts. */
|
||||
export async function prepareAiBox(ctx: SceneCtx): Promise<void> {
|
||||
const { page } = ctx;
|
||||
|
||||
// Open the AI prompt. The collapsed state shows a single button; clicking
|
||||
// it expands the form and reveals the textarea.
|
||||
const aiRoot = page.locator('[data-tutorial="ai-filters"]').first();
|
||||
await aiRoot.waitFor({ state: 'visible', timeout: 15000 });
|
||||
|
||||
|
|
@ -247,13 +244,13 @@ export async function preZoomToAiBox(ctx: SceneCtx): Promise<void> {
|
|||
}
|
||||
await textarea.waitFor({ state: 'visible', timeout: 15000 });
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
// Snap-zoom to the AI card centre. The recording opens already zoomed in
|
||||
// — there's no awkward "from 1× to 2.4×" intro animation.
|
||||
async function zoomToAiBox(page: Page, durationMs: number): Promise<void> {
|
||||
const aiCard = page.locator('[data-tutorial="ai-filters"]');
|
||||
const cardBox = await aiCard.boundingBox();
|
||||
if (!cardBox) throw new Error('AI card has no bounding box');
|
||||
const focusX = cardBox.x + cardBox.width / 2;
|
||||
const focusY = cardBox.y + cardBox.height / 2;
|
||||
await zoomToInstant(page, { scale: AI_ZOOM_SCALE, focusX, focusY });
|
||||
await zoomTo(page, { scale: AI_ZOOM_SCALE, focusX, focusY, durationMs });
|
||||
}
|
||||
|
|
|
|||
58
video/src/timeline.ts
Normal file
58
video/src/timeline.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import type { Page } from 'playwright';
|
||||
import { installCursor, installZoomWrapper, waitForCurrentDemoMapSettled } from './dom.js';
|
||||
import { sleep } from './motion.js';
|
||||
import { dashboardUrl } from './routes.js';
|
||||
import {
|
||||
prepareAiBox,
|
||||
sceneAiCloseUp,
|
||||
sceneClusterClick,
|
||||
sceneExportAndOutro,
|
||||
sceneTravelTimeSlider,
|
||||
sceneZoomOutResults,
|
||||
type SceneCtx,
|
||||
} from './scenes.js';
|
||||
|
||||
export interface TimelineResult {
|
||||
sceneStartMs: number;
|
||||
sceneEndMs: number;
|
||||
}
|
||||
|
||||
export async function prepareTimeline(page: Page): Promise<SceneCtx> {
|
||||
await page.goto(dashboardUrl(), { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForLoadState('load', { timeout: 15000 }).catch(() => {});
|
||||
await page
|
||||
.locator('[data-tutorial="ai-filters"]')
|
||||
.waitFor({ state: 'visible', timeout: 15000 });
|
||||
await page.locator('canvas').first().waitFor({ state: 'attached', timeout: 15000 });
|
||||
await waitForCurrentDemoMapSettled(page, 15000);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
await installZoomWrapper(page);
|
||||
await installCursor(page);
|
||||
|
||||
const ctx: SceneCtx = { page, cursor: { x: 200, y: 240 } };
|
||||
await page.mouse.move(ctx.cursor.x, ctx.cursor.y);
|
||||
await prepareAiBox(ctx);
|
||||
await sleep(80);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export async function runTimeline(ctx: SceneCtx): Promise<TimelineResult> {
|
||||
const sceneStartMs = Date.now();
|
||||
let mark = sceneStartMs;
|
||||
|
||||
mark = await runScene('AI close-up', mark, () => sceneAiCloseUp(ctx));
|
||||
mark = await runScene('Zoom out', mark, () => sceneZoomOutResults(ctx));
|
||||
mark = await runScene('TT slider', mark, () => sceneTravelTimeSlider(ctx));
|
||||
mark = await runScene('Cluster click', mark, () => sceneClusterClick(ctx));
|
||||
mark = await runScene('Export + outro', mark, () => sceneExportAndOutro(ctx));
|
||||
|
||||
return { sceneStartMs, sceneEndMs: mark };
|
||||
}
|
||||
|
||||
async function runScene(label: string, prev: number, scene: () => Promise<void>): Promise<number> {
|
||||
await scene();
|
||||
const now = Date.now();
|
||||
console.log(`[scene] ${label}: ${((now - prev) / 1000).toFixed(2)}s wall`);
|
||||
return now;
|
||||
}
|
||||
88
video/src/verify.ts
Normal file
88
video/src/verify.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, statSync } from 'node:fs';
|
||||
import { MAX_DURATION_S, MIN_DURATION_S, OUTPUT_FPS, OUTPUT_DIR, VIDEO_SIZE } from './config.js';
|
||||
|
||||
interface Probe {
|
||||
streams?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
avg_frame_rate?: string;
|
||||
r_frame_rate?: string;
|
||||
}[];
|
||||
format?: {
|
||||
duration?: string;
|
||||
size?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function fail(message: string): never {
|
||||
console.error(`[verify] FAIL: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function parseRate(rate: string | undefined): number {
|
||||
if (!rate) return 0;
|
||||
const [num, den] = rate.split('/').map(Number);
|
||||
if (!num || !den) return Number(rate) || 0;
|
||||
return num / den;
|
||||
}
|
||||
|
||||
function probe(path: string): Probe {
|
||||
const raw = execFileSync(
|
||||
'ffprobe',
|
||||
[
|
||||
'-v',
|
||||
'error',
|
||||
'-select_streams',
|
||||
'v:0',
|
||||
'-show_entries',
|
||||
'stream=width,height,r_frame_rate,avg_frame_rate',
|
||||
'-show_entries',
|
||||
'format=duration,size',
|
||||
'-of',
|
||||
'json',
|
||||
path,
|
||||
],
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
return JSON.parse(raw) as Probe;
|
||||
}
|
||||
|
||||
function verifyVideo(path: string) {
|
||||
if (!existsSync(path)) fail(`${path} is missing`);
|
||||
if (statSync(path).size === 0) fail(`${path} is empty`);
|
||||
|
||||
const data = probe(path);
|
||||
const stream = data.streams?.[0];
|
||||
if (!stream) fail(`${path} has no video stream`);
|
||||
|
||||
const duration = Number(data.format?.duration ?? 0);
|
||||
const fps = parseRate(stream.avg_frame_rate || stream.r_frame_rate);
|
||||
if (stream.width !== VIDEO_SIZE.width || stream.height !== VIDEO_SIZE.height) {
|
||||
fail(`${path} is ${stream.width}x${stream.height}, expected ${VIDEO_SIZE.width}x${VIDEO_SIZE.height}`);
|
||||
}
|
||||
if (duration < MIN_DURATION_S || duration > MAX_DURATION_S) {
|
||||
fail(
|
||||
`${path} duration is ${duration.toFixed(2)}s, expected ${MIN_DURATION_S}-${MAX_DURATION_S}s`
|
||||
);
|
||||
}
|
||||
if (Math.abs(fps - OUTPUT_FPS) > 0.1) {
|
||||
fail(`${path} is ${fps.toFixed(2)}fps, expected ${OUTPUT_FPS}fps`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[verify] ${path}: ${stream.width}x${stream.height}, ${duration.toFixed(2)}s, ${fps.toFixed(2)}fps`
|
||||
);
|
||||
}
|
||||
|
||||
function verifyImage(path: string) {
|
||||
if (!existsSync(path)) fail(`${path} is missing`);
|
||||
if (statSync(path).size === 0) fail(`${path} is empty`);
|
||||
console.log(`[verify] ${path}: ${statSync(path).size} bytes`);
|
||||
}
|
||||
|
||||
const videoPath = process.argv[2] ?? `${OUTPUT_DIR}/recording.mp4`;
|
||||
const posterPath = process.argv[3] ?? (process.argv[2] ? undefined : `${OUTPUT_DIR}/poster.jpg`);
|
||||
|
||||
verifyVideo(videoPath);
|
||||
if (posterPath) verifyImage(posterPath);
|
||||
58
video/src/video.ts
Normal file
58
video/src/video.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { execSync } from 'node:child_process';
|
||||
import { renameSync, statSync } from 'node:fs';
|
||||
import {
|
||||
MAX_DURATION_S,
|
||||
OUTPUT_FPS,
|
||||
RECORD_SCALE,
|
||||
VIDEO_SIZE,
|
||||
WEBM_BITRATE,
|
||||
} from './config.js';
|
||||
|
||||
const LEAD_IN_S = 0.12;
|
||||
|
||||
export function trimRecording(
|
||||
rawPath: string,
|
||||
trimmedPath: string,
|
||||
times: { recordStartMs: number; sceneStartMs: number; sceneEndMs: number }
|
||||
) {
|
||||
const sceneSpan = (times.sceneEndMs - times.sceneStartMs) / 1000;
|
||||
const trimStart = Math.max(
|
||||
0,
|
||||
(times.sceneStartMs - times.recordStartMs) / 1000 - LEAD_IN_S * RECORD_SCALE
|
||||
);
|
||||
const trimEnd = (times.sceneEndMs - times.recordStartMs) / 1000;
|
||||
const wallDuration = trimEnd - trimStart;
|
||||
const finalDuration = wallDuration / RECORD_SCALE;
|
||||
|
||||
if (finalDuration > MAX_DURATION_S) {
|
||||
console.log(
|
||||
`Scene output duration is ${finalDuration.toFixed(2)}s (guard ${MAX_DURATION_S.toFixed(2)}s); keeping the full take.`
|
||||
);
|
||||
}
|
||||
|
||||
const filter =
|
||||
`trim=start=${trimStart.toFixed(3)}:duration=${wallDuration.toFixed(3)},` +
|
||||
`setpts=(PTS-STARTPTS)/${RECORD_SCALE},fps=${OUTPUT_FPS},` +
|
||||
`trim=duration=${finalDuration.toFixed(3)},setpts=PTS-STARTPTS`;
|
||||
|
||||
// Keep trimming inside the filter graph: it is frame-accurate for WebM
|
||||
// without the keyframe leakage of input seeking or the post-speedup timing
|
||||
// ambiguity of output seeking.
|
||||
execSync(
|
||||
`ffmpeg -y -i "${rawPath}" -vf "${filter}" ` +
|
||||
`-fps_mode cfr -r ${OUTPUT_FPS} -c:v libvpx -b:v ${WEBM_BITRATE} -deadline good -cpu-used 5 ` +
|
||||
`"${trimmedPath}"`,
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
|
||||
try {
|
||||
statSync(rawPath);
|
||||
renameSync(rawPath, rawPath + '.untrimmed');
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Wrote ${trimmedPath} (${finalDuration.toFixed(2)}s, scene=${(sceneSpan / RECORD_SCALE).toFixed(2)}s, scale=${RECORD_SCALE}, capture=${VIDEO_SIZE.width}x${VIDEO_SIZE.height})`
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue