fix(spots): editing a spot no longer drops its city/country tags

EditSpotPage rebuilds eventTags from scratch, so saving a spot replaced the
addressable event WITHOUT its city/country/country_code — silently losing the
region tags it had. It also meant re-saving a legacy spot could never fix a
missing tag.

Both edit pages (and both create pages) now go through the shared
reverseGeocodeRegions()/pushRegionTags() helper, so region tags are re-derived
on every save. Re-saving a legacy spot now tags it.

Docs: AGENTS.md (any page that republishes an addressable event must
re-derive or preserve its region tags).
This commit is contained in:
SkateSpots 2026-10-01 13:37:54 +07:00
parent 6d2492d847
commit cc4e1cd5ca
2 changed files with 19 additions and 2 deletions

View file

@ -867,14 +867,23 @@ session with **no other memory of this project**. So:
the stats can't know about reports without querying them). the stats can't know about reports without querying them).
- **Reverse geocoding hits Nominatim — disclosed (2026-10-01).** - **Reverse geocoding hits Nominatim — disclosed (2026-10-01).**
`src/lib/geocodeRegions.ts` (`reverseGeocodeRegions` / `pushRegionTags`) is the `src/lib/geocodeRegions.ts` (`reverseGeocodeRegions` / `pushRegionTags`) is the
**single source of truth**, used by **spot create, shop create and shop edit**; **single source of truth**, used by **spot create, spot edit, shop create and
it fills `city`/`country`/`country_code` via shop edit**; it fills `city`/`country`/`country_code` via
`nominatim.openstreetmap.org/reverse?lat=…&lon=…` (`Accept-Language: en`) and `nominatim.openstreetmap.org/reverse?lat=…&lon=…` (`Accept-Language: en`) and
**never throws** (a geocoder outage can't block a publish). Nominatim **sees the **never throws** (a geocoder outage can't block a publish). Nominatim **sees the
creator's IP** and was missing from `/privacy`'s "servers that see your IP" creator's IP** and was missing from `/privacy`'s "servers that see your IP"
list — now disclosed as "Spot/shop geocoding". Coordinates are the **pin you list — now disclosed as "Spot/shop geocoding". Coordinates are the **pin you
placed**, never the device's live GPS. ⚠️ Any new outbound call that receives placed**, never the device's live GPS. ⚠️ Any new outbound call that receives
coordinates must be added to `/privacy` in the same change. coordinates must be added to `/privacy` in the same change.
⚠️ **`EditSpotPage` used to DROP region tags (fixed 2026-10-01):** like
`EditShopPage` it rebuilds `eventTags` from scratch, so any `city`/`country`/
`country_code` the spot already had were silently lost on save (the new
addressable event replaced the old one without them). Both edit pages now
`reverseGeocodeRegions()` on save. **Any page that republishes an addressable
event must re-derive or preserve its region tags** — check this whenever a new
editor is added. Related: spots created before the tags existed stay
"Uncategorised" in `/stats` until they're re-saved (only the author can update
an addressable event).
- **NIP-05 names — `you@skate-spots.org` (2026-09-28):** SkateSpots hands out - **NIP-05 names — `you@skate-spots.org` (2026-09-28):** SkateSpots hands out
free handles so a new skater has a human name from minute one. The registry is free handles so a new skater has a human name from minute one. The registry is
the Netlify Function **`netlify/functions/nip05.mjs`** the Netlify Function **`netlify/functions/nip05.mjs`**

View file

@ -17,6 +17,7 @@ import { LoginArea } from '@/components/auth/LoginArea';
import { PhotoDropzone } from '@/components/PhotoDropzone'; import { PhotoDropzone } from '@/components/PhotoDropzone';
import { TagInput } from '@/components/TagInput'; import { TagInput } from '@/components/TagInput';
import { SKATE_SPOT_KIND } from '@/lib/types'; import { SKATE_SPOT_KIND } from '@/lib/types';
import { reverseGeocodeRegions, pushRegionTags } from '@/lib/geocodeRegions';
import { spotCategories, MAX_TAGS, shelterOptions, DEFAULT_SHELTER, SHELTERED_VALUES, type ShelterType } from '@/lib/skateTags'; import { spotCategories, MAX_TAGS, shelterOptions, DEFAULT_SHELTER, SHELTERED_VALUES, type ShelterType } from '@/lib/skateTags';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
@ -232,6 +233,12 @@ export default function EditSpotPage() {
// Generate geohash from coordinates (7 chars = ~153m precision) // Generate geohash from coordinates (7 chars = ~153m precision)
const geohash = ngeohash.encode(lat, lng, 7); const geohash = ngeohash.encode(lat, lng, 7);
// Best-effort reverse geocode → city/country/country_code. Re-run on edit
// so moving a spot updates its region, AND so a legacy spot that has no
// tags gets them the next time it's saved. (This page rebuilds tags from
// scratch, so without this it silently DROPPED any existing region tags.)
const geo = await reverseGeocodeRegions(lat, lng);
// Build event tags - use the same identifier to update the addressable event // Build event tags - use the same identifier to update the addressable event
const eventTags: string[][] = [ const eventTags: string[][] = [
['d', spot.identifier], ['d', spot.identifier],
@ -243,6 +250,7 @@ export default function EditSpotPage() {
['lon', lng.toString()], ['lon', lng.toString()],
['alt', `Skate spot: ${title.trim()}`], ['alt', `Skate spot: ${title.trim()}`],
]; ];
pushRegionTags(eventTags, geo);
if (location.trim()) { if (location.trim()) {
eventTags.push(['location', location.trim()]); eventTags.push(['location', location.trim()]);