<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bob D</title>
    <description>The latest articles on DEV Community by Bob D (@bdeb1337).</description>
    <link>https://dev.to/bdeb1337</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4153208%2F0a8d2ca4-d225-4b1a-9f9b-800b140ddd94.jpg</url>
      <title>DEV Community: Bob D</title>
      <link>https://dev.to/bdeb1337</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bdeb1337"/>
    <language>en</language>
    <item>
      <title>workout-suggester: checks the weather and Gemma sends you outside</title>
      <dc:creator>Bob D</dc:creator>
      <pubDate>Sun, 11 Oct 2026 17:50:44 +0000</pubDate>
      <link>https://dev.to/bdeb1337/workout-suggester-checks-the-weather-and-gemma-sends-you-outside-dgo</link>
      <guid>https://dev.to/bdeb1337/workout-suggester-checks-the-weather-and-gemma-sends-you-outside-dgo</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;In Belgium the weather gets a vote on every workout: is it nice enough to go out, or is it another day inside? A weather app doesn't really answer that. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;workout-suggester&lt;/strong&gt; answers it for you. You tap &lt;strong&gt;📍 Here&lt;/strong&gt; and pick what you're up for (run, ride, walk, or "surprise me"), how hard, and how long. A few seconds later you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scores for the next hour or two:&lt;/strong&gt; run, ride and walk each scored 0–100 for the actual weather window, with the reasons ("strong gusts up to 50 km/h", "dark: bring lights").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A pick and a plan:&lt;/strong&gt; what to do, when to go, a warm-up / main set / cool-down that fits your intensity and time, what to wear, and a plan B. If it's truly grim, it sends you inside with a concrete HIIT session instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A route:&lt;/strong&gt; a loop of the right length that starts and ends at your door, heading out into the wind so you come home with a tailwind. Nearby signposted OpenStreetMap routes (fietsroutes, wandelroutes, the nearest knooppunt) are listed too. Every route has a GPX download for your watch or bike computer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow-ups:&lt;/strong&gt; "make it harder", "I only have 30 minutes".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The screen part takes about a minute. After that the GPX is on your watch and your phone stays in your pocket.&lt;/p&gt;

