feat: workout-suggester, weather-aware workouts from a local Gemma

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.
This commit is contained in:
bdeb1337 committed 2026-10-11 19:29:58 +02:00
commit d7342d6096
42 files changed
+7219

No files matched your search

+1
View File
@@ -0,0 +1 @@
{"latitude":51.052,"longitude":3.712,"generationtime_ms":0.3993511199951172,"utc_offset_seconds":7200,"timezone":"Europe/Brussels","timezone_abbreviation":"GMT+2","elevation":9.0,"current_units":{"time":"iso8601","interval":"seconds","temperature_2m":"°C","apparent_temperature":"°C","precipitation":"mm","weather_code":"wmo code","wind_speed_10m":"km/h","wind_gusts_10m":"km/h","wind_direction_10m":"°","is_day":""},"current":{"time":"2026-10-11T16:30","interval":900,"temperature_2m":14.7,"apparent_temperature":11.7,"precipitation":0.00,"weather_code":0,"wind_speed_10m":12.2,"wind_gusts_10m":27.7,"wind_direction_10m":276,"is_day":1},"hourly_units":{"time":"iso8601","temperature_2m":"°C","apparent_temperature":"°C","precipitation":"mm","weather_code":"wmo code","wind_speed_10m":"km/h","wind_gusts_10m":"km/h","wind_direction_10m":"°","is_day":"","precipitation_probability":"%"},"hourly":{"time":["2026-10-11T16:00","2026-10-11T17:00","2026-10-11T18:00","2026-10-11T19:00","2026-10-11T20:00","2026-10-11T21:00","2026-10-11T22:00","2026-10-11T23:00","2026-10-12T00:00","2026-10-12T01:00","2026-10-12T02:00","2026-10-12T03:00","2026-10-12T04:00","2026-10-12T05:00","2026-10-12T06:00","2026-10-12T07:00","2026-10-12T08:00","2026-10-12T09:00","2026-10-12T10:00","2026-10-12T11:00","2026-10-12T12:00","2026-10-12T13:00","2026-10-12T14:00","2026-10-12T15:00"],"temperature_2m":[14.7,14.6,14.1,13.0,11.4,10.6,10.7,10.7,10.2,10.2,9.4,8.8,9.2,10.0,10.2,10.2,10.6,11.0,12.0,13.1,15.0,16.5,17.3,17.9],"apparent_temperature":[11.7,11.6,11.4,11.0,9.4,8.5,8.9,8.2,7.5,7.7,7.6,7.2,7.7,8.8,9.0,8.7,9.2,9.6,10.7,11.6,13.0,14.0,14.7,15.2],"precipitation":[0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.10,0.10,0.00,0.00,0.00,0.00,0.00,0.00],"weather_code":[0,0,0,0,0,0,3,0,0,0,0,0,3,3,3,3,51,51,2,3,0,2,2,1],"wind_speed_10m":[12.6,11.9,10.4,6.8,6.5,6.5,4.7,8.3,9.0,9.7,8.6,7.2,7.6,6.8,6.1,8.3,8.6,8.6,8.6,9.4,12.2,14.0,14.0,14.4],"wind_gusts_10m":[28.8,26.3,24.1,19.8,9.4,7.6,7.6,11.5,11.9,16.6,14.8,12.6,12.6,13.0,11.5,14.4,15.8,15.8,16.2,18.7,25.2,28.8,29.5,30.6],"wind_direction_10m":[274,277,276,249,206,177,166,179,186,182,189,169,184,175,139,142,141,141,152,171,184,184,190,187],"is_day":[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1],"precipitation_probability":[12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},"daily_units":{"time":"iso8601","sunrise":"iso8601","sunset":"iso8601"},"daily":{"time":["2026-10-11","2026-10-12"],"sunrise":["2026-10-11T08:01","2026-10-12T08:03"],"sunset":["2026-10-11T19:01","2026-10-12T18:59"]}}
+1
View File
@@ -0,0 +1 @@
{"results":[{"id":2797656,"name":"Ghent","latitude":51.05,"longitude":3.71667,"elevation":10.0,"feature_code":"PPL","country_code":"BE","admin1_id":3337388,"admin2_id":2789733,"admin3_id":2797655,"admin4_id":2797657,"timezone":"Europe/Brussels","population":265086,"postcodes":["9000"],"country_id":2802361,"country":"Belgium","admin1":"Flanders","admin2":"East Flanders","admin3":"Arrondissement of Ghent","admin4":"Gent"}],"generationtime_ms":0.7789135}
+1
View File
@@ -0,0 +1 @@
{"type":"Feature","geometry":{"type":"Point","coordinates":[3.72,51.05,13]},"properties":{"meta":{"updated_at":"2026-10-11T13:19:22Z","units":{"air_pressure_at_sea_level":"hPa","air_temperature":"celsius","air_temperature_max":"celsius","air_temperature_min":"celsius","apparent_air_temperature":"celsius","cloud_area_fraction":"%","cloud_area_fraction_high":"%","cloud_area_fraction_low":"%","cloud_area_fraction_medium":"%","dew_point_temperature":"celsius","fog_area_fraction":"%","precipitation_amount":"mm","relative_humidity":"%","ultraviolet_index_clear_sky":"1","wind_from_direction":"degrees","wind_speed":"m/s"}},"timeseries":[{"time":"2026-10-11T16:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1014.1,"air_temperature":13.1,"apparent_air_temperature":12.5,"cloud_area_fraction":19.5,"cloud_area_fraction_high":0.0,"cloud_area_fraction_low":19.5,"cloud_area_fraction_medium":0.0,"dew_point_temperature":6.2,"fog_area_fraction":0.0,"relative_humidity":62.8,"ultraviolet_index_clear_sky":0.1,"wind_from_direction":283.8,"wind_speed":2.3}},"next_12_hours":{"summary":{"symbol_code":"fair_night"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"fair_day"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"clearsky_night"},"details":{"air_temperature_max":11.1,"air_temperature_min":8.0,"precipitation_amount":0.0}}}},{"time":"2026-10-11T17:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1014.6,"air_temperature":11.1,"apparent_air_temperature":10.8,"cloud_area_fraction":6.2,"cloud_area_fraction_high":0.0,"cloud_area_fraction_low":6.2,"cloud_area_fraction_medium":0.0,"dew_point_temperature":6.6,"fog_area_fraction":0.0,"relative_humidity":74.1,"ultraviolet_index_clear_sky":0.0,"wind_from_direction":263.2,"wind_speed":1.6}},"next_12_hours":{"summary":{"symbol_code":"fair_night"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"clearsky_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"clearsky_night"},"details":{"air_temperature_max":10.3,"air_temperature_min":7.8,"precipitation_amount":0.0}}}},{"time":"2026-10-11T18:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1015.2,"air_temperature":10.3,"apparent_air_temperature":9.7,"cloud_area_fraction":2.3,"cloud_area_fraction_high":0.0,"cloud_area_fraction_low":2.3,"cloud_area_fraction_medium":0.0,"dew_point_temperature":6.5,"fog_area_fraction":0.0,"relative_humidity":78.3,"ultraviolet_index_clear_sky":0.0,"wind_from_direction":247.4,"wind_speed":1.7}},"next_12_hours":{"summary":{"symbol_code":"fair_night"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"clearsky_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"clearsky_night"},"details":{"air_temperature_max":9.5,"air_temperature_min":7.6,"precipitation_amount":0.0}}}},{"time":"2026-10-11T19:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1015.6,"air_temperature":9.5,"apparent_air_temperature":8.8,"cloud_area_fraction":0.0,"cloud_area_fraction_high":0.0,"cloud_area_fraction_low":0.0,"cloud_area_fraction_medium":0.0,"dew_point_temperature":6.2,"fog_area_fraction":0.0,"relative_humidity":80.3,"ultraviolet_index_clear_sky":0.0,"wind_from_direction":231.5,"wind_speed":1.8}},"next_12_hours":{"summary":{"symbol_code":"fair_night"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"clearsky_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"clearsky_night"},"details":{"air_temperature_max":9.0,"air_temperature_min":7.4,"precipitation_amount":0.0}}}},{"time":"2026-10-11T20:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1015.9,"air_temperature":9.0,"apparent_air_temperature":8.1,"cloud_area_fraction":4.7,"cloud_area_fraction_high":0.0,"cloud_area_fraction_low":4.7,"cloud_area_fraction_medium":0.0,"dew_point_temperature":5.9,"fog_area_fraction":0.0,"relative_humidity":81.3,"ultraviolet_index_clear_sky":0.0,"wind_from_direction":218.5,"wind_speed":1.9}},"next_12_hours":{"summary":{"symbol_code":"fair_night"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"clearsky_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"clearsky_night"},"details":{"air_temperature_max":8.3,"air_temperature_min":7.1,"precipitation_amount":0.0}}}},{"time":"2026-10-11T21:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1016.4,"air_temperature":8.3,"apparent_air_temperature":7.4,"cloud_area_fraction":0.8,"cloud_area_fraction_high":0.0,"cloud_area_fraction_low":0.8,"cloud_area_fraction_medium":0.0,"dew_point_temperature":5.6,"fog_area_fraction":0.0,"relative_humidity":83.0,"ultraviolet_index_clear_sky":0.0,"wind_from_direction":204.9,"wind_speed":1.8}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"clearsky_night"},"details":{"precipitation_amount":0.0}},"next_6_hoLine truncated
+1
View File
@@ -0,0 +1 @@
[{"place_id":101749274,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":897671,"lat":"51.0538286","lon":"3.7250121","category":"boundary","type":"administrative","place_rank":14,"importance":0.6930824855023677,"addresstype":"city","name":"Ghent","display_name":"Ghent, Gent, East Flanders, Flanders, Belgium","boundingbox":["50.9795422","51.1888910","3.5797616","3.8493413"]}]