Commit graph

706 commits

Author SHA1 Message Date
Ullrich Schäfer
5905cdadea
Apply public-content-visibility: visibility flag + public profile
Implements the public-content-visibility OpenSpec change. Adds the
smallest social surface that lets us demo the product to logged-out
visitors without user signup.

Schema:
- `visibility text NOT NULL DEFAULT 'private'` on routes + activities.
- Shared Visibility type exported from the schema module.

Access:
- New canView(content, viewer, { asDirectLink }) helper in auth.server.ts
  centralises the rule: public → anyone; unlisted → anyone on direct
  link; private → owner only.
- routes.$id and activities.$id loaders return 404 (not 403) when
  canView rejects, so existence of private content isn't leaked.
- Detail pages emit Open Graph + Twitter Card meta on public/unlisted
  content only.

Editing:
- Visibility <select> on routes/:id/edit with owner-only access.
- Activity detail page gets a small visibility form + set-visibility
  action intent (no separate activity-edit page needed).
- EN + DE i18n under routes.visibility.* and activities.visibility.*.

Listings:
- Listing helpers listPublicRoutesForOwner / listPublicActivitiesForOwner
  for cross-user queries. Existing owner-scoped listRoutes/listActivities
  stay — owners see their own content regardless of visibility.

Public profile:
- /users/:username is now truly public. Renders the user's public
  routes + activities, 404s when no public content exists AND viewer
  isn't the owner (prevents account enumeration).
- Owner sees a short "this is your profile" note linking to settings.
- Open Graph meta (og:type=profile) for shareable preview.

Privacy manifest:
- Added a bullet noting public content is world-visible on profile
  and indexable by search engines.
- Bumped PRIVACY_LAST_UPDATED to 2026-04-20 + rendered legal-archive
  snapshot.

Tests:
- 13 unit tests for canView covering the full matrix.
- 6 E2E tests in e2e/public-content.test.ts covering:
  - Private route → 404 for logged-out visitor
  - Public route → reachable + OG tags present (og:title, og:type,
    og:site_name)
  - Owner still sees own private content
  - Profile 404 when no public content
  - Profile renders when at least one public route exists
  - Unlisted route reachable via direct URL but hidden from profile
- Test file runs serially (describe.configure mode=serial) to avoid
  WebAuthn virtual-authenticator races under Playwright's default
  parallel workers.
- New public-content Playwright project added to config.

