Portail météo pour les services ATM — agrège MetGate (Météo-France), OpenSky Network (ADS-B) et EUMETSAT MTG dans une interface web unifiée.
- METAR / TAF / SIGMET / AIRMET / MAA — flux WFS MetGate convertis GML → GeoJSON, décodés en français
- Produits plats SA_last / FT_last / FC_last (fallback stations non IWXXM : ED*, K*, UNOO…)
- Popup enrichie : T / Td / QNH / vent / visibilité / nuages pour METAR et TAF
- Vent — overlay particules animées (WIND 850 hPa / JET stream), WCS NetCDF-4
- Tropopause — canvas overlay altitude (m) avec upscale bilinéaire
- Cendres volcaniques (QVACIS) — concentration mg/m³, déterministe ou probabiliste
- Cloud Top Height (EUMETSAT MTG-FCI CTTH) — PNG colorisé par FL, cache stale-while-revalidate
- Foudre (EUMETSAT MTG-LI) — flashes GeoJSON dernière fenêtre 10 min
- Satellite FCI IR / RGB Convection — proxy tuiles EUMETView WMS
- Slider temporel — navigation dans les fenêtres de validité SIGMET / RDT / CAT
- Recherche par callsign / immatriculation via OpenSky Network
- Suivi temps réel avec historique en mémoire (trace passée D3)
- Plan de vol projeté depuis la position courante : grand cercle, profil FL, événements WFS croisés
- Profil vent waypoint par waypoint (head / tail / cross wind)
- Globe Three.js centré sur un aérodrome ICAO
- Pistes OurAirports, trafic ADS-B dans un rayon configurable
- Mât vent animé, cellules convectives RDT / OPIC_GTD
- Plan de vol synthétique DEP → ARR : grand cercle, montée / croisière / descente
- Météo le long de la route : SIGMET, AIRMET, MAA, CAT actifs
- Radar météo canvas style phosphore vert, heading-up, 40/80/160/320 NM
- Mode SIM : route DEP → ARR avec heure de départ UTC, slider play/pause ×1–×30
- Mode LIVE : suivi ADS-B temps réel (polling OpenSky 15 s), données FL / GS / TRK / V/S / LAT / LON
- Phénomènes météo calés sur l'ETA de l'avion : RDT T+0…T+60, SIGMET par fenêtre de validité, flashes MTG-LI avec fade temporel (blanc → rouge → masqué sur 10 min)
- Contours pulsants alternés (cycle cos² par type), popup au survol
- Mini-carte géographique (Natural Earth + FIR/UIR EUROCONTROL, 336 zones OACI)
- Indicateur de mise à jour météo (heure + compteurs cellules / flashes)
cmd/portal/ point d'entrée (HTTP server, graceful shutdown)
internal/
catalog/ client MetGate WFS/WCS/RAW, cache TTL + singleflight, GML→GeoJSON, NetCDF
decoder/ traducteur TAC français (METAR, TAF, SIGMET, AIRMET, MAA)
aircraft/ client OpenSky (OAuth2 + Basic), ring buffer historique ADS-B
airports/ base OurAirports embarquée CSV → lookup map ICAO
cloudtop/ EUMETSAT MTG-FCI CTTH, stale-while-revalidate, pré-chargement boot
lightning/ EUMETSAT MTG-LI LFL, cache par product ID
eumetsat/ client OAuth2 EUMETSAT (token cache)
satellite/ proxy tuiles EUMETView WMS (cache 60 s / tuile)
fir/ FIR/UIR mondiaux embarqués (EUROCONTROL EAD, 336 zones, GeoJSON ; cf. gen_fir.py)
geo/ pays Natural Earth 110m embarqués (GeoJSON)
http/ routes HTTP, handlers, spec OpenAPI embarquée
web/ go:embed du frontend buildé (dist/)
web/src/ React 19 + TypeScript + MapLibre 5 + Three.js / R3F
Le frontend est embarqué dans le binaire via go:embed — un seul fichier à déployer (~30 MB).
- Token applicatif MetGate (Météo-France) — obligatoire
- (optionnel) Clés EUMETSAT consumer_key / consumer_secret pour CTH / foudre / satellite
- (optionnel) Credentials OpenSky pour le suivi ADS-B
- Pour build source : Go ≥ 1.24, Node.js ≥ 22, GNU make
L'installation se fait en 4 étapes : récupérer les credentials → choisir un mode de déploiement → démarrer le service → valider.
Toutes les valeurs ci-dessous se mettent dans un fichier .env (ou un Secret Kubernetes selon le mode choisi). Le fichier .env.example du repo liste l'intégralité des variables avec leur commentaire.
| Source | Comment l'obtenir | Indispensable ? |
|---|---|---|
| MetGate (Météo-France) | Demander un token applicatif au gestionnaire MetGate (metgate.meteo.fr). Deux environnements : https://metgate-int.meteo.fr (intégration / dev) et https://metgate-mf.meteo.fr (PROD). |
✅ Oui |
| EUMETSAT | Créer un compte sur eoportal.eumetsat.int → My Profile → API Key → générer un couple consumer_key / consumer_secret. |
/api/lightning, /api/cloudtop, /api/satellite/tile renvoient 503. |
| OpenSky Network | Compte gratuit sur opensky-network.org → Personal Account Settings → Download credentials (JSON {clientId, clientSecret}). |
/api/aircraft/* renvoie 503. |
⚠️ Sécurité :chmod 600 .envsystématiquement. En K8s, utiliser un Secret (ou mieux : Sealed Secrets / External Secrets Operator).
| Mode | Quand l'utiliser | Effort |
|---|---|---|
| A. Docker | Serveur unique, VM, Raspberry Pi, démo locale. C'est la voie recommandée par défaut. | 5 min |
| B. Kubernetes | Cluster existant, plusieurs replicas, exposition Internet avec TLS / auth en frontal. | 10 min |
| C. Build source | Dev local avec hot reload, modification du code, tests, contribution. | 10 min + dépendances Go/Node |
Image multi-stage distroless publiée sur GHCR à chaque tag : ~32 MB, user nonroot (uid 65532), pas de shell, base pinée par digest, scan Trivy CVE en CI.
# 2.A.1 — Préparer le fichier de configuration
curl -O https://raw.githubusercontent.com/benedictemarty/metgate/main/.env.example
mv .env.example .env
$EDITOR .env # remplir METGATE_TOKEN, EUMETSAT_*, OPENSKY_*
chmod 600 .env # ⚠️ contient des secrets
# 2.A.2 — Lancer le conteneur
docker run -d --name metgate \
--env-file .env \
-p 8080:8080 \
--restart unless-stopped \
--read-only --tmpfs /tmp:size=64m \
--security-opt no-new-privileges \
--cap-drop ALL \
ghcr.io/benedictemarty/metgate:latestTags disponibles : latest (HEAD de main), vX.Y.Z (release sémantique), X.Y, X, sha-<short> (commit GitHub).
Build local (sans pull GHCR) : docker build -t metgate:dev . && docker run … metgate:dev.
Options recommandées :
--read-only --tmpfs /tmp: rootfs immuable, seul/tmpaccessible en écriture en mémoire.--security-opt no-new-privileges: impossibilité de gagner des privilèges au runtime.--cap-drop ALL: aucune capability Linux conservée.--restart unless-stopped: redémarrage auto sauf arrêt manuel.
Docker Compose équivalent (si tu préfères) : un docker-compose.yml prêt à
l'emploi est fourni à la racine du repo — docker compose up -d. Il charge le
.env et expose en ${VAR:-défaut} les URLs des services extérieurs (MetGate,
OpenSky, adsb.fi, EUMETSAT, EUMETView) ainsi que la configuration du proxy
sortant (OUTBOUND_PROXY_URL, NO_PROXY, OUTBOUND_CA_FILE), pour pointer un
miroir interne ou traverser un proxy d'entreprise sans rebuild.
Manifest minimal fourni dans deploy/k8s/metgate.yaml : Deployment 2 replicas + Service ClusterIP. SecurityContext durci : runAsNonRoot (uid 65532), readOnlyRootFilesystem, allowPrivilegeEscalation: false, capabilities.drop: [ALL], seccompProfile: RuntimeDefault. Probes HTTP /healthz (readiness 10 s, liveness 30 s), requests modestes (50m CPU / 64 Mi).
# 2.B.1 — Créer le Secret (JAMAIS committé)
kubectl create secret generic metgate-secret \
--from-literal=METGATE_TOKEN='votre-token-metgate' \
--from-literal=EUMETSAT_CONSUMER_KEY='votre-key' \
--from-literal=EUMETSAT_CONSUMER_SECRET='votre-secret' \
--from-literal=OPENSKY_CLIENT_ID='votre-client-id' \
--from-literal=OPENSKY_CLIENT_SECRET='votre-client-secret'
# 2.B.2 — Appliquer le manifest
kubectl apply -f deploy/k8s/metgate.yaml
# 2.B.3 — Vérifier
kubectl get pods -l app=metgate
kubectl logs -l app=metgate --tail=20Pour modifier le namespace, l'image ou les replicas : éditer deploy/k8s/metgate.yaml (champ metadata.namespace, spec.template.spec.containers[0].image, spec.replicas).
Pour exposer publiquement (non inclus dans le manifest, à ajouter selon ton stack) :
- Ingress + cert-manager pour TLS Let's Encrypt.
- oauth2-proxy / Authelia / Pomerium en frontal —
⚠️ le portail n'a pas d'auth applicative, ne pas exposer sans rideau. - NetworkPolicy egress restreinte aux IPs MetGate / OpenSky / EUMETSAT.
Exemple snippet Ingress nginx + cert-manager (à adapter, non fourni par défaut) :
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: metgate
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/auth-url: "https://oauth2-proxy.example.com/oauth2/auth"
spec:
ingressClassName: nginx
tls:
- hosts: [metgate.example.com]
secretName: metgate-tls
rules:
- host: metgate.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: metgate
port: { number: 80 }À utiliser pour le développement, la modification du code ou la contribution.
# 2.C.1 — Cloner et configurer
git clone https://github.com/benedictemarty/metgate
cd metgate
cp .env.example .env
$EDITOR .env && chmod 600 .env
# 2.C.2 — Dépendances frontend (lockfile committé : utiliser ci pour install reproductible)
cd web && npm ci && cd ..
# 2.C.3 — Build complet (frontend → dist embarqué dans le binaire Go)
make build # produit ./bin/portal (~32 MB, statique, CGO=0)
./bin/portalHot-reload pour le dev frontend (2 terminaux) :
# Terminal 1 — backend Go
make run # :8080
# Terminal 2 — Vite avec proxy /api → :8080
make web-dev # :5173Ouvrir http://localhost:5173 (dev) ou http://localhost:8080 (prod).
# Sonde santé (toujours 200 si le binaire démarre)
curl http://localhost:8080/healthz
# → {"status":"ok"}
# Vérifier la connexion à MetGate (token + base URL OK ?)
curl 'http://localhost:8080/api/products' | jq '.[0]'
# Vérifier EUMETSAT (si configuré)
curl -I 'http://localhost:8080/api/cloudtop?bbox=-10,40,15,55&minfl=200'
# → 200 OK si EUMETSAT_CONSUMER_KEY/SECRET valides
# → 503 si clés absentes ou invalides
# Vérifier OpenSky (si configuré)
curl 'http://localhost:8080/api/aircraft/search?q=AFR'
# → 200 OK si OAuth OpenSky OK ; 503 sinonLogs structurés (slog JSON) à surveiller :
| Niveau / message | Signification |
|---|---|
INFO eumetsat MTG: désactivé (clés absentes…) |
EUMETSAT_CONSUMER_KEY/SECRET vides — endpoints CTH / foudre / sat HS, normal en dev. |
INFO opensky: désactivé (credentials absents) |
OPENSKY_* vides — endpoints /api/aircraft/* HS, normal en dev. |
ERROR METGATE_BASE_URL et METGATE_TOKEN doivent être définis |
.env non chargé ou variables vides — vérifier --env-file ou le Secret. |
WARN impossible de lire .env |
Mode Docker / K8s : normal (pas de fichier .env dans l'image, tout passe par les variables). |
| Symptôme | Cause probable | Action |
|---|---|---|
503 metgate WFS GetFeature |
Token MetGate invalide, expiré ou environnement faux (int vs mf). |
Vérifier METGATE_TOKEN et METGATE_BASE_URL. |
503 lightning indisponible / 503 cloudtop indisponible |
EUMETSAT_CONSUMER_KEY/SECRET absents ou révoqués. |
Régénérer les clés sur eoportal, mettre à jour Secret / .env, redémarrer le pod / conteneur. |
| Carte vide, pas de POI | CORS / Content Security Policy bloqué par un reverse proxy. | Vérifier que le proxy laisse passer application/geo+json et image/png. |
Pod K8s en CrashLoopBackOff |
Secret manquant ou nom de clé incorrect. | kubectl describe pod -l app=metgate puis kubectl logs. |
| Conteneur Docker s'arrête tout de suite | .env non monté ou variables manquantes. |
docker logs metgate : doit afficher l'erreur METGATE_BASE_URL et METGATE_TOKEN doivent être définis. |
Premier appel /api/lightning lent (~30 s) |
Téléchargement du NetCDF MTG-LI initial chez EUMETSAT, comportement normal. | Le 2e appel passe par le cache (latence < 100 ms). |
| Trace ADS-B disparaît au restart | Le buffer history est en mémoire (cf. CLAUDE.md). |
Comportement attendu, persistance non implémentée. |
| Variable | Req. | Description |
|---|---|---|
METGATE_BASE_URL |
✓ | https://metgate-mf.meteo.fr (PROD) ou https://metgate-int.meteo.fr (INT) |
METGATE_TOKEN |
✓ | Token applicatif MetGate |
PORT |
Port d'écoute (défaut : 8080) |
|
METGATE_CACHE_TTL_SECONDS |
TTL cache MetGate en secondes (défaut : 60) |
|
OPENSKY_CLIENT_ID |
OAuth2 client ID OpenSky (recommandé) | |
OPENSKY_CLIENT_SECRET |
OAuth2 client secret OpenSky | |
OPENSKY_USER |
Basic auth OpenSky (legacy) | |
OPENSKY_PASS |
Basic auth OpenSky (legacy) | |
EUMETSAT_CONSUMER_KEY |
Clé EUMETSAT Data Store (CTH / foudre / satellite) | |
EUMETSAT_CONSUMER_SECRET |
Secret EUMETSAT |
Sans credentials OpenSky, /api/aircraft/* renvoie 503. Sans credentials EUMETSAT, CTH / foudre / satellite sont désactivés.
# Deux terminaux
make run # backend Go sur :8080
make web-dev # Vite sur :5173 avec proxy /api → :8080
# Qualité
make test # go test ./...
make lint # golangci-lint (nécessite golangci-lint v2)
make tidy # go mod tidyDocumentation interactive : /api/docs (Swagger UI)
Spec OpenAPI : /api/openapi.yaml
| Endpoint | Description |
|---|---|
GET /healthz |
Santé du service |
GET /api/products |
Catalogue produits MetGate (RAW + WFS + WCS) |
GET /api/feature?type=METAR_last |
Features WFS → GeoJSON + décodage FR |
GET /api/wind?bbox=...&level=85000 |
Grille vent u/v depuis WCS NetCDF |
GET /api/tropo?bbox=... |
Grille altitude tropopause |
GET /api/qvacis?fl=325 |
Grille concentration cendres volcaniques |
GET /api/route?dep=LFPG&arr=LFBO |
Plan de vol synthétique + météo route |
GET /api/aircraft/search?cs=AFR123 |
Recherche ADS-B via OpenSky |
GET /api/aircraft/{icao24} |
État ADS-B courant (200 toujours, stale si hors couverture) |
GET /api/cloudtop?bbox=...&minfl=200 |
PNG sommets nuageux MTG-FCI CTTH |
GET /api/lightning?bbox=... |
Flashes foudre MTG-LI (GeoJSON) |
GET /api/airports/search?q=LFPG |
Recherche aérodromes OurAirports |
GET /api/airport/{icao} |
Fiche aérodrome + pistes |
À chaque tag v* poussé, le workflow .github/workflows/release.yml :
- Build amd64 local.
- Scan Trivy (CRITICAL+HIGH,
ignore-unfixed) — bloque le job si CVE corrigeables. Résultats SARIF remontés dans l'onglet Security GitHub. - Si scan OK : build multi-arch
linux/amd64+linux/arm64et push surghcr.io/benedictemarty/metgate.
Lancement ad-hoc sans tag : Actions → Release → Run workflow.
Build cross-compilé sortant en bin/portal (~32 MB statique, CGO désactivé) :
make build
scp bin/portal user@server:/opt/metgate/portalExemple systemd :
[Unit]
Description=MetGate Portal
After=network.target
[Service]
WorkingDirectory=/opt/metgate
ExecStart=/opt/metgate/portal
EnvironmentFile=/opt/metgate/.env
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.targetLe Makefile inclut une cible make deploy qui pousse le binaire sur un LXC Proxmox via SCP + pct mount (variables PROXMOX_SSH, PROXMOX_PORT, LXC_ID, LXC_DEST en haut du Makefile à adapter).
| Source | Usage |
|---|---|
| MetGate — Météo-France | METAR, TAF, SIGMET, AIRMET, MAA, vent, tropopause, cendres |
| OpenSky Network | Suivi ADS-B temps réel |
| EUMETSAT Data Store | MTG-FCI CTTH (sommets nuageux), MTG-LI (foudre) |
| EUMETView WMS | Tuiles satellite FCI IR / RGB Convection |
| OurAirports | Aérodromes, pistes, coordonnées (CC0) |
| EUROCONTROL atlas (EAD) | FIR/UIR OACI mondiales, 336 zones, contours haute résolution (MIT) |
| Natural Earth | Polygones pays 110m (domaine public) |
Note EUMETSAT : CTH, foudre et satellite MTG sont des données situationnelles non OPMET. Elles complètent la vision opérationnelle mais ne remplacent pas les produits OPMET officiels.
- Auth locale : le portail n'a pas de gestion d'utilisateurs. Avant exposition publique, ajouter login/JWT.
- OpenSky history : in-memory uniquement, perdu au redémarrage.
- WFS pagination : count=2000 ; MetGate peut en avoir davantage (non critique pour la visu).
European Union Public Licence v. 1.2 (EUPL-1.2)
Licence officielle de l'Union Européenne — copyleft compatible AGPL/LGPL/MPL. Toute modification distribuée ou déployée en service doit être publiée sous EUPL-1.2 ou licence compatible.
Les données tierces conservent leur licence d'origine : FIR/UIR EUROCONTROL atlas/EAD (MIT), Natural Earth (domaine public), OurAirports (CC0).