The most useful civic data for Stadt Herdecke on one page — live weather & official DWD warnings, the next public-transport departures, the Ruhr water level, air quality, the waste calendar, and the city council — plus keyword alerts that email you when your topics (a street, „Radweg", „Kita", the Hengsteysee) appear on a council agenda. An open civic-tech project built entirely on public open data.
Status: working MVP — a live overview dashboard plus council keyword alerts (double opt-in, daily cron email). Runs locally with zero setup; deploy to Vercel with Neon + Resend. See Deploy.
Every source is keyless and queried server-side (cached briefly, fails gracefully per card):
| Card | Source | Notes |
|---|---|---|
| Wetter + Unwetterwarnungen | DWD via Bright Sky (api.brightsky.dev) |
resolves to warn-cell 805954020 (Stadt Herdecke) |
| Nächste Abfahrten | VRR EFA (efa.vrr.de, rapidJSON) |
live departures from any of Herdecke's ~62 stops; pick + save a default at /abfahrten |
| Ruhr-Pegel | PegelOnline (WSV) | nearest gauge Hattingen (~18.7 km), with 6 h trend |
| Luftqualität | Umweltbundesamt Air Data | station „Herdecke" (DENW014, ~2.2 km); best-effort |
| Müll-Wecker | AHE (ahe.atino.net) |
per-street collection dates (Restabfall/Bio/Papier/Gelber Sack) at /muell |
| Schulferien NRW | OpenHolidays API | next school holidays; full list + Herdecke school directory at /schulen |
| Nächste Ratssitzung | SessionNet (this repo's scraper) | next meeting + agenda preview |
Source clients live in src/lib/sources/*; cards in src/app/components/cards.tsx.
Herdecke's council data lives in a Somacos SessionNet portal that nobody checks and that exposes no OParl API. Decisions affecting your street are public but practically invisible. Digital.Herdecke turns that portal into a subscribable feed.
SessionNet portal ──▶ scraper (src/sessionnet.ts) ──▶ agenda items
│
your keywords ──▶ matcher (src/match.ts) ─────────────┘
▼
email alert on a match
The portal is https://sessionnet.owl-it.de/herdecke/bi/. No OParl — we read HTML.
| Page | Purpose |
|---|---|
si0040.asp?__cjahr=Y&__cmonat=M&__canz=1&__cselect=0 |
month calendar (near-term meetings) |
info.asp (yvcs.asp links) |
"next sessions" widget (announced future meetings + ids) |
si0057.asp?__ksinr=<id> |
a meeting and its agenda items (TOPs) |
vo0050.asp?__kvonr=<id> |
a Vorlage (paper) with a clean Betreff |
getfile.asp?id=<id>&type=do |
a document (PDF) |
rssfeed.asp |
"what's new" feed |
Each agenda item exposes: TOP number, Betreff (the matchable subject), public/non-public flag, optional Vorlage, and attached documents.
src/sessionnet.ts— framework-agnostic client + parser. Lists upcoming meetings (calendar + announced), fetches a meeting's agenda, reads the RSS feed. Polite by default: descriptive User-Agent with a contact, timeouts, resilient to a single source failing.src/match.ts— German-aware matching. Folds umlauts/ß and case soBahnhofstrassematchesBahnhofstraße.src/scan.ts— CLI to verify extraction/matching against the live site.
The UI is built on KERN, the German government's open design system
(pattern library, EUPL-1.2),
via the React kit @publicplan/kern-react-kit
(which wraps @kern-ux/native). Buttons, badges, inputs and the select are the kit's
own components; colours, typography (Fira Sans) and light/dark theming come from KERN
design tokens (--kern-color-*) — so the app tracks KERN upstream instead of
re-implementing it.
- KERN components cross a single client boundary in
src/app/components/kern.tsx; Server Components render them as islands with serializable props. src/app/globals.cssbridges the app's own bespoke layout (cards, lists, flags) onto KERN tokens; dark mode flips automatically via thedata-kern-themeattribute on<html>— there is no hand-maintained dark palette.- The kit is a local
file:dependency installed withinstall-links=true(see.npmrc) so@kern-ux/nativeresolves under Turbopack; re-runnpm installafter updating the kit. A credit link sits in the site footer.
npm install
# List upcoming meetings
npm run scan -- --list --months 4
# Scan published agendas for keywords
npm run scan -- --keywords "Radweg,Kita,Klima,Hengsteysee,Haushalt" --months 4
# Machine-readable
npm run scan -- --keywords "Radweg" --json
npm run typecheck
# Web app — local dev (JSON store + console emails, no accounts needed)
npm run dev # http://localhost:3000
# Exercise the full alert pipeline from the CLI
npm run pipeline -- --add du@example.com "Hengsteysee,Radweg,Kita"
npm run pipeline # scan + "send" (prints emails to console in dev)Example (real output, Rat 18.06.2026):
📅 Rat — 2026-06-18
• TOP Ö 20 [Hengstey]
Machbarkeitsstudie Koepchenwerk Weiterentwicklung des Naturraums Hengsteysee
• TOP Ö 21 [Klima]
Bundesprogramm „Anpassung … an den Klimawandel" Bewerbung auf Fördermittel
- Core engine ✅ — live extraction + German keyword matching.
- Web app (Next.js) ✅ — upcoming meetings & agenda pages, a subscribe form with double opt-in, confirm/unsubscribe, Impressum/Datenschutz scaffolds.
- Persistence ✅ — pluggable store: local JSON file for dev, Postgres
(Neon) in production. (No
@vercel/postgres/@vercel/kv— those are sunset.) - Scheduled scan ✅ —
vercel.jsoncron hits/api/cron/scandaily; it matches agenda items to subscriptions and emails via Resend. Idempotent: never alerts twice for the same (subscriber, item). - Civic dashboard ("Digital.Herdecke") ✅ — homepage overview with live weather + DWD warnings, VRR departures, Ruhr pegel, air quality, waste links, and a next-meeting agenda preview. See Live data.
- Müll-Wecker ✅ — per-street waste schedule at
/muellfrom live AHE data (ahe.atino.net: CSRF token → PLZ/city/street resolution → iCal parse). - Schulen & Ferien ✅ — Herdecke school directory (NRW Schulgrunddaten,
filtered by AGS
05954020with the official Schulform key) + upcoming NRW school holidays (OpenHolidays API) at/schulen, plus a dashboard card. - Abfahrten ✅ — pick any of Herdecke's ~62 stops (EFA 9 km coordinate search,
filtered to locality Herdecke), live board at
/abfahrten, with a saved default stop reflected on the homepage card. Departures addressed by global id (de:05954:NNNN). - Müll-Wecker reminders ✅ — opt-in email reminders the evening before a
collection (double opt-in on
/muell; evening cron/api/cron/waste). Reuses the same store + idempotency as the council alerts. - CI ✅ — GitHub Actions runs typecheck + build on every push/PR.
- Next — subscribable flood / severe-weather alerts for Herdecke (the data is already wired into the dashboard); council per-committee filters & daily digest.
- Council archive search — make past council meetings, agendas and decisions full-text searchable (the SessionNet portal offers neither search nor an OParl API); index scraped agenda items so residents can look up a street, topic, or past decision across previous sessions.
- Upgrade the KERN kit — bump
@publicplan/kern-react-kitfrom the pinned1.0.1to the latest 1.3.5 (re-verify component props + dark-mode tokens first). Pinned for now so the Vercel build is reproducible from npm. - Adopt KERN's official "Method 1" npm integration — install
@kern-ux/nativedirectly and import its CSS (@import "@kern-ux/native/dist/kern.min.css"+".../fonts/fira-sans.css", per kern-ux.de), decoupling the design tokens/CSS from the React kit.
By default NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED is off, so the site stores no user
data: weather, departures, river level, air, waste lookup, schools and the council
viewer are all read-only public data. That means a one-command deploy with no env
vars and no database:
npm i -g vercel
vercel --prodAdd your own domain in the Vercel dashboard (Settings → Domains) and set
APP_URL=https://your-domain so internal links are correct.
Council keyword alerts + the Müll-Wecker reminders are built but disabled. Turn them on once you have an Impressum, a Datenschutzerklärung, a verified sending domain and the data-processing agreements (Resend/Neon):
- Database — add a Neon Postgres integration (EU region) from the Vercel
Marketplace; it injects
DATABASE_URL. Tables are created on first run. - Email — create a Resend API key, verify a sending
domain, set
RESEND_API_KEYandEMAIL_FROM(e.g.Digital.Herdecke <alerts@your-domain>). - Secrets —
APP_URL, a randomCRON_SECRET, andNEXT_PUBLIC_SUBSCRIPTIONS_ENABLED=true. - Fill in
ImpressumandDatenschutz(placeholders insrc/app/). - Restore the cron jobs — create
vercel.json:{ "$schema": "https://openapi.vercel.sh/vercel.json", "crons": [ { "path": "/api/cron/scan", "schedule": "0 6 * * *" }, { "path": "/api/cron/waste", "schedule": "0 16 * * *" } ] } vercel --prod. Trigger a manual run withcurl -H "Authorization: Bearer $CRON_SECRET" https://<app>/api/cron/scan.
- Data is public, but be a good neighbour: low request rate, honest UA with a contact, cache aggressively. Consider emailing the Stadt / OWL-IT to introduce the service — they may link or even host it.
- GDPR: subscriber emails are personal data — double opt-in, easy unsubscribe, a plain-language privacy note, and store only what's needed.
- Best-effort: the portal can change its HTML; the scraper is selector-based and may need occasional upkeep. Alerts are informational, not authoritative — always link back to the official agenda.
- The
src/sessionnet.tsclient generalises to other SessionNet towns (Hagen, Witten, …) by swapping the base URL — room to grow this into an EN-Kreis service.
MIT (code). Council data © Stadt Herdecke, accessed via the public portal.
