Skip to content

OdinMB/city-monitor

Repository files navigation

City Monitor

Real-time city dashboard, currently covering Berlin.
Inspired by World Monitor.

City Monitor Berlin dashboard

City Monitor Berlin traffic map City Monitor Berlin water map

Website CI License


What it does

City Monitor aggregates public data feeds into a single dashboard per city: weather, transit disruptions, news, events, police reports, air quality, water levels, pharmacies, traffic, construction, and more. Data is ingested on a schedule via cron jobs, stored in PostgreSQL, and served as pre-built JSON to a React SPA.

Stack

Layer Tech
Frontend React 19, TypeScript, Vite 6, Tailwind v4, Zustand, React Query, MapLibre GL
Backend Node.js, Express, node-cron, Drizzle ORM
Database PostgreSQL with in-memory cache
AI OpenAI GPT-5 for news summarization
Deployment Render.com

Getting Started

npm install
npm run dev          # Starts web (port 5173) + API (port 3001) via Turborepo

Requires a .env file in packages/server/ with at least DATABASE_URL pointing to a PostgreSQL instance. See .context/deployment.md for the full list of environment variables.

Project Structure

packages/
  web/          React SPA (Vite)
  server/       Express API + cron jobs
shared/         Shared TypeScript types
.context/       Architecture docs and guides
.plans/         Milestone plans

Adding a City

The goal is a pure data-config layer: drop in a config file, get a dashboard. In practice that's not fully realistic — some data sources have city-specific APIs, formats, or quirks that need custom processing logic. The approach is: use as much config as possible, and add your own ingestion/parsing logic where needed.

There are three ways to use this:

  • Single-city fork — swap Berlin's config for your own city and run your own dashboard.
  • Contribute a city — add your city's config to this repo so it appears on citymonitor.app.
  • Run your own multi-city site — fork the repo, configure multiple cities, and deploy your own instance.

License note: City Monitor is AGPL-3.0. If you run a modified version as a network service, you must make your source code available to users under the same license. See Section 13 for details.

Steps

  1. Define the shared type — the CityConfig interface lives in shared/types.ts. You shouldn't need to change it unless your city needs a new data source type.

  2. Create a server config — add packages/server/src/config/cities/<city>.ts exporting a CityConfig. Use berlin.ts or hamburg.ts as a template. This is where you declare RSS feeds, transit stations, weather coordinates, police feeds, water level gauges, and every other data source the cron jobs will ingest.

  3. Register the server config — import your city in packages/server/src/config/index.ts and add it to the ALL_CITIES map.

  4. Create a web config — add packages/web/src/config/cities/<city>.ts. The frontend config is minimal (coordinates, map bounds, theme accent) since the SPA reads pre-built data from the API.

  5. Register the web config — import your city in packages/web/src/config/index.ts, add it to ALL_CITIES, and add the city ID to the ACTIVE_CITY_IDS set.

  6. Activate on the server — set the ACTIVE_CITIES environment variable to include your city (comma-separated, e.g. berlin,hamburg,munich). This controls which cities the cron jobs ingest data for and which city IDs the API accepts.

  7. Add translations — add the city name and any city-specific UI strings to all four locale files in packages/web/src/i18n/ (en.json, de.json, tr.json, ar.json).

  8. Add to Sources page — create a sources array for your city in packages/web/src/pages/SourcesPage.tsx listing all data sources with attribution links.

  9. (Optional) Add a city skyline — add an SVG skyline function in packages/web/src/components/layout/SkylineSeparator.tsx and wire it to your city ID. This renders a silhouette separator between the hero map and the dashboard tiles. If omitted, a generic skyline is used. You can also delete the skyline entirely if you prefer a clean edge.

How it works under the hood

  • Server: getActiveCities() reads ACTIVE_CITIES and returns configs from ALL_CITIES. Every cron job iterates over active cities. The validateCity middleware rejects requests for unknown or inactive city IDs.
  • Frontend: The router matches /:cityId and looks it up via getCityConfig(). If the city isn't in ACTIVE_CITY_IDS, the user is redirected to the home page.
  • Database: All cities share the same snapshots table — rows are keyed by (city_id, type). No schema changes needed.
  • Cache: Each city gets its own namespaced cache keys (e.g. berlin:weather, munich:weather). Cache warming runs for all active cities on startup.

Deployment

See Deploy on Render.com for a step-by-step guide covering automated blueprint setup, manual service creation, environment variables, custom domains, and troubleshooting.

Contributing

Contributions are welcome! Please read CONTRIBUTING.md before submitting a pull request.

Support

If you find this project useful, consider supporting it on Ko-fi.

License

AGPL-3.0-or-later — Copyright (C) 2026 Odin Muhlenbein

About

Real-time dashboard for cities (currently covering Berlin). Inspired by worldmonitor. Weather, air quality, wastewater virus measures, traffic, news, events, demographic data, etc.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

Packages

 
 
 

Contributors

Languages