FINAL STEP (user-authorized): once every item below is done, AUTO-MERGE
claude/monument-display-optimize-x1j6leinto the deployed default branchclaude/repo-setup-speckit-3magw3so GitHub Pages ships it (no manual merge).TRANSLATION POLICY (user): do fr/ko translation AT THE END, not per feature. New features add en.ts keys + fr/ko placeholders (English, to keep the compile-time key-parity build green); a single final pass translates them all. Quality bar (user): keep it secure & done the right way — inert/validated data, sanitized text, no injection, tokens on-device only.
Running checklist for the current batch of requests. Kept so nothing is
forgotten. Grouped by area; each item links to where it lives. Push directly to
claude/monument-display-optimize-x1j6le.
- Reset all data button, gated behind a typed
RESETconfirmation (features/backup/Backup.tsx) - Journal: writing form sits directly under the daily button (export note moved below composer)
Decisions locked with the user:
-
Access: client-side passphrase encryption (AES; decrypts in-browser; works on GitHub Pages, no server)
-
Target: portable site bundle for any static host / git; "push to GitHub" is one optional connector (zero lock-in)
-
Scope: selectable — one trip / date range / tag, with preview; strip photo EXIF/GPS on export
-
Journey model: Trips drive the route (from→to legs + transport mode); stories + photos attach to each place
-
Write
specs/012-journal-publish/spec.md+plan.md(Constitution Check) -
Foundation: passphrase encryption (
lib/publish/encrypt.ts, AES-GCM+PBKDF2, tested) -
Foundation: journey-bundle assembler (
lib/publish/bundle.ts, trips-driven route + steps, tested) -
Reader UI (Polarsteps-style book — inline-SVG route map, transport legs, photo-led steps, paging, cover) — DONE
-
Self-contained HTML export + optional GitHub push + passphrase-gate variant — DONE
-
Docs for the host (HOSTING.md shipped in export + docs/publishing.md) — DONE
My take (WDYT): this fits the constitution perfectly (local-first, decentralized, no server, zero lock-in) and shares the "push to any git remote" seam with Publish mode, so it's high-value and not much extra surface. Recommendation:
-
Git mode first — the portable JSON lives in a git repo; each device pulls, merges at the record level (id-keyed upsert, newest-wins — the app already has non-destructive
mergeVisits), and pushes. Robust, offline-friendly, reuses the publish Sync button. The only hard part is merge/conflict handling → do it per-record, never whole-file, so two devices always converge. -
P2P (WebRTC / local network) later — direct phone↔laptop over a QR-paired data channel, no cloud at all. Great for the constitution but heavier (pairing, signaling); worth a second phase once git mode proves the merge model.
-
Spec
specs/013-device-sync/spec.md(git mode first, P2P as phase 2) -
Record-level convergent merge core (
lib/sync/merge.ts, newest-wins + tombstones, tested — deterministic/commutative/idempotent) -
Sync engine wiring (git pull→merge→push, conditional-push retry) + Sync UI, shared connector — DONE
-
updatedAton records + tombstones (schema v6, DB v4, backfilled) — DONE
- Auto-select the country from the coordinates (nearest gazetteer city,
countryAtPoint) so adding a place is just naming the spot — country pre-fills from a typed/dropped point, still editable, blank in open ocean. DONE.
- Reframe the published site from a paged "book (read once at the end)" into a LIVING BLOG: a scrollable, dated feed of entries you re-publish over time; masthead with "Last updated" + a "latest entry" jump link; per-post permalinks; keep the labeled-city map. Keep "book" as an alternate layout (Blog default). Self-contained, a11y, encryption preserved. (in progress)
- Name a trip (folder) + "By trip" publish scope (optional
nameon trips, TravelScreen field + folder grouping, PublishScreen selector, title from the name). DONE. - Editorial published site (paper/ink, serif+sans, cover) + a fitted map labeling EACH city (pins + names, curved route, compass/legend) — self-contained, zero external requests. DONE.
The whole app must feel instant/snappy on mobile — add-a-city is the sharpest case but the bar is EVERYWHERE (tab switches, list scroll, filters, opening a place, toggles).
- Add-a-city instant paint: instrument tap→flag-visible (Playwright perf assertion, mobile viewport); kill every lag source — IndexedDB write OFF the paint path, imperative flag repaint with NO full-app React re-render, cached marker image, don't rebuild the whole visited GeoJSON; loop-measure until synchronous; leave a regression perf test.
- App-wide mobile reactivity loop: profile the main interactions on a mobile viewport, find the jank (needless re-renders / missing memo & selectors, heavy synchronous work on the main thread, big list rendering, IndexedDB on the hot path, layout thrash, over-eager effects), fix, re-measure, repeat until snappy. Add perf guards where sensible. NOTE: the running map-perf agent owns MapView — sequence these right after it so they don't collide.
- The Settings ("parameters") page is too text-heavy / overwhelming (well written, but a lot to read). Make it concise + scannable: progressive disclosure (collapsible sections / "learn more"), shorter copy, clear grouping, sensible defaults hidden behind advanced. Keep all functionality; move the long explanations into help disclosures. (SettingsScreen.tsx, i18n, styles.css.)
- Bundle only the top ~10,000 cities (by population) in the app for instant first paint; the rest of the gazetteer (full ~135k) + monuments/airports/stations become downloadable + cached on demand, NOT precached. Fetch on first need (same-origin), cache (Cache API/IndexedDB), work offline once downloaded — like the offline tile "save area" model. Settings "data packs" UI (download / remove / sizes). Shrinks the install; honors local-first/offline. (split the gazetteer build into core-10k + rest; vite-plugin-pwa precache excludes the big sets; lib/reference loaders + gazetteerWorker fetch-and-cache seam; Settings.)
- General git support (push/pull to any git remote via the existing PublishTarget/sync seam —
GitHub is one impl; keep it vendor-neutral) AND a convenient GitHub Pages integration for
PUBLIC publishing: when you publish a public site, streamline create/use repo → push site → enable
Pages via the API (token with pages:write) → surface the live
username.github.io/repoURL. Builds on gitTarget + PublishScreen + the connector guide. Token stays on-device.
- Let anyone create + share a data pack, and add one by pasting a raw file URL or a GitHub link (auto-convert github.com/blob → raw). The app FETCHES (same seam as downloadable packs), VALIDATES + SANITIZES it (inert — Zod schema, never executed), CACHES it offline, MERGES it via the reference seam (supplemental gazetteer / stations / intra-city / moments / etc.), and shows its named source + license (provenance). A documented, versioned pack format (schema + a "how to make a pack" doc). Manage packs in Settings (list / update / remove / re-fetch), each labelled by origin. Convenience: GitHub-link handling, maybe a small curated index of known packs (opt-in, just links). Constitution: decentralized (any URL, no central server), local-first (cached, offline after), privacy (explicit action), inert + provenance-tagged. Builds on the downloadable-data cache seam.
- Android Auto Backup OFF — was leaking the sync token + journal to Google Drive. FIXED (allowBackup=false + extraction rules).
- Photos as Blobs in a dedicated IDB store (biggest reactivity win — toggles re-put ~2.4MB today). IN PROGRESS (priority-#1 agent).
- iOS Info.plist usage strings (NSCamera/NSPhotoLibrary) — file picker can crash / fail App Review without them. (doing now)
- CSP — add
<meta http-equiv=CSP>(covers native WebView) + createpublic/_headers(nosniff, referrer-policy, permissions-policy). ⚖️ - Vendor code-splitting (
manualChunks: maplibre+pmtiles, vendor) — stop re-downloading a 1.09MB chunk on every map edit. - Small thumbnail variant (~160px) for list/grid rows (they decode 1000px data URLs today) + lazy/async decode.
- ESLint (typescript-eslint + react-hooks + jsx-a11y) + lint CI step — no linter exists.
- useAutoSync/runSync unit tests — the sync glue (infinite-push-loop risk) is untested.
- Capacitor 6→7 (own PR: android/ + ios/) → unblocks targetSdk 34→35 (Play rejects 34).
- @capacitor/status-bar + splash-screen + keyboard + theme-aware
theme-color(the native-feel pass). - Lower the 256MB import ceiling + parse/validate off-thread; tsconfig
noUncheckedIndexedAccess/verbatimModuleSyntax; dep hygiene (@types/node ^22, drop world-atlas), off-thread image decode.
- Iterative review-and-optimize pass over the whole codebase (correctness, perf, simplification, dead code, consistency) — loop until clean. Pairs with the running upgrade-sweep + the mobile reactivity loop. Use /code-review + /simplify style passes per area; verify each round.
- Railway stations — add a "station" place type like airports: a real, openly-licensed dataset of the world's MAJOR train stations (Wikidata/OSM coords + QIDs, cited provenance — invent nothing; be conservative, only well-attested stations), a map layer + marker, list/search/stats integration, and its provenance rows. (schema PlaceRef kind, public/reference/stations.json + loader in lib/reference, MapView layer, PlacesScreen, provenance.)
- Remove the "+ Add place" button from the map controls (keep long-press/right-click + search add).
- List rows to ONE column (flag + name); move the date into a per-row "more" menu that also lets you set a folder etc. Implies folders on visits/places, surfaced via the row menu.
- BUG: zooming OUT doesn't show all flags on the first try — they load in late. Make the visited-flag / in-view marker repaint immediate on zoom-out (debounce/idle/recompute lag).
- Round out Capacitor for a native feel (status bar, safe-area insets, splash, haptics, keyboard handling) — verify config + platforms.
- BUG: long-press to add a place → the add-place modal is BROKEN on mobile. Fix the overlay/dialog sizing/positioning on small screens.
- BUG/UX: the Publish PREVIEW feels weird on mobile, and tapping a city in it "reopens the app" (the iframe escapes to top-navigation). Sandbox the preview iframe so links/taps stay INSIDE it (no allow-top-navigation), size it well on mobile (proper scroll / a phone-ish frame or a full-screen preview), and make city taps behave within the preview. (PublishScreen preview iframe.)
- Do all remaining backlog items, integrated (below).
- Launch a workflow on "whatever could be upgraded" → implement top picks.
- Map-data perf (from the read-only audit): gazetteer spatial index for the viewport query; geometry prime/low-res-seed + kill first-load double-fetch.
- Intra-city places (spec 014) — structure + loading seam + UI with graceful per-city availability; small provenance-cited sample where feasible (full dataset stays a follow-up).
- Tile audit #7 (no full remount on basemap/theme toggle) + #8 (robust offline first-paint).
- ShortcutsHelp.tsx i18n (interleaved — deferred earlier).
- Calendar view — Feed/Calendar toggle; a month grid tinting each entry-day by its dominant place's continent (alpha by count), with count badges + aria labels (not colour-only); tap a day to pin the feed to it, an empty day opens the composer pre-dated. Respects the place + year filters. DONE (accessible table, +tests).
- Journal folders — optional
folderon stories (additive, sanitized, schema v7→v8), a datalist combobox in the composer with proposed folders (existing + place/country/trip context), a "By folder" feed filter, folder tag on cards. DONE (+tests).
- Filter the map by DATE — a year chip filter (Any / each visited year / No date) driving visited markers + country shading + the in-view list AND the trip arcs from ONE shared selection (the useUi tripYear). DONE (+ mapDateFilter tests).
- Removed the map "Show list / Show map" toggle (+ listTall state + .list-tall CSS + dead i18n keys). DONE.
Goal: one-time setup, then it just works — auto fetch/pull/push, and data transferred so
there's no conflict (or as little as possible) with a remediation path for the rare case.
The conflict-free ENGINE already exists (lib/sync/merge.ts — record-level newest-wins +
tombstones, converges). Remaining is the UX:
- One-button setup (stored once on-device, connected/disconnected chip, Disconnect, Download-data-file for the no-token path). DONE.
- Opt-in auto push/pull (off by default; pull on launch/focus/visible/online, debounced push on edit/backgrounding; single in-flight lock, self-write loop guarded; manual Sync-now stays). DONE.
- Conflict-free transfer — reuses the record-level merge + conditional-push retry. DONE (pre-existing).
- Remediation — sync log (last 10 runs, per-store counts) + a safety guard that blocks a mass-deletion pull with an "apply anyway / skip" prompt (never silent loss) + distinct error codes. DONE.
- Token-free clarity + token guide + guided GitHub proposition:
- Make clear you DON'T need a token: "Download" the site/data and host/
git pushit yourself on any static host (GitHub Pages, Netlify, Nextcloud, USB) — zero lock-in. Token is only for the in-app convenience push. - Add an in-app GUIDE for the fine-grained PAT (how to create it, minimal scope: contents:write on the one repo, stays on-device, never in exports) inGitHubConnectorFields. - Make GitHub a recommended, walked-through option (create repo → token → push → your URL at username.github.io/repo). Optionally OAuth Device Flow later (needs a client_id — maintainer setup). — queued after the blog agent (shares PublishScreen / i18n).
- City name never cropped; on hover show full name (tooltip) — shared CityLine + picker/passport/airports
- Favorites-only mode — already a first-class view; unified the glyph on the heart (was amber star)
- Back-online: show a small "Reconnect" button when connection returns — do NOT auto-reconnect
- Show current mode (Offline / Online) explicitly somewhere
- Make it obvious how to add a non-existent spot on the map (custom place discoverability)
- Light / dark mode toggle (today dark only follows the OS; add an explicit switch)
- Show percentage of countries done, not only "3/50" (added to the compact strip)
- Make the Stats page much more ergonomic & UX-friendly — DONE via workflow (coverage hero, continent constellation, KPI strip, unified cards, by-country disclosure). 220 tests green.
- Order Moments by continent → country (grouping UI + grouping.ts) and expand the curated list 75 → 105 with real, provenance-cited experiences (Wikidata/GeoNames coords; UNESCO/Wikivoyage concepts). +15 tests. DONE.
- Spec
specs/014-intra-city-places/spec.md+ implementationtasks.md— DONE. - DEFERRED (documented follow-up): the actual per-city sub-place dataset (OSM metro
stations, admin districts, Wikidata) needs external data this environment's egress blocks;
inventing coordinates would violate Constitution I. The structure (spec + tasks.md runway)
is committed; the dataset + loading seam + UI are the follow-up. See
wiki/Roadmap.md.
- Add languages: French and Korean — DONE. Zero-dep i18n layer (typed en/fr/ko with compile-time key parity), useT hook, language switcher, locale-aware Intl formatting. +14 tests.
- Final translation pass (per "translate at the end") — DONE. Real fr/ko for every deferred placeholder (blog/map/sync/journal, ~132 values) + extracted & translated City/Country/Photo/ BoardingPass/Publish/About screens (~208 more values). 610 keys each, exact parity. Only ShortcutsHelp (interleaved ) + the "My travels" default-title sentinel left in English.
- Why is "Gili Meno" (Indonesia) missing? ANSWER: it's an upstream GeoNames data gap, not a Postcards cutoff. The gazetteer (135,233 rows via all-the-cities 3.1.0) applies NO population threshold and includes Gili Air (pop 1800) and Gili Trawangan (pop 1500) — but GeoNames never emitted a record for Gili Meno, so there's nothing upstream to include. Fixes: (a) add it as a custom place now (~ -8.349, 116.056, Indonesia); (b) durable = contribute it to GeoNames / ship a provenance-tagged supplemental gazetteer — never bake a world fact into app code.
- Answer: how to add a non-existing spot today — only via AddPlaceForm, mounted ONLY in the global search's "no results" branch (must type an exact zero-match name). Discoverability is poor → see UX item below.
- Improve add-a-spot discoverability: map long-press/right-click "+ Add here" (seeds coords), keyboard "+ Add place" control, search near-miss add-row. (AddPlaceForm, MapView, MapScreen, PlaceSearch)
- Write a full wiki (9 pages under
wiki/, written from the real code + a polish pass for accuracy/consistency/cross-links) — DONE via workflow.
- P1 spatial grid index for the gazetteer viewport query — replaces the per-pan linear scan of 135k rows with a 1°×1° grid (city/region zoom now visits a few cells).
- P2a prime geometry early — getCountries() fetch starts at map mount, overlapping init.
- Follow-ups from the audit (larger/build-step): P2b 110m land seed for instant coastline, P2c geometry in a worker, P3 don't gate first paint on all reference JSON, P4 columnar binary gazetteer (build step), P5 zero-copy worker handoff, P6 kill duplicate downloads, P7 pre-simplify country geometry. (Full ranked plan captured from the audit.)
- Tile loading workflow — desktop & mobile; tiles sometimes not loaded though on-screen (DONE: audit + fixes #1–6 shipped; #7 no-remount-on-toggle and #8 robust-offline-base deferred as follow-ups)
- Discovery + specs workflow — implementation maps for the UX/stats/i18n/moments batch + write specs 012/013 (running)
- Stats UX workflow — run after all the above is finished
- Wiki workflow — perfect the full project wiki (last)
Note: user asked to lean on workflows generously ("launch as many as you need, for every task").