// Approximate-location fallback. // // The only reason this exists: a visitor who declines (or cannot use) browser // geolocation currently hits a dead end — search or the map picker, or nothing. // This asks the backend for a coarse guess derived from the request's own IP, // and offers it as a *suggestion*. // // Three rules the UI must never break, because the accuracy gap is enormous // (browser geolocation is metres; a city-level IP database is tens of // kilometres, against a ~2 mile grid cell): // // 1. Say it is a guess, in the visitor's own words, right next to the result. // Never present it as "your location". // 2. Put the correction one obvious tap away, permanently visible — not // hidden behind a dismissed toast. // 3. Never persist it. An explicit choice is remembered (localStorage) and // written into the URL hash; a guess is neither. A guess that became // sticky would be indistinguishable from a choice on the next visit, and // a guessed URL would propagate someone else's rough location when shared. // // The endpoint answers 204 for every "no" — feature off, no database, private // or unparsable address, no match, match too coarse — so there is exactly one // branch here and it is "behave exactly as the site does today". import { uv } from "./account.js"; import { track } from "./digest.js"; import { esc } from "./shared.js"; /** * Ask the backend for an approximate location. * Resolves to the suggestion object, or null when there is nothing to suggest. * Never rejects: a failed fetch is just "no suggestion". */ export async function fetchApprox() { try { const res = await fetch(uv("geoip"), { headers: { Accept: "application/json" } }); if (res.status !== 200) return null; // 204 is the normal "no" const data = await res.json(); if (!data || !data.approximate) return null; // never trust an unlabelled payload if (typeof data.lat !== "number" || typeof data.lon !== "number") return null; return data; } catch (e) { return null; } } function attributionHtml(a) { // Both candidate databases are free *with attribution* and both ask for the // credit on the page that displays a result — so it rides with the result // rather than living only in a footer that this view doesn't have. if (!a || !a.text) return ""; const text = esc(a.text); return a.url ? ` ${text}` : ` ${text}`; } /** * Render the suggestion banner into `host` and load the guessed place. * * @param host container element (hidden until there is something to show) * @param data a suggestion from fetchApprox() * @param onFix called when the visitor corrects the guess */ export function renderApprox(host, data, onFix) { if (!host) return; host.innerHTML = `

Showing weather near ${esc(data.label)}.

` + `

That is a rough guess from your internet connection, ` + `not from your device — it can be tens of kilometres out.` + attributionHtml(data.attribution) + `

` + ``; host.hidden = false; host.querySelector(".geo-approx-fix")?.addEventListener("click", () => { track("home.geoip_fixed"); onFix?.(); }); track("home.geoip_shown"); } export function clearApprox(host) { if (!host) return; host.hidden = true; host.innerHTML = ""; }