Go + Templ + HTMX app that checks the weather (Open-Meteo, MET Norway fallback), scores run/ride/walk with plain rules, and has a local open-weight model (Gemma 4 E2B via any OpenAI-compatible server) write the plan. Routes are loops from your door (BRouter) plus signposted OpenStreetMap routes (Overpass), with GPX export.
workout-suggester
Tell it where you are, how hard you want to go and for how long. It checks the weather and tells you whether to run, ride or walk, or to stay in and do HIIT when it's grim. It also gives you a signposted OpenStreetMap route that fits the distance, as a GPX file for your watch.
The advice comes from Gemma 4 E2B running on your own machine. It's a Go + Templ + HTMX app in one ~9 MB binary.
How it works
- Weather: Open-Meteo (open source, no key) gives current conditions and the next 24 hours. If it's overloaded, the app retries once and then falls back to MET Norway. MET Norway has no gusts or rain chance outside the Nordics, so gusts are estimated (shown as ≈), the rain chance shows as unknown, and sunrise/sunset are computed locally. If both fail, it uses the last forecast it had, up to 3 h old. Place search falls back from Open-Meteo to Nominatim.
- Scoring (plain Go rules, not the model): each activity starts at 100 and loses points for rain, gusts, cold, heat, ice, fog and darkness. The amount depends on the activity, e.g. 50 km/h gusts cost a ride 40 points and a run only 15. Below 40 everywhere, you're sent inside. The app also looks up to 12 hours ahead for a clearly better daylight window ("better at 17:00").
- Routes:
- Loops from your door: BRouter, an open-source OpenStreetMap router, generates round trips of your target length in three directions. It uses a foot profile for runs and walks and a bike profile for rides. The first loop heads out into the wind so the way home has a tailwind. Each loop shows its climb and how much of it is paved.
- Signposted routes nearby: the Overpass API finds named cycling and walking routes (Belgian fietsroutes, wandelroutes) and the nearest knooppunt (node-network junction). They're matched to your distance as loops, laps or out-and-back. The way to the start counts towards your distance, and long-distance trails like the Streek-GR are left out.
- Coach: all of the above goes into the system prompt as a short sheet of facts. Gemma writes the plan: why, when, the session (warm-up, main set, cool-down), route, what to wear and a plan B. You can ask follow-ups ("only 30 min", "make it harder").
The split is deliberate: small models get numbers wrong, so code decides the facts and the model explains them and builds the workout.
Why open
- The model runs on your machine. Your location, your notes ("sore knee") and your questions never reach an AI company. There's no API key and nothing to pay per token.
- Only rough coordinates leave the machine. Weather lookups use about 1 km precision, signposted-route lookups a ~2 km grid, and loop planning ~100 m. Distances to routes are computed locally with your exact position.
- Every part can be swapped or self-hosted. Point
LLM_BASE_URLat oMLX, llama.cpp, Ollama or LM Studio and use any model. Open-Meteo, Overpass and BRouter are open source too, soOPEN_METEO_URL,OVERPASS_URLandBROUTER_URLcan point at your own instances. - Routes keep working without a connection. Route data is cached on disk for a week. GPX export means your phone can stay in your pocket.
- OpenStreetMap knows the local routes, down to the knooppunten network and park loops. No closed fitness API gives you that for free.
Run it
You need an OpenAI-compatible model server. On a Mac:
omlx serve # or: llama-server -hf unsloth/gemma-4-E2B-it-GGUF:Q4_K_M --no-mmproj
cp .env.example .env # set LLM_BASE_URL / LLM_MODEL (and LLM_API_KEY if your server wants one)
make run # → http://127.0.0.1:3000
Tap 📍 Here (the browser asks for your location) or type a town.
| Setting | Default |
|---|---|
LLM_BASE_URL |
http://127.0.0.1:8000/v1 (oMLX) |
LLM_MODEL |
first model the server lists |
LLM_API_KEY |
none |
DEFAULT_PLACE |
none (prefills the town field) |
ADDR |
127.0.0.1:3000 |
BROUTER_URL |
https://brouter.de/brouter (self-hostable; - turns loops off) |
OVERPASS_URL |
https://overpass-api.de/api/interpreter (space-separated list, tried in order) |
OPEN_METEO_URL, OPEN_METEO_GEOCODE_URL |
the public Open-Meteo API |
gemma-4-E2B-it-qat-4bit answers in about 10 s on an M-series Mac. E4B follows the format more closely.
Develop
make test # unit + handler tests, no network or model needed
uv run scripts/e2e.py --url http://127.0.0.1:3000 [--mobile] [--dark] # browser test + screenshots
make dist # cross-compile for Linux, macOS, Windows
cmd/workout-suggester entry point
internal/weather Open-Meteo + MET Norway fallback, local sunrise maths (rounded coords, 15 min cache)
internal/coach scoring rules, distance targets, the prompt
internal/routes BRouter loops, Overpass client (retries, disk cache), route matching, GPX
internal/session per-browser plan + chat
internal/llm minimal OpenAI-compatible streaming client
internal/web handlers, Templ views, HTMX + SSE, Leaflet map
Weather data by Open-Meteo and MET Norway (both CC BY 4.0). Route data © OpenStreetMap contributors (ODbL). Loops by BRouter.