&lt;p&gt;It's for anyone who wants to move more but loses the first 20 minutes to weather apps and indecision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgit.b0b.be%2Fbdeb%2Fworkout-suggester%2Fraw%2Fbranch%2Fmain%2Fdocs%2Fscreenshot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgit.b0b.be%2Fbdeb%2Fworkout-suggester%2Fraw%2Fbranch%2Fmain%2Fdocs%2Fscreenshot.png" alt="Desktop: weather, activity scores, the coach's plan and a route map" width="800" height="1765"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a phone, which is where you'll use it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgit.b0b.be%2Fbdeb%2Fworkout-suggester%2Fraw%2Fbranch%2Fmain%2Fdocs%2Fscreenshot-mobile.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgit.b0b.be%2Fbdeb%2Fworkout-suggester%2Fraw%2Fbranch%2Fmain%2Fdocs%2Fscreenshot-mobile.png" alt="Mobile view" width="800" height="5252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://git.b0b.be/bdeb/workout-suggester" rel="noopener noreferrer"&gt;git.b0b.be/bdeb/workout-suggester&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's one ~9 MB Go binary. Point it at any OpenAI-compatible model server and run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# LLM_BASE_URL=http://127.0.0.1:8000/v1 (oMLX), or llama.cpp, Ollama, LM Studio...&lt;/span&gt;
make run               &lt;span class="c"&gt;# → http://127.0.0.1:3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The model: Gemma 4 E2B, running locally.&lt;/strong&gt; I used &lt;code&gt;gemma-4-E2B-it-qat-4bit&lt;/code&gt; through &lt;a href="https://github.com/jundot/omlx" rel="noopener noreferrer"&gt;oMLX&lt;/a&gt; on a MacBook. Any OpenAI-compatible server works (llama.cpp, Ollama, LM Studio), and you swap models with one environment variable. A plan streams in about 10 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Go, &lt;a href="https://templ.guide/" rel="noopener noreferrer"&gt;Templ&lt;/a&gt; and &lt;a href="https://htmx.org/" rel="noopener noreferrer"&gt;HTMX&lt;/a&gt;, with the model's reply streamed over server-sent events. A &lt;a href="https://leafletjs.com/" rel="noopener noreferrer"&gt;Leaflet&lt;/a&gt; map shows the routes. The stack comes from my previous week's project and stays small and fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The most important design decision: don't let the small model do the maths.&lt;/strong&gt; A 2B model will happily say "light breeze" about 50 km/h gusts, or invent a sunset time. So the work is split:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The weather comes from open data.&lt;/strong&gt; &lt;a href="https://open-meteo.com/" rel="noopener noreferrer"&gt;Open-Meteo&lt;/a&gt; is open source and needs no key. If it's overloaded (that happened during the build), the app falls back to &lt;a href="https://api.met.no/" rel="noopener noreferrer"&gt;MET Norway&lt;/a&gt;, then to the last forecast it had. MET Norway has no gusts for Belgium, so gusts are estimated from wind speed and marked "≈". Sunrise and sunset are calculated locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain Go rules do the scoring.&lt;/strong&gt; Each activity starts at 100 and loses points per condition, weighted by activity. Gusts over 45 km/h cost a ride 40 points but a run only 15. Heavy rain costs everything a lot. Thunder is a flat zero. Below 40 everywhere, you go inside. The app also looks 12 hours ahead for a clearly better daylight window ("better at 17:00").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routes come from open routing.&lt;/strong&gt; &lt;a href="https://github.com/abrensch/brouter" rel="noopener noreferrer"&gt;BRouter&lt;/a&gt; generates round trips from your door in three directions, starting into the wind. &lt;a href="https://overpass-api.de/" rel="noopener noreferrer"&gt;Overpass&lt;/a&gt; finds signposted OpenStreetMap routes nearby and the nearest knooppunt. BRouter's round-trip parameter turned out to behave like a radius (ask for 10 km, get ~51 km), so the app scales it down. Long-distance trails like the 130 km Streek-GR get filtered out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma writes the plan.&lt;/strong&gt; All of that goes into the system prompt as a short sheet of facts: the weather, the scores and reasons, the pick, the target distance and pace, and the routes with their climb, paved percentage and wind direction. Gemma does what small models are good at. It explains the pick in plain language and builds a workout around it, and follow-ups like "only 30 minutes" can reshape the session.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Splitting it like this is what made E2B good enough. It never decides whether it's safe to ride. It gets told, and it explains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Every part can be self-hosted or swapped.&lt;/strong&gt; The model, Open-Meteo, Overpass and BRouter are all open source, and each has one environment variable to point it at your own instance. When Open-Meteo went down mid-build, adding a fallback was a small change, not a vendor problem. If E2B isn't good enough, I switch &lt;code&gt;LLM_MODEL&lt;/code&gt; to E4B, no new API key or pricing page needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; There's no per-token bill for a question I ask every morning, and no subscription for weather or routing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenStreetMap knows the local routes.&lt;/strong&gt; Flanders has signposted cycling and walking networks, numbered junctions and small park loops. They're all in OSM, mapped by volunteers. No closed fitness API gives me that for free, and none would let me generate a loop from my front door that heads into the wind first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Route data keeps working without a connection.&lt;/strong&gt; Signposted routes are cached on disk for a week, and the GPX goes to your watch. Once you've planned, you don't need the screen or the signal.&lt;/p&gt;

&lt;p&gt;A closed stack would have given me a nicer chatbot and a worse product. The useful part is having the data, the routing and the model under my own control, glued together with about 3,000 lines of Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 4 E2B, running locally, is the coach: it turns rules-based weather scores and OpenStreetMap routes into a concrete workout plan, and handles the follow-ups.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>go</category>
      <category>opensource</category>
    </item>
    <item>
      <title>localchat: yet another local chat app POC w/GoLang + HTMX</title>
      <dc:creator>Bob D</dc:creator>
      <pubDate>Mon, 05 Oct 2026 06:01:14 +0000</pubDate>
      <link>https://dev.to/bdeb1337/localchat-yet-another-local-chat-app-poc-wgolang-htmx-265d</link>
      <guid>https://dev.to/bdeb1337/localchat-yet-another-local-chat-app-poc-wgolang-htmx-265d</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;localchat is a chat app for an AI model that runs on your own machine. A Go server renders the UI, and Google's &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; writes the replies, streamed into your browser token by token. Your messages stay in memory on your laptop for as long as you run the app.&lt;/p&gt;

&lt;p&gt;I talked to a friend before about what a bare-basic and simple Go web app with AI integration would look like, without too many difficult parts. So I spent some time setting this up and kept the project small enough to read in one sitting.&lt;/p&gt;

