- Ensure PostGIS: the postgis image doesn't auto-create the extension in
POSTGRES_DB, so drizzle-kit push's geometry columns died silently (behind
its spinner). Create it explicitly before db:push in e2e + image-smoke.
- e2e: wipe leftover containers/volumes on the persistent per-slot DinD at
job start (stale pgdata left postgres unhealthy).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- verify the segment download is non-empty (retry x3); brouter.de is flaky
and a silent empty download surfaced only as a 240s routing timeout.
- bump routing readiness wait 120s->240s (segment load on a busy runner).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- @trails-cool/journal test --testTimeout=20000 (heavy fedify/drizzle/RR
cold import; payloads + api.v1.routes tests hit the 5s default).
- e2e/image-smoke db:push piped through cat so drizzle-kit prints the real
error instead of a TTY spinner that swallows it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Runners migrated to 2 isolated runner+dind pairs (capacity 1 each): each job
gets its own docker daemon (job-to-job isolation; host daemon untouched).
- e2e: seed BRouter segment into the compose volume via a DinD container
(no cross-daemon bind mount); services addressed at host.docker.internal;
compose project pinned to 'trails'.
- image-smoke: db:push reaches postgres at host.docker.internal; the
--network-host journal container uses localhost; health-check via
host.docker.internal.
DOCKER_HOST is also injected globally via the runner's config (runner.envs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same runner cold-import slowness as @trails-cool/db: the gpx→fit round-trip
test's import exceeds vitest's 5s default under CI CPU contention.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified empirically: from a job container the DinD daemon is reachable at
host.docker.internal:2375 (host-gateway 172.17.0.1), not docker-in-docker
(a host-network alias absent inside DinD). Runner container.network reverted
to "" (a host network name is not found inside DinD and fails every job).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- forgejo/upload-artifact -> full code.forgejo.org URL (not on github.com,
where DEFAULT_ACTIONS_URL sent it).
- e2e + journal-image-smoke: set DOCKER_HOST=tcp://docker-in-docker:2375 so
the job's docker CLI reaches the DinD daemon (no /var/run/docker.sock).
(Runner config also now attaches jobs to the runner network.)
- @trails-cool/db test: --testTimeout=20000; the cold drizzle/postgres import
exceeds vitest's 5s default under runner CPU contention.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- gitleaks: GITLEAKS_ENABLE_UPLOAD_ARTIFACT=false (its @actions/artifact v2
upload throws GHESNotSupportedError on Forgejo; the scan itself passes).
- swap actions/upload-artifact@v7 -> forgejo/upload-artifact@v4 (v4+ artifact
API is unsupported on non-github.com servers).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ci.yml: all 10 jobs (security/dockerfile/openspec/typecheck/lint/test/
build/visual/e2e/image-smoke); drop merge_group, gitleaks via full URL,
visual-diff PR comment via Forgejo API.
- cd-apps/cd-brouter/cd-infra/cd-staging: drop GitHub Environments, GHCR
push auth via DEPLOY_GHCR_TOKEN. cd-staging flags Forgejo PR-event and
peter-evans comment-action risks for validation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First step of porting CI/CD from GitHub Actions to the self-hosted
Forgejo at git.ullrich.is. Proves the trails-forgejo runner executes
a real job (checkout + pnpm + typecheck) before porting the full suite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sync the wahoo-import "FIT to GPX conversion" requirement into the main
spec and move the completed change to
openspec/changes/archive/2026-07-16-fit-parsing-hardening.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the fit-parsing-hardening change. The shared converter now:
- splits output into <trkseg>s on timer stop/start events, falling back to
record gaps > 5 min, so downstream moving-time never bridges a pause;
- slices records into per-session windows (multisport → one activity, one
segment per session), single-session behavior unchanged;
- validates records: finite/in-range coordinates required, timestamp
required, non-finite altitude dropped (point kept), prefers
enhanced_altitude;
- returns { gpx, sport }, mapping FIT session sport/sub-sport to a Journal
SportType (first session wins), consumed by the Wahoo importer + webhook
and Garmin importer as a fallback when the provider sends no type.
Tests drive the converter via mocked fit-file-parser output (segmentation,
validation, sport mapping, no-GPS) per the chosen fixtures approach.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deleting waypoints down to <2 left the last computed route rendered —
nothing cleared the stale geometry (recompute only runs for >=2). Add a
waypoints observer that drops the computed geometry once fewer than two
waypoints remain, via a new clearComputedRoute (keeps the routing
profile, unlike clearRouteData). Covers all delete paths (sidebar, map,
undo).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fetchNearbyPois defaulted sessionId to the placeholder "nearby", which
/api/pois's requireSession rejects (401 Unauthorized) — so the nearby-POI
lookup for a selected waypoint always failed on real deployments. Thread
the live planner sessionId through useNearbyPois → fetchNearbyPois →
queryPois (same session the main POI markers already use), and make
sessionId a required arg so the placeholder can't silently return.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ColoredRoute rendered one interactive-registered <Polyline> per
coordinate pair — ~14.6k on a 485 km route — flooding Leaflet's SVG
layer and event-target map. Run-length group consecutive same-color
segments into a single polyline each (new pure buildColorRuns helper,
unit-tested). Discrete modes (surface/highway/…) collapse to a handful;
the elevation gradient is quantized into 24 buckets so its runs merge
too. Rendering is otherwise identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the waypoint marker into a React.memo'd component. The marker's
position/icon were new references every render, so any unrelated
PlannerMap re-render during a drag (e.g. the route-hover chart-sync
state update) re-applied the stale saved position mid-drag — orphaning
the drag so it never committed and the pin jumped back on release.
Memoizing means unrelated re-renders no longer touch the marker, so the
drag commits and the moved position persists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Waypoint markers had no click listener, so Leaflet routed the click to
the map — MapClickHandler then added a new waypoint on top of the one
you clicked (and made markers feel unresponsive). Add a click handler
that consumes the event, so Leaflet treats the marker as the target and
suppresses the map click.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Docked-but-collapsible placement (the chosen direction):
- A collapse toggle (chevron) in the chart header; state persists to
localStorage.
- Collapsed = a thin summary bar: a mini sage sparkline + distance and
ascent/descent + an expand toggle. Reclaims map space on demand.
- Summary figures come from the same authoritative routeStats the
sidebar uses (distance, elevationGain, elevationLoss), so the numbers
match the sidebar exactly — not recomputed from the raw elevation
points (which over-counted ascent ~2x from noise).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Drag-to-zoom on the chart now tracks via window mousemove/mouseup, so
it keeps going when the pointer leaves the chart and still completes if
released outside it (previously mouseleave aborted the drag and an
outside release was lost). Listeners are detached on mouseup/unmount.
- Move the "reset zoom" button from top-right (which overlapped the
color-mode dropdown) to the bottom-right of the chart, and restyle it
on tokens.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The topbar's "Route wird berechnet…" text appeared/disappeared with
routing, shifting the topbar layout. Move it onto the map as a floating
status pill (spinner + label, top-center, pointer-events-none) that
appears temporarily without affecting the topbar. Restyle the ambient
top-edge progress bar to accent tokens. Drop the now-unused `computing`
prop from Topbar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The restyled Export split-button's dropdown toggle got aria-label
"Export GPX", colliding with the main button's text and breaking the
E2E selector that clicked the old "▾" glyph. Give it a dedicated
exportOptions label (en + de) and target it in the E2E test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring the two slotted topbar actions onto the design system so the bar
reads as one cohesive surface:
- SaveToJournalButton: primary Button primitive; "saved" uses the
accent, error uses a new --color-danger token, and the return link
gets secondary token styling.
- ExportButton: token split-button (secondary look, chevron icon,
single divider) and a token dropdown menu (raised surface, soft
shadow, muted descriptions).
- New --color-danger token (#a03c3c, the no-go hue at full strength)
for error text.
Behavior and i18n unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The elevation chart's 10-mode color selector now uses the Select
primitive (token-styled, consistent with ProfileSelector). Chart-header
chrome (top border, title link, legend text) moved onto tokens too.
Kept as a dropdown rather than a segmented control — 10 modes don't fit
a segmented toggle. Canvas/legend data-viz colors are unchanged (task
1.5).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New Select primitive in @trails-cool/ui: token-styled native <select>
(appearance-none + overlaid chevron for a consistent closed control),
sm/md sizes, sibling of Input. Unit-tested + added to /dev/ui.
- ProfileSelector uses Select (size sm) with token label; the profile
label now hides on small screens. Completes the topbar's migration
onto the design system.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app body is h-screen overflow-hidden (for the full-screen map
editor), which clipped the taller gallery so it couldn't scroll. Give
the gallery route its own full-height scroll container.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>