Location: say why it failed, retry once, show permission status
Publish Container / publish (push) Successful in 2m15s

The ⌖ button reported 'Location access denied' for every failure, including timeouts and
no-GPS-fix errors that have nothing to do with permission.

- Specific messages for denied / no fix / timeout, with the exact iPhone steps (Safari tab vs
  installed web app) and the error code for diagnosis
- One patient high-accuracy retry for slow or cold GPS fixes (a real denial is not retried)
- Live permission status line under the search row (allowed / not asked / blocked)
- Mentions Safari content blockers as a possible cause

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nate EmmertandClaude Sonnet 5.5 committed 2026-10-15 15:07:19 -04:00
1 parent 0cbebfef2a
commit eaff2f55c8
4 files changed
+83 -8

No files matched your search

+6
View File
@@ -324,3 +324,9 @@ body.map-fs #tab-unified-map { padding: 0; }
.alert-full p { margin: 0 0 0.6rem; } .alert-full p { margin: 0 0 0.6rem; }
.alert-pre { white-space: pre-wrap; } .alert-pre { white-space: pre-wrap; }
.alert-meta { font-size: 0.74rem; color: var(--ink-light); } .alert-meta { font-size: 0.74rem; color: var(--ink-light); }
/* Location permission status line */
.geo-status { font-size: 0.8rem; line-height: 1.45; color: var(--ink-light); margin: 0.5rem 0.15rem 0; padding: 0.5rem 0.75rem; border-radius: 12px; background: var(--paper-darker); }
.geo-status.hidden { display: none; }
.geo-status.ok { color: #22c55e; }
.geo-status.warn { color: var(--ink); background: color-mix(in srgb, #ef4444 14%, var(--paper-darker)); border: 1px solid color-mix(in srgb, #ef4444 45%, transparent); }
+31
View File
@@ -405,6 +405,36 @@ function centerMap(city, force) {
$('map-recenter-btn')?.addEventListener('click', () => { if (state.currentCity) centerMap(state.currentCity, true); }); $('map-recenter-btn')?.addEventListener('click', () => { if (state.currentCity) centerMap(state.currentCity, true); });
})(); })();
// ── Location permission status (so "blocked" is visible, not guessed) ────────
const GeoStatus = (() => {
const el = $('geo-status');
let status = null;
async function render() {
if (!el || !navigator.permissions || !navigator.geolocation) return;
try {
if (!status) {
status = await navigator.permissions.query({ name: 'geolocation' });
status.addEventListener('change', render);
}
} catch { return; }
const s = status.state;
const ios = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const standalone = window.matchMedia('(display-mode: standalone)').matches || navigator.standalone === true;
if (s === 'granted') { el.className = 'geo-status ok'; el.textContent = '📍 Location: allowed. Tap ⌖ to use it.'; }
else if (s === 'prompt') { el.className = 'geo-status'; el.textContent = '📍 Location: not asked yet. Tap ⌖ and choose Allow when your phone asks.'; }
else if (s === 'denied') {
el.className = 'geo-status warn';
el.textContent = ios
? `📍 Location: your phone has this ${standalone ? 'app' : 'site'} set to Don't Allow. Fix it in Settings → Privacy & Security → Location Services → ${standalone ? '"NeCloud Weather"' : 'Safari Websites'}. A Safari content blocker can also do this: try aA → Turn Off Content Blockers.`
: '📍 Location: blocked for this site. Click the lock icon in the address bar → Location → Allow.';
} else { el.classList.add('hidden'); return; }
el.classList.remove('hidden');
}
render();
return { refresh: render };
})();
// ── Public hooks ───────────────────────────────────────────────────────────── // ── Public hooks ─────────────────────────────────────────────────────────────
let lastOutlook = null; let lastOutlook = null;
window.AppUI = { window.AppUI = {
@@ -427,6 +457,7 @@ window.AppUI = {
if (stationFresh) renderStationContext(); if (stationFresh) renderStationContext();
}, },
centerMap, centerMap,
refreshGeoStatus: () => GeoStatus.refresh(),
onAir(air) { renderAqi(air); }, onAir(air) { renderAqi(air); },
refreshFx() { const c = $('sky-fx'); if (c && fx.kind) { cancelAnimationFrame(fx.raf); fxResize(c); fxSeed(fx.kind); if (fx.parts.length) fxFrame(performance.now()); } }, refreshFx() { const c = $('sky-fx'); if (c && fx.kind) { cancelAnimationFrame(fx.raf); fxResize(c); fxSeed(fx.kind); if (fx.parts.length) fxFrame(performance.now()); } },
onStation() { syncTopbarLocation(); renderStationContext(); }, onStation() { syncTopbarLocation(); renderStationContext(); },
+45 -8
View File
@@ -2273,26 +2273,63 @@ searchBtn.addEventListener('click', doSearch);
searchInput.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); }); searchInput.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
// ── Geolocation ─────────────────────────────────────────────────────────────── // ── Geolocation ───────────────────────────────────────────────────────────────
locateBtn.addEventListener('click', () => { // Why a location request failed, in plain language (the browser tells us: 1 denied, 2 unavailable, 3 timeout)
const IS_IOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const IS_STANDALONE = window.matchMedia('(display-mode: standalone)').matches || navigator.standalone === true;
async function geoPermissionState() {
try { return (await navigator.permissions.query({ name: 'geolocation' })).state; } catch { return 'unknown'; }
}
function geoErrorHelp(err, perm) {
const code = err && err.code;
const tag = ` (error ${code ?? '?'}${perm && perm !== 'unknown' ? ', permission: ' + perm : ''})`;
if (code === 1) {
if (IS_IOS) {
const where = IS_STANDALONE
? 'Settings → Privacy & Security → Location Services → "NeCloud Weather" → While Using the App'
: 'Settings → Privacy & Security → Location Services → Safari Websites → While Using, then in Safari tap aA → Website Settings → Location → Allow';
return `Your phone says location is off for ${IS_STANDALONE ? 'this app' : 'this site'}. Turn it on in ${where}. If you use a Safari content blocker, also try aA → Turn Off Content Blockers for this site.${tag}`;
}
return `Location is blocked for this site. Click the lock icon in the address bar → Location → Allow, then try again.${tag}`;
}
if (code === 2) return `Your device couldn't find your position yet (no GPS or Wi-Fi fix). Make sure Location Services is on, turn off Low Power Mode, and try again, ideally on Wi-Fi or outdoors.${tag}`;
if (code === 3) return `Timed out waiting for your location. Try again in a moment.${tag}`;
return `Could not get your location.${tag}`;
}
function geoOnce(opts) {
return new Promise((resolve, reject) => navigator.geolocation.getCurrentPosition(resolve, reject, opts));
}
// A slow or cold GPS often times out the first time: retry once with a patient, high-accuracy request.
// A real "denied" is final, so don't retry that.
async function getPositionRobust() {
try {
return await geoOnce({ enableHighAccuracy: false, timeout: 12000, maximumAge: 5 * 60 * 1000 });
} catch (e) {
if (e && e.code === 1) throw e;
return await geoOnce({ enableHighAccuracy: true, timeout: 20000, maximumAge: 0 });
}
}
locateBtn.addEventListener('click', async () => {
if (!navigator.geolocation) { showErr('Geolocation not supported.'); return; } if (!navigator.geolocation) { showErr('Geolocation not supported.'); return; }
clearErr(); clearErr();
hide(weatherDiv); hide(weatherDiv);
show(loadingEl); show(loadingEl);
try {
navigator.geolocation.getCurrentPosition(async pos => { const pos = await getPositionRobust();
const { latitude: lat, longitude: lon } = pos.coords; const { latitude: lat, longitude: lon } = pos.coords;
try { try {
const city = await buildDetectedCity(lat, lon); const city = await buildDetectedCity(lat, lon);
await loadAll(city, 'gps-button'); await loadAll(city, 'gps-button');
} catch { } catch {
showErr('Could not fetch weather for your location.'); showErr('Could not fetch weather for your location.');
} finally {
hide(loadingEl);
} }
}, () => { } catch (err) {
if (state.currentWeather) show(weatherDiv);
showErr(geoErrorHelp(err, await geoPermissionState()));
} finally {
hide(loadingEl); hide(loadingEl);
showErr('Location access denied.'); if (window.AppUI && window.AppUI.refreshGeoStatus) window.AppUI.refreshGeoStatus();
}); }
}); });
unitsBtn.addEventListener('click', () => { unitsBtn.addEventListener('click', () => {
+1
View File
@@ -449,6 +449,7 @@
<button id="units-btn" title="Toggle units">US</button> <button id="units-btn" title="Toggle units">US</button>
</div> </div>
<ul id="suggestions" class="suggestions hidden"></ul> <ul id="suggestions" class="suggestions hidden"></ul>
<p id="geo-status" class="geo-status hidden" role="status"></p>
<p id="error-msg" class="error hidden"></p> <p id="error-msg" class="error hidden"></p>
</div> </div>