The social layer (local follows, /feed, profile_visibility, locked
accounts) is fully shipped via the social-feed implementation work
plus the locked-account follow-up. Closing out the change.
Pre-archive ticks:
- 1.4: schema migrated on prod via cd-apps drizzle-kit push; column
+ table verified on the running DB.
- 3.3: follower/following counts on /users/:username shipped in #310.
- 7.1: cd-apps drizzle-kit push --force ran; verified post-deploy.
- 7.2: smoke inputs verified (bruno is public on prod, has 17 public
activities, /users/bruno returns 200). Live click-through is
operator-discretion; the listSocialFeed query correctness is proven
by integration tests.
- 7.3 / 7.4: forward-pointers, not deliverables for this change.
One task explicitly deferred:
- 6.2: full activity-creation E2E for the /feed assertion. Equivalent
coverage at the integration level + the e2e Follow-button +
visibility tests; not worth wiring an e2e activity-creation helper
just for this one path.
Spec sync:
+ journal-landing: 1 added
~ public-profiles: 1 added, 1 modified
+ social-follows: new spec (5 added)
Move to openspec/changes/archive/2026-04-25-social-feed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final tasks ticked post-merge:
- 10.2: verified on prod that journal.routes + journal.activities still
default to 'private' NOT NULL, with the only public rows being the
15-each demo-bot seeded content
- 10.3: demo-activity-bot already inserts with visibility='public'
directly in demo-bot.server.ts
Syncs the three delta specs into main:
+ activity-feed: 2 added, 1 modified
+ public-profiles: new spec (1 added)
+ route-management: 2 added, 1 modified
Moves change to openspec/changes/archive/2026-04-24-public-content-visibility.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously a signed-in user landing on `/` saw the same page as an
anonymous visitor: the visitor hero, the flagship marketing cards, and
the instance-wide public feed. "Home" should mean "your stuff."
The home route now branches on session: signed-out visitors keep the
marketing + public-feed layout from #303; signed-in users get a
personal dashboard showing their own activities reverse-chronologically
(all visibilities), a welcome line linking to their profile, and a
"New Activity" CTA. The public instance feed and marketing blurbs are
suppressed for signed-in users — they'd be redundant on a personal
landing surface.
Loader picks `listActivities(user.id)` vs `listRecentPublicActivities`
based on session, so only one feed query runs per request.
Updates the `journal-landing` spec with the new session-based split and
a "Personal dashboard for signed-in users" requirement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old home was an h1, subtitle, and two auth buttons — visitors
arriving at trails.cool had no idea what it was, and self-hosted
instances had nothing interesting to land on.
The new home is one layout that serves both audiences:
- Hero: a product-describing h1 ("Federated outdoor journal") +
tagline. The site name already lives in the top banner and nav brand,
so the h1 carries the pitch instead of triplicating "trails.cool".
- Auth CTAs: Register (blue) + Sign In (outlined) get primary weight.
Planner demotes to a small "Or try the Planner without an account →"
link below them — it's a nice escape hatch but not the goal.
- Marketing cards: on the flagship only, a 2x2 grid of emoji-icon cards
for Planner / Journal / Federation / Ownership, matching the Planner
home's visual weight. `IS_FLAGSHIP=true` toggles it. Self-hosters get
a "Powered by trails.cool — about the project" link back to the
flagship instead, so they don't have to write their own marketing.
- Public feed: reverse-chronological list of the 20 most recent public
activities on the instance, with owner + distance + date. Empty state
for fresh installs.
Plumbing:
- `listRecentPublicActivities(limit)` in activities.server.ts joins
users so the feed can render "by <displayName>" in one query.
- `IS_FLAGSHIP` env wired through docker-compose.yml; cd-apps and
cd-infra both write `IS_FLAGSHIP=true` alongside `DOMAIN=trails.cool`,
so self-hosters (who deploy without these workflows) default to off.
Specs:
- `activity-feed`: new "Instance-wide public activity feed" requirement
so the visibility contract is pinned (public in feed, unlisted +
private are not).
- `journal-landing`: new small spec capturing the hero / marketing /
feed layout contract and the `IS_FLAGSHIP` toggle, so future
instance-branding or empty-state changes have a stable anchor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final step (task 9.4) of the BRouter relocation. The change is moved to
openspec/changes/archive/2026-04-24-relocate-brouter-to-dedicated-host/
and the four delta specs (brouter-integration, infrastructure,
observability, security-hardening) are merged into the main specs:
+ 6 added requirements
~ 4 modified requirements
The two intentionally-unchecked tasks are 2.2 (obsoleted, see crossed-out
note) and 9.4 itself (this archive).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
Main specs (7): Converted from delta format (## ADDED/MODIFIED) to
proper main spec format (## Purpose + ## Requirements)
Changes (3): Added missing spec files for e2e-test-reorganization,
journal-poi-details, and mobile-nearby-sync
Mobile testing: Added missing scenario for "No Vitest" requirement
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Synced new spec (chart ↔ map interaction, mobile touch, iOS safe area,
responsive header) to openspec/specs/elevation-map-interaction/.
All 4 artifacts complete. All 14 tasks complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract highway=* tags from BRouter tiledesc messages alongside surface
data and add a new "Road Type" color mode that colors the route polyline
and elevation chart by OSM highway classification (cycleway, residential,
path, etc.). Includes color palette, legend, hover labels, i18n (EN+DE),
unit tests for tag extraction, and E2E tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full notes lifecycle:
- GPX: description field in GpxData, <metadata><desc> in generate/parse
- Export: Plan GPX and Save to Journal include notes as description
- Journal: updateRoute extracts description from GPX, stores on route
- Reimport: Edit in Planner passes notes via URL params → Yjs Y.Text
- Drop import: GPX with <desc> restores notes in session
Spec updated: session-notes gains GPX export, Journal sync, and
reimport requirements.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added scenarios for character-level notes sync and cursor awareness
between participants.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Synced delta spec (6 requirements) to openspec/specs/multi-day-routes/.
Archived change to openspec/changes/archive/2026-04-11-multi-day-routes/.
All 4 artifacts complete. All 35 tasks complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add ## Purpose sections and convert delta headers to ## Requirements
on all 25 specs
- Add SHALL keywords to requirements missing them (gpx-import,
planner-session, planner-journal-handoff)
- Convert prose GPX format section to proper scenarios (no-go-areas)
- Create specs/ delta files for 7 changes that were missing them
(activity-photos, local-dev-stack, multi-day-routes, route-discovery,
route-sharing, visual-redesign, waypoint-notes)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove Garage from service list (commented out, not deployed)
- Replace S3_ENDPOINT/S3_BUCKET with actual Journal env vars
(JWT_SECRET, SESSION_SECRET, WAHOO_*)
- Add monitoring exporters (postgres-exporter, node-exporter, cAdvisor)
to monitoring stack scenario
- Add metrics collection requirement documenting Prometheus scrape
targets and pg_stat_statements custom queries
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bugs fixed:
- FIT parser returns timestamps as Date objects, not strings — convert
to ISO 8601 before passing to generateGpx (caused str.replace crash)
- FIT parser already converts semicircles to degrees — remove redundant
conversion that produced near-zero coordinates
- Wahoo CDN URLs are pre-signed S3 URLs — remove Bearer auth header
from download requests (caused 400 "Unsupported Authorization Type")
- Filter out third-party workouts (fitness_app_id >= 1000) since Wahoo
does not share their data via the API
UX improvements:
- Individual imports use fetcher (no page refresh), button shows
"Importing..." inline
- "Import all" button imports all unimported workouts on the page
sequentially with progress indicator
- Add @vitejs/plugin-basic-ssl for local HTTPS dev (opt-in via HTTPS=1)
Also adds unit test for FIT-to-GPX conversion with real fixture file,
and updates wahoo-import spec to reflect all behaviors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Archive completed changes and sync their delta specs to main:
- wahoo-import: new wahoo-import spec, updated journal-auth and account-settings
- journal-route-previews: new route-preview spec, updated map-display and route-management
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents:
- Right-click to delete
- Save to Journal preserves no-go areas in GPX extensions
- Export Plan includes full planning state
- Export Route is clean track only
- GPX extensions format with trails:planning namespace
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BRouter supports a `polygons` parameter with full vertex coordinates.
We were approximating polygons as circles (centroid + max radius),
which poorly represented elongated or concave shapes.
Now passes polygon vertices directly — no approximation, exact
no-go boundaries.
Also removes unused haversineMeters helper and updates the spec.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exporters:
- postgres_exporter: DB connections, transactions, cache hit ratio, query stats
- node_exporter: host CPU, memory, disk, network
- cAdvisor: per-container CPU and memory usage
PostgreSQL:
- Enable pg_stat_statements for query-level performance tracking
- Track index scans vs sequential scans, cache hit ratio
Dashboard (service-health.json):
- DB: connections, size, transactions/s, slow queries, cache hit ratio, index usage
- Host: disk gauge, CPU, memory, network I/O, disk I/O
- BRouter: request latency p50/p95/p99, container CPU + memory
- All containers: CPU and memory comparison
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Enrich BRouter response with per-point 3D coordinates and segment boundary
tracking (EnrichedRoute interface)
- ColoredRoute component: plain, elevation gradient (green→yellow→red), and
surface color modes with invisible wide polyline for click targeting
- Click-to-split: click on route polyline inserts waypoint at nearest point,
mapped to correct segment via boundary indices
- MidpointHandles: draggable CircleMarkers at route segment midpoints for
reshaping, hidden below zoom 12, opaque on hover
- Color mode toggle (select) synced via Yjs routeData
- i18n keys for color mode labels (en + de)
- Unit tests for segment boundary tracking (13 tests)
- E2E tests for enriched route response and color mode toggle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both changes fully implemented. Sync delta specs to main:
- map-display: add Planner home page nav and cursor rendering requirements
- planner-session: add session participant awareness requirement
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace blank home page with landing page: hero with "Start Planning"
CTA, 5 feature cards, Journal CTA, footer with attribution. All strings
in en + de. Sync planner-session spec and archive change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 85 tasks complete. Archive change to openspec/changes/archive/ and
sync 9 capability specs to openspec/specs/ for future reference.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>