promote: dev → main (bookmarks fixes: SSR script tag, phantom save at cap) #123

Merged
admin_emi merged 2 commits from dev into main 2026-07-26 19:17:48 +00:00
3 changed files with 28 additions and 4 deletions

View file

@ -175,7 +175,10 @@
<p><a href="{{.Base}}/climate" data-event="home.nav_cities">Browse all cities →</a></p>
</section>
{{template "base_body_end" .}} <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script type="module" src="{{.AssetBase}}/app.js"></script>{{/* The records strip renders real temperatures server-side as .temp spans, so
<script type="module" src="{{.AssetBase}}/app.js"></script>{{/* Bookmarked locations: a star toggle in the results share row + a "Saved"
dropdown by the Find button. Loaded as its own module alongside app.js
(not merged into it) — see bookmarks-ui.js's header comment for why. */}}
<script type="module" src="{{.AssetBase}}/bookmarks-ui.js"></script>{{/* The records strip renders real temperatures server-side as .temp spans, so
it needs the same converter the SEO pages use or they'd stay in °F while
the toggle says °C. Both import units.js, which the module loader dedupes. */}}
<script type="module" src="{{.AssetBase}}/climate.js"></script>

View file

@ -111,6 +111,10 @@ function buildStar() {
try {
if (existing) await bookmarks.remove(existing.id);
else await bookmarks.add(loc.lat, loc.lon, currentLabel());
} catch (e) {
// A real server rejection (e.g. the per-user cap) — bookmarks.add() no
// longer fakes a local save for this, so say why nothing changed.
alert(e.message || "Couldn't save this location.");
} finally {
btn.disabled = false;
}

View file

@ -97,17 +97,34 @@ async function syncFromServer(preSyncLocal) {
notify();
}
/** Save (or re-save with a new label) the given spot. Upserts either way. */
/** Save (or re-save with a new label) the given spot. Upserts either way.
*
* Throws if signed in and the server explicitly rejects the request (e.g. the
* 409 per-user cap, a 422 on bad input) that is a real failure, not the
* "offline" case below, and must not be papered over with a local-only
* bookmark that doesn't actually exist server-side and would just vanish,
* unexplained, on the next sync. A genuine network failure (no response at
* all) still falls through to the local-only save so the star visibly
* sticks even offline. */
export async function add(lat, lon, label) {
const id = cellId(lat, lon);
if (signedIn) {
let res = null;
try {
const res = await apiFetch(uv("bookmarks"), { method: "POST", json: { lat, lon, label: label || undefined } });
res = await apiFetch(uv("bookmarks"), { method: "POST", json: { lat, lon, label: label || undefined } });
} catch (e) {
// offline / network failure: fall through to the local-only path below.
}
if (res) {
if (res.ok) {
const rows = await fetchServerList();
if (rows != null) { cache = rows.map(fromServerRow); writeLocal(cache); notify(); return find(lat, lon); }
} else {
let msg = `Couldn't save this location (${res.status}).`;
try { const d = await res.json(); if (typeof d.detail === "string") msg = d.detail; } catch (e) { /* no body */ }
throw new Error(msg);
}
} catch (e) { /* fall through so the star still visibly sticks, even offline */ }
}
}
let b = cache.find((x) => cellId(x.lat, x.lon) === id);
if (b) { if (label) b.label = label; }