Easy-to-manage touch screens
무료 오픈소스 터치스크린으로 USB 설정 가이드, 드래그‑드롭 편집기, 로컬 제어를 제공하며 Home Assistant용으로 제작되었습니다
무료 한 페이지 보물찾기로 공원의 실제 지도와 최근 14일 동물 목격 정보를 AI가 단서로 제공한다
A free one-page treasure hunt for your local park. Gemma 4, an open-weight AI model, reads that park's real map and the last 14 days of wildlife sightings and writes the clues. Code checks every one.
라이프스타일TypeScript · CSSGitHub @c0de128온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Grass Pass: your ticket to get outside. Printable park treasure-hunt passes built from real park data with open-weight Gemma 4. Hacktoberfest 2026 Week 1.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Grass Pass: Family time is back! Powered by AI.
Your ticket to get outside. Pick a park. Print a pass. Phone away.
Read the write-up: our DEV submission post · Watch the 90-second tour: YouTube
Try it live: https://grass-pass.vercel.app (live since Oct 8, 2026). The example passes open with no
sign-in. Your first new pass each day needs no sign-in either; for more, press Try as a judge (one click, no
sign-up) or sign in with GitHub or Google.README 전문 보기 ↗Your ticket to get outside. Pick a park. Print a pass. Phone away.
Read the write-up: our DEV submission post · Watch the 90-second tour: YouTube
Try it live: https://grass-pass.vercel.app (live since Oct 8, 2026). The example passes open with no sign-in. Your first new pass each day needs no sign-in either; for more, press Try as a judge (one click, no sign-up) or sign in with GitHub or Google.
Grass Pass turns your local park into a one-page treasure hunt, usually in 10-30 seconds (up to about a minute and a half for a big park or a slow model; measured, see Limitations). Pick a park and your kid's age (4-6, 6-10 or 10-13; or 13+ for teens and adults). Code collects what is really in that park. Gemma 4 (open weights, Apache-2.0, on DigitalOcean) picks a fair mix and writes kid-level clues, usually in one call (at most 3 clue calls: a retry if the first call fails, refills if too few pass), plus one more short call for the trip tips. Code then fact-checks every clue against its source, drops any that fail, and writes every number, date and safety line itself. You print one black-and-white page: the kid ticks boxes with a pencil; you keep a tear-off stub with the answers, safety notes and sources. The phone stays in your pocket.
Why not a generic printable hunt? Because "Find a pinecone" fails, and parks aren't generic. Two parks in Allen, TX, measured on Oct 5, 2026: Connemara Meadow had 70 wildlife species photographed in 14 days and no playgrounds, courts or shelters on the map; Celebration Park had 25 soccer fields and no recent sightings. Each gets its own pass, from:
If a source has nothing, the pass says "No data available" and why. It never pads the pass with generic items.
Built for the DEV Hacktoberfest 2026 Open-Source AI Challenge, Week 1 "Touch Grass".
Live demo: https://grass-pass.vercel.app (live since Oct 8, 2026; sign-in with GitHub or Google).
You pick how far to look: 1, 3, 5 or 10 miles (3 by default). The list shows the 20 nearest named parks in that circle on a street map and as numbered cards, so a park you know may be missing (measured Oct 10, 2026: "Allen, TX" at 3 miles found 46 named parks and listed the 20 nearest, without Connemara Meadow Preserve; at 10 miles it found 302). Search the park's own name, or press "Use my location".
The app's How it works page (/how-it-works) has every step, what the model is and isn't given, every reason a
clue is removed, the refill rules, caching, limits and measured numbers. The short version:
flowchart LR
S["Real data<br/>OpenStreetMap (Overpass)<br/>iNaturalist + Wikipedia summaries<br/>Google review counts (SerpApi)"] --> P["Pools, by code<br/>what is really in this park"]
P --> F["Code safety<br/>blocked species removed<br/>by iNaturalist taxon"]
F --> AI["Gemma 4 31B<br/>usually one clue call<br/>(at most 3 with retry/refills)<br/>+ 1 trip-tips call<br/>strict JSON schema"]
AI --> V["Code checks<br/>quote must be in the source,<br/>no answer names, no added numbers"]
V --> PR["Print<br/>kid pass + tear line<br/>+ grown-up stub"]
google_maps: same name, within 1 km of the map centre, a park and not a court inside it), then counts reviews
from the last 24 months whose text mentions dogs, bikes, and ducks (parks with a pond) or skateboards
(google_maps_reviews, newest first, up to 3 searches). A keyword needs 3 or more. Only the word, the count and
the newest month go into the pool; counts are kept 30 days.src/lib/safety/danger-taxa.ts (venomous snakes, recluse and
widow spiders, fire ants, poison ivy...) are removed by iNaturalist taxon and ancestor ids before the model sees the
list, and checked again after. Any species whose own description says it is poisonous, toxic, venomous, stings or
burns the skin is left off too, and a clue with one of those words is dropped (a live pass once asked kids to find
white snakeroot, "a poisonous perennial herb"). Every Wild Find gets a fixed "look, don't touch" line from code.gemma-4-31B-it on DigitalOcean serverless inference by default): it picks items by
id and writes the clues. The JSON schema allows only the real pool ids.sourceQuote must appear word for word in the item's source; it must not name its
answer, add a number or contain a link; a "how many" question must not give its own number; a "listen" clue needs a
source that names a sound. A failing clue is dropped; code never rewrites what a clue says. It makes four small edits: it cuts a
filler opener ("Quick!"), turns "?" after a command into a full stop, swaps a worn-out opening ("Somewhere you will
see a") for a plain word ("Spot a"), and says "What", not "Who", for a plant, fungus or lichen. Too few left: up to two refill calls (at most 3 clue calls per pass; the trip tips are one more call).Passes are cached per park, age band and day (Chicago time), so the next visitor gets them at once. Per-IP limits and daily caps protect the free model budget and the public map servers.
Weather on the pass page (screen only, never printed; the card's words are code-written, and only a few code-written facts from it go to the model, for the trip tips in step 7): code asks
Open-Meteo for the park's forecast (today, or tomorrow from 6 PM local time) and, for US
parks, the National Weather Service (api.weather.gov) for active alerts, then writes the
words itself (src/lib/weather/summary.ts: thresholds for storms, rain, heat, cold and wind; "Rain likely from about
3 PM" comes from the hourly forecast). Only official alerts with status: Actual are shown, with their real sender;
we never write our own instructions for an alert. The page waits up to 1.2 s for the forecast (usually cached, so the
card is in the first page and nothing below it jumps); a slower lookup streams in after the pass; if the weather service does not answer within 8 s the card says "No weather data available" and why. Cached 30
minutes per park position (rounded to about 1 km); if a refresh fails, the last good forecast (at most 3 hours old)
stays, with its own "forecast updated" time. Daily cap WEATHER_DAILY_CALLS (default 5,000 calls each for Open-Meteo and
weather.gov, per UTC day, shared by all server instances through Upstash; Open-Meteo's free API allows 10,000
non-commercial calls a day). After a 429 the park waits 10 minutes before asking again.
Needs Node 22 and pnpm.
pnpm install
cp .env.example .env.local # add DO_INFERENCE_API_KEY (or MODEL_BASE_URL for a local Ollama)
# and AUTH_SECRET: npx auth secret
pnpm dev # http://localhost:3000
Every environment variable is explained in .env.example; keys are server-only. Lucky Finds need
SERPAPI_API_KEY (free SerpApi account), capped by SERPAPI_DAILY_CAP and SERPAPI_MONTHLY_CAP. Without
UPSTASH_REDIS_REST_URL/_TOKEN, caches and limits live in memory (fine locally). Without a model key, park search
works and a new pass says the model is not configured. AUTH_SECRET switches on sign-in and "Try as a judge";
without it, examples, search, shared passes and printing work, and each browser still gets its 1 free new pass a day
(no sign-in, so no more than that).
Stopping spend in an emergency: AI_DAILY_CAP=0 pauses new model passes and JUDGE_DEMO_DAILY_CAP=0 stops new judge
passes (SERPAPI_DAILY_CAP=0 does the same for SerpApi); an empty DO_INFERENCE_API_KEY stops every model call.
At the default AI_DAILY_CAP=400 calls (a pass makes 2-4: 1-3 for the clues plus 1 for the trip tips, so about 100-200
passes a day), a full day costs about $0.28 typical and at most about $0.55
at DigitalOcean list prices; the $10 prepaid credit is the hard ceiling.
| Script | What it does |
|---|---|
pnpm lint | ESLint, zero warnings allowed |
pnpm typecheck | next typegen && tsc --noEmit |
pnpm test | Vitest unit tests (recorded real API answers, no network) |
pnpm build / pnpm start | production build / server |
pnpm e2e | Playwright against pnpm start (port 3123, or E2E_BASE_URL); live tests skip honestly, with the server's error code, when an upstream or a limit says no |
pnpm osm:snapshot | re-records the saved OpenStreetMap data (the 4 example parks and the Dallas-area fallback park list, src/data/osm/) |
pnpm eval | the evals: 20 recorded real parks, live open models on DigitalOcean (about $0.07-0.10; last full run $0.092; capped at $1), plus a no-AI baseline. See evals/README.md |
pnpm eval:check | free dry run of every recorded park through the real pass builder, model off |
pnpm eval:record | re-records the 20 eval parks live from OpenStreetMap and iNaturalist |
pnpm feedback:report | private: prints the pass-rating counts per park and per tag from the shared Upstash store (read-only; "No data available" without Upstash or ratings). There is no public page for them |
node scripts/render-brand.mjs | re-renders every logo, icon and share image from scripts/brand/art.mjs and scripts/brand/v3.mjs |
Measured cold start (laptop: build 17.2 s, first page 0.98 s, first example pass 30.9 s), abuse limits, the storage
quota, the optional firewall rule, accounts and report moderation: docs/OPERATIONS.md.
The app talks to any OpenAI-compatible server. Measured on Oct 6, 2026 with the smallest Gemma 4 on a laptop with no GPU (Intel Core Ultra 7 155H, 32 GB RAM, Ollama 0.32.15), 5 test parks, $0:
-9), about 1-3
minutes a pass, about 5-6 GB of RAM (5.2 GB working set, 5.8 GB at most). It writes about 18 answer tokens a second
here.LOCAL_MODEL_TIMEOUT_MS=270000), it works, slowly: one browser
click-through (Celebration Park, ages 6-10) made a pass with 7 of 8 finds and its Find This Spot map in 96 s
(2 model calls: 62.8 s and 30.7 s), while the page said "A model on this computer can take a few minutes". One run,
not a benchmark: 2026-10-06-selfhost-browser-1959.md.Every number, the hardware and the caveats: evals/results/2026-10-06-selfhost-notes.md.
A computer with a GPU should be much faster (not measured).
# 1. Get the model: Gemma 4 E2B, 4-bit QAT, Apache-2.0, 4.3 GB
ollama pull gemma4:e2b-it-qat
# 2. Same weights with an 8,192-token context (Ollama's default 4,096 is shorter than our longest prompt)
ollama create gemma4-e2b-8k -f evals/selfhost/Modelfile
# 3. Point the app at it: put these in .env.local (no DigitalOcean key needed)
# MODEL_BASE_URL=http://localhost:11434/v1
# MODEL_ID=gemma4-e2b-8k
# MODEL_REASONING_EFFORT=none # Gemma 4 "thinking" off; Ollama turns it on by default
# LOCAL_MODEL_TIMEOUT_MS=270000 # longer clock for a model on THIS computer (off by default, never on Vercel):
# # 4.5 min a call, 10 min a pass; the page waits and says why
# AUTH_SECRET=... # npx auth secret (1 free pass a day without it; more need a sign-in; "Try as a judge" works)
pnpm dev
# Then open http://localhost:3000, press "Try as a judge", pick a park and "Make my pass" (96 s in our one try)
# 4. Or measure it yourself, free, on the recorded parks (about 6 + 10 minutes on the laptop above)
EVAL_MODELS=gemma4-e2b-8k EVAL_CASES=1,2,3,13,15 pnpm eval # the app's limits
EVAL_MODELS=gemma4-e2b-8k EVAL_CASES=1,2,3,13,15 EVAL_LOCAL_PATIENT=1 pnpm eval # eval-only longer clock
Only for a server that you alone use: don't put it on the public internet with LOCAL_MODEL_TIMEOUT_MS set (one
pass can hold the model for up to 20 minutes). With it on, the app builds one new pass at a time (LOCAL_MAX_PASSES,
at most 2) and asks anyone else to try again in a minute.
The eval runs the app's own pass builder and checks. The browser flow was clicked through once with
LOCAL_MODEL_TIMEOUT_MS=270000 (above). The eval's "app clock" row is the app's own clock at its 70 s maximum per call (the most MODEL_TIMEOUT_MS allows),
without that setting. The hosted site sizes each call instead: a first call gets 30-40 s, a refill 15-30 s, all inside
the 85 s pass deadline.
The same list as the app's /about page, from eval run 2026-10-10-3.md, the first
full run after the Oct 10 clue-wording rewrite (ages 6-10; the other bands are under Evals):
2026-10-06-9). A first call gets up to 40 s: 3 first calls hit that limit, the whole retry answered for 2 of them
(1 complete pass, 1 short), and 1 test run was lost (Celebration Park r3: its retry timed out too; 0 in the run
before). 4 of 87 Gemma calls timed out, no HTTP 403. Llama 4 Maverick completes more passes (94.1%) but takes about
twice as long (20.6 s typical), costs more ($0.00188 a pass) and names its own answer in 12.7% of its clues before
the checks, so Gemma stays the default.-9: 16.5 s per pass, median, on recorded park data), but a new pass also reads
the live map and sightings: a judge's own pass for Prospect Park (Brooklyn, a big park) took about 57 s plus an
11.6 s park search on Oct 6, and passes took 58-67 s when the free map servers were slow (measured Oct 6). The page
waits up to 95 s and the server keeps a pass it started, so "Try again" opens it.20 real parks (recorded live from OpenStreetMap and iNaturalist on Oct 5, 2026; 5 taxa and 15 season counts added on
Oct 6 after the blocklist grew), ages 6-10, run through the real pass builder. Current run:
2026-10-10-3.md, one full run on Oct 10 (app commit 731d357) after the clue wording
was rewritten that day: a "kid voice" for every age band (a fun grown-up for kids, a naturalist's challenge for 13+,
code checks for sign words, sentence pieces and repeated words) and plain-words Park Finds fact sheets (the model quotes
them word for word). The Oct 6 run 2026-10-06-9.md is kept as history (last column).
Two full runs of the same day missed and are kept too: 2026-10-10.md (M3 86.3%, M10
6.4%) and 2026-10-10-2.md (M3 78.4%, M10 0.8%). Their misses were provider timeouts
(two passes lost in each run when both model calls hung for 40 s), the weak-data parks Connemara Meadow and Klyde Warren, and a prompt trim
that was reverted (details in docs/EVALS.md). Open models only: the closed models on our DigitalOcean
tier answered 403 on Oct 5.
| Gemma 4 31B (3 runs) | Llama 4 Maverick (1 run) | No-AI template | Gemma, Oct 6 (2026-10-06-9) | |
|---|---|---|---|---|
| M1 Blocked taxa printed, by taxon id (target 0) | 0 | 0 | 0 | 0 |
| M2 Clues quoting their source word for word, before the filter (target 85%) | 98.0% (541/552) | 94.4% | 100% (by construction) | 99.8% |
| M3 Passes with >= n-1 items (target 90%) | 90.2% (46/51), PASS (1 run lost) | 94.1% (16/17) | 17.6% (FAIL) | 90.2% (0 runs lost) |
| M4 Honest empty sections (target 100%) | 100% | 100% | 100% | 100% |
| M5 Reading level, FK grade median (target <= 3.5) | 2.5 | 2.5 | 2.5 | 2.3 |
| M6 Name leaks in clue or hint, before the filter (target <= 5%) | 3.3% (clue only 2.9%) | 12.7% (FAIL) | 1.4% | 4.0% |
| M7 Model call p50 / p95 (target 10 s / 20 s) | 9.5 s / 25.3 s, p95 FAIL (first calls alone 11 s; 44.8 answer tokens/s) | 20.6 s / 31.3 s (FAIL) | none | 9.9 s / 27.1 s (p95 FAIL) |
| M8 Cost per pass, clue calls only (target $0.001) | $0.00107 to $0.00111, FAIL (4 timed-out calls: prompt only, or billed in full; $0.00104 at $0) | $0.00188 (FAIL) | $0 | $0.00108 to $0.00109 (FAIL) |
| M10 Printed clues repeated across parks (target <= 5%) | 1.6% (6/383), PASS | 2.3% (3/130) | 20.7% (FAIL) | 4.1% |
| M11 Printed clues with a wrong count (target 0) | 0 of 74 (15 removed by the check) | 0 of 12 (5 removed) | 0 of 3 | 0 of 97 (11 removed) |
Gemma calls: 87 for 60 test runs (53 passes: 27 used 1 call, 20 used 2, 6 used 3; 6 runs on the 2 no-data parks made none; 1 run lost). 4 calls timed out (3 first calls at their 40 s limit, 1 whole retry), no HTTP 403. The run cost $0.0916 for all models. Llama 4 Maverick now completes more passes than Gemma, but it is about twice as slow, costs about 1.75x more and leaks names in 12.7% of its clues (code removes them, and those clues are lost), so Gemma stays the default.
Other age bands, partial checks on the same day (6 parks, one Gemma run each), run on the fact sheets before the last
fixes of 731d357: ages 4-6 2026-10-10-partial-1051.md, 10-13
2026-10-10-partial-1052.md and 13+
2026-10-10-partial-1054.md: 6 of 6 complete in each band, 0 risky
species printed. Cost per pass $0.00094 (4-6, under the mark), $0.00113 (10-13) and $0.00145 (13+), both over the
$0.001 mark; 10-13 kept its 2 hard finds on 5 of 6 passes and 13+ its 3 hard finds on 3 of 6 (the stub says so when
short). Small samples. The older band checks (2026-10-06-partial-2320.md
for 10-13 and 2026-10-07-partial-1617.md for 13+) are kept; the app's
pages still quote those two. pnpm eval:check replays every recorded park for 6-10 and 13+.
What changed since the previous run and why, earlier runs, and every tuning change:
docs/EVALS.md. The app's /about page shows the same numbers (src/lib/about/eval-summary.ts,
checked against the results JSON by a unit test).
MODEL_ID), and Llama 4 Maverick ran through the same code in the eval.LOCAL_MODEL_TIMEOUT_MS), one
browser try made a 7-of-8 pass in 96 s; the eval gave 1-3 minutes a pass
(details).Anyone can search, open the example passes and any shared link, and print. Without signing in, a browser gets 1
free new pass a day (Chicago day), counted by one small signed cookie (below), and at most
ANON_PASSES_PER_IP_PER_DAY (default 3) signed-out new passes a day come from one internet connection, so clearing
cookies can't drain the model budget. After that, a grown-up signs in with GitHub or Google (both set up on the live
site; a self-hosted copy shows the ones it has keys for; Auth.js / next-auth v5; no password stored): 5 new passes a
day per account (ACCOUNT_DAILY_PASSES). A pass already made today for that park and age is served to anyone and
counts for nobody; a park without enough real data for a pass (no model call) never uses up a pass, and the
wizard then offers up to 3 other parks from the same search. Try as a judge signs in to a shared demo
account in one click (JUDGE_DEMO_DAILY_CAP, default 60 a day for all judges, at most 3 per connection). Signed-in
visitors can report each find (Found it / Didn't find it / Not safe); thresholds count different accounts, and judge
demo reports are only logged. Signed-in visitors can also rate a pass: 1 to 5 stars and tags (Too easy, Too hard,
Kids loved it, Something was missing, Not safe), no free text; one rating per account per pass (the newest counts),
kept 90 days, never shown publicly (the owner reads the totals with pnpm feedback:report); judge demo ratings are
only logged. Limits, sessions, moderation and provider setup:
docs/OPERATIONS.md.
No names, no photos, no analytics, and nothing about the child is asked for or sent. Browsing, examples, shared links
and printing set no cookie. Using the free pass sets one small httpOnly, SameSite=Lax cookie (Secure on https) that
holds only the date and a count, and a grown-up who signs in gets one encrypted, httpOnly, SameSite=Lax cookie (Secure
on https). What leaves the device (also on /about):
| What | Where it goes | Why |
|---|---|---|
| Typed place text | our server (in the request body, never the web address), then Nominatim; cached 30 days in Upstash Redis by the text, not by who typed it | find the town or park |
| "Use my location" | rounded in the browser to 2 decimals (~1 km), then our server, then Overpass | list nearby parks |
| The map area of your search results | your browser, straight to the OpenFreeMap tile server (tiles.openfreemap.org), so it sees your IP address like any website; no account, no cookies. OpenFreeMap's privacy page says its normal access logs keep no IP address and are deleted after 7 days; error logs and logs kept after misuse can hold IP addresses, for up to 30 days. Cloudflare delivers the tiles under its own privacy policy | draw the street map of the results |
| The chosen park (public place + map position) | our server, then Overpass, iNaturalist and SerpApi (name and position only); the pass page, and making a pass, send only the position, rounded to about 1 km, to Open-Meteo and (US parks) api.weather.gov | park map, sightings, monarch counts, review counts, the weather card and trip tips |
| Age band | our server, then the model on DigitalOcean (in the prompt) | item count and reading level |
| IP address | our server; in Upstash Redis only as a keyed hash (HMAC), never the address, in rate-limit counters that expire within about a day (IPv6 by its /64 and /48 network) | abuse and cost limits |
| Every request (IP, web address, time) | Vercel request logs, about 1 hour on the Hobby plan; searches are POSTs, so the logs never hold the typed place or location | running the site |
| Signing in (grown-ups only) | GitHub or Google sends the public profile (account number, name, picture link; for GitHub any public email). We store only an HMAC of provider + account number (keyed with AUTH_SECRET); the rest is dropped at once: no email, name or avatar is stored. A first name goes only into the person's own encrypted cookie. Scopes: GitHub read:user, Google openid profile. 7 days from sign-in; the judge demo sign-in stops working after 1 day | count 5 new passes a day, the reports and the ratings |
| Free-pass cookie (signed out) | stays in the browser: gp-free (__Host-gp-free on https) = the Chicago date + how many free passes were used today + an HMAC signature (keyed from , or without sign-in), so it can't be edited. No ID. httpOnly, SameSite=Lax, Secure on https, expires at the next Chicago midnight, set only when a free pass is really used. The server keeps no copy (only the per-connection count above, as a keyed hash) |
Our server logs record source/model, timing, outcome and pass ids; never the prompt, the IP address or the typed
text. Review text is never shown, stored or sent to the AI (only the keyword, count and newest month), and the SerpApi
key never leaves the server or appears in a log. The IP hash key is LIMITER_KEY_SECRET (if unset, derived from the
Upstash token; random per process without Upstash). The browser keeps only the light/dark choice and the last age
band (localStorage), plus the park and age picked before signing in (sessionStorage, removed once restored).
Contributions are welcome. See CONTRIBUTING.md for setup, checks and the house rules (no made-up data, every number written by code), and docs/good-first-issues.md for three starter ideas. How the app was built, slice by slice: docs/BUILD-LOG.md.
Built during the Hacktoberfest 2026 Week 1 entry period (first commit Oct 5, 2026, 5:11 PM CDT). Any commit made after the submission deadline (Mon Oct 12, 2026, 06:59 UTC) will be listed here.
create-next-app (Next.js, MIT).src/lib/model.ts), rate limits, caps and breakers (src/lib/limits/rate.ts, quota.ts,
breaker.ts, ip.ts), request guards (src/lib/http/guard.ts), in-flight de-duplication
(src/lib/cache/inflight.ts) and small helpers (src/lib/security-headers.ts, src/lib/zod-config.ts). They have
changed a lot since. Everything specific to Grass Pass was written from Oct 5, 2026: data sources, pools,
safety filter, prompt, clue checks, the pass, print layout, Find This Spot map, October box, evals and brand.lucide-react, ISC).The code is MIT. MIT covers the code only; recorded data keeps its source licence:
src/data/osm/ and the OpenStreetMap answers in tests/fixtures/ and tests/fixtures/evals/: © OpenStreetMap
contributors, ODbL 1.0 (share-alike applies to derived databases).public/vendor/maplibre-gl-worker-6.13.0.mjs is MapLibre GL JS's own build, BSD-3-Clause (see Credits).src/lib/tips/src/lib/pass/budget.ts): the
first call gets up to 40 s, the retry gets the time left and asks for only as many finds as can come back in it, a
refill gets 15-30 s, and the whole pass 85 s. A short HTTP 403 refusal from the provider is asked once more after 1 s.
Runs 2026-10-06-9 and 2026-10-10-3 used these limits (run -8 and earlier had a fixed 30 s a call and 20 s a refill).evals/results/human-check.md) is planned with Kevin's park walk on Sun Oct 11,
2026. Pending, not passed.SERPAPI_DAILY_CAP (12) a day and SERPAPI_MONTHLY_CAP (200, never above 250) a month, counted from the plan's
renewal day (SERPAPI_RENEWS_DAY), and keeps counts 30 days. Over a limit, the pass says Lucky Finds are off for
today (or this month). A count means visitors wrote about it, not that it is there today, so the pass prints
"Maybe!" ("If you're lucky:" on a teens & adults pass). A review counts only if its own text names the thing; a page holds 20 reviews, so a busy park can read "at
least 20". Without SERPAPI_API_KEY the section says "not connected". No photos are printed. A park whose map and
wildlife data can't fill a pass sends no SerpApi search (Lucky Finds are only extras).LOCAL_MODEL_TIMEOUT_MS,
off by default), one browser try made a 7-of-8 pass in 96 s. See
Run it yourself.SERPAPI_DAILY_CAP=0, no key) to save the free searches for the live
site.src/lib/pinned.ts), so they show a good day, not a typical one. A new pass can come out short; it says
how many finds are missing.AUTH_SECRETLIMITER_KEY_SECRET| count 1 free new pass a day |
| Pass ratings (signed in) | per park, each account's latest stars, tags and day per pass under the same per-park reporter ID as item reports; no text; judge demo ratings only logged; deleted after 90 days; never shown publicly | learn which passes work for kids |
| Item reports | per park and item, each visitor's latest kind and day under a per-park reporter ID (an HMAC, so IDs can't be linked across parks); judge demo reports only logged; deleted after 90 days | learn what is findable, leave out unfindable or unsafe finds |
| The finished pass | Upstash Redis, 30 days | the pass link and print page |
pnpm copy:gemma/about; details in src/data/photo-credits.ts), resized and converted to WebP:
brand/, scripts/brand/art.mjs; the kid's clothes and hair are cleaned traces in
scripts/brand/kid-trace.json).scripts/brand/v3.mjs and rasterised by scripts/render-brand.mjs. Text is outlined
from Bricolage Grotesque 800 and
DM Sans 500/700 (@fontsource, dev only, SIL OFL 1.1). The share
image's pass shows only real section names, no clue text..woff2, @fontsource-variable 5.3.0);
Fredoka 600/700 and Nunito
400/600/700 for the printed pass (Fontsource 5.3.0). All are committed in
src/app/fonts/ with their OFL texts and served by next/font/local, so nothing contacts Google Fonts. The logo
lettering is outlined from M PLUS Rounded 1c ExtraBold and
Varela Round (@fontsource, dev only). All six are SIL Open Font
License 1.1.src/data/osm/ is OpenStreetMap
data too (ODbL).public/vendor/ unchanged except its source-map comment) draws OpenFreeMap street tiles (Positron style;
free, no key, no cookies), shown with their attribution under the map: "OpenFreeMap © OpenMapTiles Data from
OpenStreetMap".gemma-4-31B-it, Apache-2.0) on
DigitalOcean serverless inference.MODEL_ID is switched to it; then the pass and /about show "Built with Llama".google_maps and google_maps_reviews;
counts and months only, never review text or reviewer names).api.weather.gov (US public domain). The weather pictures
are our own code-drawn SVG.