DEV Community

Cover image for ShadeLoop: an open-model field journal that gets you outside for 20 minutes
Priyanshu Jha
Priyanshu Jha

Posted on

ShadeLoop: an open-model field journal that gets you outside for 20 minutes

Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿

This is a submission for the DEV Hacktoberfest 2026 "Touch Grass" challenge.

You have 20 minutes. ShadeLoop finds you a shady bench, walks you there, and turns what you notice into evidence for the next person.
An open-weight model drafts the plan. A deterministic rule engine has the final word. Every step is logged, timed and shown to you.

ShadeLoop Discover screen on a laptop: describe a 20 minute outing and get rule-checked missions near you
Discover on a laptop: one sentence in, rule-checked missions out.

What I Built

ShadeLoop is a field journal that turns "I have 20 minutes" into a short, safe outdoor mission near you, and then turns what you notice outside into shared evidence for the next person.

You describe the outing you want ("a quiet 20 minute break somewhere shady with a bench"), and ShadeLoop:

  1. Reads your request with an open-weight model (Gemma 4), and remembers your earlier requests only if you opt in (Backboard).
  2. Checks the real world: current weather (Open-Meteo, with MET Norway as an automatic fallback) and mapped parks and gardens near your rounded location (OpenStreetMap plus SerpApi).
  3. Drafts missions with Gemma, then runs every draft through a deterministic rule engine that checks your time budget, walking limits, accessibility needs and past field reports. The engine has the final word: it can shorten or refuse a draft, and then template missions are used and labelled as such.
  4. Field Mode walks you through each step, with read-aloud steps (ElevenLabs, or your browser's voice offline), quick observation taps (shade, noise, bench, crowding, path, step-free) and dictated notes that Gemma turns into suggested observations you confirm.
  5. Closes the loop: you get a report you can listen to, and your observations become evidence: time-of-day patterns and "places with similar field reports" (Tiger Data, TimescaleDB plus pgvector).

It's an installable PWA that works offline (an IndexedDB queue that syncs idempotently), and every plan shows a process log: what ran, how long it took, what failed and what covered for it. Nothing is presented as more certain than it is.

ShadeLoop on a phone: the same Discover flow as an installable, offline-capable PWA
The same flow on a phone. It installs as a PWA and keeps working offline.

Try it in 60 seconds

  1. Open https://shadeloop.onrender.com (give the free instance up to a minute to wake).
  2. Allow location; it's rounded to ~1 km before it leaves your device. No account needed.
  3. Type something like "a quiet 20 minute break somewhere shady with a bench" and hit plan.
  4. Open the process log under the plan to see every step, its timing and what covered for any failure.
  5. Start Field Mode, tap what you notice, and finish to get your report.
  6. Done? Settings → Delete my data wipes everything in one call.

Demo

Live app: https://shadeloop.onrender.com (free Render instance, so the first load can take ~30–60 s to wake up)

Code

Repo: github.com/CodewithJha/shadeloop (MIT)

How I Built It

Stack: npm workspaces (shared, server, web) · React 19 + Vite 8 PWA (vite-plugin-pwa, IndexedDB offline queue) · Express 5 + Zod · MongoDB Atlas · Tiger Data (TimescaleDB hypertable + 15-min continuous aggregate, pgvector) · Gemma 4 gemma-4-26b-a4b-it via Google AI Studio (or gemma3:4b locally via Ollama) · Backboard · ElevenLabs · SerpApi · OpenStreetMap Overpass (with mirror failover) · Open-Meteo → MET Norway · Sentry agent tracing · Docker on Render · Vitest (with mongodb-memory-server) · GitHub Actions CI.

Architecture at a glance

flowchart TD
    R[Your request + rounded location] --> M[Backboard: recall earlier requests, opt-in]
    R --> W[Weather: Open-Meteo]
    W -- 429 / failure --> MET[MET Norway fallback]
    R --> P[Places: OpenStreetMap Overpass + SerpApi]
    M --> G[Gemma 4: parse + draft missions]
    W --> G
    MET --> G
    P --> G
    G --> E{Deterministic rule engine}
    E -- accepted --> MS[Missions]
    E -- refused --> T[Template missions, labelled]
    MS --> F[Field Mode: steps read aloud by ElevenLabs]
    T --> F
    F --> O[Observations: MongoDB + Tiger Data hypertable + pgvector fingerprints]
    O --> RP[Report: read aloud, time-of-day, similar places]
    S[[Sentry agent tracing: plan run, tool spans, model spans]] -.-> M & W & P & G & E & F & RP

How a mission travels from your phone to the server and back

sequenceDiagram
    participant U as You (PWA)
    participant W as Web app (React + IndexedDB)
    participant A as Express API
    participant X as Gemma / Weather / Places / Backboard
    participant D as MongoDB + Tiger Data
    U->>W: Describe the outing (20 min, shade, bench)
    W->>A: POST /sessions (anonymous token)
    W->>A: POST /missions/plan
    A->>X: recall memory, weather, places, Gemma draft
    A->>A: rule engine accepts or refuses every candidate
    A->>D: save missions + process log
    A-->>W: missions + timed process log
    U->>W: Start mission (Field Mode)
    W->>A: GET /missions/:id/steps/:n/speech (ElevenLabs)
    U->>W: Tap or dictate observations (works offline)
    W->>W: queue in IndexedDB when offline
    W->>A: POST /missions/:id/observations or POST /sync
    A->>D: store once (idempotent), hypertable row + pgvector fingerprint
    W->>A: GET /reports/:id, /locations/:id/hourly, /locations/:id/similar
    A-->>U: report read aloud, time-of-day pattern, similar places

Frontend vs backend: who does what

Layer What it does Where in the repo
Web (React 19 + Vite PWA) Discover form, missions list, Field Mode with voice and observation taps, History, Settings > Integrations with live status, offline queue in IndexedDB, installable PWA with offline fonts web/src/App.tsx, Field.tsx, Settings.tsx, Integrations.tsx, store.ts, api.ts
API (Express 5 + Zod) Validates every request, anonymous sessions, rate limits on expensive routes, OpenAPI spec at /openapi.json, health and dependency checks server/src/app.ts, openapi.ts
Agent pipeline Reads the request with Gemma, recalls Backboard memory, fetches weather and places, drafts missions, logs every step with timings server/src/missions.ts, ai.ts, providers.ts
Rule engine Deterministic veto on walking time, limits and past field reports; refused plans fall back to labelled templates server/src/domain.ts, fallback.ts
Data MongoDB for missions, observations and reports; Tiger Data hypertable for time-of-day patterns and pgvector for similar places server/src/db.ts, tiger.ts
Privacy Rounded location, export everything (GET /me/export), delete everything (DELETE /me) server/src/account.ts
Observability Sentry spans for the plan run, each tool call and each model call server/src/instrument.ts

The model proposes, the rules decide

This is the actual veto, from server/src/domain.ts. Every candidate, from Gemma or a template, goes through it, and no model text can override it:

/** Deterministic checks every candidate (AI or template) must pass. AI text can't override these. */
export function validateCandidate(c: Candidate, ctx: PlanContext) {
  const errors: string[] = [];
  const unsafe = findUnsafeText([c.title, c.description, c.objective, c.whyItMatches, ...c.steps.map((s) => s.text)].join('\n'));
  if (unsafe) errors.push(`unsafe text: "${unsafe}"`);
  // ...
  const maxMinutes = Math.min(ctx.availableMinutes, RULES.maxMissionMinutes);
  if (estimatedMinutes > maxMinutes) errors.push(`estimated ${estimatedMinutes} min exceeds limit ${maxMinutes} min`);
  if (walkingMinutes > ctx.prefs.maxWalkMinutes) errors.push(`walking ${walkingMinutes} min exceeds your limit of ${ctx.prefs.maxWalkMinutes} min`);
  for (const chk of conditionChecksFor(loc?.id ?? null, ctx)) {
    if (chk.required && chk.status === 'contradicted') errors.push(`required condition ${chk.condition} is contradicted by reports`);
  }
  if (errors.length) return { ok: false as const, errors };
  // ...unverified accessibility conditions are surfaced as explicit uncertainty, never as facts
}
Enter fullscreen mode Exit fullscreen mode

That walking … exceeds your limit line is exactly the refusal you'll see in the live process log below. Even when a draft passes, an accessibility condition backed only by anonymous reports is shown as unverified, with a note to check access yourself.

Real numbers from today's live runs on https://shadeloop.onrender.com (12 Oct 2026, a throwaway session in Bengaluru, deleted afterwards via DELETE /me):

  • Process log of a full plan (28.1 s total): Gemma read the request in 10.6 s → Backboard recall 63 ms → places 3.5 s (OpenStreetMap: 45 places, SerpApi: 20 places) → Gemma drafted 2 candidates in 11.7 s → rule engine accepted both → Backboard save 1.9 s.
  • Second plan: Backboard recalled 1 earlier request.
  • Rule engine refusal (in an earlier live run today): Gemma proposed a walk of 23 min against a 20 min limit, the engine refused it ("walking 23 min exceeds your limit of 20 min") and fell back to labelled templates.
  • Weather: Open-Meteo returned HTTP 429 from Render's shared IP; the process log read Open-Meteo failed (Open-Meteo HTTP 429) → MET Norway: 28.5°C (1.3 s).
  • ElevenLabs: step audio 17.6 KB in 1.5 s; report audio 61 KB in 1.6 s.
  • Tiger Data pgvector: two parks with different field reports matched at similarity 0.4 (not a fake 1.0), and time-of-day patterns were read back from the hypertable.
  • Offline replay idempotency: sending the same observation twice returned 201 then 200 and stored it once.
  • Delete my data: removed missions, observations, reports, Tiger rows and the Backboard memory; the old token then got 401.
  • Tests: 129 server and 43 web tests (Vitest), plus lint, typecheck, PWA build and Docker build in CI.

A real process log from the live app

Copied from live plans on 12 Oct 2026 (Bengaluru, throwaway session). The refusal line comes from an earlier live run the same day; the other lines are the 11:44 IST and 11:14 IST runs.

PROCESS LOG
+0.1s   ✓ READ REQUEST   gemma-4-26b-a4b-it read the request (1 call) · 10.6s
+10.7s  ✓ MEMORY         Backboard recalled 0 earlier requests · 0.06s
+2.4s   ✓ WEATHER        Open-Meteo failed (Open-Meteo HTTP 429) → MET Norway: 28.5°C · 1.3s
+10.8s  ✓ PLACES         openstreetmap: 45 places · serpapi: 20 places · 3.5s
+14.4s  ✓ MODEL DRAFTS   gemma-4-26b-a4b-it drafted 2 candidates (1 call) · 11.7s
+26.1s  ✓ RULE ENGINE    accepted "Park Stillness"
+26.1s  ✓ RULE ENGINE    accepted "Garden Observation"
+26.2s  ✓ MEMORY SAVE    Backboard saved this request · 1.9s
        ✗ RULE ENGINE    refused a draft: walking 23 min exceeds your limit of 20 min → labelled templates
Enter fullscreen mode Exit fullscreen mode

What happens when things fail

Free public APIs fail. ShadeLoop is built so a failure is a labelled step in the log, not a blank screen:

What fails What covers for it What you see
Open-Meteo (e.g. HTTP 429 on Render's shared IP) MET Norway forecast Open-Meteo failed (HTTP 429) → MET Norway: 28.5°C
OSM Overpass main endpoint Mirror failover (overpass.kumi.systems, overpass.private.coffee), then SerpApi places Amber "covered by SerpApi" in Integrations
Gemma unavailable or every draft refused Labelled template missions, still checked by the same rule engine "template" label on the mission
No network at all IndexedDB queue in the PWA, synced idempotently later Replay returns 201 then 200, stored once
ElevenLabs unavailable Your browser's built-in voice Steps are still read aloud

Settings > Integrations: all 10 sponsors with live status, verified only after a real call succeeded
Settings → Integrations. "Verified" only appears after a real call succeeded; covered failures show in amber.

Privacy and offline by design

  • Coarse location: rounded to ~1 km before it leaves the device.
  • No accounts: an anonymous session token, nothing else.
  • Opt-in memory: Backboard only remembers requests if you turn it on.
  • One-tap delete: DELETE /me removes missions, observations, reports, Tiger Data rows and the Backboard memory; the old token then gets 401.
  • Offline-first PWA: observations queue in IndexedDB and sync idempotently, so a flaky park Wi-Fi never duplicates a report.

History: past missions and field reports, with time-of-day patterns and similar places
History: your past missions and reports become evidence: time-of-day patterns and pgvector "similar places".

What makes it different

  • The model proposes, the rules decide. Gemma drafts; a tested, deterministic engine accepts, shortens or refuses every draft.
  • Nothing is hidden. A per-plan process log with real timings, and an integrations panel that says "verified" only after a real call succeeded.
  • Failures are covered, and named. Open-Meteo 429 → MET Norway; OSM down → SerpApi, shown as amber "covered by …".
  • It closes the loop. Your field notes become shared evidence: time-of-day patterns and pgvector "similar places".
  • Private and offline-first. Location rounded to ~1 km, no accounts, opt-in memory, one-tap delete, and an offline PWA with idempotent sync.

Why Does Open Innovation Matter?

Getting someone outside for 20 minutes sounds small, but the tool that does it knows where you are, when you're free and what you struggle with. That's exactly the kind of software that should be open.

  • Open weights. The planner is Gemma, an open-weight model. Anyone can inspect what it is, run the same model, and reproduce the behaviour. There's no black box between you and the suggestion to walk somewhere.
  • Runs locally. Set AI_PROVIDER=ollama and gemma3:4b runs on your own laptop (about 20–30 s per plan on my Mac). Your request never has to leave your machine, and the app still works with no AI at all: it falls back to labelled templates.
  • The model is not the authority. A deterministic, tested rule engine checks every draft against your time, walking limit and accessibility needs. Open code means anyone can read the exact rules that keep a suggestion safe.
  • A transparent process log. Every plan shows each step, its timing, what failed and what covered for it ("Open-Meteo 429 → MET Norway"). The integrations panel only says "verified" after a real call succeeded; a key that's merely set says "waiting for first use".
  • Open data, given back. Places come from OpenStreetMap (credited on every mission), weather from Open-Meteo and MET Norway. Your field reports become evidence that helps the next person find shade or a free bench.
  • Privacy by design. Location is rounded to about 1 km before it leaves your device, memory is opt-in, there are no accounts (just an anonymous session), and Delete my data wipes MongoDB, Tiger Data and Backboard in one call.
  • MIT licence. Fork it for your campus, your city or your accessibility needs.

My Agent Session

Prize Categories

Sponsor What it does in ShadeLoop Live evidence (12 Oct 2026)
Gemma Parses requests and field notes, drafts missions; gemma3:4b runs locally via Ollama gemma-4-26b-a4b-it drafted 2 missions, rule engine accepted them
Sentry Agent Tracing Each plan traced as an agent run with tool and model spans Traces sent as agent runs
MongoDB Atlas Missions, observations, reports, place evidence, idempotent sync Replay returned 201 then 200, stored once
ElevenLabs Reads Field Mode steps and the report aloud Step audio 17.6 KB in 1.5 s, report 61 KB
Backboard Opt-in memory of your earlier requests Recalled 1 on the second plan; deleted with your data
Tiger Data Time-of-day hypertable + pgvector similar places Similarity 0.4 between two parks
SerpApi Google Maps places alongside OpenStreetMap 20 places
GitHub Copilot Copilot CLI fixed issue #1; CI runs 129 + 43 tests Commit 89b8ead
Render Hosts the API and PWA as one Docker service https://shadeloop.onrender.com
Entire Publishes the agent session behind the Copilot fix Session link above

Limitations and honesty

  • Cold start: the free Render instance sleeps when idle, so the first request can take 30–60 s.
  • Weather: Open-Meteo rate-limits Render's shared IP (HTTP 429). MET Norway now covers it automatically, but MET Norway has no "feels like" temperature, so heat and cold notes based on it don't appear on those plans.
  • Places: public OpenStreetMap Overpass sometimes fails from Render (its mirrors too); SerpApi still supplies places and the panel marks OSM "covered by SerpApi".
  • Sentry: traces are sent, but the app can't confirm delivery itself, and says so.
  • Evidence is young: field-report patterns and similarity need real reports to be meaningful; with few reports the app says "insufficient data" rather than guessing.
  • Search results show that a place is mapped, not that it is shaded, safe, accessible or open.

Challenges I hit

  • Open-Meteo rate limits. Render's shared IP started getting HTTP 429. Instead of hiding it, I added MET Norway as an automatic fallback and made the log name the switch.
  • Overpass from the cloud. The public OSM endpoint fails intermittently from Render, so it fails over across mirrors and SerpApi fills the gap.
  • Keeping the model honest. Gemma happily proposed a 23 min walk against a 20 min limit. That's why the rule engine, not the model, has the final word, and why its refusals are logged.
  • "Verified" has to mean something. A key that's only set says "waiting for first use"; the panel turns green only after a real call.
  • Similarity that isn't fake. pgvector place fingerprints only mean something with real reports, so the app shows real scores (0.4, not 1.0) and says "insufficient data" when it should.

What's next

  • More field reports, so time-of-day patterns and "similar places" get genuinely useful.
  • Local-first by default: make the Ollama gemma3:4b path the easy one-command setup.
  • A "feels like" estimate when MET Norway is the weather source.
  • Forks for campuses and cities, with their own accessibility rules. It's MIT, so go for it.

If you try it, tell me what mission you got and what you noticed. That's the whole point: get outside for 20 minutes. 🌳

Top comments (0)