MarschMellow

About

MarschMellow plans and tracks walking, running, and cycling activities on OpenStreetMap data.

Build

Version
0.4.1-2
Commit
c086c37
Built

Latest changes

  1. v0.4.1-2

    • addedSaved trips now show the estimated time on the detail page, mirroring the planner result card. Derived client-side from distance_m / pace_kmh × 60 — same math as the planner, and TripDetail already carries pace_kmh (added with the live-tracking work), so no DB column, no migration. Inserted between Distance and Ascent / descent to match the planner card's row order.
  2. v0.4.1-1

    • addedSilent token refresh. The backend now exposes POST /auth/refresh, which consumes the long-lived refresh cookie and mints a fresh access cookie + a rotated CSRF token (the refresh cookie itself stays put). The frontend api() helper automatically calls /auth/refresh on a 401, retries the original request once on success, and dedupes concurrent refresh attempts with a single in-flight promise. Sessions now last the full REFRESH_TOKEN_DAYS window (default 14 days) of active use — users no longer get logged out after the access cookie's short TTL. The access cookie's TTL (ACCESS_TOKEN_MINUTES, default 15) is unchanged on purpose: it remains the stolen-cookie containment window.
    • changedRefresh-aware 401 handling: bootstrap auth paths (/auth/login, /auth/register, /auth/confirm, /auth/forgot, /auth/reset, /auth/refresh) are excluded from the refresh-and-retry interceptor so a wrong-password login still surfaces as 401 instead of triggering a phantom refresh round trip. Signed-out page loads (no mm_csrf cookie present) also skip the refresh attempt.
  3. v0.4.1-0

    • addedSelf-service account deletion. A new "Danger zone" section at the bottom of the Account page exposes a Delete my account button. Confirming the explicit prompt sends DELETE /account, which mirrors the admin user-delete path: removes the matching whitelist entry, deletes the user (CASCADE wipes trips, activities, forum posts, replies, favourites, group memberships, and visibility rows), clears the session cookies on the response, and redirects the browser to /login. Re-registration with the same email requires an admin to re-whitelist it.
  4. v0.4.0-0

    • addedLive position on the map. A "Track my location" pill in the top-left of the map (planner, trip detail, activity detail) starts a high-accuracy navigator.geolocation.watchPosition watcher. While on, the user's current position renders as a blue dot with a translucent accuracy halo that scales with the basemap zoom. Toggling off clears the watcher and the dot. The browser surfaces its native permission prompt on first use; the button hides on browsers without Geolocation.
    • addedRemaining distance + time to the endpoint, shown next to the route summary on the planner page and trip-detail page when both a route and a live position are present. Recomputes on every position update by projecting the user onto the polyline (closest point on the line) and summing the route's remaining length, then dividing by the route's pace_kmh (Shenandoah bucket for walks, flat for run/bike). Activity detail shows the position dot but no remaining metrics — historical tracks have no endpoint to reach.
    • addedOff-route hint flashes a discreet "≈ N m off the route" when the perpendicular distance from the user to the polyline exceeds NEXT_PUBLIC_OFF_ROUTE_THRESHOLD_M (build-time default 50 m). Adjust at build time if your routes routinely run alongside parallel paths.
    • changedBackend: RouteResponse and TripDetail gain a pace_kmh field (km/h) carrying the same pace used for the displayed duration. Frontend uses it to derive remaining time without re-implementing the Shenandoah pace table client-side.
  5. v0.3.5-0

    • addedSigned-out visitors now see a "Log in" button in the header, left of the language picker. It appears on every page (not just the home page) and only when /auth/me has confirmed there is no session — the button waits for the auth check to resolve before rendering so signed-in users never see a flicker. Reuses the existing `nav.logIn` i18n key, so the label reads "Log in" in English and "Anmelden" in German.
  6. v0.3.4-0

    • addedForum threads can carry an optional image attachment. The new-post form on /forum has a file picker (PNG / JPEG / GIF / WebP) with inline preview, size hint, and a remove button; the bytes are stored in Postgres (BYTEA columns on forum_post — migration 0007) and rendered inline on the thread detail page via the new GET /forum/{id}/attachment endpoint (auth-required, Content-Disposition: inline, one-day cache). Replies stay text-only.
    • addedFORUM_ATTACHMENT_MAX_BYTES env var (default 5 * 1024 * 1024 = 5 MiB) caps the upload size. The cap is also surfaced via GET /config/planner-defaults so the new-post form can show the live limit and refuse oversize images client-side before the round-trip; the backend re-enforces with a 413 regardless.
    • changedPOST /forum is now multipart/form-data (subject + body as form fields, optional file). Old clients sending JSON will get a 422; the bundled frontend was updated in the same release. The response shape gained `attachment` (PostDetail) and `has_attachment` (PostSummary).
  7. v0.3.3-1

    • fixedWalking time-mode planner now matches the requested duration. Previously the planner converted minutes → distance once using the easiest-bucket pace, then optimised for distance closeness only, ignoring which bucket the chosen route actually landed in. A 60-min walk that came back as moderately_strenuous showed ≈ 70 min on the result card; strenuous routes overshot by ~25 %. The reroll loop now scores routes by realised duration (distance ÷ chosen-bucket pace) and aims the next GraphHopper ask at the distance that hits target_minutes for the bucket the previous attempt landed in, converging within the existing ±10 % PLANNER_DISTANCE_TOLERANCE band. Run and bike are unaffected — their per-activity paces are flat, so distance and duration closeness are equivalent.
  8. v0.3.3-0

    • addedAdmin Accounts page (replaces the old Whitelist page). Lists every email known to the system — registered users and whitelist-only pre-registration entries — in one view, each row showing whitelist status, activation state, email-confirmation state, role, and display name. Admins can whitelist a new email, remove a whitelist entry (which also deactivates the matching user, preserving the existing behaviour), deactivate or reactivate a user without touching the whitelist, and permanently delete a user (cascades trips, activities, forum posts, replies, favourites, group memberships, visibility rows — and removes the matching whitelist entry so re-registration requires explicit re-whitelisting).
    • addedSafety rails: admins cannot deactivate or delete their own account through this UI (the buttons disable on the admin's own row, and the backend returns a 400 with a clear message as a defence in depth). Reactivation is allowed self-targetingly because it's idempotent for an already-active account.
    • changedAdmin menu link "Whitelist" is gone — replaced by "Accounts" pointing at the unified page. Existing /admin/whitelist GET/POST/DELETE endpoints are kept for backwards compatibility and still tested; the frontend now uses /admin/accounts for the list view and the new /admin/accounts/users/{id}/{deactivate,activate} POSTs plus DELETE /admin/accounts/users/{id}.
  9. v0.3.2-0

    • addedForum page for feedback, ideas, and wishes. Every signed-in user can start a thread (subject + body) and reply to existing ones. Threads are flat — one level of replies — to keep mobile readable. The list page shows reply counts and last-activity timestamps; clicking opens the thread detail with the reply form inline.
    • addedEmail notifications on new replies. When someone replies to a thread, every previous participant (the original poster and everyone who replied earlier) receives an email — except the new reply author. Deactivated and unconfirmed accounts are skipped. The mail body shows the new replier's display name (falling back to email) and a direct link to the thread.
    • addedAuthor/admin moderation: the thread starter can delete their own thread (replies cascade out with it), reply authors can delete their own reply, and admins can delete anyone's post or reply. Non-author non-admin viewers get a 404 on delete attempts — no existence leak.
    • changedBackend: new /forum API (GET /, POST /, GET /{id}, DELETE /{id}, POST /{id}/replies, DELETE /{id}/replies/{rid}). Migration 0006 adds forum_post and forum_reply tables with CASCADE on user deletion. Reply notification fan-out happens in a FastAPI BackgroundTasks — the POST returns immediately while the SMTP sends complete in the background.
  10. v0.3.1-0

    • addedSelf-service password reset. The login page has a new "Forgot your password?" link to /forgot, where the user enters their email and submits. POST /auth/forgot always returns the same generic 200 — we never reveal whether the address is registered, unconfirmed, or deactivated (privacy + anti-enumeration). When the account is real, active, and email-confirmed, a reset link is emailed via the existing SMTP path; the link lands on /reset and accepts a new password (8 chars minimum). The reset token is a new "reset"-kind JWT with its own TTL (RESET_TOKEN_HOURS, default 1).
    • added/auth/forgot is rate-limited via the shared login_limiter (8 attempts per IP+email per 5 minutes). When the limit hits, the response is still the same generic 200 — not a 429 — so the rate limit itself can't be used to fingerprint accounts.
    • addedRESET_TOKEN_HOURS env var (default 1) controls how long the reset link stays valid.
  11. v0.3.0-0

    • addedAccount menu — every signed-in user can now self-edit a display name, a home map area (point + zoom), and their password. The page lives at /account (linked from the burger menu) and uses three independent save buttons so each section commits separately. Password change requires the current password (defends against session-hijack drive-by rotation), enforces an 8-character minimum, and rejects setting the new password equal to the current one.
    • addedHome map area drives the planner's default view. After login the /plan page opens centered on the user's saved home_lat/lon at home_zoom (falling back to the previous Zug centre when unset). A click on the map still takes priority — once you pick a start point that wins until you reload.
    • addedDisplay name replaces email in the UI when set. The header pill, burger-menu user block, shared-track "by …" labels, and the "shared with you by …" callouts on trip / activity detail all prefer the owner's name and fall back to email otherwise. The fallback is centralised in two helpers (displayNameOf / ownerLabel in lib/auth.ts).
    • changedBackend: new /account router with GET /account/me, PATCH /account (name + home, with null-clears semantics), POST /account/password. /auth/me and /auth/login now also surface name + home_lat/lon/zoom so the app shell has them without a second request. Trip and activity summaries gained owner_name alongside owner_email. Migration 0005 adds the four nullable columns (name, home_lat, home_lon, home_zoom) to user_account.
  12. v0.2.5-0

    • fixedReverted the Node.js requirement from >=24.15.0 back to >=18.19.0 — the 0.2.4-2 bump broke `bash deploy/debian/build.sh` on hosts that only have Node 18 (Ubuntu 24.04 default). frontend/Dockerfile is `node:20-alpine` again; package.json engines.node is `>=18.19.0`, @types/node back to ^22.9.0; .nvmrc pins 18.19; the deb-build minimum is 18.19.0. Verified under Node 18.19.1: npm install, tsc --noEmit, next build, and the build.sh gate all pass (one EBADENGINE warning from a transitive dep wanting >=20.19; non-blocking).
  13. v0.2.4-2

    • changedNode.js requirement raised to 24.15. frontend/Dockerfile uses `node:24-alpine`; the deb build script (deploy/debian/build.sh) now refuses to run below 24.15; package.json gained an `engines` field (`node >=24.15.0`) and bumped `@types/node` to ^24.0.0 to match the runtime; a `.nvmrc` pinning 24.15 was added so `nvm use` in frontend/ picks the right toolchain. Operators rebuilding the deb need to install Node 24.x first (NodeSource: `curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt install -y nodejs`).
  14. v0.2.4-1

    • fixedbackend.env.example and frontend.env.example are now actually shipped in the debs. In 0.2.4-0 the template files existed but deploy/debian/build.sh was not updated to copy them into /etc/marschmellow/, so a fresh install produced none. The build script now stages both files in the backend and frontend packages. They are intentionally not listed in conffiles so each upgrade refreshes them without a dpkg prompt — operators are expected to read, not edit, them.
  15. v0.2.4-0

    • added/etc/marschmellow/backend.env.example and frontend.env.example — exhaustive, commented templates listing every supported configuration variable with its default. The backend example covers DATABASE_URL, BACKEND_SECRET_KEY, all HTTP/cookie/CORS knobs, SMTP, GraphHopper, the five PACE_WALK_*_KMH per-difficulty paces, PACE_RUN_KMH / PACE_BIKE_KMH, planner tuning (DEFAULT_TRIP_*, MAX_PLANNER_REROLLS, PLANNER_DISTANCE_TOLERANCE), the four DIFFICULTY_*_MAX bucket boundaries, and the advanced token-lifetime overrides. The frontend example documents the three runtime variables (HOSTNAME, PORT, NODE_ENV) plus BACKEND_INTERNAL_URL, and lists every NEXT_PUBLIC_* build-time variable (API URL, app name, build metadata, track styling, map tiles / styles) with a reminder that NEXT_PUBLIC_* values are inlined at `next build` time.
    • changedShipped /etc/marschmellow/backend.env aligned with the current schema: PACE_WALK_KMH replaced by the five PACE_WALK_*_KMH per-difficulty paces; DIFFICULTY_EASY_MAX_ASCENT_PCT / DIFFICULTY_MODERATE_MAX_ASCENT_PCT replaced by the four Shenandoah DIFFICULTY_*_MAX thresholds; PLANNER_DISTANCE_TOLERANCE added. The exhaustive variable list moved to backend.env.example; this file now ships only the curated subset operators typically tune.
  16. v0.2.3-0

    • addedApp display name is now configurable via the NEXT_PUBLIC_APP_NAME env var (default `MarschMellow`). It drives the HTML <title>, header brand, footer, and the brand mentions inside translated strings (home page lede, About page description). NEXT_PUBLIC_* vars are inlined at build time, so the name only updates on a rebuild — same convention as NEXT_PUBLIC_BUILD_VERSION. The static `public/manifest.webmanifest` still hardcodes the name; replace it at deploy time if you need the PWA install prompt to match.
    • removedThree feature info boxes on the home page (activity-aware basemap, distance accuracy, sharing) are gone. The hero, CTAs, and registration hint remain. The matching i18n strings (home.feature1Title/Body, feature2*, feature3*) were also removed.
  17. v0.2.2-0

    • removedPace input is gone from the planner form. Walking duration is now derived from the Shenandoah pace table for the bucket the chosen route lands in (1.5 mph easiest down to 1.2 mph strenuous / very strenuous, configurable via PACE_WALK_EASIEST_KMH … PACE_WALK_VERY_STRENUOUS_KMH). Run and bike still use flat per-activity defaults (PACE_RUN_KMH, PACE_BIKE_KMH). The planner's time → distance conversion uses the easiest-bucket walk pace (difficulty isn't known until the route is built), so a route that lands in a harder bucket may exceed the requested time; the displayed estimate reflects the actual bucket.
    • removedPACE_WALK_KMH env var (replaced by the five PACE_WALK_*_KMH per-difficulty settings) and the pace_kmh field on POST /trips/plan (silently ignored if old clients still send it).
    • changedGET /config/planner-defaults no longer returns walk_kmh / run_kmh / bike_kmh — the form has no pace control to seed.
  18. v0.2.1-0

    • changedDifficulty now follows the Shenandoah National Park hike-difficulty formula: rating = √(2 × elevation_gain_ft × distance_mi). The previous three buckets (easy / moderate / hard) become five — easiest, moderate, moderately strenuous, strenuous, very strenuous — with default rating thresholds at 50 / 100 / 150 / 200 (configurable via DIFFICULTY_EASIEST_MAX, DIFFICULTY_MODERATE_MAX, DIFFICULTY_MODERATELY_STRENUOUS_MAX, DIFFICULTY_STRENUOUS_MAX). Existing saved trips were name-remapped (easy → easiest, hard → strenuous); new saves use the Shenandoah bucket directly.
    • addedLanguage picker — every UI string is now available in English and German, switchable from a small EN/DE pill in the header next to the theme toggle. The choice persists in localStorage; without an explicit choice the app uses the browser's `navigator.language`. A `?lang=en|de` URL query parameter can also force a language for the visit (and persists it), useful for sharing localized links.
    • changedAll page texts, form labels, button captions, error messages, confirm dialogs, and the route-overview labels are translated. The route's difficulty label reads `leicht` / `mittel` / `schwer` in German; activity kinds become `Wandern` / `Laufen` / `Radfahren`. Dates on the trip / activity detail and changelog use the active locale's format.
    • changedUI refresh: refined typography (Inter), refreshed colour palette, softer borders and shadows, larger rounded corners on cards. The home page now leads with a proper hero — gradient title, tagline pill, and a feature row highlighting the activity-aware basemap, ±10 % distance accuracy, and group sharing.
    • addedHamburger menu in the header packs all navigation (Plan / Trips / Stats / About / admin links / log out) into a single dropdown, with a translucent backdrop, Esc to close, and smooth open / close animation. Works on every viewport size.
    • addedTheme picker — sun / moon / auto icons cycle Light, Dark, and System. The choice is persisted in localStorage and applied before first paint by an inline bootstrap script in the layout, so there's no flash of the wrong theme on reload.
    • changedButtons, inputs, checkbox cards, and trip / activity rows pick up the new tokens automatically: subtle shadows, accent-coloured focus rings, hover elevation, and a checked state on the visibility multi-select that highlights selected groups.
  19. v0.1.11

    • removedDifficulty selector dropped from the planner. Plans are now driven only by distance or time; the resulting difficulty bucket is still computed from the route's ascent percentage and shown in the route overview / trip detail. The difficulty buckets stay configurable via DIFFICULTY_EASY_MAX_ASCENT_PCT and DIFFICULTY_MODERATE_MAX_ASCENT_PCT.
    • changedSaved trips now have their difficulty computed server-side from the geometry being saved (previously the client picked it). Old trips keep their stored difficulty; new trips reflect the current settings. The reroll loop is correspondingly simpler — it picks routes purely by distance closeness.
    • fixedMAX_PLANNER_REROLLS now actually triggers retries when GraphHopper's round_trip fails per-seed. Previously a single "Could not find a valid point after 3 tries" (an internal GraphHopper limit, unrelated to our reroll count) aborted the planner with a 502 on the first attempt, ignoring the configured reroll budget. The planner now distinguishes per-seed routing failures from real engine outages: per-seed failures continue the reroll loop with a fresh seed; if all attempts exhaust, a 422 with operator guidance is returned instead of a 502. Persistent failures (network error, 5xx) still abort immediately to avoid hammering a broken upstream.
    • fixedDifficulty selector now actually steers the planner. When no reroll lands inside the requested bucket (e.g., asking for "hard" in flat terrain), the planner returns the route whose ascent percentage is closest to the requested band — "hard" → steepest available, "easy" → flattest available — instead of falling back on distance closeness alone. The DIFFICULTY_EASY_MAX_ASCENT_PCT and DIFFICULTY_MODERATE_MAX_ASCENT_PCT settings now drive this selection logic, not just the post-classification of the chosen route.
    • changedDifficulty is now derived purely from the route's ascent expressed as a percentage of its distance: ≤ 2 % easy, ≤ 5 % moderate, > 5 % hard. The previous activity-specific weighted score (which mixed distance and ascent) is gone, so a long-but-flat ride is now correctly classified "easy" rather than tipping into "moderate" or "hard" because of length alone.
    • addedDIFFICULTY_EASY_MAX_ASCENT_PCT and DIFFICULTY_MODERATE_MAX_ASCENT_PCT env vars (defaults 2.0 and 5.0) make the bucket boundaries configurable per deployment.
    • addedAverage-pace input box now also appears in the planner's distance-target mode (previously only shown in time mode). The pace drives the estimated-time preview shown beneath the field, and is sent with the plan request so the result-card duration matches the previewed estimate.
  20. v0.1.10

    • changedBumped to version 0.1.10
  21. v0.1.10-rc2

    • fixedEstimated time on the planner result card now uses the user's chosen pace (or the configured PACE_*_KMH default) instead of GraphHopper's internal profile speed. Asking for 60 min @ 4 km/h now shows ≈60 min on the card; previously it showed GraphHopper's ~48 min estimate.
  22. v0.1.10-rc1

    • fixedPlanner now honors the entered distance / time more accurately. Each attempt's GraphHopper response is scored against the request, and a route within ±10 % of the target is preferred over a difficulty-only match. The next reroll's GraphHopper ask is compensated for the previous attempt's over- or undershoot, so urban round-trips that previously inflated by 20–30 % now converge inside the tolerance band.
    • addedPLANNER_DISTANCE_TOLERANCE env var (default 0.10) configures the acceptable deviation.
  23. v0.1.8-alpha4

    • addedActivity-aware basemap: walking and running show OpenTopoMap (contour lines, hillshading, hiking trails); cycling shows the openfreemap "Liberty" general-purpose map. Switches live as you change the activity in the planner.
    • addedUser groups with per-track visibility. Admins create groups and manage membership; users can mark a saved trip or imported activity as private or share it with one or more of their groups.
    • addedStats now have a Mine / Including shared toggle so group members' shared activities can count toward your totals.
    • addedImported (GPX) activities can now be renamed, just like saved trips.
    • addedImported activities can be exported as GPX. The original uploaded bytes are served byte-for-byte; for live-recorded activities the GPX is synthesized from the stored geometry.
    • addedAbout page that shows the build version (sourced from the deb build script's VERSION argument) and a changelog of recent updates.
    • changedMobile-friendly layout: viewport meta, larger touch targets (44px), iOS-zoom-resistant inputs, and responsive stacking on the planner, trip detail, activity detail, and admin pages.
    • changedTrack lines on the map are now red (#dc2626) and bold (6 px) by default, configurable via NEXT_PUBLIC_TRACK_COLOR / WIDTH / OPACITY at build time.
    • fixedThe route line is now displayed reliably when the cycling basemap is active. The map is fully rebuilt on basemap-kind change to dodge a setStyle quirk that dropped imperatively-added GeoJSON sources when transitioning between raster and vector styles.
    • changedChangelog moved to a single central file (frontend/CHANGELOG.json). The About page imports this file at build time, so a new release just needs one file edit.
  24. v0.1.0

    • addedInitial release: GraphHopper-backed round-trip planner, GPX import, per-user trips and activities, email-confirmed accounts behind an admin-managed whitelist, and personal stats.

Map tiles: OpenTopoMap (CC-BY-SA) for hiking, openfreemap (Liberty) for cycling. Data © OpenStreetMap contributors. Routing by GraphHopper.

← Back to home