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
This commit is contained in:
@@ -58,6 +58,18 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['search'])
|
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 postcode = ref('')
|
||||||
const locating = ref(false)
|
const locating = ref(false)
|
||||||
|
|
||||||
@@ -88,8 +100,8 @@ function useMyLocation() {
|
|||||||
postcode.value = ''
|
postcode.value = ''
|
||||||
emit('search', {
|
emit('search', {
|
||||||
postcode: null,
|
postcode: null,
|
||||||
lat: coords.latitude,
|
lat: coarsenCoordinate(coords.latitude),
|
||||||
lng: coords.longitude,
|
lng: coarsenCoordinate(coords.longitude),
|
||||||
fuelType: props.fuelType,
|
fuelType: props.fuelType,
|
||||||
radius: props.radius,
|
radius: props.radius,
|
||||||
sort: props.sort,
|
sort: props.sort,
|
||||||
|
|||||||
@@ -625,6 +625,14 @@ function queryFromParams(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runSearch(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
|
lastParams.value = params
|
||||||
sort.value = params.sort ?? sort.value
|
sort.value = params.sort ?? sort.value
|
||||||
radiusMiles.value = params.radius ?? radiusMiles.value
|
radiusMiles.value = params.radius ?? radiusMiles.value
|
||||||
@@ -646,9 +654,6 @@ watch(() => route.query, (query) => {
|
|||||||
reset()
|
reset()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const sameAsLast = lastParams.value
|
|
||||||
&& JSON.stringify(queryFromParams(lastParams.value)) === JSON.stringify(queryFromParams(params))
|
|
||||||
if (sameAsLast) return
|
|
||||||
runSearch(params)
|
runSearch(params)
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user