Skip to content

feat: Ali je vroče? ERA5 — live climate explorer for Slovenia#448

Open
kesma01 wants to merge 6 commits into
mainfrom
feature/ali-je-vroce-era5
Open

feat: Ali je vroče? ERA5 — live climate explorer for Slovenia#448
kesma01 wants to merge 6 commits into
mainfrom
feature/ali-je-vroce-era5

Conversation

@kesma01

@kesma01 kesma01 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds a new page /ali-je-vroce-era5/ — a live ERA5-Land climate explorer for Slovenia powered by an independent Python sidecar service. It runs alongside the existing website stack without touching any existing code paths.

  • New nav link ERA5 (alongside the existing Ali je vroče?) pointing to /ali-je-vroce-era5/
  • Python sidecar (scripts/era5/) fetches ERA5-Land data from Open-Meteo, precomputes statistics into SQLite, and serves them at /api/live/*
  • nginx proxies /api/live/* to the sidecar — frontend sees a single origin, no CORS
  • All charts are SolidJS + Highcharts, consistent with the existing site style
  • Feature-flagged via scripts/era5/si.yaml so sections can be toggled without code changes

What's new

Charts / sections (all confirmed working in Docker)

Section Model / method
Today status — national temperature percentile KDE on ERA5 history 1950–present
Last-7-days strip Rolling daily percentiles
Per-station regression + annual trend Theil-Sen + TFPW Mann-Kendall, projected to 2050, lapse-rate corrected
Season heatmap Seasonal anomaly calendar
SPEI heatmap + drought trend 3-parameter log-logistic SPEI, per station and season
Tropical days / nights Annual counts above threshold — Negative Binomial GLM (correct model for overdispersed count data), 95% CI + PI, streak filter, lapse-rate corrected
Sea level — Koper IPCC AR6 projections (SSP2-4.5 / SSP5-8.5) + interactive flood-zone overlay for northern Adriatic

New files

  • code/ali-je-vroce-era5/ — 22 SolidJS/TypeScript components and charts
  • scripts/era5/ — sidecar: mk_collect.py, mk_precompute.py, mk_sidecar.py, si.yaml, pyproject.toml, docker-entrypoint.sh, locales/
  • deployment/Dockerfile.era5-sidecar, Dockerfile.website.era5, default.conf.era5.template
  • pages/ali-je-vroce-era5/index.md — new page
  • styles/era5.css — component styles
  • public/data/flood-stats.json + public/data/flood/flood-{010..250}cm.png — static flood data for Koper sea-level widget

Modified files (minimal, non-breaking)

  • pages/_includes/base.html — adds ERA5 nav + footer link
  • styles/main.css@import "./era5.css"
  • eleventy.config.mjs — ERA5 page route
  • compose.yamlera5-sidecar service + era5-data volume
  • package.json / yarn.lock — no new runtime deps beyond what's already in the project
  • README.md — ERA5 section with Docker setup instructions

Docker test results

The full stack was tested locally using the production Docker setup (Dockerfile.website.era5 + Dockerfile.era5-sidecar) — the same build that will run on the Hetzner servers. Both images built cleanly, all 8 page sections rendered correctly, API routes confirmed via network inspector.

Deploy notes

The sidecar needs to run as a separate process/container alongside nginx:

  • ERA5_SIDECAR_HOST env var tells nginx where to proxy /api/live/ (default: era5-sidecar in compose)
  • DATA_DIR defaults to /app/data/si — mount a persistent volume here
  • On first start the sidecar collects ~75 MB of ERA5 CSVs from Open-Meteo (~10 min), then precomputes SQLite, then starts serving
  • Nightly cron (mk_collect.py) keeps data current

Test plan

  • docker-compose -f compose.yaml up starts cleanly with era5-sidecar alongside existing services
  • /ali-je-vroce-era5/ loads and shows today's temperature percentile for Slovenia
  • All 8 chart sections render (today, last 7 days, regression, season heatmap, SPEI, drought trend, tropical days/nights, sea level Koper)
  • Station switcher works across all per-station charts
  • Tropical days: changing threshold and streak reloads chart correctly
  • Sea level: year slider and scenario selector work; flood PNG overlays load
  • Existing /ali-je-vroce/ page unaffected
  • No console errors on either page

🤖 Generated with Claude Code

kesma01 and others added 2 commits July 5, 2026 12:03
Adds a full ERA5-Land climate dashboard for Slovenia as a new page at
/ali-je-vroce-era5/, powered by a dedicated Flask sidecar that serves
precomputed SQLite data. New nav link "ERA5" added alongside existing
"Ali je vroče?".

- code/ali-je-vroce-era5/: SolidJS app — today card, distribution chart,
  regression panel, station map (Highcharts), season heatmap, SPEI heatmap,
  hero cards, trend/year-round charts; tropical/sea-level sections
  feature-flagged off for Slovenia
- scripts/era5/: mk_collect.py, mk_precompute.py (SQLite), mk_sidecar.py
  (Flask /api/live/* endpoints), si.yaml config, locales, entrypoint
- deployment/: Dockerfile.era5-sidecar, Dockerfile.website.era5,
  default.conf.era5.template (nginx proxy to sidecar on :5052)
- compose.yaml: era5-sidecar + website-era5 services added
- eleventy.config.mjs: era5 entry wired into Vite build
- styles/era5.css: ERA5 dashboard styles
- pages/ali-je-vroce-era5/index.md: new page mounting the SolidJS app
- pages/_includes/base.html: ERA5 nav link + footer link added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ions

- si.yaml: enable drought_trend_chart, tropical_days_chart,
  tropical_nights_chart, sea_level_section (was false for all four)
- mk_sidecar.py: add /api/live/tropical_days and /api/live/tropical_nights
  endpoints using Negative Binomial GLM (statsmodels) — correct model for
  integer count data with overdispersion; includes 95% CI and PI bands,
  current-year exclusion from fit, streak filter, lapse-rate correction
- pyproject.toml: add statsmodels>=0.14 dependency
- TropicalChart.tsx: fix endpoint URLs to /api/live/ prefix (via SIDECAR
  env var); update model_used type to boolean | "nb"
- public/data/: add flood-stats.json and 25 flood PNG overlays (10–250 cm)
  for the Koper sea-level widget
- README.md: document ERA5 sidecar, Docker setup, and chart inventory

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread scripts/era5/mk_sidecar.py Fixed
Comment thread scripts/era5/mk_sidecar.py Fixed
@kesma01 kesma01 assigned kesma01, stefanb, ntadej and jalezi and unassigned kesma01 Jul 5, 2026
kesma01 and others added 2 commits July 7, 2026 08:07
… through an exception'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
… through an exception'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@ntadej

ntadej commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

We use kubernetes and not docker-compose. The config is AFAIK only provided for local development needs. I think if you want to have a separate service, we'd have to setup its running on the (staging) server.

Two new GitHub Actions workflows build and push to GHCR on merge to main:
- ghcr.io/podnebnik/era5-sidecar (Flask API + data collection)
- ghcr.io/podnebnik/website-era5 (nginx with /api/live/ proxy)

deployment/kubernetes/ provides illustrative manifests (PVC, Deployment
with initContainer + two containers, Service, nightly CronJob) for the
DevOps team to adapt into their FluxCD/Kustomize GitOps repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new /ali-je-vroce-era5/ SolidJS-based “live ERA5-Land climate explorer” page for Slovenia, backed by a Python sidecar that collects ERA5-Land data from Open‑Meteo, precomputes derived statistics into SQLite, and serves the results via /api/live/* behind an nginx (and dev-server) reverse proxy.

Changes:

  • Adds a new ERA5 page entrypoint and a substantial new SolidJS/Highcharts UI surface (today status, trends, regression, SPEI, tropical days/nights, sea-level widget, etc.).
  • Adds Python data pipeline scripts (mk_collect.py, mk_precompute.py) plus containerization and deployment manifests (Docker, Compose, Kubernetes) for a sidecar API.
  • Integrates the feature into the existing site with a nav/footer link, Tailwind CSS import, and a local dev proxy for /api/live.

Reviewed changes

Copilot reviewed 47 out of 73 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
yarn.lock Adds Leaflet + type packages to lockfile.
styles/main.css Imports ERA5-specific stylesheet.
scripts/era5/si.yaml Slovenia config: stations, feature flags, map + branding metadata.
scripts/era5/pyproject.toml Declares Python sidecar dependencies.
scripts/era5/mk_precompute.py Precomputes SQLite tables (daily windows, annual trend, season heatmap).
scripts/era5/mk_collect.py Collects ERA5-Land daily series from Open‑Meteo and writes per-station CSVs.
scripts/era5/docker-entrypoint.sh Container entrypoint: initial collect, precompute, then run sidecar.
README.md Documents local Docker setup and explains ERA5 stack.
public/data/flood-stats.json Static flood impact metadata for sea-level widget.
pages/ali-je-vroce-era5/index.md Adds the new page and bootstraps the SolidJS entry module.
pages/_includes/base.html Adds ERA5 link to nav and footer.
package.json Adds Leaflet and Leaflet types to dependencies.
eleventy.config.mjs Adds dev proxy for /api/live to sidecar URL.
deployment/kubernetes/era5-service.yaml Kubernetes Service for the ERA5 pod.
deployment/kubernetes/era5-pvc.yaml Persistent volume claim for ERA5 collected/precomputed data.
deployment/kubernetes/era5-deployment.yaml K8s Deployment for website + sidecar in one pod plus initContainer.
deployment/kubernetes/era5-cronjob.yaml Nightly refresh CronJob (collect + precompute).
deployment/Dockerfile.website.era5 Builds an ERA5-enabled website image with nginx proxy template.
deployment/Dockerfile.era5-sidecar Builds the Python sidecar image using uv.
deployment/default.conf.era5.template nginx config to proxy /api/live/* to sidecar container.
compose.yaml Adds era5-sidecar service and wires dev env/proxy.
code/ali-je-vroce-era5/types.ts TypeScript API response types for the ERA5 frontend.
code/ali-je-vroce-era5/entry.tsx SolidJS entrypoint mounting the ERA5 app with query client.
code/ali-je-vroce-era5/components/TodayTrendChart.tsx Highcharts trend chart for today’s date context.
code/ali-je-vroce-era5/components/TodayLast7Chart.tsx Last-7-days category strip chart.
code/ali-je-vroce-era5/components/TodayFlag.tsx SVG-based Slovenian flag overlay widget by category.
code/ali-je-vroce-era5/components/TodayCard.tsx “Today status” card with controls, gauge, last-7-days, and narrative.
code/ali-je-vroce-era5/components/StationMap.tsx Highcharts map-based station selector.
code/ali-je-vroce-era5/components/RegressionSection.tsx Regression UI section (variable/day/window/method controls).
code/ali-je-vroce-era5/components/RegressionPanel.tsx Regression panel state store + toolbar + charts.
code/ali-je-vroce-era5/charts/YearRoundChart.tsx Year-round trend/decade column visualization.
code/ali-je-vroce-era5/charts/TropicalChart.tsx Tropical days/nights chart with NB fit bands and controls.
code/ali-je-vroce-era5/charts/TodayGauge.tsx Highcharts gauge for today percentile/category.
code/ali-je-vroce-era5/charts/SpeiTrendChart.tsx SPEI station/season trend scatter + stats.
code/ali-je-vroce-era5/charts/SpeiHeatmap.tsx SPEI seasonal heatmap grid with animation + tooltip.
code/ali-je-vroce-era5/charts/SeasonHeatmap.tsx Seasonal temperature heatmap grid with animation + tooltip.
code/ali-je-vroce-era5/charts/SeaLevelWidget.tsx Leaflet-based sea-level rise widget with flood overlays.
code/ali-je-vroce-era5/charts/RegressionChart.tsx Regression scatter + trend line + CI band rendering.
code/ali-je-vroce-era5/charts/DistributionChart.tsx KDE distribution chart with percentile zone bands.
code/ali-je-vroce-era5/charts/AnnualTrendChart.tsx Annual trend chart (hist + projection bands + points).
code/ali-je-vroce-era5/api.ts Fetch helpers for all /api/live/* endpoints.
code/ali-je-vroce-era5/AliJeVroceERA5.tsx Main page composition and section wiring / feature gating.
.github/workflows/docker-era5-website.yaml CI workflow to build/push website-era5 image.
.github/workflows/docker-era5-sidecar.yaml CI workflow to build/push era5-sidecar image.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/era5/mk_collect.py Outdated
Comment on lines +92 to +97
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

cache_session = requests_cache.CachedSession(".openmeteo_cache", expire_after=-1)
cache_session.verify = False # disable SSL verification (corporate proxy workaround)
retry_session = retry(cache_session, retries=3, backoff_factor=2)
openmeteo = openmeteo_requests.Client(session=retry_session)
Comment on lines +298 to +310
df_new = pd.DataFrame({
"date": pd.date_range(
start=pd.to_datetime(daily.Time(), unit="s", utc=True),
end=pd.to_datetime(daily.TimeEnd(), unit="s", utc=True),
freq=pd.Timedelta(seconds=daily.Interval()),
inclusive="left",
).date,
"temperature_max": daily.Variables(0).ValuesAsNumpy(),
"temperature_min": daily.Variables(1).ValuesAsNumpy(),
"temperature_mean": daily.Variables(2).ValuesAsNumpy(),
"precipitation_sum": daily.Variables(3).ValuesAsNumpy(),
"et0_evapotranspiration": daily.Variables(4).ValuesAsNumpy(),
})
Comment on lines +7 to +13
location /api/live/ {
proxy_pass http://${ERA5_SIDECAR_HOST}:5052;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 30s;
add_header Access-Control-Allow-Origin *;
}
Comment on lines +303 to +306
const onUpdate = () => { renderFloodCanvas(); setViewFrac(calcViewFrac()); };
leafletMap.on('move zoom moveend zoomend viewreset resize', onUpdate);
new ResizeObserver(onUpdate).observe(mapWrapEl);

- Remove TLS verify=False from mk_collect.py (MITM risk)
- Make precipitation/ET0 DataFrame columns conditional on DAILY_VARIABLES
  to prevent IndexError when SPEI features are disabled
- Remove flask_cors CORS(app) wildcard from mk_sidecar.py — sidecar is
  always accessed via nginx proxy, no CORS headers needed
- Remove Access-Control-Allow-Origin header from nginx config (was also
  redundant since the sidecar itself set it)
- Disconnect ResizeObserver in SeaLevelWidget onCleanup to prevent leak
  on component unmount

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants