| backend | ||
| docs | ||
| panel | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
Weather Aggregator Dashboard
Collects current weather for multiple cities from multiple providers and shows a comparison dashboard. Compares data across providers, monitors response times, and surfaces failures/stale data.
- Backend: Go (
backend/), REST API, SQLite (pure-Gomodernc.org/sqlite, no CGO) - Panel: React + TypeScript, Bun + Vite (
panel/), dark theme - Providers: 11 built-in total — Open-Meteo, MET Norway, 7Timer!, wttr.in, IRIMO (Iran) work with no key; OpenWeatherMap, WeatherAPI.com, Tomorrow.io, Visual Crossing, Weatherbit, Weatherstack need free API keys. Custom generic JSON providers can be added from the panel with no code.
- Fresh installs seed 3 demo cities (Amsterdam, London, Sydney), so the first cycle already shows live data
Quick start with Docker (recommended)
cp .env.example .env # optional; set OPENWEATHER_API_KEY / ports
docker compose up --build -d
docker compose ps # backend should become "healthy"
- Panel: http://localhost:5173 (nginx serves the built panel and proxies
/apito the backend, same-origin) - Backend API: http://localhost:8080/api/health
- Data persists in the
weather-datavolume (DATABASE_PATH=/data/weather.dbin the container). - Override host ports:
BACKEND_PORT=18080 PANEL_PORT=15173 docker compose up --build -d.
docker compose logs -f backend # watch collection cycles
docker compose down # stop (keeps data)
docker compose down -v # stop and delete all stored data
Only the backend talks to weather-provider APIs; the browser talks to the panel container, which proxies /api/* to backend:8080.
Quick start without Docker
1. Backend
cd backend
cp .env.example .env # edit keys / ports as needed
go mod download
go run ./cmd/server
# or: go build -o server ./cmd/server && ./server
Env vars (see backend/.env.example):
| Var | Default | Purpose |
|---|---|---|
HTTP_ADDR |
127.0.0.1:8080 |
Bind address (keep localhost for MVP) |
DATABASE_PATH |
./data/weather.db |
SQLite file |
REFRESH_INTERVAL_SECONDS |
300 |
Full collection cycle period |
GLOBAL_REQUEST_DELAY_MS |
1000 |
Delay between outgoing provider requests |
REQUEST_TIMEOUT_SECONDS |
10 |
Per-request timeout |
HISTORY_RETENTION_DAYS |
7 |
Prune readings/attempts/cycles older than this |
OPENWEATHER_API_KEY |
— | Required only for the OpenWeatherMap provider |
Backend runs a startup cycle (if cities + providers are enabled), then schedules every REFRESH_INTERVAL_SECONDS. Manual refresh: POST /api/refresh (409 if a cycle is already running).
2. Panel
cd panel
bun install
bun run dev # http://127.0.0.1:5173, proxies /api -> 127.0.0.1:8080
# production:
bun run build # dist/
The panel never calls weather APIs directly; it only talks to the Go backend and polls /api/dashboard every 15s. Loading the dashboard never triggers collection.
Usage
- Open the panel — Amsterdam, London and Sydney are pre-seeded and the startup cycle already fetched weather from the 5 keyless providers (IRIMO records
skippedfor non-Iran cities). - Add cities: type a name → Search → Select, or enter name + latitude/longitude directly with Add with coordinates.
- Enable providers: Provider settings. Open-Meteo, MET Norway, 7Timer! and wttr.in work immediately; the other six show
needs keywith the exact env var until you set it and restart the backend. Seedocs/providers.md. - Wait for the automatic cycle or press Refresh now.
- Compare per-city × per-provider cards (temp, feels-like, condition, humidity, wind, fetch vs observation time, status, duration).
- Tune Scheduler settings (interval, delay, timeout, retention) and inspect Recent activity (cycles, failures, timeouts).
REST API
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/health |
Health check |
| GET | /api/dashboard |
Cycle + locations + providers + latest readings |
| GET | /api/locations |
List locations |
| GET | /api/locations/search?q= |
Geocode candidates |
| POST | /api/locations |
Resolve + add ({name} or full {name,latitude,longitude,...}; 300 + candidates if ambiguous) |
| PATCH | /api/locations/{id} |
{name?, is_enabled?} |
| DELETE | /api/locations/{id} |
Remove |
| GET | /api/locations/{id}/readings?limit= |
Recent readings |
| GET | /api/weather/latest?location_id=&city=&country_code=&provider=&enabled_only= |
Latest weather per city (for other backends, see docs/service-api.md) |
| POST | /api/weather/fetch |
{"location_id"|"city"+country_code?, "provider?"} → fresh fetch now |
| GET | /api/providers |
List providers + config status (kind, redacted generic config) |
| POST | /api/providers |
Add provider ({id, name, kind?, config?}) |
| PATCH | /api/providers/{id} |
{name?, is_enabled?, request_delay_ms?, clear_delay?, config?} |
| DELETE | /api/providers/{id} |
Remove provider + its history |
| GET/PATCH | /api/settings |
Scheduler settings (validated) |
| POST | /api/refresh |
Manual cycle (202 started, 409 already running) |
| GET | /api/cycles?limit= |
Recent cycles |
| GET | /api/cycles/{id} |
Cycle + attempts |
| GET | /api/activity?limit= |
Recent attempts |
Architecture
Browser (React+TS, Bun/Vite) ──REST──▶ Go backend ──▶ provider APIs
├─ REST API (stdlib ServeMux)
├─ Location service (Open-Meteo geocoding)
├─ Scheduler (ticker, mutex, no overlap)
├─ Provider adapters (common interface)
└─ SQLite (locations, providers, settings,
readings, attempts, cycles)
Each cycle iterates enabled locations × enabled providers, sleeps the provider (or global) delay between requests, applies a per-request timeout, records every attempt, and keeps the last successful reading on failure. Retry-After/429 is recorded as rate_limited with no retry loop.
Adding a provider
For a JSON API you can add a generic provider from the panel (Provider settings → Add provider) — no code needed. See docs/providers.md.
For a built-in adapter (custom auth, HTML scraping, ...):
- Create
backend/internal/providers/myprov.goimplementing:
Normalize units inside the adapter (°C, m/s, hPa, mm); usetype WeatherProvider interface { ID() string Name() string CurrentWeather(ctx context.Context, location Location) (WeatherReading, *CallLog, error) }nilfor missing fields. Return a*CallLog(redacted URL + response snippet) so attempts are inspectable, or a*SkipErrorwhen the provider has no coverage for the city. - Register it in
providers.NewRegistry()and extendIsConfigured()if it needs a key. - The
providersseed table picks up the new id automatically; scheduler and dashboard need no changes.
Testing
cd backend && go test ./...
cd ../panel && bunx tsc --noEmit && bun run build
Covers: N×M fan-out, disabled provider/location exclusion, failure isolation, delay respected, no concurrent cycles, timeout isolation, persistence round-trip, settings validation.
Security notes
- Keys live only in env/backend; never sent to the browser or logged (error messages are sanitized).
- Binds to localhost by default; set
API_TOKENand terminate TLS in a reverse proxy before exposing beyond a trusted network. - All outbound HTTP has timeouts; inputs and settings are validated.
See docs/providers.md for provider setup, docs/storage.md for where your data lives, docs/api.md for endpoint details, and docs/service-api.md for consuming the API from another backend.