Commit graph

25 commits

Author SHA1 Message Date
Ullrich Schäfer
d20ec30bce
Add docs/roadmap.md and update CLAUDE.md
- Add docs/roadmap.md: strategic four-phase launch plan (feature complete →
  polish → beta → announce) with launch-blocking changes, post-launch backlog,
  and links to ideas/
- Update CLAUDE.md: fix stale phase-1-mvp OpenSpec reference, remove "Phase 2"
  label from Federation, add roadmap + ideas pointers, expand packages list
  with all 11 packages and accurate descriptions (including map vs. map-core
  split rationale)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 10:41:51 +02:00
Ullrich Schäfer
d55981d1bf
Add docs/gpx-extensions.md documenting the trails: GPX namespace
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 23:35:06 +02:00
Ullrich Schäfer
78b8b8f55f
Atomic GPX save: validate + persist row + geometry in one transaction
Eliminates the silent-failure pattern where a route/activity row could
be committed with gpx IS NOT NULL but geom IS NULL if the PostGIS write
failed after the row insert.

- New gpx-save.server.ts owns all GPX validation (GpxValidationError,
  validateGpx) and PostGIS geometry writes (writeGeom, tx-aware)
- createRoute, updateRoute, createActivity, createRouteFromActivity all
  wrapped in db.transaction() covering row + geom + version snapshot
- demo-bot uses createRoute/createActivity instead of raw inserts;
  errors propagate loudly
- Callback endpoint returns 400 for GpxValidationError instead of 401
- ADR-0006 documents the invariant for future explorers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:02:56 +02:00
Ullrich Schäfer
7e22f1260b
Add unify-auth-completion architecture artifacts
Extract the post-verify orchestration shared across passkey
register-finish, passkey login-finish, magic-link verify-code, and
magic-link click-through into a single completeAuth function. Two
ADRs record the decision:

- ADR-0004: centralize web auth completion (record terms + create
  session + redirect) in apps/journal/app/lib/auth/completion.ts.
- ADR-0005: explicitly no AuthMethod polymorphism. Passkey + magic-
  link is the entire identity surface; OAuth2/PKCE is session
  transport, not a peer method. Recorded as a negative decision so
  future architecture passes don't re-suggest extracting the
  interface.

CONTEXT.md gains an Authentication section covering completeAuth, the
two methods, the OAuth2-as-transport distinction, and where the Terms
gate enforcement lives (root loader for web, requireApiUser for API
per the just-merged mobile-terms-gate).

OpenSpec change unify-auth-completion captures the proposal, design
(5 decisions including the negative-scope choices), spec delta on
authentication-methods, and 14 tasks. Implementation follows on this
branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 02:18:06 +02:00
Ullrich Schäfer
cfba3146e2
Add deepen-connected-services architecture artifacts
Reshape the sync-providers seam before Komoot (web-login) and Apple
Health (device) adapters land. Captures the decisions in three ADRs,
seeds CONTEXT.md with Connected Services vocabulary, and proposes the
OpenSpec change covering schema rename + ConnectedServiceManager +
capability seams (Importer / RoutePusher / WebhookReceiver).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:14:38 +02:00
Ullrich Schäfer
c7331ed056 Refresh IA doc — all six streams shipped on 2026-04-26
Streams A, B, C, D, E, F all landed today. This refresh marks them
shipped with their PR numbers, brings the sitemap and navbar
diagrams in sync with the deployed state, and rewrites the
"Notifications vs follow requests" and "Settings" sections to
describe the new structure rather than the historical split.

Snapshot date bumped to "2026-04-26 (post-streams)" so a future
re-read knows what state this snapshot reflects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 10:03:05 +02:00
Ullrich Schäfer
a847ff2e52 Add /ia-review and /spec-drift-review process skills
Captures two recurring review processes as repeatable Claude skills so
they don't have to be reinvented from scratch each time:

- /ia-review: walks the route tables + nav surfaces, builds the
  sitemap, flags drift, and writes/refreshes
  docs/information-architecture.md. Refresh mode preserves the user's
  accumulated decisions and implementation backlog while only
  rewriting the snapshot sections.

- /spec-drift-review: walks every spec in openspec/specs/, compares
  it to shipped code, and produces a categorized drift report
  (high/medium/low severity) plus code-without-spec findings and
  structural suggestions (split/merge/new specs, CAPABILITIES.md
  catch-up). Excludes specs touched by active openspec changes since
  that mismatch is intended in-flight work, not drift.