Rollout safety: every existing row in prod keeps visibility='private'
by default — nothing becomes visible to outsiders until an owner
explicitly opts in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 09:11:39 +02:00
Ullrich Schäfer
74dca52ecc
Merge pull request #255 from trails-cool/spec/social-mvp
Propose social MVP: public-content-visibility + demo-activity-bot
2026-04-19 08:50:58 +02:00
Ullrich Schäfer
dcbd703f47
Propose social MVP: public-content-visibility + demo-activity-bot
Two stacked OpenSpec change proposals for a demoable social layer,
with scope deliberately minimal ("enough to send a URL and have it
open to real-looking content without signup"):

1. public-content-visibility
   - Adds visibility enum {private, unlisted, public} on routes +
     activities, defaulting to private for every existing row.
   - Detail pages (/routes/:id, /activities/:id) become accessible to
     logged-out visitors when content is public or unlisted; private
     → 404 (not 403) to avoid existence leaks.
   - Broadens /users/:username into a public profile listing only
     public routes + activities; 404s when there's no public content
     to prevent account enumeration.
   - Open Graph / Twitter Card meta on public detail + profile pages.
   - Visibility selector in the owner's edit flow.
   - Out of scope: follow/follower, cross-user feed, reactions,
     federation.

2. demo-activity-bot  (depends on #1)
   - A single bot user, Bruno the trail dog, seeded on worker startup
     when DEMO_BOT_ENABLED=true. Reserved username, sentinel email,
     no credentials.
   - pg-boss recurring job fires every 90 min, decides-to-walk with
     p=0.12 during 07:00-21:00 local, yielding ~2-3 walks/day at
     organic times.
   - Each walk: random start + end within inner-Berlin bbox, trekking
     only (dogs don't ride bikes), 2-12 km crow. BRouter plans the
     route; the route GPX is also attached as the activity's trace.
   - Everything inserted with visibility=public, synthetic=true.
   - Daily prune deletes synthetic rows > DEMO_BOT_RETENTION_DAYS old
     (default 14). Hard cap of 40 items/14d protects against runaway
     growth.
   - Small "🐕 demo account" badge on /users/bruno for honesty.

Both changes are artifacts only — no code lands with this PR. Apply
in order after merge: public-content-visibility first, then
demo-activity-bot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:49:59 +02:00
Ullrich Schäfer
94ce350565
Merge pull request #254 from trails-cool/docs/park-mobile-specs
Park mobile-activity-recording + mobile-nearby-sync under docs/ideas
2026-04-19 08:24:18 +02:00
Ullrich Schäfer
621d91e14f
Park mobile-activity-recording + mobile-nearby-sync under docs/ideas
Both are blocked on the mobile-app foundation and on a user base that
actually records activities or needs offline peer sync. With 3 users
and 0 activities on prod today, these are engineering for a
hypothetical population.

Move the artifact sets out of openspec/changes/ (so openspec list
stays clean) and under docs/ideas/ where self-host-overpass already
lives. Each gets a short README documenting:

- current parked status
- revive triggers (when should we look at this again?)
- key constraints not to rediscover (background-GPS privacy review;
  BLE dev-build requirement; QR-only subset as a cheaper path)

Reviving is `git mv docs/ideas/<name> openspec/changes/<name>` and
running /opsx:apply, same pattern as self-host-overpass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:23:44 +02:00
Ullrich Schäfer
29cbcef844
Merge pull request #253 from trails-cool/feat/route-empty-state-cta
Show empty-state CTA on Route detail when no waypoints
2026-04-19 08:21:17 +02:00
Ullrich Schäfer
5b40bd9b00
Show empty-state CTA on Route detail page when no waypoints
Observed: both non-maintainer users (pistazie, nelli) created a route,
never planned in it, and stopped. The Route detail page rendered as a
dead end — name, a row of buttons, then blank.

Changes:
- Detect "empty" as no geometry AND no distance (the state every new
  route has before any planning happens).
- When empty, hide the top-row button cluster (Edit in Planner / Edit
  / Export GPX) and render a centered dashed-border card in its place.
- Card shows: large headline, short body (owner-specific vs
  viewer-only copy), primary "Open in Planner" button (same action as
  the existing top-row button, just visually prominent), and a
  secondary "or upload a GPX file" link to the edit page.
- Bilingual copy added under routes.empty.*.

No non-empty route loses any UI — those paths stay identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:13:24 +02:00
Ullrich Schäfer
1158080b26
Merge pull request #252 from trails-cool/feat/reaccept-terms
Prompt users with stale terms_version to re-accept
2026-04-19 08:09:34 +02:00
Ullrich Schäfer
f16e80a2eb
Prompt users with stale terms_version to re-accept
The three pre-legal-disclaimer users (ullrich, pistazie, nelli) have
NULL terms_version, and any future Terms update would leave every
existing user in the same state. Close the loop now that we have
version storage by redirecting any logged-in user whose
users.terms_version doesn't match the currently-published
TERMS_VERSION to a dedicated acceptance page.

Changes:
- auth.server: new recordTermsAcceptance(userId, version) helper that
  writes both terms_accepted_at and terms_version.
- root loader: if the session user has a stale or NULL terms_version,
  throw redirect("/auth/accept-terms?returnTo=<pathname>") unless the
  request is already on an allow-listed path
  (/auth/accept-terms, /auth/logout, /legal/*) so Terms are reachable
  and logout works.
- New route /auth/accept-terms (GET renders the prompt, POST records
  acceptance and bounces to a sanitised returnTo). Same-origin check
  on returnTo to avoid open-redirect abuse. Logout button is provided
  as an escape hatch.
- i18n: new auth.reaccept.* keys for EN and DE.
- Spec: new Requirement + five scenarios (redirect, allow-list,
  successful re-accept, missing consent, returnTo sanitisation).

No action on the three legacy users is required beyond what they'll
experience on their next visit — the gate takes care of it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:06:16 +02:00
Ullrich Schäfer
18fc023cb6
Merge pull request #251 from trails-cool/docs/legal-archive
Add docs/legal-archive + scripts/render-legal.ts
2026-04-19 07:59:44 +02:00
Ullrich Schäfer
374102e633
Make scripts/ a pnpm workspace + add README
render-legal.ts needs @types/node available to the TS language server
so editors (VS Code, cursor, etc.) don't complain about missing Node
globals. Without a tsconfig in this folder VS Code falls back to a
default config with no types and shows red squiggles on every
process/fs/path reference.

Solution: scripts/ becomes its own pnpm workspace (@trails-cool/scripts),
carrying @types/node as a devDep from the catalog, and a tiny
tsconfig.json with types: [node] and include *.ts. This also wires the
folder into `pnpm typecheck` as task #14, so CI type-checks scripts
alongside every other package. No runtime impact — the workspace is
never bundled or deployed.

README documents the layout, current contents (render-legal.ts +
check-dockerfiles.sh), and how to add new scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 07:56:01 +02:00
Ullrich Schäfer
2abc207c2f
Convert render-legal to TypeScript
The repo is a Node/TS stack, so a Python renderer is out of place.
Port the script to TypeScript, runnable via
`node --experimental-strip-types scripts/render-legal.ts <doc>`, and
update the README to match.

Verified byte-identical output against the Python version before
removing it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 07:52:53 +02:00
Ullrich Schäfer
d52828543a
Seed docs/legal-archive with Terms / Privacy / Imprint snapshots
Adds a simple legal-archive folder that preserves the full text of the
Terms, Privacy Policy, and Impressum at each version, plus a small
renderer script so future snapshots are mechanical:

    python3 scripts/render-legal.py terms > docs/legal-archive/terms-YYYY-MM-DD.md

Why:
- Terms are per-user contract. users.terms_version maps each user to a
  file in this folder so we can always tell what text they agreed to.
- Privacy is informational but GDPR Art. 13/14 still expects us to be
  able to say what the policy disclosed on a given date.
- Impressum archived for symmetry; cheap.

How it works:
- scripts/render-legal.py reads the TSX source, resolves operator.*
  placeholders from operator.ts and TERMS_VERSION / PRIVACY_LAST_UPDATED
  from legal.ts, strips JSX tags, and prints clean markdown to stdout.
- A README in the folder explains when to bump and when to add a
  snapshot (material change only, not typo fixes).

Seeded entries:
- terms-2026-04-19.md
- privacy-2026-04-19.md
- imprint-2026-04-19.md

No automation / CI check yet — trust-based process documented in the
folder's README. Revisit if discipline slips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 07:51:41 +02:00
Ullrich Schäfer
c6c5ad3765
Merge pull request #250 from trails-cool/feat/terms-version
Store Terms version alongside acceptance timestamp
2026-04-19 07:49:28 +02:00
Ullrich Schäfer
9c4c3d6444
Store Terms version alongside acceptance timestamp
Reviewer's follow-up: it's not enough to record when a user accepted
the Terms; we also need to record which version of the text they saw.

Changes:
- journal.users gains a nullable `terms_version` text column (nullable
  so the three pre-existing users without a version are kept as-is).
- New apps/journal/app/lib/legal.ts exports TERMS_VERSION as a single
  source of truth, reused by the legal pages' "Last updated" header
  and by the registration flow as the value to send/store.
- Registration form posts `termsVersion` alongside `termsAccepted` on
  all three relevant steps (start, finish, register-magic-link).
- API route validates that `termsVersion` is a non-empty string on
  any step that requires terms, and forwards it to the auth server.
- auth.server finishRegistration and registerWithMagicLink now take
  `termsVersion` and persist it on the users row.
- journal-auth spec gets a new scenario for version storage and a
  rejection scenario for missing version.

PRIVACY_LAST_UPDATED is also exported from the same module and used
by the Privacy page header, keeping both pages on a single legal.ts
source of truth for "last updated" labels. Privacy is not per-user
stored — it's informational, not contract.

Existing users have NULL terms_version; if we ever prompt them to
re-accept updated Terms, we can backfill with the version they
re-accept at that point.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 07:46:34 +02:00
Ullrich Schäfer
c532a1024b
Merge pull request #249 from trails-cool/docs/park-self-host-overpass
Park self-host-overpass spec under docs/ideas
2026-04-19 07:41:56 +02:00
Ullrich Schäfer
c4874dc04c
Park self-host-overpass spec under docs/ideas
The interim proxy (#239/#240/#242/#243) covers the day-one needs for
Overpass — User-Agent compliance via server-side proxy, same-origin +
rate limit + cache with coalescing, bbox quantization, observability.
Further work on self-hosting the upstream is no longer urgent.

Move the full OpenSpec artifact set out of openspec/changes/ so it
doesn't clutter the active change list, and park it under
docs/ideas/self-host-overpass/ as a reference for when we revive it.
Adds a short README at the new location capturing:
- current interim solution
- triggers that would justify reviving (rate limits, >1 req/s, etc.)
- key decisions already made (Hetzner-to-Hetzner firewall model,
  DOCKER-USER chain, capacity ceiling at DACH on current box)
- how to switch when the time comes (flip OVERPASS_URL, no client
  changes)

The files were never committed in the first place (WIP in the working
tree through the proposal session), so no git history to preserve.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 07:40:44 +02:00
Ullrich Schäfer
4966ded583
Merge pull request #248 from trails-cool/docs/realign-legal-specs
Realign legal-disclaimers spec with post-review implementation
2026-04-19 07:36:25 +02:00
Ullrich Schäfer
2c512ce75e
Realign legal-disclaimers spec with post-review implementation
Fixes one contradiction and closes five gaps where the code (after the
legal-review rounds in #235, #246, #247) now exceeds what the spec
required.

Contradiction fixed:
- Alpha banner was specified as dismissible via sessionStorage, with
  "reappears in a new session". In reality the dismiss control and
  sessionStorage were removed during privacy hardening — the banner
  is now always visible. Updated the requirement to match.

New scenarios documenting what's already shipped:
- Impressum: EU ODR link + non-participation notice; English
  translation noted as informational.
- Terms of Service: service availability, minimum age 16 for Journal
  accounts, content usage-rights licence, acceptable-use rules
  (including no mass automated data extraction), German-law-tiered
  liability, bilingual structure.
- Datenschutzerklärung: explicit data-categories-and-purposes list;
  Terms acceptance classified under Art. 6(1)(b) and explicitly NOT
  (1)(a); dedicated server-logs section (≤14 days, Art. 6(1)(f));
  storage-durations list; alpha-reset caveat; per-third-party
  disclosures with transfer details (Sentry, OSM tile servers,
  Overpass proxy, BRouter, SMTP); Art. 44 ff. DSGVO third-country
  transfer note for Sentry with SCC basis; Berlin supervisory
  authority; Privacy Manifest appendix.
- Footer: now includes a link to the source repository on both apps.

No functional changes — only spec text catching up with what is live
in production.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 07:33:27 +02:00
Ullrich Schäfer
b92367147f
Merge pull request #247 from trails-cool/docs/legal-followups
Apply minimal legal-review follow-ups
2026-04-19 07:31:00 +02:00
Ullrich Schäfer
bc311ab7bc
Apply minimal legal-review follow-ups
Privacy:
- Sentry: add Art. 44 ff. DSGVO third-country transfer note (Sentry
  is US-based; we rely on SCCs). Soften "no IPs" → "IP-Adressen
  werden nicht aktiv gespeichert" per reviewer's preferred wording.
  Drop the unverified "Frankfurt" hosting claim; keep
  Auftragsverarbeiter framing.
- SMTP → "externer SMTP-Dienst" per reviewer's phrasing.
- Storage-durations section: add explicit alpha-reset caveat so it
  aligns with Terms §4 (database resets).

Terms:
- § 1 service wording: "kostenloser Dienst" → "derzeit kostenloser
  Dienst" (leaves pricing model open without promising free-forever).
- § 7 acceptable use: scraping clause softened to "kein massenhaftes
  automatisiertes Auslesen von Daten" (drops the specific "to build
  competing services" qualifier).

Imprint:
- Normalise address formatting: multi-line across both DE blocks and
  the EN § 5 TMG block (§ 18 MStV and EN block previously used a
  comma-separated single line). One style only, everywhere.
- Add whitespace-nowrap to the mailto link so the email address
  never breaks mid-word on narrow viewports.

No changes to legal bases, server-logs section, storage durations
themselves, or any third-party other than Sentry + SMTP wording.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 07:27:56 +02:00
Ullrich Schäfer
c9758a65d2
Merge pull request #246 from trails-cool/docs/legal-rewrite
Rewrite legal pages per legal review feedback
2026-04-18 03:00:39 +02:00
Ullrich Schäfer
1462c54df7
Rewrite legal pages per legal review feedback
Imprint:
- Add explicit Streitbeilegung section with the EU ODR link and the
  required statement that we don't participate in consumer dispute
  resolution proceedings.
- Keep structure tight; drop the Haftungsausschluss boilerplate that
  repeated what's already covered by §§ 7-10 TMG.
- Bilingual with EN summaries below each section.

Privacy:
- Reshape around proper GDPR structure: Controller → Data categories &
  purposes → Legal bases → Server logs → Storage durations → Third
  parties → Rights → Complaint authority → (plain-language) Privacy
  Manifest at the end.
- FIX: acceptance of the Terms is NOT consent. Moved from Art. 6(1)(a)
  to Art. 6(1)(b) (Vertragserfüllung), with an explicit note that
  contract acceptance is not consent within the meaning of (1)(a).
- Add explicit Server-Logfiles section (IP / timestamp / method / path
  / status / user-agent; Art. 6(1)(f); 14-day retention).
- Add explicit Storage-duration list (account, Planner sessions,
  magic-link tokens, logs, Sentry).
- Expand third-parties: Sentry, OpenStreetMap (explicit that the
  browser sends IP+UA directly to OSM tile servers, with OSMF privacy
  policy link), Overpass (via our proxy, upstream only sees our
  server), BRouter (self-hosted), SMTP, hosting inside EU under DPA.
- Each section has a short "English." summary paragraph inline below
  the German text, per the feedback format request.
- Privacy Manifest kept as a developer-friendly epilogue; trimmed and
  aligned with current reality (no cookies/localStorage/sessionStorage
  in Planner; no replays; no PII via Sentry).

Terms:
- Add § 2 Mindestalter (16) for Journal accounts; Planner anonymous
  has no age requirement.
- Add § 3 Dienstverfügbarkeit (service availability): operator may
  modify/limit/interrupt/discontinue the service or individual
  accounts.
- Add § 6 Inhalte und Nutzungsrechte: user retains ownership; grants
  operator a limited, non-transferable licence to store/process/
  display content only for operating the service.
- Tighten §4 DB reset, §5 user backup responsibility, and §9 liability
  (standard German DACH tiering: unlimited for intent/gross neg./life-
  body-health; limited to foreseeable damages on cardinal-duty breach
  for slight negligence; otherwise excluded).
- Add §7 rule: no bulk scraping to build competing services.
- Same bilingual format as the other two pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 02:57:29 +02:00
Ullrich Schäfer
559642ea30
Merge pull request #245 from trails-cool/fix/grafana-overpass-panel-queries
Fix Overpass health + cache-hit stat panel queries
2026-04-18 02:47:23 +02:00
Ullrich Schäfer
278e74f08e
Fix Overpass health + cache-hit stat panels: vector(0) not clamp_min
The original formulas used clamp_min(denominator, 1) to avoid divide-
by-zero, but that inflates the denominator when request rate is < 1/s.
Real example from prod: ~0.04 req/s, all successful, displayed as
1.02% health (red) instead of ~100%.

Switch to (numerator or vector(0)) / denominator:
- Missing "hit" series (no cache hits yet) → numerator resolves to 0
  instead of empty set, so the stat shows 0% instead of "No data".
- Zero traffic (empty denominator) → whole expr is empty, Grafana
  renders "No data", which is the correct behaviour for a health
  stat when nothing's happening.
- Non-zero low traffic → ratio is true ratio, not artificially
  depressed by the clamp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 02:42:50 +02:00
Ullrich Schäfer
7a61550ddd
Merge pull request #244 from trails-cool/docs/privacy-manifest-update
Align privacy manifest with Sentry + Overpass hardening
2026-04-18 02:35:40 +02:00
Ullrich Schäfer
248849318f
Align privacy manifest with Sentry + Overpass hardening
Three sections drifted from the actual behaviour of the deployed apps:

Planner section:
- Spells out no cookies / no localStorage / no sessionStorage (we
  removed i18n localStorage caching and alpha-banner sessionStorage).
- Notes the browser-side Planner does not load Sentry at all.

Sentry section:
- Drops the "session replays on error" claim — replay integration is
  not installed and replaysSessionSampleRate / replaysOnErrorSampleRate
  are both 0.
- Documents the actual scope: Journal server-side always; Journal
  client-side only after login; Planner server-side only.
- Documents that IPs/cookies/headers are not sent (sendDefaultPii=false)
  and that only the user ID is attached to logged-in Journal errors.

Third Parties section:
- Adds Overpass API with an explicit note that queries are proxied
  through our own /api/overpass so the upstream host sees our server,
  not end users.

Also bumps "Last updated" to today.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 02:32:48 +02:00
Ullrich Schäfer
6bba7e31a7
Merge pull request #243 from trails-cool/fix/overpass-proxy-forwarded-origin
Fix /api/overpass 403 behind Caddy reverse proxy
2026-04-18 02:25:28 +02:00
Ullrich Schäfer
2f5ce0e80e
Merge branch 'main' into fix/overpass-proxy-forwarded-origin 2026-04-18 02:22:30 +02:00
Ullrich Schäfer
9925880019
Merge pull request #241 from trails-cool/archive-specs
Archive legal-disclaimers change and sync specs
2026-04-18 02:22:11 +02:00
Ullrich Schäfer
48dfa059fd
Merge pull request #242 from trails-cool/feat/overpass-bbox-quantize
Quantize Overpass query bbox to align cache keys across clients
2026-04-18 02:21:57 +02:00
Ullrich Schäfer
e11c9ab58b
Fix /api/overpass 403 behind Caddy reverse proxy
The same-origin check compared the browser's Origin header against
new URL(request.url), but inside the Planner container request.url
is http://planner:3001/... while the browser sends Origin
https://planner.trails.cool — so production always 403'd.

Trust Caddy's X-Forwarded-Host and X-Forwarded-Proto headers when
present (both are set by Caddy's reverse_proxy directive by default)
to reconstruct the external origin the browser actually connected to.
Falls back to request.url for dev and any direct (non-proxied) access.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 02:21:31 +02:00
Ullrich Schäfer
6c1148d5b7
Quantize Overpass query bbox to align cache keys across clients
Near-identical viewports from different collaborators (slightly
different pan/zoom because Yjs awareness doesn't enforce pixel-exact
alignment) previously produced byte-different Overpass queries and
therefore different server-side cache keys. Each client missed cache
and triggered its own upstream fetch.

buildQuery now quantizes the bbox to a 0.01° grid (~1 km), expanding
outward (south/west floor, north/east ceil) so the viewport is always
covered, and formats coords with 3 decimals for a stable string.

Trade-offs:
- Slightly larger query bbox: ≤ one grid cell (≈ 1 km) of padding on
  each side. For a zoom-12 viewport (~11 km wide) that's <10% extra
  data; bounded by the existing `[maxsize:1048576]` + `out 100` limits.
- Coordinates in the query are now 3-decimal strings regardless of the
  caller's precision — existing buildQuery test updated to match.

Two new tests cover the cache-alignment property and the outward-
expansion invariant. Hit ratio impact will be visible on the Grafana
"Overpass Cache Hit Ratio" stat added in the prior PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 02:18:46 +02:00
Ullrich Schäfer
21a00afa85
Archive legal-disclaimers change and sync specs
Move completed legal-disclaimers change to archive, apply deltas to
main specs: append "Terms acknowledgement at signup" requirement to
journal-auth and create the legal-disclaimers capability spec
covering Impressum, Terms, Datenschutzerklärung, alpha banner, and
footer links.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 02:17:48 +02:00
Ullrich Schäfer
97ad15db98
Merge pull request #240 from trails-cool/feat/overpass-metrics
Add Prometheus metrics + Grafana panels for Overpass proxy
2026-04-18 02:15:15 +02:00
Ullrich Schäfer
62e208d850
Add Prometheus metrics + Grafana panels for Overpass proxy
Adds four metrics surfaced via the existing /metrics endpoint:
- overpass_cache_events_total{result=hit|miss|coalesced}
- overpass_cache_size (gauge)
- overpass_upstream_duration_seconds (histogram)
- overpass_upstream_requests_total{status} — covers 2xx/4xx/5xx and
  an explicit "error" label for network-level failures

Grafana dashboards/planner.json gets a new row:
- Upstream health (5m 2xx success ratio, red <90%, green >99%)
- Cache hit ratio
- Cache size
- Upstream p95 latency
Plus timeseries panels for cache event breakdown, upstream status
over time, and p50/p95/p99 upstream latency.

The success-rate formula uses clamp_min(..., 1) on the denominator so
it doesn't NaN when traffic is zero.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 02:12:19 +02:00
Ullrich Schäfer
febdf6a9d3
Merge pull request #239 from trails-cool/feat/overpass-proxy
Route Overpass through server-side proxy with cache + coalescing
2026-04-18 02:08:09 +02:00
Ullrich Schäfer
a4df5a43f6
Route Overpass through server-side proxy with cache + coalescing
Adds apps/planner/app/routes/api.overpass.ts — a same-origin, rate-limited
server-side proxy that forwards Overpass QL to the upstream endpoint
configured via OVERPASS_URL (default: overpass.private.coffee).

Motivation:
- private.coffee's best-practices require a meaningful User-Agent
  identifying the project. Browsers cannot set User-Agent on fetch()
  (forbidden header), so the request has to originate server-side.
- Collaborative sessions commonly have N clients pan/zoom the same map,
  so the same bbox query arrives multiple times within seconds.
- Setting up the proxy now lets us swap OVERPASS_URL to a self-hosted
  Overpass later without client changes.

What the proxy does:
- Sets User-Agent "trails.cool Planner (https://trails.cool; legal@trails.cool)"
- Enforces same-origin via the Origin header
- Rate-limits per client IP (120 req/min)
- In-memory LRU cache of upstream responses keyed on the form-encoded body
  (TTL 10 min, max 200 entries)
- Coalesces concurrent misses for the same key so N simultaneous clients
  in one session incur exactly one upstream call

Client change: apps/planner/app/lib/overpass.ts POSTs to /api/overpass
instead of iterating over public Overpass endpoints. Fallback list
removed; resilience is now the proxy's responsibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 02:04:50 +02:00
Ullrich Schäfer
bf8cdd5cdd
Merge pull request #238 from trails-cool/fix/overpass-endpoint
Use overpass.private.coffee as primary Overpass endpoint
2026-04-18 01:51:04 +02:00
Ullrich Schäfer
f7c2604121
Use overpass.private.coffee as primary Overpass endpoint
Switch the Planner's primary Overpass endpoint from
overpass.kumi.systems to overpass.private.coffee. private.coffee is a
privacy-focused public Overpass instance that doesn't log queries,
which fits the Planner's privacy-first principle better than the
previous default. Keep overpass-api.de as the silent fallback for
resilience while the self-host-overpass change is in flight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 01:48:09 +02:00
Ullrich Schäfer
32a844d8de
Merge pull request #235 from trails-cool/feat/legal-disclaimers
Add legal pages, ToS acceptance, and alpha banner
2026-04-18 00:46:36 +02:00
Ullrich Schäfer
4c089756a1
Fix sentry-config typecheck: explicit types: [node] in tsconfig
@types/node is a devDependency of the package but tsc only auto-picks
it up locally due to root node_modules hoisting. CI runs with pnpm's
isolated resolution so the dep isn't visible to tsc without an
explicit types field — matching what packages/jobs/tsconfig.json does.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 00:43:48 +02:00
Ullrich Schäfer
01f002cc46
Stop Sentry on logout; fix E2E + Dockerfile for merged consolidation
- sentry.client.ts: add stopSentryClient() that awaits Sentry.close() so
  the SDK tears down when a user logs out. Hub methods become no-ops
  until initSentryClient() is called again on next login.
- root.tsx: call stopSentryClient() from the user-effect when user === null.
- Add sentry-config to journal + planner Dockerfiles (was missing after
  #237 introduced the new workspace package, breaking the Dockerfile
  package check).
- Footer: replace inner <nav> with <div> — nested nav landmarks broke
  the journal "nav bar shows on all pages" e2e test (two navigation
  roles on the page).
- e2e auth: tick the required ToS checkbox in the shared registerUser
  helper so passkey registration tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 00:40:36 +02:00
Ullrich Schäfer
adbf507101
Merge pull request #237 from trails-cool/feat/sentry-consolidation-v2
Consolidate Sentry config; fix Planner PII & missing captureException
2026-04-18 00:31:25 +02:00
Ullrich Schäfer
8e576ac578
Consolidate Sentry config into shared package; fix inconsistencies
Introduces @trails-cool/sentry-config with three helpers that encode
the common Sentry.init options:
- nodeSentryConfig(appContext) for the two HTTP servers
- browserSentryConfig(appContext, env) for the Journal client
- mobileSentryConfig(__DEV__) for the React Native app
- drop404s beforeSend for servers

Also fixes three inconsistencies found in the audit:
- Planner server was missing sendDefaultPii: false (IPs could leak)
- Planner entry.server.tsx didn't call Sentry.captureException on SSR
  render errors; Journal's SSR did
- Sentry.init location now matches across apps (both in server.ts;
  Journal previously had it in app/entry.server.tsx, which meant init
  ran lazily on first request rather than at server startup)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 00:30:09 +02:00
Ullrich Schäfer
27c9892bab
Merge pull request #236 from trails-cool/feat/sentry-dev-log
Log Sentry dev-inert status; explicit replay=0
2026-04-18 00:15:48 +02:00
Ullrich Schäfer
4092f21475
Log Sentry dev-inert status; explicit replay=0
When Sentry init runs but is disabled (local dev / CI), log a debug
message so developers can see Sentry *would* send in production.

Also explicitly set replaysSessionSampleRate/replaysOnErrorSampleRate
to 0 in the client and mobile configs. Replay integration isn't
installed, so this is documentation — it prevents future accidental
enablement if an integration is ever added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 00:15:31 +02:00
Ullrich Schäfer
cc0f44258a
Add legal pages, ToS acceptance, and alpha banner
- Journal: Impressum (§5 TMG), Terms of Service, GDPR privacy page
- Registration flow: required ToS checkbox, termsAcceptedAt persisted
- Alpha banner on Journal (always visible); alpha badge on Planner hero
- Footer with legal links + GitHub on both apps
- Reduce PII: sendDefaultPii=false, Sentry init only after login (Journal)
- Drop Sentry from Planner (no login, no consent possible)
- Drop localStorage caching from i18n client detection
- Sync SSR i18n resources each request so locale edits HMR without restart

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 00:08:48 +02:00
Ullrich Schäfer
6c1cd23358
Merge pull request #232 from trails-cool/feat/login-code
Add login code for mobile authentication
2026-04-17 22:53:43 +02:00
Ullrich Schäfer
a056ec286b
Merge branch 'main' into feat/login-code 2026-04-17 22:38:57 +02:00