Commit graph

35 commits

Author SHA1 Message Date
Ullrich Schäfer
3db59d99e3
feat(gpx): per-day ascent/descent from cleaned elevation
Task group 2 (part b) + verification of elevation-profile-hardening.

compute-days.ts no longer sums every raw point-to-point delta for
cumulative ascent/descent (which overstated day totals the same 20–50%).
It now despikes each segment, runs cumulativeFilteredTotals once over the
despiked track, and maps the running filtered ascent/descent back onto the
flat allPoints indices (carrying forward across ele-less points). Day
totals = cumulative[end] − cumulative[start] therefore match the filtered
route total by construction. DayStage shape + rounding unchanged.

Test: a two-day split over the shared noisy/spiky track reports filtered
(not inflated) per-day ascent, and the day ascents sum exactly to the
whole-route filtered ascent.

Completes the change (11/11). Verified: gpx 112/112; full pnpm typecheck
13/13, lint 13/13, test 11/11 (journal + planner compile unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 01:04:12 +02:00
Ullrich Schäfer
29be9cbe5f
feat(gpx): apply elevation cleaning to totals + profile chart
Task group 2 (part a) of elevation-profile-hardening — wire the group-1
primitives into the headline stat and the chart.

- computeElevation (parse.ts): despike each segment's elevation sequence,
  then take filteredTotals; gain/loss are now noise-filtered (no more
  20–50% inflation from per-point jitter), the profile is built from the
  despiked data, and additive gainRaw/lossRaw expose the unfiltered sums.
- elevationSeries: despike per segment (no cross-trkseg interpolation)
  before downsampling, so the chart and the headline totals derive from
  the same cleaned data.
- types.ts: GpxData.elevation gains gainRaw/lossRaw (additive).

Well-formed monotonic climbs are unchanged (steps exceed the 5 m
threshold → filtered == raw); a shared noisy-track test proves jitter is
filtered and a 300 m single-point spike is interpolated out of both the
totals and the chart.

Verified: gpx 111/111; full pnpm typecheck 13/13, lint 13/13, test 11/11
(journal + planner compile with the additive fields).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:56:36 +02:00
Ullrich Schäfer
990bbaa99e
feat(gpx): elevation cleaning primitives (despike + hysteresis totals)
Task group 1 of elevation-profile-hardening. New pure module
elevation-clean.ts (not yet wired — group 2 does that):

- despike(points): interpolates out isolated peak/pit spikes (opposite-sign
  slope outliers both exceeding MAX_SLOPE_PERCENT=100); steep monotonic
  terrain (same-sign) is never touched; per-segment, no cross-gap interp.
- filteredTotals(points): hysteresis ascent/descent (NOISE_THRESHOLD_M=5)
  that suppresses sub-threshold jitter, returning {gain,loss,gainRaw,lossRaw}.
- cumulativeFilteredTotals(points): running filtered arrays for per-day
  splits (day total = cumulative[end] − cumulative[start]).

DESIGN NOTE for review: the spec's task 1.2 says the zero-filtered
fallback should "report raw", but the raw sum-of-deltas re-inflates the
very jitter we remove (a ±2 m wobble sums to several metres) — directly
contradicting task 1.4's "±2 m jitter → 0 totals". Resolved toward the
stated goal: fall back to NET change (end−start), so flat wobble reads ~0
while a genuine small net climb still surfaces. gainRaw/lossRaw still
expose the raw sums.

Verified: gpx typecheck+lint clean, 108/108; full pnpm test 11/11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:46:13 +02:00
Ullrich Schäfer
36c5a8beaa
test(gpx): fixture corpus + verification (groups 5–6)
Task groups 5 and 6 of gpx-parser-robustness.

Group 5 — packages/gpx/fixtures/*.gpx (synthetic, no personal data):
route-only, missing-coords, garbage-ele, timestamps-partial,
timestamps-mostly-invalid, multi-track, unicode-name,
namespaced-extensions, plus trimmed Komoot/Wahoo-style exports.
parse-node.test.ts gains a fixture table test that auto-discovers every
*.gpx from disk, asserts finite stats + per-fixture shape, and fails
loudly if a file has no EXPECTATIONS entry (the corpus is the regression
mechanism).

Group 6 — verification: GpxData shape unchanged; journal + planner
compile with no changes; gpx suite 98/98; full pnpm typecheck 13/13,
lint 13/13, test 11/11. Route-only + broken-file import behaviour is
covered by the fixtures + consumer compile; the browser leg rides e2e.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:30:46 +02:00
Ullrich Schäfer
b7b8906ce9
feat(gpx): metadata name/desc fallback + <cmt> merge
Task group 4 of gpx-parser-robustness. Many apps put the only
human-readable title on the <trk>/<rte>, not in <metadata>.

- name/description now fall back to the first <trk>/<rte>'s <name>/<desc>
  when <metadata> lacks them (<metadata> still wins when present).
- that track/route's <cmt> is appended to the description (blank-line
  separated) when present and not already identical (OM BuildDescription
  dedup); used as the description outright when there's no <desc>.

`directChildText` reads direct-child text without relying on `:scope`
(portable across the jsdom + linkedom paths). No GpxData shape change;
well-formed files with metadata are unaffected.

Verified: gpx typecheck+lint clean, 87/87; full `pnpm test` 11/11;
journal + planner typecheck unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:23:53 +02:00
Ullrich Schäfer
4536470eb9
feat(gpx): post-parse timestamp repair
Task group 3 of gpx-parser-robustness. Timestamps were passed through raw;
a partially-broken time channel from a flaky recorder silently shrank
moving time and skewed start-time derivation.

New pure `timestamp-repair.ts` applied per segment after parsing:
- validity = Date.parse yields a finite epoch;
- a segment with no valid timestamps is left untouched (untimed track);
- >50% invalid → drop all the segment's timestamps (noise, not signal);
- otherwise → linearly interpolate invalid runs between valid neighbours
  (by point index), leading/trailing runs clamp to the nearest valid
  timestamp; only repaired points are rewritten as ISO 8601, valid points
  keep their original string.

Monotonicity intentionally not enforced (movingTime already skips
non-positive intervals; reordering would be fabrication). No GpxData shape
change. Wired into parseTracks output in parse.ts.

Verified: gpx typecheck + lint clean, 81/81 tests pass, journal + planner
typecheck unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:10:57 +02:00
Ullrich Schäfer
99278fd305
feat(gpx): lenient point parsing + route (<rte>) support
Task groups 1–2 of gpx-parser-robustness. The parser trusted its input:
`parseFloat(attr ?? "0")` turned a missing lat/lon into a 0,0 Null Island
point (which passes range validation) and garbage into NaN that poisoned
distance and gain/loss totals; `<rte>`/`rtept` files (Garmin courses, many
exporters) parsed to zero track points and were rejected.

- `parsePoint`: skip a trkpt/rtept whose lat/lon is missing or non-finite
  (no more 0,0 default); a non-finite `<ele>` becomes `undefined` so it
  never leaks NaN into totals. Parsing stays parseFloat-lenient (trailing
  junk like `471.0m` still accepted), gated by Number.isFinite.
- Drop segments left with fewer than 2 points (render nothing / break
  distance math).
- Parse `<rte>` as track segments appended after `<trk>` segments, rtept
  handled identically — route-only files now import.

No GpxData shape change; well-formed files parse identically. Updated the
geom single-point test to the new drop-invariant.

Verified: gpx typecheck + lint clean, 76/76 tests pass, journal + planner
typecheck unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:06:35 +02:00
Ullrich Schäfer
d9eee37e39
build(deps): bump the production group across 1 directory with 35 updates
Bumps 35 dependencies in the production group; lockfile regenerated and deduped against latest main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 09:29:56 +02:00
Ullrich Schäfer
536a8f98b9
journal-elevation-profile: elevation chart + map↔chart sync
Implements the journal-elevation-profile change (specs/journal-elevation-profile):

- gpx: `elevationSeries(tracks)` → { d, e, lat, lng }[] with cumulative distance,
  downsampled (keeps first/last), empty when <2 points carry elevation.
- ElevationProfile: read-only SVG area chart (vertical gradient fill), highest/
  lowest summary + a hover readout. Reports hovered index (onActive) and clicked
  index (onSeek); draws a marker at the active index. Renders nothing for an
  empty series.
- RouteMapThumbnail: ActiveMarker (CircleMarker at the chart's active point),
  HoverTracker (route hover → nearest sample → onHoverIndex), Recenter (panTo on
  chart click). Props forwarded through ClientMap.
- Wired into the activity + route detail pages via a shared activeIndex/centerOn
  state; loaders expose the series (activity reuses the moving-time parse).
- i18n journal.elevation.{highest,lowest} in en + de.

Ascent/descent stay in the stat row (#532); the chart summary shows highest/
lowest to avoid duplication.

Tests: elevationSeries unit; ElevationProfile component (jsdom); e2e creates an
activity from an elevation GPX and asserts the chart renders. typecheck + lint +
unit (gpx 67, journal 315) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 15:34:45 +02:00
Ullrich Schäfer
0325d01bca
activity-stats: shared StatRow + canonical metrics across surfaces
Implements the activity-stats change (specs/activity-stats):

- gpx: `movingTime(tracks)` — moving seconds from trackpoint timestamps,
  excluding stationary spans + long gaps; null when no timestamps.
- stats.ts: pure formatters (distance/elevation/duration/speed/pace),
  sport-aware `deriveRate` (pace for foot sports, speed otherwise, speed
  default), and `activityStatItems` encoding the canonical order
  (distance · time · [moving] · pace/speed · ascent · descent; compact subset).
- StatRow: one shared presentational component (size sm/lg), adopted on the
  activity detail (full set; loader derives moving time), route detail
  (distance/ascent/descent), feed card + profile list (compact).
- i18n: journal.stats.* in en + de.

Tests: movingTime (stationary/gap exclusion, moving ≤ elapsed), deriveRate,
formatters, activityStatItems ordering + compact, StatRow render (jsdom).
typecheck + lint + unit (gpx 63, journal 311) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 14:48:36 +02:00
Ullrich Schäfer
7918ba052a
chore(ts): drop the last 2 \eslint-disable\ comments in prod code
After this, \`grep -rn 'eslint-disable' apps/ packages/\` returns 0
(excluding tests and node_modules).

**\`apps/planner/app/lib/brouter.ts\`** — \`while (true)\` in
\`readBodyWithCap\` tripped \`no-constant-condition\`. Replaced with
\`for (;;)\` which the rule explicitly allows. No behavior change.

**\`packages/gpx/src/parse.ts\`** — the sync \`parseGpx\` exported a
fallback path that did \`require(\"linkedom\")\` for non-browser sync
use, with an \`eslint-disable\` for \`no-require-imports\`. It was dead
code: \`packages/gpx/src/index.ts\` only re-exports \`parseGpxAsync\`,
and grepping the monorepo finds no caller of the sync version. Deleted
the function entirely; \`getDOMParser\` (the async helper) still serves
the async path with a clean ESM \`await import(\"linkedom\")\`.

Full repo: pnpm typecheck / lint / test all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 07:34:09 +02:00
Ullrich Schäfer
aef67aab7a
Include waypoint note in GPX export and extractWaypoints type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 07:38:55 +02:00
Ullrich Schäfer
c2abb64ee0
Add per-waypoint notes and nearby POI snap to Planner
- `note` field on Waypoint type, stored in Yjs Y.Map and exported as
  `<desc>` inside `<wpt>` in GPX
- Inline textarea note editing in WaypointSidebar with auto-resize,
  character counter (500 max), save-on-blur, Escape cancel
- Note indicator dot on map markers; note tooltip on hover
- `useNearbyPois` hook: fetches POIs within 500m of selected waypoint
  via Overpass proxy, 500ms debounce, AbortController, 60s rate-limit
  suppression
- NearbyPoiMarkers component: renders POI markers on map for selected
  waypoint with snap-to-POI on click
- Nearby section in WaypointSidebar: list with snap buttons, spinner,
  empty/rate-limited states, "Show more" toggle (5 → all)
- Unit tests for fetchNearbyPois bbox geometry and snap-to-POI Yjs
  transaction behaviour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 23:58:21 +02:00
Ullrich Schäfer
861701e881
Show POI details (phone, website, opening hours) on Journal route detail page
Waypoints snapped to OSM POIs in the Planner now carry their metadata all
the way through to the Journal:

- Extend Waypoint type with osmId and poiTags fields
- Extract osmId/poiTags from Yjs Y.Map in ExportButton and SaveToJournalButton
- Encode POI metadata as <trails:poi> extensions in GPX <wpt> elements
- Parse <trails:poi> extensions back in the GPX parser
- Display phone, website, opening hours, address on Journal route detail
- E2E test for the full roundtrip; seed endpoint now defaults to public visibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 23:32:38 +02:00
Ullrich Schäfer
2ac4014521
Standardize monorepo pipeline: test, lint, typecheck across all workspaces
Previously only 2-3 workspaces participated in each turbo task. This
expands test, lint, and typecheck to cover all 11 workspaces with
parallel execution and caching.

Test pipeline:
- Move from single root vitest to per-workspace test scripts via turbo
- Shared vitest config (vitest.shared.ts) with passWithNoTests
- Per-workspace configs re-export the shared base
- Mobile uses Jest (jest-expo + React Native Testing Library)
- Add node-environment GPX tests verifying linkedom fallback
- Add i18n mobile init tests
- Exclude apps/mobile from root vitest (uses Jest separately)

Lint pipeline:
- Add eslint lint script to all 9 previously unlinted workspaces
- Standardize all scripts to "eslint ." with shared root config
- Add .expo/ to global ESLint ignores
- Fix lint errors: unused imports in api/types, export parseGpx

Typecheck pipeline:
- Add "typecheck": "tsc" to all 8 packages
- Add @types/node (catalog) to gpx and db packages
- Fix mobile app.config.ts: remove deprecated experiments.monorepo
  and newArchEnabled (both default in Expo SDK 55)
- Add allowImportingTsExtensions to mobile tsconfig

Shared package compatibility (mobile-app Phase 1.3):
- Add linkedom as explicit dependency to @trails-cool/gpx
- Add initI18nMobile() export to @trails-cool/i18n
- Confirm @trails-cool/types is pure interfaces (no DOM deps)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:00:43 +02:00
Ullrich Schäfer
08070cdd90
Sync notes through GPX, Journal, and Planner roundtrip
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>
2026-04-11 03:35:40 +02:00
Ullrich Schäfer
857100ef94
Fix overnight flag lost on Journal roundtrip
Three gaps in the Planner → Journal → Planner roundtrip:

1. SaveToJournalButton: wasn't including isDayBreak when building
   waypoints for GPX generation
2. PlannerMap GPX import: wasn't setting overnight on Y.Maps when
   importing waypoints with isDayBreak
3. use-yjs initial waypoints: wasn't setting overnight from isDayBreak

Also widened the waypoint type throughout the chain (api.sessions,
session.$id, SessionView, extractWaypoints) to carry isDayBreak.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:27:09 +02:00
Ullrich Schäfer
5604fe1c82
Add per-day map coloring and remaining unit tests
- RouteMapThumbnail: Color route segments per day with alternating colors
  when dayBreaks are present, pass through from route detail page
- overnight.test.ts: 6 tests for isOvernight/setOvernight with Y.Doc
- daybreaks-extraction.test.ts: 3 tests for GPX → dayBreaks index extraction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:12:27 +02:00
Ullrich Schäfer
77174f192d
Add unit tests for computeDays and GPX overnight roundtrip
- compute-days.test.ts: 8 tests covering single day, two days, three days,
  ascent/descent, edge cases (first/last overnight, single waypoint, empty)
- generate.test.ts: 4 new tests for isDayBreak emission, GPX roundtrip with
  isDayBreak preserved, and splitByDay multi-track output

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:05:54 +02:00
Ullrich Schäfer
774e2b1ae3
Add multi-track GPX export option (splitByDay)
generateGpx() gains a splitByDay option that splits the track into
separate <trk> elements per day, each named "Day N: Start - End".
Track points are split at the closest points to overnight waypoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:04:48 +02:00
Ullrich Schäfer
efdb3c1973
Add multi-day data model, computeDays, and GPX roundtrip
- overnight.ts: Yjs helpers to set/clear/check overnight flag on waypoints
- compute-days.ts: Pure function splitting routes into DayStage[] at day breaks
- use-days.ts: Reactive hook mapping Yjs state into computeDays() input
- GPX generate: emit <type>overnight</type> for isDayBreak waypoints
- GPX parse: recognize <type>overnight</type> and set isDayBreak on waypoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:51:31 +02:00
Ullrich Schäfer
180f1fc4bd
Add unit tests for no-go area round-trip and waypoint extraction
15 tests covering:
- No-go area generation (namespace, structure, multiple areas)
- No-go area parsing (namespaced, non-namespaced, min 3 points, empty)
- Full round-trip: generate → parse → verify no-go areas match
- Complete planning state round-trip (waypoints + tracks + no-go areas)
- extractWaypoints: explicit waypoints, multi-segment, Douglas-Peucker,
  empty tracks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:50:57 +01:00
Ullrich Schäfer
6ccf7af7b1
Fix no-go area parsing for namespaced XML elements
querySelectorAll('nogo') doesn't match <trails:nogo>. Now matches
both unprefixed and prefixed element names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:26:54 +01:00
Ullrich Schäfer
4b711abaec
Add split export: Export Route vs Export Plan
Split button with default "Export GPX" (track only) and dropdown:
- Export Route: clean track for use in any app
- Export Plan: track + waypoints + no-go areas in GPX extensions
  (trails:planning namespace) for reimporting into the planner

Also:
- Add no-go area serialization to generateGpx (GPX extensions)
- Parse no-go areas from GPX extensions on import
- Initialize no-go areas in Yjs session from imported GPX
- Save to Journal now exports track only (consistent with Export Route)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:52:39 +01:00
Ullrich Schäfer
2b0b89add8
Use epsilon 0.005 for Douglas-Peucker waypoint extraction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:40:01 +01:00
Ullrich Schäfer
0e27d6eaef
Use Douglas-Peucker simplification for single-segment GPX
For GPX files with one long track segment, extracting only start/end
gives just 2 waypoints. Now uses Douglas-Peucker line simplification
(epsilon=0.05° ≈ 5km) to find significant turning points.

Result: berlin-dresden-radweg (249km, 5660 points) → 10 waypoints
that capture the route's key direction changes.

Multi-segment GPX files still use segment endpoints as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:36:13 +01:00
Ullrich Schäfer
cc90c32a0e
Extract shared extractWaypoints into @trails-cool/gpx
Both new.tsx and api.sessions.ts had duplicated track-segment
waypoint extraction logic. Moved to packages/gpx as
extractWaypoints(gpxData) — uses <wpt> elements when present,
falls back to start of each track segment + end of last.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:47:17 +01:00
Ullrich Schäfer
5f3d4ae846
Remove sync parseGpx, use parseGpxAsync everywhere
parseGpx (sync) needs browser DOMParser which doesn't exist on the
server — it silently failed in every server-side caller. Removed the
export and migrated all callers to parseGpxAsync. Updated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:32:48 +01:00
Ullrich Schäfer
d5bcecb345
Fix distance calculation for GPX files without elevation data
Distance was only stored in the elevation profile array, which was
empty when track points had no <ele> elements. Now computed via
haversine independently and exposed as gpxData.distance.

Tested: berlin-dresden-radweg-2021.gpx (5660 points, no elevation)
now correctly reports 248.8 km.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:32:47 +01:00
Ullrich Schäfer
6ed828ef9c
Fix PostGIS geom population: use parseGpxAsync + raw SQL
The initial approach (#150) didn't work because:
1. routes.server.ts used parseGpx (sync, needs browser DOMParser)
   instead of parseGpxAsync (uses linkedom on Node.js) — GPX parsing
   silently failed, so stats AND geom were never populated
2. Drizzle's customType doesn't pass SQL expressions through toDriver

Fixes:
- Switch to parseGpxAsync for all server-side GPX parsing
- Use db.execute() with raw ST_GeomFromGeoJSON SQL after insert
- Remove unused lineStringFromCoords helper from schema
- Share setGeomFromGpx between routes and activities
- Add unit tests for GPX→GeoJSON coordinate extraction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:32:07 +01:00
Ullrich Schäfer
62b5fb998e
Fix planner production server + use .ts extensions everywhere
- Fix createRequestHandler → createRequestListener (@react-router/node
  removed the old API)
- Move noEmit + allowImportingTsExtensions to tsconfig.base.json so all
  packages and apps inherit them consistently
- Add explicit .ts extensions to all remaining relative imports across
  packages and apps

Verified locally: node --experimental-strip-types server.ts starts and
responds 200.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 01:41:33 +01:00
Ullrich Schäfer
14b179bb0c Fix waypoint loading in Edit in Planner flow
Three issues fixed:

1. GPX parser used browser DOMParser which doesn't exist in Node/Vite SSR.
   Added async parseGpxAsync() using linkedom for server-side parsing.

2. Server-side session initialization stored waypoints in a Yjs doc
   instance separate from the Vite WebSocket plugin's doc store.
   Moved waypoint initialization to client-side: API returns parsed
   waypoints, client adds them to Yjs after sync.

3. GPX was encoded in URL params causing HTTP 431. Now the Journal
   creates a Planner session via API (POST body), and only passes
   compact waypoint coordinates in URL params.

Verified: Journal route → Edit in Planner → 4 waypoints loaded,
route computed (79.3km), elevation profile, Save to Journal ready.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:22:58 +00:00
Ullrich Schäfer
2cfa5e54e7
Implement shared packages (tasks 2.1-2.6)
- @trails-cool/types: Route, Activity, Waypoint, RouteVersion, RouteMetadata
- @trails-cool/gpx: GPX parser (XML→waypoints/tracks/elevation) and generator
  with round-trip test, haversine distance, elevation gain/loss computation
- @trails-cool/map: MapView (Leaflet + OSM/OpenTopoMap/CyclOSM layer switcher),
  RouteLayer (GeoJSON polyline rendering)
- @trails-cool/ui: Button (primary/secondary/ghost), Input (with label/error),
  Card components with Tailwind styling
- @trails-cool/i18n: react-i18next config with English + German translations,
  browser language detection, fallback to English

All 13 unit tests pass. Both apps build successfully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:09:35 +01:00
Ullrich Schäfer
30d40de7cf
Complete monorepo toolchain setup (tasks 1.1-1.7)
- Turborepo + pnpm workspaces with catalog for shared dependency versions
- TypeScript strict config (base + per-package extends)
- Tailwind CSS v4 via @tailwindcss/vite plugin
- ESLint + Prettier shared config
- Planner app scaffolded with React Router 7 (port 3001)
- Journal app scaffolded with React Router 7 (port 3000)
- Both apps build and start successfully via turbo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:12:57 +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