Also extends the IA doc with the items resolved/deferred during the
review (observations 1-10), three new streams (D: drop redundant Feed
button on logged-in /, E: break Settings apart, F: propose /explore
spec), and an "Open exploration" section for the routes-vs-activities
terminology question.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 08:41:07 +02:00
Ullrich Schäfer
0306d90de8 Merge /follows/requests into /notifications as a tabbed inbox
Folds the actionable follow-requests surface into the Notifications page
as a Requests tab (alongside the existing Activity tab), so the navbar
exposes a single bell instead of two adjacent inboxes. The Requests tab
shows a count badge for pending rows regardless of read state, while the
bell badge keeps reflecting the unread-notifications count (which already
covers `follow_request_received` rows). The standalone /follows/requests
URL is preserved as a 301 redirect so prior notification deep-links,
emails, and bookmarks still resolve.

Driven by the IA review captured in docs/information-architecture.md.
Specs (notifications, social-follows, journal-landing) are updated in
the same change to reflect the new structure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 08:24:59 +02:00
Ullrich Schäfer
9e598fb6a1
BRouter host observability + docs
Lands sections 6 and 8 of relocate-brouter-to-dedicated-host on top
of the host compose in #291.

## Observability (section 6)

- **Prometheus**: new `brouter-cadvisor` job scraping
  `10.0.1.10:8080` over the vSwitch, labeled `host="brouter"`.
- **cAdvisor sidecar** on the dedicated host's compose
  (`--docker_only --whitelisted_container_labels=trails.cool.service`)
  so metrics only cover trails containers, never the operator's
  unrelated workloads on the shared box.
- **Promtail sidecar** on the dedicated host, Docker SD with relabel-
  drop on missing `trails.cool.service` label, pushing to flagship
  Loki at `http://10.0.0.2:3100/loki/api/v1/push`.
- **Flagship compose**: Loki now publishes port 3100 on the vSwitch
  IP only (10.0.0.2:3100) — Hetzner Cloud firewall blocks it from
  the public internet.
- **Grafana dashboard**: `brouter.json` — scrape up/down, request
  rate (from Planner-side `brouter_request_duration_seconds`),
  p50/p95/p99, container memory/CPU, Loki logs panel.
- **Alert**: `brouter-scrape-down` fires on
  `up{job="brouter-cadvisor"} < 1 for 2m`; `noDataState: Alerting`
  so a total scrape failure still pages.

Operator needs one UFW rule on the dedicated host for the cAdvisor
port — documented in `infrastructure/brouter-host/README.md`.

## Documentation (section 8)

- `CLAUDE.md` — hosts table + updated deployment table with SSH
  targets per workflow; BRouter host SSH is `-p 2232 trails@...`,
  different key.
- `docs/architecture.md` — Hosting section rewritten to cover both
  hosts, vSwitch boundary, and the observability-scoping rationale
  for the shared dedicated host.
- `docs/deployment.md` (new) — full operator runbook: host layout,
  first-time BRouter provisioning, SOPS rotation (including the
  macOS `SOPS_AGE_KEY_FILE` gotcha), cutover procedure with
  rollback, manual workflow triggers.

Task 8.4 (infrastructure/README.md) skipped: that file doesn't
exist and the ground is covered by brouter-host/README.md +
docs/deployment.md.

## Validation

- `docker compose config` on both the flagship and brouter-host
  compose files — both validate.
