From adf0b93a455ed131ff648112f3e2bc04566b83a8 Mon Sep 17 00:00:00 2001 From: Ovidiu U Date: Wed, 10 Jun 2026 11:08:55 +0100 Subject: [PATCH] Coarsen GPS coordinates to 3 dp before API/URL and deduplicate runSearch triggers - HeroSearch: round lat/lng to 3 decimal places (~111m precision) before emit to prevent exact location leakage in shareable URLs and server logs - Home: move duplicate-search guard into runSearch (single choke point) instead of watcher, eliminating race between route.query sync and direct onSearch call - Add inline documentation referencing .claude/rules/frontend.md privacy guidance --- resources/js/components/landing/HeroSearch.vue | 16 ++++++++++++++-- resources/js/views/Home.vue | 11 ++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/resources/js/components/landing/HeroSearch.vue b/resources/js/components/landing/HeroSearch.vue index 398d38e..028f937 100644 --- a/resources/js/components/landing/HeroSearch.vue +++ b/resources/js/components/landing/HeroSearch.vue @@ -58,6 +58,18 @@ const props = defineProps({ const emit = defineEmits(['search']) +// Coarsen GPS coordinates to ~111 m (3 dp) before they leave the browser. +// "Use my location" coords flow into the shareable URL, the /api/stations +// request, and server/access logs — full precision would broadcast the user's +// exact position to anyone they share the resulting link with. 3 dp is ample +// for a radius station search. See .claude/rules/frontend.md. +const COORDINATE_DECIMALS = 3 + +function coarsenCoordinate(value) { + const factor = 10 ** COORDINATE_DECIMALS + return Math.round(value * factor) / factor +} + const postcode = ref('') const locating = ref(false) @@ -88,8 +100,8 @@ function useMyLocation() { postcode.value = '' emit('search', { postcode: null, - lat: coords.latitude, - lng: coords.longitude, + lat: coarsenCoordinate(coords.latitude), + lng: coarsenCoordinate(coords.longitude), fuelType: props.fuelType, radius: props.radius, sort: props.sort, diff --git a/resources/js/views/Home.vue b/resources/js/views/Home.vue index fcf61de..3f0c6bc 100644 --- a/resources/js/views/Home.vue +++ b/resources/js/views/Home.vue @@ -625,6 +625,14 @@ function queryFromParams(params) { } async function runSearch(params) { + // Single dedup choke point. A user search calls onSearch, which both pushes to + // the URL (synchronously firing the route.query watcher → runSearch) and then + // calls runSearch directly — two triggers for one intent. Guarding here, where + // both paths funnel through, collapses them into one request for a given query. + if (lastParams.value + && JSON.stringify(queryFromParams(lastParams.value)) === JSON.stringify(queryFromParams(params))) { + return + } lastParams.value = params sort.value = params.sort ?? sort.value radiusMiles.value = params.radius ?? radiusMiles.value @@ -646,9 +654,6 @@ watch(() => route.query, (query) => { reset() return } - const sameAsLast = lastParams.value - && JSON.stringify(queryFromParams(lastParams.value)) === JSON.stringify(queryFromParams(params)) - if (sameAsLast) return runSearch(params) }, { immediate: true })