&lt;p&gt;I picked Gemma while developing because I like running small models on my own hardware. Gemma 4 E2B fits in a few gigabytes of RAM and starts answering in about a second on my laptop. It's fast and easy to develop against while still having realistic output.&lt;/p&gt;

&lt;p&gt;My friend and I get two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code you can read in an evening.&lt;/strong&gt; A HTML form posts to a Go handler, the handler calls the model, and the reply streams back into the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A base to build on.&lt;/strong&gt; You can change the system prompt, swap the model with one environment variable, or install the app as a background service on Linux, macOS or Windows. You can also run it as a container if you like.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://git.b0b.be/bdeb/localchat" rel="noopener noreferrer"&gt;git.b0b.be/bdeb/localchat&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cmd/localchat/          entry point: serve + install/start/stop as an OS service
internal/llm/           ~150-line OpenAI-compatible streaming client (stdlib only)
internal/chat/          in-memory conversations, one per browser session
internal/web/           routes, SSE streaming, embedded static assets
internal/web/views/     Templ components
scripts/e2e.py          Playwright browser test against the real model
compose.yaml            app + llama.cpp + Gemma 4 E2B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78cot3ygsv3378ybyp9p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78cot3ygsv3378ybyp9p.png" alt="localchat answering with streamed Markdown" width="800" height="676"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://streamable.com/e/4frgmk" rel="noopener noreferrer"&gt;Video Demonstration&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On my M3 MacBook Air, Gemma 4 E2B (4-bit, through oMLX) sends its first token after about a second and finishes a short answer with a code block in 3 to 5 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Open-source AI:&lt;/strong&gt; Google's Gemma 4 E2B (open weights, 4-bit). I ran it with &lt;strong&gt;oMLX&lt;/strong&gt; (Apple MLX) on my Mac while developing, and the container uses &lt;strong&gt;llama.cpp&lt;/strong&gt; (&lt;code&gt;llama-server&lt;/code&gt; with a GGUF build).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App stack:&lt;/strong&gt; Go 1.27, Templ, HTMX 2 with its SSE extension, goldmark and kardianos/service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser ── HTMX + SSE extension
  │  POST /chat              → user bubble + empty reply bubble (sse-connect)
  │  GET  /chat/stream/{id}  ← "token" events (append) … "done" (swap in Markdown)
Go (net/http + Templ)
  │  POST /v1/chat/completions {stream: true}
Local model server ── Gemma 4 E2B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Streaming works as plain HTML over server-sent events:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your browser posts the message, and the server answers with two Templ fragments: your message and an empty reply bubble with &lt;code&gt;sse-connect="/chat/stream/{id}"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Go handler streams the model's reply and wraps each chunk in an HTML-escaped &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; inside a &lt;code&gt;token&lt;/code&gt; event. HTMX appends each span with &lt;code&gt;hx-swap="beforeend"&lt;/code&gt;, so you see the answer appear word by word. I wrote no JavaScript for the streaming.&lt;/li&gt;
&lt;li&gt;Once the model finishes, the handler sends a &lt;code&gt;done&lt;/code&gt; event carrying the reply as server-rendered Markdown (goldmark, raw HTML stripped). HTMX swaps the whole bubble for it, and removing the &lt;code&gt;sse-connect&lt;/code&gt; element closes the stream.&lt;/li&gt;
&lt;li&gt;Browsers reconnect a dropped EventSource on their own. If one reconnects for a finished reply, the handler sends back the final HTML and skips the model. A test checks that the model runs once per reply.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A few more details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I used no AI SDK. The client reads OpenAI-compatible streaming with a &lt;code&gt;bufio.Scanner&lt;/code&gt; over &lt;code&gt;data:&lt;/code&gt; lines, in about 150 lines of standard-library Go.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;localchat install --user&lt;/code&gt; registers the app as a launchd agent, a systemd unit or a Windows service, pointed at your &lt;code&gt;.env&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;I embedded htmx, the SSE extension, the CSS and the icon in the binary, so the app works offline.&lt;/li&gt;
&lt;li&gt;Handler tests run against a fake OpenAI-style server and cover streaming, history, session isolation, XSS-safe Markdown and an offline model. A Playwright test drives a real browser against Gemma, both on the Mac and in Docker.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; localchat runs Google's Gemma 4 E2B on your own machine, through MLX on a Mac or a GGUF build in llama.cpp under Docker.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
