- Python 68.8%
- Svelte 14.1%
- TypeScript 11.4%
- Shell 2.4%
- CSS 1.5%
- Other 1.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Fact-based multimodal graph (data/transport-network.gexf, GEXF 1.3) for the
Bonn / Rhein-Sieg region, generated by scripts/build_transport_network.py from
authoritative sources — nothing hand-invented (see data/AGENTS.md):
- Fernverkehr (ICE/IC): gtfs.de de_fv feed (DELFI, CC-BY 4.0), clipped to the
region — real line numbers and the Flughafen Köln/Bonn ICE halt
- Regionalbahn + tram/Stadtbahn: VRS GTFS-static (rail_bonn/tram_bonn)
- Autobahn A-junctions + B/L crossings: OpenStreetMap via Overpass
- Waterways: WSV Bundeswasserstraßen
Roads model real Anschlussstelle chains, so a closure (e.g. the A555 at
Bornheim) severs one segment and forces a detour — the graph can answer
"if the Autobahn is blocked, how do I get from A to B?" across all modes.
Served copy at apps/web/static/geo for the /_transport route.
just update-transport-{fernverkehr,vrs,osm,osm-bl} refresh each layer.
|
||
| .agents/skills | ||
| .beads | ||
| .castra | ||
| .claude | ||
| .config | ||
| .direnv | ||
| .tekton | ||
| apps | ||
| bin | ||
| contrib/systemd-unit-files | ||
| data | ||
| docs | ||
| karte | ||
| manifest | ||
| openspec | ||
| sandbox | ||
| scripts | ||
| src/lage | ||
| tests | ||
| .containerignore | ||
| .envrc.example | ||
| .gitignore | ||
| .gitleaks.toml | ||
| .pre-commit-config.yaml | ||
| .python-version | ||
| .sops.yaml | ||
| AGENTS.md | ||
| ARCHITECTURE.md | ||
| CLAUDE.md | ||
| Containerfile | ||
| DEVELOPER.md | ||
| Justfile | ||
| logo.svg | ||
| openapi.json | ||
| OWNERS | ||
| podman-compose.yaml | ||
| pyproject.toml | ||
| README.md | ||
| renovate.json5 | ||
| skills-lock.json | ||
| sources.yaml | ||
| sqi.jsonl | ||
| uv.lock | ||
| VISION.de.md | ||
| VISION.md | ||
Lage Bonn
Live: https://lage.bonn.machdenstaat.de/
A Lagebild for Bonn: aggregates local RSS/Atom feeds and open government APIs into one geo-tagged, deduplicated Atom stream. Deep at the local (Ortsteil) level rather than thin and broad.
Approach (a): deterministic curation — fetch, geo-tag, dedup, emit. No LLM, no rewriting of source text. LLM synthesis and a scheduled Morgenlage digest are later phases.
Pipeline
SQLite registry ─▶ fetch ─▶ geo-tag ─▶ dedup ─▶ SQLite ─▶ Atom (out/feed.atom)
feeds (DB) RSS+JSON gazetteer cluster items georss:point
Both the feed registry and all items live in SQLite, with pydantic
as the single schema authority (LageItem + a discriminated Source union).
sources.yaml is a git-tracked seed: lage feed import validates it through
pydantic and upserts it into the DB (also auto-seeded on first collect when
the registry is empty). After that the DB is authoritative — edit feeds with
lage feed create/delete.
- fetch — RSS via
feedparser; Autobahn + DWD via JSON/WFS (no auth). - geo-tag — Bonn gazetteer (4 Stadtbezirke, ~50 Ortsteile). Ortsteil names
matched in item text (
Bonn-Röttgen → Röttgen/Bonn); Autobahn/DWD use native coordinates; GA district feeds tag at fetch time. Rhein-Sieg neighbor Gemeinden (Alfter, Siegburg, …) are detected so the bare word "Bonn" cannot mis-claim them → "Umland (Rhein-Sieg)". No-place items → "Bonn allgemein". Region-wide feeds can setbonn_only: trueto drop items that resolve to no Bonn/Umland place. - dedup — exact (guid/link hash, SQLite PK) + fuzzy (normalized-title similarity within 36h) so the same event from Polizei + GA + Rundschau collapses into one Atom entry that cites all sources.
- out — Atom 1.0 with
georss:point, geo/category/source categories.
Sources (verified live, auth-free)
| Category | Sources |
|---|---|
| Blaulicht | Polizei Bonn, Feuerwehr & Rettungsdienst Bonn, Bundespolizei Dir. Sankt Augustin (NRW-wide, bonn_only-filtered) |
| News | General-Anzeiger (Stadt + Bonn/Bad Godesberg/Beuel/Hardtberg), Bonner Rundschau |
| Amtlich | Bundesstadt Bonn Presse |
| Wissenschaft | Universität Bonn |
| Events | Bonn Veranstaltungskalender |
| Verkehr | Autobahn API (A565/A555/A59 roadworks + warnings) |
| Wetter | DWD weather warnings (Stadt Bonn) |
Deferred (need work/auth): Bonn OParl RIS (HTTP 500 at build time), WDR Lokalzeit (no working feed), SWB/VRS disruptions (scrape or API key), Amtsblatt (PDF only).
Usage
uv run lage feed collect # one poll cycle, respects per-source cadence
uv run lage feed collect --force # ignore cadence, fetch everything now
uv run lage feed collect -vv # DEBUG: per-item geo + dedup scores
uv run lage feed collect --window-hours 48 # Atom output time window
uv run lage # bare alias for `feed collect` (back-compat)
Output: out/feed.atom. State: data/lage.db (SQLite).
Status
uv run lage status # data-store stats + per-source fetch health
Reports store size, item counts (total / last 24h / 72h), dedup effectiveness,
the covered time span, breakdowns by category / Stadtbezirk / geo-source, and
per source the last fetch outcome (ok/fail), consecutive failure count, last
item count, and an overdue flag when a source has missed twice its cadence.
Manage sources
uv run lage feed import # seed/refresh the DB registry from sources.yaml
uv run lage feed list # show configured sources (from the DB)
uv run lage feed show polizei-bonn # one source: config + live stats (DB)
uv run lage feed create --id radio-bonn --title "Radio Bonn" \
--url https://example.org/feed.xml --category news --poll-minutes 30
uv run lage feed create --id bpol --title "Bundespolizei" \
--url https://.../feed.rss --category blaulicht --bonn-only
uv run lage feed delete --id radio-bonn
uv run lage feed delete --all # wipe the registry (re-seed with `feed import`)
create/delete write the SQLite registry directly (DB authority); import
seeds/refreshes it from the git-tracked sources.yaml. Pydantic validates
every source — a bad --category is rejected before it reaches the DB. Flags:
--stadtbezirk (district scope), --bonn-only (drop non-Bonn items),
--user-agent browser (for hosts that block bots).
Serve the feed
uv run lage serve # http://127.0.0.1:8080/
uv run lage serve --host 0.0.0.0 --port 8099
Serves a human-readable HTML view at / and /feed, and the raw
out/feed.atom as application/atom+xml at /feed.atom and /atom
(GET + HEAD). Dev/local convenience — it does not run the pipeline; keep
fetching on the timer/cron. For production, front the file with nginx or any
static server.
Google Maps traffic panel (optional)
The dashboard at / can show a live traffic map of Bonn powered by the
Google Maps JavaScript API. It is hidden when GOOGLE_MAPS_API_KEY is unset.
One-time setup:
- Open Google Cloud Console → APIs & Services → Library.
- Search for Maps JavaScript API and click Enable.
- Go to APIs & Services → Credentials → Create Credentials → API key.
- Copy the key, then restrict it:
- Application restrictions: HTTP referrers → add
http://localhost:8080/*(and your production domain if applicable). - API restrictions: restrict to Maps JavaScript API only.
- Application restrictions: HTTP referrers → add
- Add the key to
.envrc:
export GOOGLE_MAPS_API_KEY=AIza...your_key_here
- Reload the env (
direnv allowif you use direnv, orsource .envrc), then restartlage serve. The Verkehrslage panel appears automatically.
Deployment (continuous stream)
Deterministic, crash-safe — a timer wakes the app every 5 min; the per-source
poll_minutes in the SQLite registry decides what actually gets fetched.
mkdir -p ~/.config/systemd/user
cp contrib/systemd-unit-files/lage.{service,timer} ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now lage.timer
Or cron:
*/5 * * * * cd ~/Source/lage && uv run lage feed collect
Serve out/feed.atom with any static file server; feed readers consume it.
Roadmap
- (b) LLM synthesis: cluster → one written Lage paragraph per event.
- Scheduled Morgenlage digest (06:00) as a second Atom feed / email.
- OSM street-level gazetteer (Overpass) for sub-Ortsteil precision.
- Recover OParl (council decisions), add SWB/VRS disruptions.