- `pnpm typecheck`, `pnpm lint`, `pnpm test` — all clean (full
  turbo cache hits; no code changes in this commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:10:45 +02:00
Ullrich Schäfer
fc4485f6ef
Apply configurable-demo-persona: per-instance demo identity + voice
Adds DEMO_BOT_PERSONA env var (inline JSON or file:<path>) so
self-hosted instances can replace Bruno-in-Berlin with their own
demo account identity and content pools. No-op for trails.cool — the
built-in Bruno persona remains the default.

- DemoPersona type + Zod schema validation
- loadPersona() cached at boot; falls back to default on any failure
- ensureDemoUser throws DemoPersonaUsernameClashError when the
  persona username is already a real user; server declines to
  schedule demo jobs for that process
- isDemoUser flag moved from hardcoded "bruno" check to loader-supplied
  boolean computed from the running persona
- docs/demo-persona.md explains the schema + operator flow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:30:32 +02:00
Ullrich Schäfer
32661a0163
Add visibility disclosure to the binding German privacy text
The previous commit only added a bullet to the English-only Privacy
Manifest appendix. The formal, binding German sections (which the
policy explicitly declares authoritative) didn't mention visibility
at all — and the paired English summary under each section likewise
didn't.

Add a sentence under §2 "Erhobene Daten und Zwecke → Nutzerinhalte"
explaining the visibility setting, defaults, and world-visibility
consequences. Mirror in the section's English summary paragraph. Keep
the Privacy Manifest bullet as an additional plain-language mention.

Re-rendered docs/legal-archive/privacy-2026-04-20.md; both languages
present (verified).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 09:14:15 +02:00
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
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
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
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
0a8dd0b766
Add transactional emails (SMTP) and planner features (no-go areas, notes, crash recovery)
Transactional emails:
- Add nodemailer SMTP email module with dev-mode console logging
- Magic link template and welcome template with HTML + plain text
- Wire sendMagicLink into login flow, sendWelcome into registration
- Update privacy page and deploy docs for SMTP configuration

Planner features:
- No-go areas: draw polygons on map (leaflet-geoman), synced via Yjs,
  passed to BRouter as nogos parameter, route recomputes on change
- Session notes: collaborative Y.Text textarea in sidebar tab
- Crash recovery: periodic localStorage save of Yjs state, restore on reconnect
- Rate limit session creation (10/IP/hour) in /new route

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:00:42 +01:00
Ullrich Schäfer
be3e69c10b
Security hardening: headers, scanning, Docker, firewall
- Caddy: HSTS, CSP, X-Frame-Options, nosniff, Referrer-Policy,
  Permissions-Policy on all responses
- Caddy: Block scanner paths (.env, .git, wp-config, etc.) with 403
- CI: Gitleaks secret scanning + pnpm audit for vulnerabilities
- Dependabot: Weekly npm + GitHub Actions + monthly Docker updates
- Docker: Non-root user in journal, planner, and brouter containers
- Server: UFW firewall (22/80/443 only) + fail2ban (8 IPs already banned)
- SECURITY.md: Vulnerability disclosure policy
- Privacy page: Security practices section added

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:58:12 +01:00
Ullrich Schäfer
c7c1c275df
Add 8 proposed changes and old trails analysis
Proposed changes (all with proposal, design, specs, tasks):
- app-navigation (9 tasks) — nav bars for both apps
- planner-landing-page (7 tasks) — standalone landing page
- planner-multiplayer-awareness (13 tasks) — participants, cursors, names
- changelog (13 tasks) — public changelog with "what's new"
- transactional-emails (15 tasks) — magic link + welcome emails
- planner-features (18 tasks) — no-go areas, notes, recovery, rate limits
- komoot-import (23 tasks) — Komoot tour import
- route-features (37 tasks) — sharing, multi-day, spatial, photos

Also adds docs/old-trails-analysis.md with feature analysis from the
older trails project.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 03:57:20 +01:00
Ullrich Schäfer
f58fe99cde
Add GitHub Actions CI, wire ESLint, lock in Caddy
CI pipeline with parallel jobs for fast feedback:
- typecheck, lint, unit tests, build run in parallel
- e2e tests run after build
- Playwright artifacts uploaded on failure

ESLint wired into both apps with sensible defaults:
- underscore-prefixed unused vars allowed
- root package.json marked as type: module

Architecture updated: Caddy as reverse proxy with auto HTTPS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:55:12 +01:00
Ullrich Schäfer
48547fdc82
Add CLAUDE.md, README, and development tooling docs
- CLAUDE.md: project context for Claude Code sessions
- README.md: human-facing project intro, structure, getting started
- docs/tooling.md: cmux, Claude Code, Crit, and OpenSpec workflow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:06:51 +01:00
Ullrich Schäfer
4214564fd8
Move conversation log into docs/conversations/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:00:45 +01:00
Ullrich Schäfer
64f7fce494
Add project philosophy document
Seven guiding principles: Privacy by Design, Data Ownership, Open Source,
Open Standards, AI-Assisted Development, Internationalization, Simplicity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:00:08 +01:00
Ullrich Schäfer
8bf00ce6f0
Add conversation log from initial architecture session
Captures the full planning journey: BRouter exploration, 4 rounds of
crit review (50 comments), key decisions, and artifacts produced.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 11:37:45 +01:00
Ullrich Schäfer
5b4d072741
Initial monorepo setup with architecture plan
- Monorepo structure: apps/ (planner, journal) + packages/ (ui, types, map, gpx, i18n)
- Tooling: pnpm workspaces + Turborepo
- Architecture plan documenting all resolved decisions from design review
- Shared types package with Route, Activity, and Waypoint interfaces
- MIT license

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 11:29:33 +01:00