Gjør romnr til autoritativ tilfluktsrom-ID (Room-PK og IndexedDB-key)

Produktbeslutning (Forgejo #15): romnr — DSBs romnummer — er den
autoritative IDen. lokalId regenereres av Geonorge ved hver eksport og
beholdes bare som informasjonsfelt; det skal aldri sammenlignes eller
nøkles på. Utredningen mot Geonorge/DSB droppes.

Dette retter også en reell feil på Android: etter «Oppdater data» holdt
selectedShelter det gamle objektet med utdatert lokalId, så
rebuildShelterList() la til en duplikatrad med «utenfor nærområdet»-
badge for et rom som var blant de nærmeste, og markørhøylysingen mistet
målet (markørkartet var nøklet på nye lokalId-er).

Android:
- Shelter: @PrimaryKey romnr; lokalId vanlig kolonne
- ShelterDatabase: versjon 2 + fallbackToDestructiveMigration() — tabellen
  er ren cache som reseedes fra asset når den er tom
- Parsere (GeoJSON + bundled): krever romnr > 0, lokalId valgfri
- MainActivity: markørkart Map<Int, Marker>, høylysing og
  isSelectedAmongNearest/indexOfFirst på romnr
- ShelterListAdapter: DiffUtil areItemsTheSame på romnr

PWA:
- shelter-db.ts: DB_VERSION 2, keyPath 'romnr', upgrade sletter og
  gjenoppretter store ved oldVersion < 2
- map-view.ts: _shelterRomnr i stedet for _shelterLokalId
- types.ts: dokumentert identitet; fetch-shelters.ts hopper over
  features uten romnr (speiler Android-parseren)

ARCHITECTURE.md: nytt avsnitt «Shelter identity — romnr is authoritative»,
felt-tabell, valideringsregel og IndexedDB-skjema oppdatert.

Verifisert: emulator med v1.10.2-DB → migrering til v2 + reseed (556 rom);
velg rad 2 → Oppdater → ingen duplikatrad, valg og høylysing beholdt.
Playwright: seedet v1-IndexedDB → app oppgraderer til v2 (keyPath romnr,
556 poster, get(1681) treffer), dyplenke og markør OK.

Forgejo: #15

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FrDGDmN2WAXvNPS85rWJGK
This commit is contained in:
Ole-Morten Duesund 2026-08-17 13:30:25 +02:00
commit c3ab73c915
13 changed files with 108 additions and 54 deletions

View file

@ -67,8 +67,8 @@ Geonorge ZIP (EPSG:25833 UTM33N)
| Field | Description |
|------------|--------------------------------------|
| `lokalId` | Unique identifier (UUID) |
| `romnr` | Shelter room number |
| `romnr` | Shelter room number — **authoritative ID / primary key** (see [Shelter identity](#shelter-identity--romnr-is-authoritative)) |
| `lokalId` | Upstream UUID, re-rolled on every export — informational only |
| `plasser` | Capacity (number of people) |
| `adresse` | Street address |
| `latitude` | WGS84 latitude (converted from UTM) |
@ -135,7 +135,7 @@ OkHttp response stream
→ Per feature:
→ Extract UTM33N coordinates
→ CoordinateConverter.utm33nToWgs84()
→ Validate: lokalId not blank, plasser ≥ 0, within Norway bounds
→ Validate: romnr > 0, plasser ≥ 0, within Norway bounds
→ Create Shelter entity (skip malformed features with warning)
```
@ -207,19 +207,21 @@ The domain is configured in one place: `DEEP_LINK_DOMAIN` in `build.gradle.kts`
Share messages include the HTTPS URL, which SMS apps auto-link as a tappable URL.
#### Deep link identifier — why `romnr`, not `lokalId`
#### Shelter identity — `romnr` is authoritative
The path component is the shelter's `romnr` (DSB room number — an integer like `776`), not the GeoJSON `lokalId` UUID, even though `lokalId` is what Room and Leaflet use as the in-memory primary key.
**Decision (2026-08-17, Forgejo #15):** `romnr` — DSB's room number — is the authoritative shelter identifier everywhere: Room primary key, IndexedDB `keyPath`, marker maps, list diffing, selection equality and the deep-link path. `lokalId` is stored as informational data only and must never be compared or keyed on. Records without a positive `romnr` are skipped by both parsers.
The deep-link path component is therefore `romnr` (an integer like `776`), not the GeoJSON `lokalId` UUID.
**Empirical reason:** the upstream GeoJSON ZIP at `nedlasting.geonorge.no/.../TilfluktsromOffentlige_GeoJSON.zip` re-rolls every `lokalId` on every export. Three snapshots of the same dataset (taken Dec 2025, Apr 20 2026, Apr 27 2026) had **556/556 different lokalIds** while every other field (`romnr`, `adresse`, `plasser`, `latitude`, `longitude`) was byte-identical and the shelter count was unchanged. The most recent two snapshots are only seven days apart, so this is regular drift, not a one-off re-issue.
That makes `lokalId` unsuitable for any cross-device or cross-build identifier:
- Sender and receiver who fetched the dataset on different days have different lokalIds for the same physical shelter, so a lokalId-keyed share link fails with "shelter not found" on the receiving side.
- Even on a single device, a user who hits "Refresh data" while a shelter is selected would lose the selection if it was tracked by lokalId.
- Even on a single device, a user who hits "Refresh data" while a shelter is selected keeps the *object* but its `lokalId` no longer matches anything in the new dataset — before the switch this produced a duplicate "outside nearest" list row and a lost marker highlight on Android.
`romnr` is the actual DSB business key — the room number physically assigned to the shelter by the civil-defence authority — and is stable across exports. Verified unique (556/556) and present (no zeros) on the current dataset.
The internal Room primary key remains `lokalId` because (a) it's already the upstream-supplied UUID and changing it would force a destructive Room schema migration, and (b) within a single fetch it's a perfectly fine in-memory key. Only the *external* deep-link identifier was switched.
Room schema v2 and IndexedDB v2 switched the primary key to `romnr` (destructive migration in both — the tables are pure caches re-seeded from the bundled asset / `data/shelters.json` when empty). The weekly romnr-stability routine (`scripts/romnr-baseline.json`) remains the safety net: it diffs each Geonorge export against the baseline and flags any romnr that disappears, appears or moves.
---
@ -292,7 +294,7 @@ bun run build # TypeScript check + Vite build + SW generation
**Storage:** IndexedDB via the `idb` library.
**Schema:**
- Object store `shelters` (keyPath: `lokalId`) — full shelter records
- Object store `shelters` (keyPath: `romnr`, DB v2) — full shelter records
- Object store `metadata``lastUpdate` timestamp
**Loading strategy:**